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>
0e6c394 parent: 0777223 modified
DESIGN.md +54 -4 | @@ -291,10 +291,13 @@ runner) rather than a wrong answer. | ||
| 291 | 291 | modules declaring the same type name would collide. Relatedly, a crate's |
| 292 | 292 | own `type Result<T>` is told apart from the builtin `Result<T, E>` by |
| 293 | 293 | 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. | |
| 298 | 301 | 11. Associated types (`impl Iterator { type Item = .. }`) and `mod` |
| 299 | 302 | directories (`specialized/mod.rs`) are not implemented. |
| 300 | 303 | 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 | 345 | handed a silently truncated hasher. The other two are honest missing |
| 343 | 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 | 395 | ## Proof of byte-identity for `base16ct` |
| 346 | 396 | |
| 347 | 397 | [`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's | 291 | 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>` by | 292 | 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 evaluated | 294 | +10. Host `#[cfg]` predicates — `unix`, `windows`, `target_os`, `target_arch`, |
| 295 | - against the *host*, since the generated Nim is compiled for it. That makes | 295 | + `target_family`, `target_pointer_width`, `target_endian` — are evaluated |
| 296 | - the output host-shaped: a crate branching on pointer width has had that | 296 | + 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 owned | 303 | 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 missing | 345 | 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: exhaustive | 397 | [`PROOF.md`](PROOF.md) sets out what is actually established: exhaustive |
modified
src/lower.rs +30 -2 | @@ -706,6 +706,30 @@ impl Lowerer { | ||
| 706 | 706 | syn::Meta::Path(p) if p.is_ident("test") => Ok(false), |
| 707 | 707 | syn::Meta::Path(p) if p.is_ident("debug_assertions") => Ok(true), |
| 708 | 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 | 733 | // The generated Nim is compiled for the same machine, so the |
| 710 | 734 | // target's word size and endianness are known rather than |
| 711 | 735 | // guessed. This does mean the output is host-shaped: a crate that |
| @@ -745,8 +769,12 @@ impl Lowerer { | ||
| 745 | 769 | Ok(acc) |
| 746 | 770 | } |
| 747 | 771 | 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", | |
| 750 | 778 | quote_meta(other) |
| 751 | 779 | )), |
| 752 | 780 | } |
| @@ -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 the | 733 | // The generated Nim is compiled for the same machine, so the |
| 710 | // target's word size and endianness are known rather than | 734 | // target's word size and endianness are known rather than |
| 711 | // guessed. This does mean the output is host-shaped: a crate that | 735 | // 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() { | ||
| 112 | 112 | let f = d.join("d.rs"); |
| 113 | 113 | fs::write( |
| 114 | 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 | 119 | .unwrap(); |
| 118 | 120 | let (ok, _, err) = run(&d, &[f.to_str().unwrap()]); |
| 119 | 121 | assert!(!ok, "an unevaluable cfg must not be silently resolved"); |
| 120 | 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 | +} | |
| @@ -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 | +} | ||