Lower the `log` facade, and add explicit enum discriminants
log is second by dependents in libcosmic's tree, 61 of 741, and has the same shape as bitflags: src/macros.rs is 20 macro_rules! and that is what the dependents use, while lib.rs is a facade over a &'static dyn Log behind atomics. So the macros get the same treatment -- lowered directly, with the behaviour pinned from the real crate by a test that links log for the oracle only. What that test caught: max_level() starts at Off; log_enabled! is false even after set_max_level when no logger is installed, because the facade consults the logger as well as the level; Display for a level is upper-case where Debug is not. A record's arguments are not evaluated when the level is disabled, so the lowering emits the enabled check around the whole call rather than computing the message first. The boundary is deliberate and stated in DESIGN.md: rustnim models log's emitting side, not its installing side. A transpiled library's info! calls work and, with no logger, do nothing -- Rust's own behaviour. Installing one is an application's job and is done from Nim via rsLogSetLogger, which is checked by hand rather than differentially, since there is no Rust counterpart to compare against. The facade's types are emitted as RsLogLevel and RsLogFilter. Using Rust's `Level` and `LevelFilter` broke six existing cases: a crate's own `Error` type and an enum field named `Error` cannot coexist in one Nim module. Also here, both general rather than log-specific: explicit enum discriminants (`Error = 1`), which Nim enums take too, so the values are preserved rather than the variants renumbered, with `as` on such an enum yielding its ordinal; and target_has_atomic joining the host cfg predicates. 41 differential cases, all green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12c0a01 parent: af6e50f modified
Cargo.lock +7 -0 | @@ -8,6 +8,12 @@ version = "2.13.2" | ||
| 8 | 8 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 9 | 9 | checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" |
| 10 | 10 | |
| 11 | +[[package]] | |
| 12 | +name = "log" | |
| 13 | +version = "0.4.34" | |
| 14 | +source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 15 | +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" | |
| 16 | + | |
| 11 | 17 | [[package]] |
| 12 | 18 | name = "proc-macro2" |
| 13 | 19 | version = "1.0.107" |
| @@ -31,6 +37,7 @@ name = "rustnim" | ||
| 31 | 37 | version = "0.1.0" |
| 32 | 38 | dependencies = [ |
| 33 | 39 | "bitflags", |
| 40 | + "log", | |
| 34 | 41 | "syn", |
| 35 | 42 | ] |
| 36 | 43 | |
| @@ -8,6 +8,12 @@ version = "2.13.2" | |||
| 8 | source = "registry+https://github.com/rust-lang/crates.io-index" | 8 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 9 | checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" | 9 | checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" |
| 10 | 10 | ||
| 11 | +[[package]] | ||
| 12 | +name = "log" | ||
| 13 | +version = "0.4.34" | ||
| 14 | +source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 15 | +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" | ||
| 16 | + | ||
| 11 | [[package]] | 17 | [[package]] |
| 12 | name = "proc-macro2" | 18 | name = "proc-macro2" |
| 13 | version = "1.0.107" | 19 | version = "1.0.107" |
| @@ -31,6 +37,7 @@ name = "rustnim" | |||
| 31 | version = "0.1.0" | 37 | version = "0.1.0" |
| 32 | dependencies = [ | 38 | dependencies = [ |
| 33 | "bitflags", | 39 | "bitflags", |
| 40 | + "log", | ||
| 34 | "syn", | 41 | "syn", |
| 35 | ] | 42 | ] |
| 36 | 43 | ||
modified
Cargo.toml +1 -0 | @@ -11,3 +11,4 @@ syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] } | ||
| 11 | 11 | # checked against it rather than against our reading of its documentation. |
| 12 | 12 | [dev-dependencies] |
| 13 | 13 | bitflags = "2" |
| 14 | +log = "0.4" | |
| @@ -11,3 +11,4 @@ syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] } | |||
| 11 | # checked against it rather than against our reading of its documentation. | 11 | # checked against it rather than against our reading of its documentation. |
| 12 | [dev-dependencies] | 12 | [dev-dependencies] |
| 13 | bitflags = "2" | 13 | bitflags = "2" |
| 14 | +log = "0.4" | ||
modified
DESIGN.md +44 -1 | @@ -321,7 +321,8 @@ runner) rather than a wrong answer. | ||
| 321 | 321 | own `type Result<T>` is told apart from the builtin `Result<T, E>` by |
| 322 | 322 | arity, which is not how Rust resolves it. |
| 323 | 323 | 10. Host `#[cfg]` predicates — `unix`, `windows`, `target_os`, `target_arch`, |
| 324 | - `target_family`, `target_pointer_width`, `target_endian` — are evaluated | |
| 324 | + `target_family`, `target_pointer_width`, `target_endian`, | |
| 325 | + `target_has_atomic` — are evaluated | |
| 325 | 326 | against the machine, since the generated Nim is compiled for it. That makes |
| 326 | 327 | the output host-shaped: a crate branching on platform has had that branch |
| 327 | 328 | decided at transpile time. `doc`/`doctest`/`miri` are false. A custom or |
| @@ -516,6 +517,48 @@ the macro generates, and the shim would not know. The test is what would catch | ||
| 516 | 517 | it, which is why it links the real crate rather than a copy of its |
| 517 | 518 | documentation. |
| 518 | 519 | |
| 520 | +## `log`, on the same argument | |
| 521 | + | |
| 522 | +`log` is second by dependents in `libcosmic`'s tree (61 of 741), and it has | |
| 523 | +the same shape as `bitflags`: `src/macros.rs` is 20 `macro_rules!`, and that | |
| 524 | +is what the dependents use. `src/lib.rs` is the facade — `Level`, | |
| 525 | +`LevelFilter`, the `Log` trait behind a `&'static dyn`, atomics and | |
| 526 | +`set_logger`. | |
| 527 | + | |
| 528 | +So the macros are lowered directly, against behaviour pinned from the real | |
| 529 | +crate by `tests/cases/035-log.rs` (again `//@ extern: log`, so the oracle | |
| 530 | +links it and rustnim does not). What that test pins: | |
| 531 | + | |
| 532 | +- `max_level()` starts at `Off`. | |
| 533 | +- `log_enabled!` is **false even after `set_max_level`** when no logger is | |
| 534 | + installed, because the facade consults the logger as well as the level. | |
| 535 | +- `Display` for a level is upper-case (`WARN`), `Debug` is not (`Warn`). | |
| 536 | +- `Level::Error as usize` is 1 through `Trace` as 5; `LevelFilter::Off` is 0. | |
| 537 | + | |
| 538 | +A record's arguments are not evaluated when the level is disabled, so the | |
| 539 | +lowering emits `if rsLogEnabled(l): rsLog(l, ..)` rather than computing the | |
| 540 | +message first. | |
| 541 | + | |
| 542 | +**The boundary is deliberate: rustnim models log's *emitting* side, not its | |
| 543 | +installing side.** A transpiled library's `info!` calls work and, with no | |
| 544 | +logger, do nothing — which is exactly Rust's behaviour. Installing a logger | |
| 545 | +is an application's job and is done from Nim: | |
| 546 | + | |
| 547 | +```nim | |
| 548 | +rsLogSetLogger(proc (level: RsLogLevel, target, msg: string) = | |
| 549 | + echo "[", rsDisplay(level), "] ", msg) | |
| 550 | +rsLogMaxLevel = int(rsLvlTrace) | |
| 551 | +``` | |
| 552 | + | |
| 553 | +Modelling `impl Log` instead would mean reproducing `Record` and `Metadata`, | |
| 554 | +which is more shim surface for something a Nim application would not write in | |
| 555 | +Rust anyway. | |
| 556 | + | |
| 557 | +The facade's types are emitted as `RsLogLevel` and `RsLogFilter`, not `Level` | |
| 558 | +and `LevelFilter`. The first attempt used Rust's names and broke six existing | |
| 559 | +cases, because a crate's own `Error` type and an enum field named `Error` | |
| 560 | +cannot coexist in one Nim module. | |
| 561 | + | |
| 519 | 562 | ## Proof of byte-identity for `base16ct` |
| 520 | 563 | |
| 521 | 564 | [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive |
| @@ -321,7 +321,8 @@ runner) rather than a wrong answer. | |||
| 321 | own `type Result<T>` is told apart from the builtin `Result<T, E>` by | 321 | own `type Result<T>` is told apart from the builtin `Result<T, E>` by |
| 322 | arity, which is not how Rust resolves it. | 322 | arity, which is not how Rust resolves it. |
| 323 | 10. Host `#[cfg]` predicates — `unix`, `windows`, `target_os`, `target_arch`, | 323 | 10. Host `#[cfg]` predicates — `unix`, `windows`, `target_os`, `target_arch`, |
| 324 | - `target_family`, `target_pointer_width`, `target_endian` — are evaluated | 324 | + `target_family`, `target_pointer_width`, `target_endian`, |
| 325 | + `target_has_atomic` — are evaluated | ||
| 325 | against the machine, since the generated Nim is compiled for it. That makes | 326 | against the machine, since the generated Nim is compiled for it. That makes |
| 326 | the output host-shaped: a crate branching on platform has had that branch | 327 | the output host-shaped: a crate branching on platform has had that branch |
| 327 | decided at transpile time. `doc`/`doctest`/`miri` are false. A custom or | 328 | decided at transpile time. `doc`/`doctest`/`miri` are false. A custom or |
| @@ -516,6 +517,48 @@ the macro generates, and the shim would not know. The test is what would catch | |||
| 516 | it, which is why it links the real crate rather than a copy of its | 517 | it, which is why it links the real crate rather than a copy of its |
| 517 | documentation. | 518 | documentation. |
| 518 | 519 | ||
| 520 | +## `log`, on the same argument | ||
| 521 | + | ||
| 522 | +`log` is second by dependents in `libcosmic`'s tree (61 of 741), and it has | ||
| 523 | +the same shape as `bitflags`: `src/macros.rs` is 20 `macro_rules!`, and that | ||
| 524 | +is what the dependents use. `src/lib.rs` is the facade — `Level`, | ||
| 525 | +`LevelFilter`, the `Log` trait behind a `&'static dyn`, atomics and | ||
| 526 | +`set_logger`. | ||
| 527 | + | ||
| 528 | +So the macros are lowered directly, against behaviour pinned from the real | ||
| 529 | +crate by `tests/cases/035-log.rs` (again `//@ extern: log`, so the oracle | ||
| 530 | +links it and rustnim does not). What that test pins: | ||
| 531 | + | ||
| 532 | +- `max_level()` starts at `Off`. | ||
| 533 | +- `log_enabled!` is **false even after `set_max_level`** when no logger is | ||
| 534 | + installed, because the facade consults the logger as well as the level. | ||
| 535 | +- `Display` for a level is upper-case (`WARN`), `Debug` is not (`Warn`). | ||
| 536 | +- `Level::Error as usize` is 1 through `Trace` as 5; `LevelFilter::Off` is 0. | ||
| 537 | + | ||
| 538 | +A record's arguments are not evaluated when the level is disabled, so the | ||
| 539 | +lowering emits `if rsLogEnabled(l): rsLog(l, ..)` rather than computing the | ||
| 540 | +message first. | ||
| 541 | + | ||
| 542 | +**The boundary is deliberate: rustnim models log's *emitting* side, not its | ||
| 543 | +installing side.** A transpiled library's `info!` calls work and, with no | ||
| 544 | +logger, do nothing — which is exactly Rust's behaviour. Installing a logger | ||
| 545 | +is an application's job and is done from Nim: | ||
| 546 | + | ||
| 547 | +```nim | ||
| 548 | +rsLogSetLogger(proc (level: RsLogLevel, target, msg: string) = | ||
| 549 | + echo "[", rsDisplay(level), "] ", msg) | ||
| 550 | +rsLogMaxLevel = int(rsLvlTrace) | ||
| 551 | +``` | ||
| 552 | + | ||
| 553 | +Modelling `impl Log` instead would mean reproducing `Record` and `Metadata`, | ||
| 554 | +which is more shim surface for something a Nim application would not write in | ||
| 555 | +Rust anyway. | ||
| 556 | + | ||
| 557 | +The facade's types are emitted as `RsLogLevel` and `RsLogFilter`, not `Level` | ||
| 558 | +and `LevelFilter`. The first attempt used Rust's names and broke six existing | ||
| 559 | +cases, because a crate's own `Error` type and an enum field named `Error` | ||
| 560 | +cannot coexist in one Nim module. | ||
| 561 | + | ||
| 519 | ## Proof of byte-identity for `base16ct` | 562 | ## Proof of byte-identity for `base16ct` |
| 520 | 563 | ||
| 521 | [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive | 564 | [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive |
modified
README.md +6 -1 | @@ -77,7 +77,7 @@ argument for longer inputs and 20,000 pseudorandom cases attacking it. | ||
| 77 | 77 | `cargo test --test proof` runs it — 151,463 cases, 9.1 MB of output, compared |
| 78 | 78 | byte for byte. |
| 79 | 79 | |
| 80 | -## `bitflags!` | |
| 80 | +## `bitflags!` and `log` | |
| 81 | 81 | |
| 82 | 82 | `bitflags` is the most-depended-on translatable crate in libcosmic's tree (79 |
| 83 | 83 | of 741), and it is 26 `macro_rules!` definitions. Expanding the macro does not |
| @@ -86,6 +86,11 @@ So the macro is lowered directly, and checked against the real crate: the test | ||
| 86 | 86 | links bitflags for the *oracle only*, and rustnim has to reproduce its |
| 87 | 87 | behaviour without it, byte for byte. |
| 88 | 88 | |
| 89 | +`log` (61 dependents) is the same shape — 20 `macro_rules!` — and gets the | |
| 90 | +same treatment. rustnim models log's *emitting* side: a transpiled library's | |
| 91 | +`info!` calls work and, with no logger installed, do nothing, exactly as in | |
| 92 | +Rust. Installing a logger is done from Nim with `rsLogSetLogger`. | |
| 93 | + | |
| 89 | 94 | ## Does it generalise? |
| 90 | 95 | |
| 91 | 96 | `base16ct` is the crate this was built toward, so a second one was tried. |
| @@ -77,7 +77,7 @@ argument for longer inputs and 20,000 pseudorandom cases attacking it. | |||
| 77 | `cargo test --test proof` runs it — 151,463 cases, 9.1 MB of output, compared | 77 | `cargo test --test proof` runs it — 151,463 cases, 9.1 MB of output, compared |
| 78 | byte for byte. | 78 | byte for byte. |
| 79 | 79 | ||
| 80 | -## `bitflags!` | 80 | +## `bitflags!` and `log` |
| 81 | 81 | ||
| 82 | `bitflags` is the most-depended-on translatable crate in libcosmic's tree (79 | 82 | `bitflags` is the most-depended-on translatable crate in libcosmic's tree (79 |
| 83 | of 741), and it is 26 `macro_rules!` definitions. Expanding the macro does not | 83 | of 741), and it is 26 `macro_rules!` definitions. Expanding the macro does not |
| @@ -86,6 +86,11 @@ So the macro is lowered directly, and checked against the real crate: the test | |||
| 86 | links bitflags for the *oracle only*, and rustnim has to reproduce its | 86 | links bitflags for the *oracle only*, and rustnim has to reproduce its |
| 87 | behaviour without it, byte for byte. | 87 | behaviour without it, byte for byte. |
| 88 | 88 | ||
| 89 | +`log` (61 dependents) is the same shape — 20 `macro_rules!` — and gets the | ||
| 90 | +same treatment. rustnim models log's *emitting* side: a transpiled library's | ||
| 91 | +`info!` calls work and, with no logger installed, do nothing, exactly as in | ||
| 92 | +Rust. Installing a logger is done from Nim with `rsLogSetLogger`. | ||
| 93 | + | ||
| 89 | ## Does it generalise? | 94 | ## Does it generalise? |
| 90 | 95 | ||
| 91 | `base16ct` is the crate this was built toward, so a second one was tried. | 96 | `base16ct` is the crate this was built toward, so a second one was tried. |
modified
src/lower.rs +127 -9 | @@ -159,6 +159,9 @@ struct Sig { | ||
| 159 | 159 | #[derive(Clone)] |
| 160 | 160 | struct Variant { |
| 161 | 161 | name: String, |
| 162 | + /// `Error = 1` — Nim enums take explicit ordinals too, so the value is | |
| 163 | + /// preserved rather than the variant being renumbered. | |
| 164 | + discriminant: Option<String>, | |
| 162 | 165 | /// `(nim field name, type)`. Empty for a unit variant. Tuple variants get |
| 163 | 166 | /// `f0`, `f1`, ...; every field is prefixed with the variant name because |
| 164 | 167 | /// Nim requires the branches of a variant object to have distinct fields. |
| @@ -595,13 +598,10 @@ impl Lowerer { | ||
| 595 | 598 | let mut variants = Vec::new(); |
| 596 | 599 | for v in &e.variants { |
| 597 | 600 | let vname = v.ident.to_string(); |
| 598 | - if v.discriminant.is_some() { | |
| 599 | - return Err(format!( | |
| 600 | - "`{name}::{vname}` has an explicit discriminant; Rust's \ | |
| 601 | - `as` on such an enum has a value this lowering does not \ | |
| 602 | - yet preserve" | |
| 603 | - )); | |
| 604 | - } | |
| 601 | + let discriminant = match &v.discriminant { | |
| 602 | + Some((_, e)) => Some(self.expr(e)?.code), | |
| 603 | + None => None, | |
| 604 | + }; | |
| 605 | 605 | let mut fields = Vec::new(); |
| 606 | 606 | for (i, f) in v.fields.iter().enumerate() { |
| 607 | 607 | // Nim requires the branches of a variant object to have |
| @@ -614,7 +614,7 @@ impl Lowerer { | ||
| 614 | 614 | let t = if returns_borrow(&f.ty) { t.unvar() } else { t.owned() }; |
| 615 | 615 | fields.push((fname, t)); |
| 616 | 616 | } |
| 617 | - variants.push(Variant { name: vname, fields }); | |
| 617 | + variants.push(Variant { name: vname, discriminant, fields }); | |
| 618 | 618 | } |
| 619 | 619 | let simple = variants.iter().all(|v| v.fields.is_empty()); |
| 620 | 620 | for v in &variants { |
| @@ -810,6 +810,18 @@ impl Lowerer { | ||
| 810 | 810 | }; |
| 811 | 811 | Ok(s.value() == (usize::BITS).to_string()) |
| 812 | 812 | } |
| 813 | + // Every integer width and pointer-sized atomic exists on the | |
| 814 | + // targets Nim builds for here; like the other host facts this is | |
| 815 | + // read off the machine rather than chosen. | |
| 816 | + syn::Meta::NameValue(nv) if nv.path.is_ident("target_has_atomic") => { | |
| 817 | + let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else { | |
| 818 | + return Err("`target_has_atomic = ..` expects a string".into()); | |
| 819 | + }; | |
| 820 | + Ok(matches!( | |
| 821 | + s.value().as_str(), | |
| 822 | + "8" | "16" | "32" | "64" | "ptr" | |
| 823 | + )) | |
| 824 | + } | |
| 813 | 825 | syn::Meta::NameValue(nv) if nv.path.is_ident("target_endian") => { |
| 814 | 826 | let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else { |
| 815 | 827 | return Err("`target_endian = ..` expects a string".into()); |
| @@ -1044,6 +1056,23 @@ impl Lowerer { | ||
| 1044 | 1056 | } |
| 1045 | 1057 | } |
| 1046 | 1058 | |
| 1059 | + /// The Nim name of a `log::Level` written as a path. | |
| 1060 | + fn log_level_of(&mut self, e: &Expr) -> Result<String, String> { | |
| 1061 | + let Expr::Path(p) = e else { | |
| 1062 | + return Err("a log level must be written as `Level::Info`".into()); | |
| 1063 | + }; | |
| 1064 | + let last = path_name(&p.path); | |
| 1065 | + Ok(match last.as_str() { | |
| 1066 | + "Error" => "rsLvlError", | |
| 1067 | + "Warn" => "rsLvlWarn", | |
| 1068 | + "Info" => "rsLvlInfo", | |
| 1069 | + "Debug" => "rsLvlDebug", | |
| 1070 | + "Trace" => "rsLvlTrace", | |
| 1071 | + other => return Err(format!("`Level::{other}` is not a log level")), | |
| 1072 | + } | |
| 1073 | + .to_string()) | |
| 1074 | + } | |
| 1075 | + | |
| 1047 | 1076 | /// `[T, U]`, or empty. |
| 1048 | 1077 | fn gen_list(params: &[String]) -> String { |
| 1049 | 1078 | if params.is_empty() { |
| @@ -1634,7 +1663,10 @@ impl Lowerer { | ||
| 1634 | 1663 | self.line(&format!("type {name}* = enum")); |
| 1635 | 1664 | self.indent += 1; |
| 1636 | 1665 | for v in &def.variants { |
| 1637 | - self.line(&format!("{}", ident(&v.name))); | |
| 1666 | + match &v.discriminant { | |
| 1667 | + Some(d) => self.line(&format!("{} = {}", ident(&v.name), d)), | |
| 1668 | + None => self.line(&ident(&v.name)), | |
| 1669 | + } | |
| 1638 | 1670 | } |
| 1639 | 1671 | self.indent -= 1; |
| 1640 | 1672 | self.blank(); |
| @@ -3061,6 +3093,24 @@ impl Lowerer { | ||
| 3061 | 3093 | if name == "None" { |
| 3062 | 3094 | return Ok(Val::new(self.none_of(expect), expect.cloned())); |
| 3063 | 3095 | } |
| 3096 | + // `log::Level` and `log::LevelFilter` come from the facade | |
| 3097 | + // shim, under names no crate can collide with. | |
| 3098 | + if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) { | |
| 3099 | + if (q == "Level" || q == "LevelFilter") && !self.enums.contains_key(&q) { | |
| 3100 | + let pre = if q == "Level" { "rsLvl" } else { "rsFlt" }; | |
| 3101 | + let t = if q == "Level" { "RsLogLevel" } else { "RsLogFilter" }; | |
| 3102 | + if q == "LevelFilter" && name == "Off" { | |
| 3103 | + return Ok(Val::new("rsFltOff", Some(Nim::Prim(t.into())))); | |
| 3104 | + } | |
| 3105 | + if matches!(name.as_str(), "Error" | "Warn" | "Info" | "Debug" | "Trace") { | |
| 3106 | + return Ok(Val::new( | |
| 3107 | + format!("{pre}{name}"), | |
| 3108 | + Some(Nim::Prim(t.into())), | |
| 3109 | + )); | |
| 3110 | + } | |
| 3111 | + } | |
| 3112 | + } | |
| 3113 | + | |
| 3064 | 3114 | // `Perms::READ`: a constant of a `bitflags!` type. |
| 3065 | 3115 | if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) { |
| 3066 | 3116 | let q = if q == "Self" { |
| @@ -3643,6 +3693,18 @@ impl Lowerer { | ||
| 3643 | 3693 | (f, Nim::Prim(p)) if f.is_integer() && (p == "float64" || p == "float32") => { |
| 3644 | 3694 | format!("{}({})", p, v.code) |
| 3645 | 3695 | } |
| 3696 | + // The facade's level enums carry their Rust discriminants, so | |
| 3697 | + // `Level::Info as usize` is the ordinal. | |
| 3698 | + (Nim::Prim(p), t) | |
| 3699 | + if t.is_integer() && (p == "RsLogLevel" || p == "RsLogFilter") => | |
| 3700 | + { | |
| 3701 | + format!("{}(ord({}))", t.render(), v.code) | |
| 3702 | + } | |
| 3703 | + // A C-like enum's `as` yields its discriminant, which is its | |
| 3704 | + // ordinal in Nim. | |
| 3705 | + (Nim::Named(n, _), t) if t.is_integer() && self.enums.get(n).is_some_and(|d| d.simple) => { | |
| 3706 | + format!("{}(ord({}))", t.render(), v.code) | |
| 3707 | + } | |
| 3646 | 3708 | (Nim::Prim(b), t) if b == "bool" && t.is_integer() => { |
| 3647 | 3709 | format!("{}(ord({}))", t.render(), v.code) |
| 3648 | 3710 | } |
| @@ -4055,6 +4117,20 @@ impl Lowerer { | ||
| 4055 | 4117 | } |
| 4056 | 4118 | } |
| 4057 | 4119 | |
| 4120 | + // `log::set_max_level` / `log::max_level`. | |
| 4121 | + if name == "set_max_level" && codes.len() == 1 { | |
| 4122 | + return Ok(Val::new( | |
| 4123 | + format!("rsLogMaxLevel = int({})", codes[0]), | |
| 4124 | + Some(Nim::Unit), | |
| 4125 | + )); | |
| 4126 | + } | |
| 4127 | + if name == "max_level" && codes.is_empty() { | |
| 4128 | + return Ok(Val::new( | |
| 4129 | + "RsLogFilter(rsLogMaxLevel)", | |
| 4130 | + Some(Nim::Prim("RsLogFilter".into())), | |
| 4131 | + )); | |
| 4132 | + } | |
| 4133 | + | |
| 4058 | 4134 | // `Spacing::from(d)`: a `From` impl called through its target type. |
| 4059 | 4135 | // Rust picks the impl by the argument's type, and so do we -- Nim |
| 4060 | 4136 | // cannot overload on return type, so each impl has its own proc name. |
| @@ -4712,6 +4788,48 @@ impl Lowerer { | ||
| 4712 | 4788 | a.code, op, b.code, fmt::nim_str(label), a.code, b.code |
| 4713 | 4789 | )) |
| 4714 | 4790 | } |
| 4791 | + // The `log` facade. See `src/prelude.nim` for why these are | |
| 4792 | + // lowered directly rather than expanded. The enabled check wraps | |
| 4793 | + // the whole thing because Rust does not evaluate a log record's | |
| 4794 | + // arguments when the level is disabled. | |
| 4795 | + "error" | "warn" | "info" | "debug" | "trace" | "log" => { | |
| 4796 | + let args: Vec<Expr> = mac | |
| 4797 | + .parse_body_with( | |
| 4798 | + syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated, | |
| 4799 | + ) | |
| 4800 | + .map_err(|e| format!("`{name}!`: {e}"))? | |
| 4801 | + .into_iter() | |
| 4802 | + .collect(); | |
| 4803 | + let (level, rest) = if name == "log" { | |
| 4804 | + let first = args.first().ok_or("`log!` needs a level")?; | |
| 4805 | + (self.log_level_of(first)?, &args[1..]) | |
| 4806 | + } else { | |
| 4807 | + ( | |
| 4808 | + match name.as_str() { | |
| 4809 | + "error" => "rsLvlError", | |
| 4810 | + "warn" => "rsLvlWarn", | |
| 4811 | + "info" => "rsLvlInfo", | |
| 4812 | + "debug" => "rsLvlDebug", | |
| 4813 | + _ => "rsLvlTrace", | |
| 4814 | + } | |
| 4815 | + .to_string(), | |
| 4816 | + &args[..], | |
| 4817 | + ) | |
| 4818 | + }; | |
| 4819 | + let msg = self.format_pieces(rest)?; | |
| 4820 | + let target = fmt::nim_str(&self.cur_mod.clone()); | |
| 4821 | + Ok(format!( | |
| 4822 | + "(if rsLogEnabled({lvl}): rsLog({lvl}, {target}, {msg}))", | |
| 4823 | + lvl = level | |
| 4824 | + )) | |
| 4825 | + } | |
| 4826 | + "log_enabled" => { | |
| 4827 | + let e: Expr = mac | |
| 4828 | + .parse_body() | |
| 4829 | + .map_err(|e| format!("`log_enabled!`: {e}"))?; | |
| 4830 | + let l = self.log_level_of(&e)?; | |
| 4831 | + Ok(format!("rsLogEnabled({l})")) | |
| 4832 | + } | |
| 4715 | 4833 | "vec" => { |
| 4716 | 4834 | let body = mac.tokens.to_string(); |
| 4717 | 4835 | if body.trim().is_empty() { |
| @@ -159,6 +159,9 @@ struct Sig { | |||
| 159 | #[derive(Clone)] | 159 | #[derive(Clone)] |
| 160 | struct Variant { | 160 | struct Variant { |
| 161 | name: String, | 161 | name: String, |
| 162 | + /// `Error = 1` — Nim enums take explicit ordinals too, so the value is | ||
| 163 | + /// preserved rather than the variant being renumbered. | ||
| 164 | + discriminant: Option<String>, | ||
| 162 | /// `(nim field name, type)`. Empty for a unit variant. Tuple variants get | 165 | /// `(nim field name, type)`. Empty for a unit variant. Tuple variants get |
| 163 | /// `f0`, `f1`, ...; every field is prefixed with the variant name because | 166 | /// `f0`, `f1`, ...; every field is prefixed with the variant name because |
| 164 | /// Nim requires the branches of a variant object to have distinct fields. | 167 | /// Nim requires the branches of a variant object to have distinct fields. |
| @@ -595,13 +598,10 @@ impl Lowerer { | |||
| 595 | let mut variants = Vec::new(); | 598 | let mut variants = Vec::new(); |
| 596 | for v in &e.variants { | 599 | for v in &e.variants { |
| 597 | let vname = v.ident.to_string(); | 600 | let vname = v.ident.to_string(); |
| 598 | - if v.discriminant.is_some() { | 601 | + let discriminant = match &v.discriminant { |
| 599 | - return Err(format!( | 602 | + Some((_, e)) => Some(self.expr(e)?.code), |
| 600 | - "`{name}::{vname}` has an explicit discriminant; Rust's \ | 603 | + None => None, |
| 601 | - `as` on such an enum has a value this lowering does not \ | 604 | + }; |
| 602 | - yet preserve" | ||
| 603 | - )); | ||
| 604 | - } | ||
| 605 | let mut fields = Vec::new(); | 605 | let mut fields = Vec::new(); |
| 606 | for (i, f) in v.fields.iter().enumerate() { | 606 | for (i, f) in v.fields.iter().enumerate() { |
| 607 | // Nim requires the branches of a variant object to have | 607 | // Nim requires the branches of a variant object to have |
| @@ -614,7 +614,7 @@ impl Lowerer { | |||
| 614 | let t = if returns_borrow(&f.ty) { t.unvar() } else { t.owned() }; | 614 | let t = if returns_borrow(&f.ty) { t.unvar() } else { t.owned() }; |
| 615 | fields.push((fname, t)); | 615 | fields.push((fname, t)); |
| 616 | } | 616 | } |
| 617 | - variants.push(Variant { name: vname, fields }); | 617 | + variants.push(Variant { name: vname, discriminant, fields }); |
| 618 | } | 618 | } |
| 619 | let simple = variants.iter().all(|v| v.fields.is_empty()); | 619 | let simple = variants.iter().all(|v| v.fields.is_empty()); |
| 620 | for v in &variants { | 620 | for v in &variants { |
| @@ -810,6 +810,18 @@ impl Lowerer { | |||
| 810 | }; | 810 | }; |
| 811 | Ok(s.value() == (usize::BITS).to_string()) | 811 | Ok(s.value() == (usize::BITS).to_string()) |
| 812 | } | 812 | } |
| 813 | + // Every integer width and pointer-sized atomic exists on the | ||
| 814 | + // targets Nim builds for here; like the other host facts this is | ||
| 815 | + // read off the machine rather than chosen. | ||
| 816 | + syn::Meta::NameValue(nv) if nv.path.is_ident("target_has_atomic") => { | ||
| 817 | + let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else { | ||
| 818 | + return Err("`target_has_atomic = ..` expects a string".into()); | ||
| 819 | + }; | ||
| 820 | + Ok(matches!( | ||
| 821 | + s.value().as_str(), | ||
| 822 | + "8" | "16" | "32" | "64" | "ptr" | ||
| 823 | + )) | ||
| 824 | + } | ||
| 813 | syn::Meta::NameValue(nv) if nv.path.is_ident("target_endian") => { | 825 | syn::Meta::NameValue(nv) if nv.path.is_ident("target_endian") => { |
| 814 | let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else { | 826 | let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else { |
| 815 | return Err("`target_endian = ..` expects a string".into()); | 827 | return Err("`target_endian = ..` expects a string".into()); |
| @@ -1044,6 +1056,23 @@ impl Lowerer { | |||
| 1044 | } | 1056 | } |
| 1045 | } | 1057 | } |
| 1046 | 1058 | ||
| 1059 | + /// The Nim name of a `log::Level` written as a path. | ||
| 1060 | + fn log_level_of(&mut self, e: &Expr) -> Result<String, String> { | ||
| 1061 | + let Expr::Path(p) = e else { | ||
| 1062 | + return Err("a log level must be written as `Level::Info`".into()); | ||
| 1063 | + }; | ||
| 1064 | + let last = path_name(&p.path); | ||
| 1065 | + Ok(match last.as_str() { | ||
| 1066 | + "Error" => "rsLvlError", | ||
| 1067 | + "Warn" => "rsLvlWarn", | ||
| 1068 | + "Info" => "rsLvlInfo", | ||
| 1069 | + "Debug" => "rsLvlDebug", | ||
| 1070 | + "Trace" => "rsLvlTrace", | ||
| 1071 | + other => return Err(format!("`Level::{other}` is not a log level")), | ||
| 1072 | + } | ||
| 1073 | + .to_string()) | ||
| 1074 | + } | ||
| 1075 | + | ||
| 1047 | /// `[T, U]`, or empty. | 1076 | /// `[T, U]`, or empty. |
| 1048 | fn gen_list(params: &[String]) -> String { | 1077 | fn gen_list(params: &[String]) -> String { |
| 1049 | if params.is_empty() { | 1078 | if params.is_empty() { |
| @@ -1634,7 +1663,10 @@ impl Lowerer { | |||
| 1634 | self.line(&format!("type {name}* = enum")); | 1663 | self.line(&format!("type {name}* = enum")); |
| 1635 | self.indent += 1; | 1664 | self.indent += 1; |
| 1636 | for v in &def.variants { | 1665 | for v in &def.variants { |
| 1637 | - self.line(&format!("{}", ident(&v.name))); | 1666 | + match &v.discriminant { |
| 1667 | + Some(d) => self.line(&format!("{} = {}", ident(&v.name), d)), | ||
| 1668 | + None => self.line(&ident(&v.name)), | ||
| 1669 | + } | ||
| 1638 | } | 1670 | } |
| 1639 | self.indent -= 1; | 1671 | self.indent -= 1; |
| 1640 | self.blank(); | 1672 | self.blank(); |
| @@ -3061,6 +3093,24 @@ impl Lowerer { | |||
| 3061 | if name == "None" { | 3093 | if name == "None" { |
| 3062 | return Ok(Val::new(self.none_of(expect), expect.cloned())); | 3094 | return Ok(Val::new(self.none_of(expect), expect.cloned())); |
| 3063 | } | 3095 | } |
| 3096 | + // `log::Level` and `log::LevelFilter` come from the facade | ||
| 3097 | + // shim, under names no crate can collide with. | ||
| 3098 | + if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) { | ||
| 3099 | + if (q == "Level" || q == "LevelFilter") && !self.enums.contains_key(&q) { | ||
| 3100 | + let pre = if q == "Level" { "rsLvl" } else { "rsFlt" }; | ||
| 3101 | + let t = if q == "Level" { "RsLogLevel" } else { "RsLogFilter" }; | ||
| 3102 | + if q == "LevelFilter" && name == "Off" { | ||
| 3103 | + return Ok(Val::new("rsFltOff", Some(Nim::Prim(t.into())))); | ||
| 3104 | + } | ||
| 3105 | + if matches!(name.as_str(), "Error" | "Warn" | "Info" | "Debug" | "Trace") { | ||
| 3106 | + return Ok(Val::new( | ||
| 3107 | + format!("{pre}{name}"), | ||
| 3108 | + Some(Nim::Prim(t.into())), | ||
| 3109 | + )); | ||
| 3110 | + } | ||
| 3111 | + } | ||
| 3112 | + } | ||
| 3113 | + | ||
| 3064 | // `Perms::READ`: a constant of a `bitflags!` type. | 3114 | // `Perms::READ`: a constant of a `bitflags!` type. |
| 3065 | if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) { | 3115 | if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) { |
| 3066 | let q = if q == "Self" { | 3116 | let q = if q == "Self" { |
| @@ -3643,6 +3693,18 @@ impl Lowerer { | |||
| 3643 | (f, Nim::Prim(p)) if f.is_integer() && (p == "float64" || p == "float32") => { | 3693 | (f, Nim::Prim(p)) if f.is_integer() && (p == "float64" || p == "float32") => { |
| 3644 | format!("{}({})", p, v.code) | 3694 | format!("{}({})", p, v.code) |
| 3645 | } | 3695 | } |
| 3696 | + // The facade's level enums carry their Rust discriminants, so | ||
| 3697 | + // `Level::Info as usize` is the ordinal. | ||
| 3698 | + (Nim::Prim(p), t) | ||
| 3699 | + if t.is_integer() && (p == "RsLogLevel" || p == "RsLogFilter") => | ||
| 3700 | + { | ||
| 3701 | + format!("{}(ord({}))", t.render(), v.code) | ||
| 3702 | + } | ||
| 3703 | + // A C-like enum's `as` yields its discriminant, which is its | ||
| 3704 | + // ordinal in Nim. | ||
| 3705 | + (Nim::Named(n, _), t) if t.is_integer() && self.enums.get(n).is_some_and(|d| d.simple) => { | ||
| 3706 | + format!("{}(ord({}))", t.render(), v.code) | ||
| 3707 | + } | ||
| 3646 | (Nim::Prim(b), t) if b == "bool" && t.is_integer() => { | 3708 | (Nim::Prim(b), t) if b == "bool" && t.is_integer() => { |
| 3647 | format!("{}(ord({}))", t.render(), v.code) | 3709 | format!("{}(ord({}))", t.render(), v.code) |
| 3648 | } | 3710 | } |
| @@ -4055,6 +4117,20 @@ impl Lowerer { | |||
| 4055 | } | 4117 | } |
| 4056 | } | 4118 | } |
| 4057 | 4119 | ||
| 4120 | + // `log::set_max_level` / `log::max_level`. | ||
| 4121 | + if name == "set_max_level" && codes.len() == 1 { | ||
| 4122 | + return Ok(Val::new( | ||
| 4123 | + format!("rsLogMaxLevel = int({})", codes[0]), | ||
| 4124 | + Some(Nim::Unit), | ||
| 4125 | + )); | ||
| 4126 | + } | ||
| 4127 | + if name == "max_level" && codes.is_empty() { | ||
| 4128 | + return Ok(Val::new( | ||
| 4129 | + "RsLogFilter(rsLogMaxLevel)", | ||
| 4130 | + Some(Nim::Prim("RsLogFilter".into())), | ||
| 4131 | + )); | ||
| 4132 | + } | ||
| 4133 | + | ||
| 4058 | // `Spacing::from(d)`: a `From` impl called through its target type. | 4134 | // `Spacing::from(d)`: a `From` impl called through its target type. |
| 4059 | // Rust picks the impl by the argument's type, and so do we -- Nim | 4135 | // Rust picks the impl by the argument's type, and so do we -- Nim |
| 4060 | // cannot overload on return type, so each impl has its own proc name. | 4136 | // cannot overload on return type, so each impl has its own proc name. |
| @@ -4712,6 +4788,48 @@ impl Lowerer { | |||
| 4712 | a.code, op, b.code, fmt::nim_str(label), a.code, b.code | 4788 | a.code, op, b.code, fmt::nim_str(label), a.code, b.code |
| 4713 | )) | 4789 | )) |
| 4714 | } | 4790 | } |
| 4791 | + // The `log` facade. See `src/prelude.nim` for why these are | ||
| 4792 | + // lowered directly rather than expanded. The enabled check wraps | ||
| 4793 | + // the whole thing because Rust does not evaluate a log record's | ||
| 4794 | + // arguments when the level is disabled. | ||
| 4795 | + "error" | "warn" | "info" | "debug" | "trace" | "log" => { | ||
| 4796 | + let args: Vec<Expr> = mac | ||
| 4797 | + .parse_body_with( | ||
| 4798 | + syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated, | ||
| 4799 | + ) | ||
| 4800 | + .map_err(|e| format!("`{name}!`: {e}"))? | ||
| 4801 | + .into_iter() | ||
| 4802 | + .collect(); | ||
| 4803 | + let (level, rest) = if name == "log" { | ||
| 4804 | + let first = args.first().ok_or("`log!` needs a level")?; | ||
| 4805 | + (self.log_level_of(first)?, &args[1..]) | ||
| 4806 | + } else { | ||
| 4807 | + ( | ||
| 4808 | + match name.as_str() { | ||
| 4809 | + "error" => "rsLvlError", | ||
| 4810 | + "warn" => "rsLvlWarn", | ||
| 4811 | + "info" => "rsLvlInfo", | ||
| 4812 | + "debug" => "rsLvlDebug", | ||
| 4813 | + _ => "rsLvlTrace", | ||
| 4814 | + } | ||
| 4815 | + .to_string(), | ||
| 4816 | + &args[..], | ||
| 4817 | + ) | ||
| 4818 | + }; | ||
| 4819 | + let msg = self.format_pieces(rest)?; | ||
| 4820 | + let target = fmt::nim_str(&self.cur_mod.clone()); | ||
| 4821 | + Ok(format!( | ||
| 4822 | + "(if rsLogEnabled({lvl}): rsLog({lvl}, {target}, {msg}))", | ||
| 4823 | + lvl = level | ||
| 4824 | + )) | ||
| 4825 | + } | ||
| 4826 | + "log_enabled" => { | ||
| 4827 | + let e: Expr = mac | ||
| 4828 | + .parse_body() | ||
| 4829 | + .map_err(|e| format!("`log_enabled!`: {e}"))?; | ||
| 4830 | + let l = self.log_level_of(&e)?; | ||
| 4831 | + Ok(format!("rsLogEnabled({l})")) | ||
| 4832 | + } | ||
| 4715 | "vec" => { | 4833 | "vec" => { |
| 4716 | let body = mac.tokens.to_string(); | 4834 | let body = mac.tokens.to_string(); |
| 4717 | if body.trim().is_empty() { | 4835 | if body.trim().is_empty() { |
modified
src/prelude.nim +69 -0 | @@ -57,6 +57,75 @@ proc unwrap*[T, E](r: Result[T, E]): T = | ||
| 57 | 57 | if not r.ok: rsPanic("called `Result::unwrap()` on an `Err` value") |
| 58 | 58 | r.val |
| 59 | 59 | |
| 60 | +# --------------------------------------------------------------------------- | |
| 61 | +# The `log` facade. | |
| 62 | +# | |
| 63 | +# `log`'s value to its dependents is 20 `macro_rules!`, which cannot be | |
| 64 | +# lowered, so the macros are lowered directly against the facade's documented | |
| 65 | +# behaviour -- see `src/macros.rs` for the same argument made about | |
| 66 | +# `bitflags!`. Verified against log 0.4.34 by `tests/cases/035-log.rs`. | |
| 67 | +# | |
| 68 | +# With no logger installed, every macro is a no-op and `log_enabled!` is false | |
| 69 | +# even after `set_max_level`, because the facade also consults the logger. A | |
| 70 | +# transpiled library therefore logs nothing by default, exactly as in Rust. | |
| 71 | +# `rsLogSetLogger` is the Nim-side analogue of `set_logger`. | |
| 72 | +# --------------------------------------------------------------------------- | |
| 73 | + | |
| 74 | +type | |
| 75 | + ## Named so that nothing a crate declares can collide: Rust's `log::Level` | |
| 76 | + ## never appears under that name in the output. | |
| 77 | + RsLogLevel* = enum | |
| 78 | + rsLvlError = 1, rsLvlWarn = 2, rsLvlInfo = 3, rsLvlDebug = 4, rsLvlTrace = 5 | |
| 79 | + RsLogFilter* = enum | |
| 80 | + rsFltOff = 0, rsFltError = 1, rsFltWarn = 2, rsFltInfo = 3, rsFltDebug = 4, | |
| 81 | + rsFltTrace = 5 | |
| 82 | + | |
| 83 | +var rsLogMaxLevel*: int = 0 ## `LevelFilter::Off`, as in log. | |
| 84 | +var rsLogSink*: proc (level: RsLogLevel, target, msg: string) {.closure.} = nil | |
| 85 | + | |
| 86 | +proc rsLogSetLogger*(f: proc (level: RsLogLevel, target, msg: string) {.closure.}) = | |
| 87 | + rsLogSink = f | |
| 88 | + | |
| 89 | +proc rsLogEnabled*(level: RsLogLevel): bool = | |
| 90 | + rsLogSink != nil and int(level) <= rsLogMaxLevel | |
| 91 | + | |
| 92 | +proc rsLog*(level: RsLogLevel, target, msg: string) = | |
| 93 | + if rsLogEnabled(level): rsLogSink(level, target, msg) | |
| 94 | + | |
| 95 | +proc rsDisplay*(x: RsLogLevel): string = | |
| 96 | + case x | |
| 97 | + of rsLvlError: "ERROR" | |
| 98 | + of rsLvlWarn: "WARN" | |
| 99 | + of rsLvlInfo: "INFO" | |
| 100 | + of rsLvlDebug: "DEBUG" | |
| 101 | + of rsLvlTrace: "TRACE" | |
| 102 | + | |
| 103 | +proc rsDebug*(x: RsLogLevel): string = | |
| 104 | + case x | |
| 105 | + of rsLvlError: "Error" | |
| 106 | + of rsLvlWarn: "Warn" | |
| 107 | + of rsLvlInfo: "Info" | |
| 108 | + of rsLvlDebug: "Debug" | |
| 109 | + of rsLvlTrace: "Trace" | |
| 110 | + | |
| 111 | +proc rsDisplay*(x: RsLogFilter): string = | |
| 112 | + case x | |
| 113 | + of rsFltOff: "OFF" | |
| 114 | + of rsFltError: "ERROR" | |
| 115 | + of rsFltWarn: "WARN" | |
| 116 | + of rsFltInfo: "INFO" | |
| 117 | + of rsFltDebug: "DEBUG" | |
| 118 | + of rsFltTrace: "TRACE" | |
| 119 | + | |
| 120 | +proc rsDebug*(x: RsLogFilter): string = | |
| 121 | + case x | |
| 122 | + of rsFltOff: "Off" | |
| 123 | + of rsFltError: "Error" | |
| 124 | + of rsFltWarn: "Warn" | |
| 125 | + of rsFltInfo: "Info" | |
| 126 | + of rsFltDebug: "Debug" | |
| 127 | + of rsFltTrace: "Trace" | |
| 128 | + | |
| 60 | 129 | # --------------------------------------------------------------------------- |
| 61 | 130 | # Rust's explicit overflow policies. |
| 62 | 131 | # |
| @@ -57,6 +57,75 @@ proc unwrap*[T, E](r: Result[T, E]): T = | |||
| 57 | if not r.ok: rsPanic("called `Result::unwrap()` on an `Err` value") | 57 | if not r.ok: rsPanic("called `Result::unwrap()` on an `Err` value") |
| 58 | r.val | 58 | r.val |
| 59 | 59 | ||
| 60 | +# --------------------------------------------------------------------------- | ||
| 61 | +# The `log` facade. | ||
| 62 | +# | ||
| 63 | +# `log`'s value to its dependents is 20 `macro_rules!`, which cannot be | ||
| 64 | +# lowered, so the macros are lowered directly against the facade's documented | ||
| 65 | +# behaviour -- see `src/macros.rs` for the same argument made about | ||
| 66 | +# `bitflags!`. Verified against log 0.4.34 by `tests/cases/035-log.rs`. | ||
| 67 | +# | ||
| 68 | +# With no logger installed, every macro is a no-op and `log_enabled!` is false | ||
| 69 | +# even after `set_max_level`, because the facade also consults the logger. A | ||
| 70 | +# transpiled library therefore logs nothing by default, exactly as in Rust. | ||
| 71 | +# `rsLogSetLogger` is the Nim-side analogue of `set_logger`. | ||
| 72 | +# --------------------------------------------------------------------------- | ||
| 73 | + | ||
| 74 | +type | ||
| 75 | + ## Named so that nothing a crate declares can collide: Rust's `log::Level` | ||
| 76 | + ## never appears under that name in the output. | ||
| 77 | + RsLogLevel* = enum | ||
| 78 | + rsLvlError = 1, rsLvlWarn = 2, rsLvlInfo = 3, rsLvlDebug = 4, rsLvlTrace = 5 | ||
| 79 | + RsLogFilter* = enum | ||
| 80 | + rsFltOff = 0, rsFltError = 1, rsFltWarn = 2, rsFltInfo = 3, rsFltDebug = 4, | ||
| 81 | + rsFltTrace = 5 | ||
| 82 | + | ||
| 83 | +var rsLogMaxLevel*: int = 0 ## `LevelFilter::Off`, as in log. | ||
| 84 | +var rsLogSink*: proc (level: RsLogLevel, target, msg: string) {.closure.} = nil | ||
| 85 | + | ||
| 86 | +proc rsLogSetLogger*(f: proc (level: RsLogLevel, target, msg: string) {.closure.}) = | ||
| 87 | + rsLogSink = f | ||
| 88 | + | ||
| 89 | +proc rsLogEnabled*(level: RsLogLevel): bool = | ||
| 90 | + rsLogSink != nil and int(level) <= rsLogMaxLevel | ||
| 91 | + | ||
| 92 | +proc rsLog*(level: RsLogLevel, target, msg: string) = | ||
| 93 | + if rsLogEnabled(level): rsLogSink(level, target, msg) | ||
| 94 | + | ||
| 95 | +proc rsDisplay*(x: RsLogLevel): string = | ||
| 96 | + case x | ||
| 97 | + of rsLvlError: "ERROR" | ||
| 98 | + of rsLvlWarn: "WARN" | ||
| 99 | + of rsLvlInfo: "INFO" | ||
| 100 | + of rsLvlDebug: "DEBUG" | ||
| 101 | + of rsLvlTrace: "TRACE" | ||
| 102 | + | ||
| 103 | +proc rsDebug*(x: RsLogLevel): string = | ||
| 104 | + case x | ||
| 105 | + of rsLvlError: "Error" | ||
| 106 | + of rsLvlWarn: "Warn" | ||
| 107 | + of rsLvlInfo: "Info" | ||
| 108 | + of rsLvlDebug: "Debug" | ||
| 109 | + of rsLvlTrace: "Trace" | ||
| 110 | + | ||
| 111 | +proc rsDisplay*(x: RsLogFilter): string = | ||
| 112 | + case x | ||
| 113 | + of rsFltOff: "OFF" | ||
| 114 | + of rsFltError: "ERROR" | ||
| 115 | + of rsFltWarn: "WARN" | ||
| 116 | + of rsFltInfo: "INFO" | ||
| 117 | + of rsFltDebug: "DEBUG" | ||
| 118 | + of rsFltTrace: "TRACE" | ||
| 119 | + | ||
| 120 | +proc rsDebug*(x: RsLogFilter): string = | ||
| 121 | + case x | ||
| 122 | + of rsFltOff: "Off" | ||
| 123 | + of rsFltError: "Error" | ||
| 124 | + of rsFltWarn: "Warn" | ||
| 125 | + of rsFltInfo: "Info" | ||
| 126 | + of rsFltDebug: "Debug" | ||
| 127 | + of rsFltTrace: "Trace" | ||
| 128 | + | ||
| 60 | # --------------------------------------------------------------------------- | 129 | # --------------------------------------------------------------------------- |
| 61 | # Rust's explicit overflow policies. | 130 | # Rust's explicit overflow policies. |
| 62 | # | 131 | # |
modified
src/ty.rs +3 -0 | @@ -181,6 +181,9 @@ pub fn map(t: &Type) -> Result<Nim, String> { | ||
| 181 | 181 | }; |
| 182 | 182 | |
| 183 | 183 | match (name.as_str(), args.len()) { |
| 184 | + // The `log` facade's types, under shim names. | |
| 185 | + ("Level", 0) => Ok(Nim::Prim("RsLogLevel".into())), | |
| 186 | + ("LevelFilter", 0) => Ok(Nim::Prim("RsLogFilter".into())), | |
| 184 | 187 | ("Vec", 1) => Ok(Nim::Seq(Box::new(args[0].clone().owned()))), |
| 185 | 188 | ("Option", 1) => Ok(Nim::Named("Option".into(), args)), |
| 186 | 189 | ("Result", 2) => Ok(Nim::Named("Result".into(), args)), |
| @@ -181,6 +181,9 @@ pub fn map(t: &Type) -> Result<Nim, String> { | |||
| 181 | }; | 181 | }; |
| 182 | 182 | ||
| 183 | match (name.as_str(), args.len()) { | 183 | match (name.as_str(), args.len()) { |
| 184 | + // The `log` facade's types, under shim names. | ||
| 185 | + ("Level", 0) => Ok(Nim::Prim("RsLogLevel".into())), | ||
| 186 | + ("LevelFilter", 0) => Ok(Nim::Prim("RsLogFilter".into())), | ||
| 184 | ("Vec", 1) => Ok(Nim::Seq(Box::new(args[0].clone().owned()))), | 187 | ("Vec", 1) => Ok(Nim::Seq(Box::new(args[0].clone().owned()))), |
| 185 | ("Option", 1) => Ok(Nim::Named("Option".into(), args)), | 188 | ("Option", 1) => Ok(Nim::Named("Option".into(), args)), |
| 186 | ("Result", 2) => Ok(Nim::Named("Result".into(), args)), | 189 | ("Result", 2) => Ok(Nim::Named("Result".into(), args)), |
added
tests/cases/035-log.rs +28 -0 | new file mode 100644 | ||
| @@ -0,0 +1,28 @@ | ||
| 1 | +//@ extern: log | |
| 2 | +// `log`'s value to its 61 dependents in libcosmic's tree is 20 | |
| 3 | +// `macro_rules!`, so the macros are lowered directly against the facade's | |
| 4 | +// documented behaviour, as `bitflags!` is. The oracle links the real crate; | |
| 5 | +// rustnim does not get it and has to reproduce it. | |
| 6 | +// | |
| 7 | +// The behaviour worth pinning: with no logger installed every macro is a | |
| 8 | +// no-op and `log_enabled!` stays false even after `set_max_level`, because | |
| 9 | +// the facade consults the logger as well as the level. `Display` for a | |
| 10 | +// level is upper-case where `Debug` is not. | |
| 11 | +use log::{debug, error, info, log, log_enabled, trace, warn, Level, LevelFilter}; | |
| 12 | +fn main() { | |
| 13 | + println!("default max_level {:?}", log::max_level()); | |
| 14 | + println!("enabled {} {}", log_enabled!(Level::Error), log_enabled!(Level::Trace)); | |
| 15 | + // With no logger installed these emit nothing at all. | |
| 16 | + error!("e"); warn!("w"); info!("i {}", 1); debug!("d"); trace!("t"); | |
| 17 | + log!(Level::Info, "explicit {}", 2); | |
| 18 | + | |
| 19 | + log::set_max_level(LevelFilter::Info); | |
| 20 | + println!("after set {:?}", log::max_level()); | |
| 21 | + println!("enabled {} {} {}", log_enabled!(Level::Error), log_enabled!(Level::Info), log_enabled!(Level::Debug)); | |
| 22 | + info!("still nothing, no logger"); | |
| 23 | + | |
| 24 | + println!("levels {} {} {} {} {}", Level::Error as usize, Level::Warn as usize, Level::Info as usize, Level::Debug as usize, Level::Trace as usize); | |
| 25 | + println!("filters {} {}", LevelFilter::Off as usize, LevelFilter::Trace as usize); | |
| 26 | + println!("ord {} {}", Level::Error < Level::Warn, Level::Trace > Level::Info); | |
| 27 | + println!("disp {} {:?}", Level::Warn, Level::Warn); | |
| 28 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | +//@ extern: log | ||
| 2 | +// `log`'s value to its 61 dependents in libcosmic's tree is 20 | ||
| 3 | +// `macro_rules!`, so the macros are lowered directly against the facade's | ||
| 4 | +// documented behaviour, as `bitflags!` is. The oracle links the real crate; | ||
| 5 | +// rustnim does not get it and has to reproduce it. | ||
| 6 | +// | ||
| 7 | +// The behaviour worth pinning: with no logger installed every macro is a | ||
| 8 | +// no-op and `log_enabled!` stays false even after `set_max_level`, because | ||
| 9 | +// the facade consults the logger as well as the level. `Display` for a | ||
| 10 | +// level is upper-case where `Debug` is not. | ||
| 11 | +use log::{debug, error, info, log, log_enabled, trace, warn, Level, LevelFilter}; | ||
| 12 | +fn main() { | ||
| 13 | + println!("default max_level {:?}", log::max_level()); | ||
| 14 | + println!("enabled {} {}", log_enabled!(Level::Error), log_enabled!(Level::Trace)); | ||
| 15 | + // With no logger installed these emit nothing at all. | ||
| 16 | + error!("e"); warn!("w"); info!("i {}", 1); debug!("d"); trace!("t"); | ||
| 17 | + log!(Level::Info, "explicit {}", 2); | ||
| 18 | + | ||
| 19 | + log::set_max_level(LevelFilter::Info); | ||
| 20 | + println!("after set {:?}", log::max_level()); | ||
| 21 | + println!("enabled {} {} {}", log_enabled!(Level::Error), log_enabled!(Level::Info), log_enabled!(Level::Debug)); | ||
| 22 | + info!("still nothing, no logger"); | ||
| 23 | + | ||
| 24 | + println!("levels {} {} {} {} {}", Level::Error as usize, Level::Warn as usize, Level::Info as usize, Level::Debug as usize, Level::Trace as usize); | ||
| 25 | + println!("filters {} {}", LevelFilter::Off as usize, LevelFilter::Trace as usize); | ||
| 26 | + println!("ord {} {}", Level::Error < Level::Warn, Level::Trace > Level::Info); | ||
| 27 | + println!("disp {} {:?}", Level::Warn, Level::Warn); | ||
| 28 | +} | ||