nandi/rustnimpublic Fork 0
0e6c394
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.

Evaluate host cfg predicates, and measure what that actually buys

libcosmic prompted a survey: 400 registry crates under 4,000 lines, all
module files passed together, first blocker recorded. The top blocker was
not generics but unevaluable #[cfg] -- 124 of 400 -- and most of those are
host facts: unix, windows, target_os, target_arch, target_family, plus
doctest and miri. Those are determined by the machine the Nim is compiled
for, so they are now evaluated rather than rejected, on the same reasoning
as target_pointer_width.

The result is the interesting part. It cleared 90 of the 124, and moved the
number of crates that fully transpile from 2 to 2. Every crate it unblocked
hit its next blocker instead. Blockers are deep, not wide, and a frequency
ranking of first blockers is not a roadmap -- it says which feature is most
often first, not which one finishes a crate. base16ct and adler2 work
because their whole stack was ground through.

DESIGN.md records the table and the libcosmic numbers: 0 of 164 files
produce any translation, against 66 generic-parameter blockers, 20 trait
objects, 51 async uses and 1,104 lifetime annotations. It is a north star,
not a next step.

The multifile test that asserted an unevaluable cfg is reported had used
target_os as its example, which is now evaluable; it uses a build-script cfg
instead, and a new test pins the host facts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-18T21:39:43-07:00 Browse files
0e6c394 parent: 0777223
modified DESIGN.md +54 -4
@@ -291,10 +291,13 @@ runner) rather than a wrong answer.
291291 modules declaring the same type name would collide. Relatedly, a crate's
292292 own `type Result<T>` is told apart from the builtin `Result<T, E>` by
293293 arity, which is not how Rust resolves it.
294-10. `#[cfg(target_pointer_width)]` and `#[cfg(target_endian)]` are evaluated
295- against the *host*, since the generated Nim is compiled for it. That makes
296- the output host-shaped: a crate branching on pointer width has had that
297- branch decided at transpile time.
294+10. Host `#[cfg]` predicates — `unix`, `windows`, `target_os`, `target_arch`,
295+ `target_family`, `target_pointer_width`, `target_endian` — are evaluated
296+ against the machine, since the generated Nim is compiled for it. That makes
297+ the output host-shaped: a crate branching on platform has had that branch
298+ decided at transpile time. `doc`/`doctest`/`miri` are false. A custom or
299+ build-script `cfg` (`crossbeam_loom`, `target_has_atomic`) has no value we
300+ could know and is rejected.
298301 11. Associated types (`impl Iterator { type Item = .. }`) and `mod`
299302 directories (`specialized/mod.rs`) are not implemented.
300303 12. `String::from_utf8_unchecked` copies, because Nim's `string` is an owned
@@ -342,6 +345,53 @@ rather than a gap: they are told they cannot be translated instead of being
342345 handed a silently truncated hasher. The other two are honest missing
343346 features — associated types, and `mod` directories.
344347
348+## How far off is a crate like `libcosmic`?
349+
350+Measured, not guessed. Running rustnim over `libcosmic`'s own `src/`:
351+
352+```
353+0 of 164 files produce any translation
354+50,633 lines, 112 direct dependencies
355+```
356+
357+with 66 generic-parameter blockers, 20 trait objects, 51 `async` uses, 235
358+`where` clauses, 73 associated types and 1,104 lifetime annotations. Those are
359+not features the crate happens to use; they are its architecture. `libcosmic`
360+is a north star, not a next step.
361+
362+### The blocker survey, and what it says about roadmaps
363+
364+400 crates from the local registry (under 4,000 lines each), all their module
365+files passed together, first blocker recorded:
366+
367+| blocker | before | after host-`cfg` |
368+|---|---|---|
369+| unevaluable `#[cfg]` | 124 | 34 |
370+| generic type parameter | 69 | 91 |
371+| unsupported item in an `impl` (associated types) | 54 | 63 |
372+| trait object | 17 | 25 |
373+| macro definition | 21 | 24 |
374+| raw pointer | 12 | 22 |
375+| **crates fully transpiled** | **2** | **2** |
376+
377+Evaluating the host `#[cfg]` predicates cleared 90 of the 124 top blockers
378+**and moved the fully-working count by zero**. Every crate it unblocked
379+simply hit its next blocker. That is the shape of the problem: blockers are
380+*deep*, not wide. `base16ct` and `adler2` work because their whole stack was
381+ground through, not because any single feature was added.
382+
383+So the ranking above is not a roadmap — it says which feature is most often
384+*first*, which is not the same as which feature finishes a crate. The only
385+honest way to add a crate is to pick it and clear its stack, as was done
386+twice.
387+
388+The next rung, if one is wanted, is **generics**: it is now the top blocker,
389+it is what stops even `cosmic-theme`'s 4,234 lines of colour data, and Nim has
390+native generics, so `fn f<T>(x: T) -> T` has a real target in
391+`proc f[T](x: T): T` rather than needing monomorphisation. Associated types
392+(`type Item = ..` inside an `impl`) are the next after that and are mostly a
393+matter of recording a type binding.
394+
345395 ## Proof of byte-identity for `base16ct`
346396
347397 [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive
@@ -291,10 +291,13 @@ runner) rather than a wrong answer.
291 modules declaring the same type name would collide. Relatedly, a crate's291 modules declaring the same type name would collide. Relatedly, a crate's
292 own `type Result<T>` is told apart from the builtin `Result<T, E>` by292 own `type Result<T>` is told apart from the builtin `Result<T, E>` by
293 arity, which is not how Rust resolves it.293 arity, which is not how Rust resolves it.
294-10. `#[cfg(target_pointer_width)]` and `#[cfg(target_endian)]` are evaluated294+10. Host `#[cfg]` predicates — `unix`, `windows`, `target_os`, `target_arch`,
295- against the *host*, since the generated Nim is compiled for it. That makes295+ `target_family`, `target_pointer_width`, `target_endian` — are evaluated
296- the output host-shaped: a crate branching on pointer width has had that296+ against the machine, since the generated Nim is compiled for it. That makes
297- branch decided at transpile time.297+ the output host-shaped: a crate branching on platform has had that branch
298+ decided at transpile time. `doc`/`doctest`/`miri` are false. A custom or
299+ build-script `cfg` (`crossbeam_loom`, `target_has_atomic`) has no value we
300+ could know and is rejected.
298 11. Associated types (`impl Iterator { type Item = .. }`) and `mod`301 11. Associated types (`impl Iterator { type Item = .. }`) and `mod`
299 directories (`specialized/mod.rs`) are not implemented.302 directories (`specialized/mod.rs`) are not implemented.
300 12. `String::from_utf8_unchecked` copies, because Nim's `string` is an owned303 12. `String::from_utf8_unchecked` copies, because Nim's `string` is an owned
@@ -342,6 +345,53 @@ rather than a gap: they are told they cannot be translated instead of being
342 handed a silently truncated hasher. The other two are honest missing345 handed a silently truncated hasher. The other two are honest missing
343 features — associated types, and `mod` directories.346 features — associated types, and `mod` directories.
344 347
348+## How far off is a crate like `libcosmic`?
349+
350+Measured, not guessed. Running rustnim over `libcosmic`'s own `src/`:
351+
352+```
353+0 of 164 files produce any translation
354+50,633 lines, 112 direct dependencies
355+```
356+
357+with 66 generic-parameter blockers, 20 trait objects, 51 `async` uses, 235
358+`where` clauses, 73 associated types and 1,104 lifetime annotations. Those are
359+not features the crate happens to use; they are its architecture. `libcosmic`
360+is a north star, not a next step.
361+
362+### The blocker survey, and what it says about roadmaps
363+
364+400 crates from the local registry (under 4,000 lines each), all their module
365+files passed together, first blocker recorded:
366+
367+| blocker | before | after host-`cfg` |
368+|---|---|---|
369+| unevaluable `#[cfg]` | 124 | 34 |
370+| generic type parameter | 69 | 91 |
371+| unsupported item in an `impl` (associated types) | 54 | 63 |
372+| trait object | 17 | 25 |
373+| macro definition | 21 | 24 |
374+| raw pointer | 12 | 22 |
375+| **crates fully transpiled** | **2** | **2** |
376+
377+Evaluating the host `#[cfg]` predicates cleared 90 of the 124 top blockers
378+**and moved the fully-working count by zero**. Every crate it unblocked
379+simply hit its next blocker. That is the shape of the problem: blockers are
380+*deep*, not wide. `base16ct` and `adler2` work because their whole stack was
381+ground through, not because any single feature was added.
382+
383+So the ranking above is not a roadmap — it says which feature is most often
384+*first*, which is not the same as which feature finishes a crate. The only
385+honest way to add a crate is to pick it and clear its stack, as was done
386+twice.
387+
388+The next rung, if one is wanted, is **generics**: it is now the top blocker,
389+it is what stops even `cosmic-theme`'s 4,234 lines of colour data, and Nim has
390+native generics, so `fn f<T>(x: T) -> T` has a real target in
391+`proc f[T](x: T): T` rather than needing monomorphisation. Associated types
392+(`type Item = ..` inside an `impl`) are the next after that and are mostly a
393+matter of recording a type binding.
394+
345 ## Proof of byte-identity for `base16ct`395 ## Proof of byte-identity for `base16ct`
346 396
347 [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive397 [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive
modified src/lower.rs +30 -2
@@ -706,6 +706,30 @@ impl Lowerer {
706706 syn::Meta::Path(p) if p.is_ident("test") => Ok(false),
707707 syn::Meta::Path(p) if p.is_ident("debug_assertions") => Ok(true),
708708 syn::Meta::Path(p) if p.is_ident("docsrs") || p.is_ident("doc") => Ok(false),
709+ syn::Meta::Path(p) if p.is_ident("doctest") || p.is_ident("miri") => Ok(false),
710+ // Host facts. The generated Nim is compiled for this machine, so
711+ // these are known rather than chosen. See DESIGN.md item 10: it
712+ // does make the output host-shaped.
713+ syn::Meta::Path(p) if p.is_ident("unix") => Ok(cfg!(unix)),
714+ syn::Meta::Path(p) if p.is_ident("windows") => Ok(cfg!(windows)),
715+ syn::Meta::NameValue(nv)
716+ if nv.path.is_ident("target_os")
717+ || nv.path.is_ident("target_arch")
718+ || nv.path.is_ident("target_family")
719+ || nv.path.is_ident("target_vendor") =>
720+ {
721+ let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else {
722+ return Err("this `cfg` key expects a string".into());
723+ };
724+ let key = nv.path.get_ident().map(|i| i.to_string()).unwrap_or_default();
725+ Ok(s.value()
726+ == match key.as_str() {
727+ "target_os" => std::env::consts::OS,
728+ "target_arch" => std::env::consts::ARCH,
729+ "target_family" => std::env::consts::FAMILY,
730+ _ => "unknown",
731+ })
732+ }
709733 // The generated Nim is compiled for the same machine, so the
710734 // target's word size and endianness are known rather than
711735 // guessed. This does mean the output is host-shaped: a crate that
@@ -745,8 +769,12 @@ impl Lowerer {
745769 Ok(acc)
746770 }
747771 other => Err(format!(
748- "`#[cfg({})]` is not a predicate rustnim can evaluate; only \
749- `feature = \"..\"`, `not`, `all` and `any` are implemented",
772+ "`#[cfg({})]` is not a predicate rustnim can evaluate. \
773+ Features (`--cfg feature=..`), host facts (`unix`, `windows`, \
774+ `target_os`, `target_arch`, `target_family`, \
775+ `target_pointer_width`, `target_endian`), `doc`/`doctest`/\
776+ `miri`, and `not`/`all`/`any` over those are. A custom or \
777+ build-script `cfg` has no value we could know",
750778 quote_meta(other)
751779 )),
752780 }
@@ -706,6 +706,30 @@ impl Lowerer {
706 syn::Meta::Path(p) if p.is_ident("test") => Ok(false),706 syn::Meta::Path(p) if p.is_ident("test") => Ok(false),
707 syn::Meta::Path(p) if p.is_ident("debug_assertions") => Ok(true),707 syn::Meta::Path(p) if p.is_ident("debug_assertions") => Ok(true),
708 syn::Meta::Path(p) if p.is_ident("docsrs") || p.is_ident("doc") => Ok(false),708 syn::Meta::Path(p) if p.is_ident("docsrs") || p.is_ident("doc") => Ok(false),
709+ syn::Meta::Path(p) if p.is_ident("doctest") || p.is_ident("miri") => Ok(false),
710+ // Host facts. The generated Nim is compiled for this machine, so
711+ // these are known rather than chosen. See DESIGN.md item 10: it
712+ // does make the output host-shaped.
713+ syn::Meta::Path(p) if p.is_ident("unix") => Ok(cfg!(unix)),
714+ syn::Meta::Path(p) if p.is_ident("windows") => Ok(cfg!(windows)),
715+ syn::Meta::NameValue(nv)
716+ if nv.path.is_ident("target_os")
717+ || nv.path.is_ident("target_arch")
718+ || nv.path.is_ident("target_family")
719+ || nv.path.is_ident("target_vendor") =>
720+ {
721+ let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else {
722+ return Err("this `cfg` key expects a string".into());
723+ };
724+ let key = nv.path.get_ident().map(|i| i.to_string()).unwrap_or_default();
725+ Ok(s.value()
726+ == match key.as_str() {
727+ "target_os" => std::env::consts::OS,
728+ "target_arch" => std::env::consts::ARCH,
729+ "target_family" => std::env::consts::FAMILY,
730+ _ => "unknown",
731+ })
732+ }
709 // The generated Nim is compiled for the same machine, so the733 // The generated Nim is compiled for the same machine, so the
710 // target's word size and endianness are known rather than734 // target's word size and endianness are known rather than
711 // guessed. This does mean the output is host-shaped: a crate that735 // guessed. This does mean the output is host-shaped: a crate that
@@ -745,8 +769,12 @@ impl Lowerer {
745 Ok(acc)769 Ok(acc)
746 }770 }
747 other => Err(format!(771 other => Err(format!(
748- "`#[cfg({})]` is not a predicate rustnim can evaluate; only \772+ "`#[cfg({})]` is not a predicate rustnim can evaluate. \
749- `feature = \"..\"`, `not`, `all` and `any` are implemented",773+ Features (`--cfg feature=..`), host facts (`unix`, `windows`, \
774+ `target_os`, `target_arch`, `target_family`, \
775+ `target_pointer_width`, `target_endian`), `doc`/`doctest`/\
776+ `miri`, and `not`/`all`/`any` over those are. A custom or \
777+ build-script `cfg` has no value we could know",
750 quote_meta(other)778 quote_meta(other)
751 )),779 )),
752 }780 }
modified tests/multifile.rs +29 -1
@@ -112,10 +112,38 @@ fn an_unevaluable_cfg_predicate_is_reported_not_assumed() {
112112 let f = d.join("d.rs");
113113 fs::write(
114114 &f,
115- "#[cfg(target_os = \"linux\")]\nfn only_linux() -> i32 { 1 }\nfn main() {}\n",
115+ // A build-script `cfg`: nothing about the host determines it, so
116+ // there is no value we could know.
117+ "#[cfg(crossbeam_loom)]\nfn under_loom() -> i32 { 1 }\nfn main() {}\n",
116118 )
117119 .unwrap();
118120 let (ok, _, err) = run(&d, &[f.to_str().unwrap()]);
119121 assert!(!ok, "an unevaluable cfg must not be silently resolved");
120122 assert!(err.contains("not a predicate rustnim can evaluate"), "unexpected: {err}");
121123 }
124+
125+#[test]
126+fn host_facts_are_evaluated() {
127+ // These are determined by the machine the generated Nim is compiled for,
128+ // so they are known rather than chosen.
129+ let d = work("cfg-host");
130+ let f = d.join("h.rs");
131+ fs::write(
132+ &f,
133+ "#[cfg(unix)]\nfn on_unix() -> i32 { 1 }\n\
134+ #[cfg(windows)]\nfn on_windows() -> i32 { 2 }\n\
135+ #[cfg(doctest)]\nfn in_doctest() -> i32 { 3 }\n\
136+ fn main() {}\n",
137+ )
138+ .unwrap();
139+ let (ok, out, err) = run(&d, &[f.to_str().unwrap()]);
140+ assert!(ok, "{err}");
141+ assert!(!out.contains("in_doctest"), "doctest item was emitted:\n{out}");
142+ if cfg!(unix) {
143+ assert!(out.contains("proc on_unix"), "{out}");
144+ assert!(!out.contains("on_windows"), "{out}");
145+ } else {
146+ assert!(out.contains("proc on_windows"), "{out}");
147+ assert!(!out.contains("on_unix"), "{out}");
148+ }
149+}
@@ -112,10 +112,38 @@ fn an_unevaluable_cfg_predicate_is_reported_not_assumed() {
112 let f = d.join("d.rs");112 let f = d.join("d.rs");
113 fs::write(113 fs::write(
114 &f,114 &f,
115- "#[cfg(target_os = \"linux\")]\nfn only_linux() -> i32 { 1 }\nfn main() {}\n",115+ // A build-script `cfg`: nothing about the host determines it, so
116+ // there is no value we could know.
117+ "#[cfg(crossbeam_loom)]\nfn under_loom() -> i32 { 1 }\nfn main() {}\n",
116 )118 )
117 .unwrap();119 .unwrap();
118 let (ok, _, err) = run(&d, &[f.to_str().unwrap()]);120 let (ok, _, err) = run(&d, &[f.to_str().unwrap()]);
119 assert!(!ok, "an unevaluable cfg must not be silently resolved");121 assert!(!ok, "an unevaluable cfg must not be silently resolved");
120 assert!(err.contains("not a predicate rustnim can evaluate"), "unexpected: {err}");122 assert!(err.contains("not a predicate rustnim can evaluate"), "unexpected: {err}");
121 }123 }
124+
125+#[test]
126+fn host_facts_are_evaluated() {
127+ // These are determined by the machine the generated Nim is compiled for,
128+ // so they are known rather than chosen.
129+ let d = work("cfg-host");
130+ let f = d.join("h.rs");
131+ fs::write(
132+ &f,
133+ "#[cfg(unix)]\nfn on_unix() -> i32 { 1 }\n\
134+ #[cfg(windows)]\nfn on_windows() -> i32 { 2 }\n\
135+ #[cfg(doctest)]\nfn in_doctest() -> i32 { 3 }\n\
136+ fn main() {}\n",
137+ )
138+ .unwrap();
139+ let (ok, out, err) = run(&d, &[f.to_str().unwrap()]);
140+ assert!(ok, "{err}");
141+ assert!(!out.contains("in_doctest"), "doctest item was emitted:\n{out}");
142+ if cfg!(unix) {
143+ assert!(out.contains("proc on_unix"), "{out}");
144+ assert!(!out.contains("on_windows"), "{out}");
145+ } else {
146+ assert!(out.contains("proc on_windows"), "{out}");
147+ assert!(!out.contains("on_unix"), "{out}");
148+ }
149+}