nandi/rustnimpublic Fork 0
12c0a01
Commits
Clone
git clone https://git.rickub.com/nandi/rustnim.git
git clone ssh://git@rickub.com/nandi/rustnim.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

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>
nandithebull committed 2026-09-18T22:50:26-07:00 Browse files
12c0a01 parent: af6e50f
modified Cargo.lock +7 -0
@@ -8,6 +8,12 @@ version = "2.13.2"
88 source = "registry+https://github.com/rust-lang/crates.io-index"
99 checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
1010
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+
1117 [[package]]
1218 name = "proc-macro2"
1319 version = "1.0.107"
@@ -31,6 +37,7 @@ name = "rustnim"
3137 version = "0.1.0"
3238 dependencies = [
3339 "bitflags",
40+ "log",
3441 "syn",
3542 ]
3643
@@ -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"] }
1111 # checked against it rather than against our reading of its documentation.
1212 [dev-dependencies]
1313 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.
321321 own `type Result<T>` is told apart from the builtin `Result<T, E>` by
322322 arity, which is not how Rust resolves it.
323323 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
325326 against the machine, since the generated Nim is compiled for it. That makes
326327 the output host-shaped: a crate branching on platform has had that branch
327328 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
516517 it, which is why it links the real crate rather than a copy of its
517518 documentation.
518519
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+
519562 ## Proof of byte-identity for `base16ct`
520563
521564 [`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>` by321 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 evaluated324+ `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 makes326 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 branch327 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 or328 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 its517 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: exhaustive564 [`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.
7777 `cargo test --test proof` runs it — 151,463 cases, 9.1 MB of output, compared
7878 byte for byte.
7979
80-## `bitflags!`
80+## `bitflags!` and `log`
8181
8282 `bitflags` is the most-depended-on translatable crate in libcosmic's tree (79
8383 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
8686 links bitflags for the *oracle only*, and rustnim has to reproduce its
8787 behaviour without it, byte for byte.
8888
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+
8994 ## Does it generalise?
9095
9196 `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, compared77 `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 (7982 `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 not83 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 its86 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 {
159159 #[derive(Clone)]
160160 struct Variant {
161161 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>,
162165 /// `(nim field name, type)`. Empty for a unit variant. Tuple variants get
163166 /// `f0`, `f1`, ...; every field is prefixed with the variant name because
164167 /// Nim requires the branches of a variant object to have distinct fields.
@@ -595,13 +598,10 @@ impl Lowerer {
595598 let mut variants = Vec::new();
596599 for v in &e.variants {
597600 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+ };
605605 let mut fields = Vec::new();
606606 for (i, f) in v.fields.iter().enumerate() {
607607 // Nim requires the branches of a variant object to have
@@ -614,7 +614,7 @@ impl Lowerer {
614614 let t = if returns_borrow(&f.ty) { t.unvar() } else { t.owned() };
615615 fields.push((fname, t));
616616 }
617- variants.push(Variant { name: vname, fields });
617+ variants.push(Variant { name: vname, discriminant, fields });
618618 }
619619 let simple = variants.iter().all(|v| v.fields.is_empty());
620620 for v in &variants {
@@ -810,6 +810,18 @@ impl Lowerer {
810810 };
811811 Ok(s.value() == (usize::BITS).to_string())
812812 }
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+ }
813825 syn::Meta::NameValue(nv) if nv.path.is_ident("target_endian") => {
814826 let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else {
815827 return Err("`target_endian = ..` expects a string".into());
@@ -1044,6 +1056,23 @@ impl Lowerer {
10441056 }
10451057 }
10461058
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+
10471076 /// `[T, U]`, or empty.
10481077 fn gen_list(params: &[String]) -> String {
10491078 if params.is_empty() {
@@ -1634,7 +1663,10 @@ impl Lowerer {
16341663 self.line(&format!("type {name}* = enum"));
16351664 self.indent += 1;
16361665 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+ }
16381670 }
16391671 self.indent -= 1;
16401672 self.blank();
@@ -3061,6 +3093,24 @@ impl Lowerer {
30613093 if name == "None" {
30623094 return Ok(Val::new(self.none_of(expect), expect.cloned()));
30633095 }
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+
30643114 // `Perms::READ`: a constant of a `bitflags!` type.
30653115 if let Some(q) = p.path.segments.iter().rev().nth(1).map(|s| s.ident.to_string()) {
30663116 let q = if q == "Self" {
@@ -3643,6 +3693,18 @@ impl Lowerer {
36433693 (f, Nim::Prim(p)) if f.is_integer() && (p == "float64" || p == "float32") => {
36443694 format!("{}({})", p, v.code)
36453695 }
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+ }
36463708 (Nim::Prim(b), t) if b == "bool" && t.is_integer() => {
36473709 format!("{}(ord({}))", t.render(), v.code)
36483710 }
@@ -4055,6 +4117,20 @@ impl Lowerer {
40554117 }
40564118 }
40574119
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+
40584134 // `Spacing::from(d)`: a `From` impl called through its target type.
40594135 // Rust picks the impl by the argument's type, and so do we -- Nim
40604136 // cannot overload on return type, so each impl has its own proc name.
@@ -4712,6 +4788,48 @@ impl Lowerer {
47124788 a.code, op, b.code, fmt::nim_str(label), a.code, b.code
47134789 ))
47144790 }
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+ }
47154833 "vec" => {
47164834 let body = mac.tokens.to_string();
47174835 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 get165 /// `(nim field name, type)`. Empty for a unit variant. Tuple variants get
163 /// `f0`, `f1`, ...; every field is prefixed with the variant name because166 /// `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 have607 // 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 -- Nim4135 // 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.code4788 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 =
5757 if not r.ok: rsPanic("called `Result::unwrap()` on an `Err` value")
5858 r.val
5959
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+
60129 # ---------------------------------------------------------------------------
61130 # Rust's explicit overflow policies.
62131 #
@@ -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.val58 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> {
181181 };
182182
183183 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())),
184187 ("Vec", 1) => Ok(Nim::Seq(Box::new(args[0].clone().owned()))),
185188 ("Option", 1) => Ok(Nim::Named("Option".into(), args)),
186189 ("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+}