nandi/rustnimpublic Fork
⑂ ba4c4ab
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.

Download ZIP

Measure libcosmic again: 0 of 164 files to 8, and the blockers changed kind

Re-ran the libcosmic measurement now that generics, associated types, impl
Trait in both positions, iterators, futures, closures, trait impls, macro
expansion and the concurrency primitives have landed.

Files that translate went from 0 of 164 to 8, with another 34 blocked only by
a `mod` declaration, which is an artifact of testing files one at a time.

The kind of blocker matters more than the count. Generics (66 first time),
trait objects (20) and async no longer appear at all. What replaced them is
the iced API: new (11), unique, spacing, Radians, .cosmic, .activate,
.scroller_width. That is the dependency closure rather than a feature list.

Two general things fell out. #[cfg(target_env)] joins the host predicates,
read from the triple this binary was built for. And `--cfg <name>` now
declares a custom flag as rustc's does: libcosmic's build script sets
wayland_platform and xdg_portal, and since we do not run build scripts the
caller declares them rather than us guessing. An undeclared flag is still
refused, which tests/multifile.rs pins in both directions.

This does not mean libcosmic is close, and DESIGN.md says so. Even if every
file lowered the result would not run, because libcosmic's behaviour is
iced's and iced is not transpiled -- at which point "files produce a
translation" measures nothing. The narrower honest reading is that the
language is no longer what stops it.

52 differential cases and 6 integration tests, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-19T07:52:00-07:00 Browse files
ba4c4ab parent: 1b93db2
modified DESIGN.md +29 -0
@@ -388,6 +388,35 @@ with 66 generic-parameter blockers, 20 trait objects, 51 `async` uses, 235
388388 not features the crate happens to use; they are its architecture. `libcosmic`
389389 is a north star, not a next step.
390390
391+### Measured again, after the language features
392+
393+Re-run once generics, associated types, `impl Trait` in both positions,
394+iterators, futures, closures, trait impls, macro expansion and the
395+concurrency primitives had landed:
396+
397+| | first attempt | now |
398+|---|---|---|
399+| files that translate | **0 of 164** | **8 of 164** |
400+| blocked only by `mod` (an artifact of testing files singly) | — | 34 |
401+| top blocker | generics (66) | calls into dependencies (36) |
402+
403+**The language blockers are essentially gone.** Generics, trait objects and
404+`async` no longer appear at all. What replaced them is the `iced` API:
405+`new` (11), `unique`, `spacing`, `Radians`, `.cosmic`, `.activate`,
406+`.scroller_width`. That is the dependency closure, not a feature list.
407+
408+Two smaller things fell out and are general: `#[cfg(target_env)]` joins the
409+host predicates, and `--cfg <name>` now declares a custom flag the way
410+rustc's does. `libcosmic`'s build script sets `wayland_platform` and
411+`xdg_portal`; we do not run build scripts, so the caller declares them rather
412+than us guessing. An undeclared flag is still refused.
413+
414+**This does not mean libcosmic is close.** Even if every file lowered, the
415+result would not run: libcosmic's behaviour *is* `iced`'s, and `iced` is not
416+transpiled. At that point "files produce a translation" would measure nothing
417+at all. The honest reading is narrower and still worth something — the
418+*language* is no longer what stops it.
419+
391420 ### The blocker survey, and what it says about roadmaps
392421
393422 400 crates from the local registry (under 4,000 lines each), all their module
@@ -388,6 +388,35 @@ with 66 generic-parameter blockers, 20 trait objects, 51 `async` uses, 235
388 not features the crate happens to use; they are its architecture. `libcosmic`388 not features the crate happens to use; they are its architecture. `libcosmic`
389 is a north star, not a next step.389 is a north star, not a next step.
390 390
391+### Measured again, after the language features
392+
393+Re-run once generics, associated types, `impl Trait` in both positions,
394+iterators, futures, closures, trait impls, macro expansion and the
395+concurrency primitives had landed:
396+
397+| | first attempt | now |
398+|---|---|---|
399+| files that translate | **0 of 164** | **8 of 164** |
400+| blocked only by `mod` (an artifact of testing files singly) | — | 34 |
401+| top blocker | generics (66) | calls into dependencies (36) |
402+
403+**The language blockers are essentially gone.** Generics, trait objects and
404+`async` no longer appear at all. What replaced them is the `iced` API:
405+`new` (11), `unique`, `spacing`, `Radians`, `.cosmic`, `.activate`,
406+`.scroller_width`. That is the dependency closure, not a feature list.
407+
408+Two smaller things fell out and are general: `#[cfg(target_env)]` joins the
409+host predicates, and `--cfg <name>` now declares a custom flag the way
410+rustc's does. `libcosmic`'s build script sets `wayland_platform` and
411+`xdg_portal`; we do not run build scripts, so the caller declares them rather
412+than us guessing. An undeclared flag is still refused.
413+
414+**This does not mean libcosmic is close.** Even if every file lowered, the
415+result would not run: libcosmic's behaviour *is* `iced`'s, and `iced` is not
416+transpiled. At that point "files produce a translation" would measure nothing
417+at all. The honest reading is narrower and still worth something — the
418+*language* is no longer what stops it.
419+
391 ### The blocker survey, and what it says about roadmaps420 ### The blocker survey, and what it says about roadmaps
392 421
393 400 crates from the local registry (under 4,000 lines each), all their module422 400 crates from the local registry (under 4,000 lines each), all their module
modified src/lower.rs +28 -1
@@ -309,6 +309,10 @@ pub struct Lowerer {
309309 /// evaluated against these exactly as rustc would, so an item that is
310310 /// dropped here is genuinely not part of the program being compiled.
311311 pub features: Vec<String>,
312+ /// Custom `cfg` flags the caller declared with `--cfg <name>`. A build
313+ /// script would normally set these; we do not run build scripts, so they
314+ /// are declared rather than guessed.
315+ pub cfgs: Vec<String>,
312316 dropped_by_cfg: usize,
313317 /// Return type of the proc being lowered, so `return e` and a trailing
314318 /// expression can type their literals the way Rust's inference would.
@@ -366,6 +370,7 @@ impl Lowerer {
366370 modules: Vec::new(),
367371 emitted: 0,
368372 features: Vec::new(),
373+ cfgs: Vec::new(),
369374 dropped_by_cfg: 0,
370375 ret: None,
371376 target: None,
@@ -889,6 +894,11 @@ impl Lowerer {
889894 // models: a normal (non-`--test`) debug build, not a docs build.
890895 // Anything platform-specific stays rejected, since we would be
891896 // picking a target on the user's behalf.
897+ syn::Meta::Path(p)
898+ if self.cfgs.iter().any(|c| p.is_ident(c.as_str())) =>
899+ {
900+ Ok(true)
901+ }
892902 syn::Meta::Path(p) if p.is_ident("test") => Ok(false),
893903 syn::Meta::Path(p) if p.is_ident("debug_assertions") => Ok(true),
894904 syn::Meta::Path(p) if p.is_ident("docsrs") || p.is_ident("doc") => Ok(false),
@@ -902,7 +912,8 @@ impl Lowerer {
902912 if nv.path.is_ident("target_os")
903913 || nv.path.is_ident("target_arch")
904914 || nv.path.is_ident("target_family")
905- || nv.path.is_ident("target_vendor") =>
915+ || nv.path.is_ident("target_vendor")
916+ || nv.path.is_ident("target_env") =>
906917 {
907918 let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else {
908919 return Err("this `cfg` key expects a string".into());
@@ -913,6 +924,11 @@ impl Lowerer {
913924 "target_os" => std::env::consts::OS,
914925 "target_arch" => std::env::consts::ARCH,
915926 "target_family" => std::env::consts::FAMILY,
927+ // `target_env` is the ABI flavour: "gnu", "musl",
928+ // "msvc" or empty. Rust exposes no constant for it, so
929+ // it is read from the target triple this binary was
930+ // built for.
931+ "target_env" => TARGET_ENV,
916932 _ => "unknown",
917933 })
918934 }
@@ -6561,6 +6577,17 @@ fn unsigned_peer(t: &Nim) -> Result<&'static str, String> {
65616577 })
65626578 }
65636579
6580+/// The ABI flavour of the host, as `#[cfg(target_env = ..)]` spells it.
6581+const TARGET_ENV: &str = if cfg!(target_env = "gnu") {
6582+ "gnu"
6583+} else if cfg!(target_env = "musl") {
6584+ "musl"
6585+} else if cfg!(target_env = "msvc") {
6586+ "msvc"
6587+} else {
6588+ ""
6589+};
6590+
65646591 fn quote_meta(m: &syn::Meta) -> String {
65656592 match m {
65666593 syn::Meta::Path(p) => path_name(p),
@@ -309,6 +309,10 @@ pub struct Lowerer {
309 /// evaluated against these exactly as rustc would, so an item that is309 /// evaluated against these exactly as rustc would, so an item that is
310 /// dropped here is genuinely not part of the program being compiled.310 /// dropped here is genuinely not part of the program being compiled.
311 pub features: Vec<String>,311 pub features: Vec<String>,
312+ /// Custom `cfg` flags the caller declared with `--cfg <name>`. A build
313+ /// script would normally set these; we do not run build scripts, so they
314+ /// are declared rather than guessed.
315+ pub cfgs: Vec<String>,
312 dropped_by_cfg: usize,316 dropped_by_cfg: usize,
313 /// Return type of the proc being lowered, so `return e` and a trailing317 /// Return type of the proc being lowered, so `return e` and a trailing
314 /// expression can type their literals the way Rust's inference would.318 /// expression can type their literals the way Rust's inference would.
@@ -366,6 +370,7 @@ impl Lowerer {
366 modules: Vec::new(),370 modules: Vec::new(),
367 emitted: 0,371 emitted: 0,
368 features: Vec::new(),372 features: Vec::new(),
373+ cfgs: Vec::new(),
369 dropped_by_cfg: 0,374 dropped_by_cfg: 0,
370 ret: None,375 ret: None,
371 target: None,376 target: None,
@@ -889,6 +894,11 @@ impl Lowerer {
889 // models: a normal (non-`--test`) debug build, not a docs build.894 // models: a normal (non-`--test`) debug build, not a docs build.
890 // Anything platform-specific stays rejected, since we would be895 // Anything platform-specific stays rejected, since we would be
891 // picking a target on the user's behalf.896 // picking a target on the user's behalf.
897+ syn::Meta::Path(p)
898+ if self.cfgs.iter().any(|c| p.is_ident(c.as_str())) =>
899+ {
900+ Ok(true)
901+ }
892 syn::Meta::Path(p) if p.is_ident("test") => Ok(false),902 syn::Meta::Path(p) if p.is_ident("test") => Ok(false),
893 syn::Meta::Path(p) if p.is_ident("debug_assertions") => Ok(true),903 syn::Meta::Path(p) if p.is_ident("debug_assertions") => Ok(true),
894 syn::Meta::Path(p) if p.is_ident("docsrs") || p.is_ident("doc") => Ok(false),904 syn::Meta::Path(p) if p.is_ident("docsrs") || p.is_ident("doc") => Ok(false),
@@ -902,7 +912,8 @@ impl Lowerer {
902 if nv.path.is_ident("target_os")912 if nv.path.is_ident("target_os")
903 || nv.path.is_ident("target_arch")913 || nv.path.is_ident("target_arch")
904 || nv.path.is_ident("target_family")914 || nv.path.is_ident("target_family")
905- || nv.path.is_ident("target_vendor") =>915+ || nv.path.is_ident("target_vendor")
916+ || nv.path.is_ident("target_env") =>
906 {917 {
907 let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else {918 let syn::Expr::Lit(syn::ExprLit { lit: Lit::Str(s), .. }) = &nv.value else {
908 return Err("this `cfg` key expects a string".into());919 return Err("this `cfg` key expects a string".into());
@@ -913,6 +924,11 @@ impl Lowerer {
913 "target_os" => std::env::consts::OS,924 "target_os" => std::env::consts::OS,
914 "target_arch" => std::env::consts::ARCH,925 "target_arch" => std::env::consts::ARCH,
915 "target_family" => std::env::consts::FAMILY,926 "target_family" => std::env::consts::FAMILY,
927+ // `target_env` is the ABI flavour: "gnu", "musl",
928+ // "msvc" or empty. Rust exposes no constant for it, so
929+ // it is read from the target triple this binary was
930+ // built for.
931+ "target_env" => TARGET_ENV,
916 _ => "unknown",932 _ => "unknown",
917 })933 })
918 }934 }
@@ -6561,6 +6577,17 @@ fn unsigned_peer(t: &Nim) -> Result<&'static str, String> {
6561 })6577 })
6562 }6578 }
6563 6579
6580+/// The ABI flavour of the host, as `#[cfg(target_env = ..)]` spells it.
6581+const TARGET_ENV: &str = if cfg!(target_env = "gnu") {
6582+ "gnu"
6583+} else if cfg!(target_env = "musl") {
6584+ "musl"
6585+} else if cfg!(target_env = "msvc") {
6586+ "msvc"
6587+} else {
6588+ ""
6589+};
6590+
6564 fn quote_meta(m: &syn::Meta) -> String {6591 fn quote_meta(m: &syn::Meta) -> String {
6565 match m {6592 match m {
6566 syn::Meta::Path(p) => path_name(p),6593 syn::Meta::Path(p) => path_name(p),
modified src/main.rs +11 -5
@@ -39,6 +39,7 @@ fn run() -> Result<(), String> {
3939 let mut inputs: Vec<PathBuf> = Vec::new();
4040 let mut output: Option<PathBuf> = None;
4141 let mut features: Vec<String> = Vec::new();
42+ let mut cfgs: Vec<String> = Vec::new();
4243
4344 while let Some(a) = args.next() {
4445 match a.to_string_lossy().as_ref() {
@@ -52,13 +53,17 @@ fn run() -> Result<(), String> {
5253 "--cfg" => {
5354 let v = args.next().ok_or("`--cfg` needs an argument")?;
5455 let v = v.to_string_lossy().into_owned();
55- let f = v
56- .strip_prefix("feature=")
57- .ok_or("only `--cfg feature=<name>` is supported")?;
58- features.push(f.trim_matches('"').to_string());
56+ match v.strip_prefix("feature=") {
57+ Some(f) => features.push(f.trim_matches('"').to_string()),
58+ // A bare `--cfg name` declares a custom flag, as rustc's
59+ // does. A build script would normally set these, and we do
60+ // not run build scripts -- so the caller says which are on
61+ // rather than us guessing.
62+ None => cfgs.push(v),
63+ }
5964 }
6065 "-h" | "--help" => {
61- println!("usage: rustnim <input.rs>... [--cfg feature=<name>]... [-o <output.nim>]");
66+ println!("usage: rustnim <input.rs>... [--cfg <name>|feature=<name>]... [-o <output.nim>]");
6267 println!();
6368 println!("Several inputs are concatenated into one Nim module, in the");
6469 println!("order given. That is how a multi-file crate is handled: Nim");
@@ -101,6 +106,7 @@ fn run() -> Result<(), String> {
101106
102107 let mut lowerer = lower::Lowerer::new();
103108 lowerer.features = features;
109+ lowerer.cfgs = cfgs;
104110 // A `mod x;` is satisfied when x.rs is one of the inputs.
105111 lowerer.modules = inputs
106112 .iter()
@@ -39,6 +39,7 @@ fn run() -> Result<(), String> {
39 let mut inputs: Vec<PathBuf> = Vec::new();39 let mut inputs: Vec<PathBuf> = Vec::new();
40 let mut output: Option<PathBuf> = None;40 let mut output: Option<PathBuf> = None;
41 let mut features: Vec<String> = Vec::new();41 let mut features: Vec<String> = Vec::new();
42+ let mut cfgs: Vec<String> = Vec::new();
42 43
43 while let Some(a) = args.next() {44 while let Some(a) = args.next() {
44 match a.to_string_lossy().as_ref() {45 match a.to_string_lossy().as_ref() {
@@ -52,13 +53,17 @@ fn run() -> Result<(), String> {
52 "--cfg" => {53 "--cfg" => {
53 let v = args.next().ok_or("`--cfg` needs an argument")?;54 let v = args.next().ok_or("`--cfg` needs an argument")?;
54 let v = v.to_string_lossy().into_owned();55 let v = v.to_string_lossy().into_owned();
55- let f = v56+ match v.strip_prefix("feature=") {
56- .strip_prefix("feature=")57+ Some(f) => features.push(f.trim_matches('"').to_string()),
57- .ok_or("only `--cfg feature=<name>` is supported")?;58+ // A bare `--cfg name` declares a custom flag, as rustc's
58- features.push(f.trim_matches('"').to_string());59+ // does. A build script would normally set these, and we do
60+ // not run build scripts -- so the caller says which are on
61+ // rather than us guessing.
62+ None => cfgs.push(v),
63+ }
59 }64 }
60 "-h" | "--help" => {65 "-h" | "--help" => {
61- println!("usage: rustnim <input.rs>... [--cfg feature=<name>]... [-o <output.nim>]");66+ println!("usage: rustnim <input.rs>... [--cfg <name>|feature=<name>]... [-o <output.nim>]");
62 println!();67 println!();
63 println!("Several inputs are concatenated into one Nim module, in the");68 println!("Several inputs are concatenated into one Nim module, in the");
64 println!("order given. That is how a multi-file crate is handled: Nim");69 println!("order given. That is how a multi-file crate is handled: Nim");
@@ -101,6 +106,7 @@ fn run() -> Result<(), String> {
101 106
102 let mut lowerer = lower::Lowerer::new();107 let mut lowerer = lower::Lowerer::new();
103 lowerer.features = features;108 lowerer.features = features;
109+ lowerer.cfgs = cfgs;
104 // A `mod x;` is satisfied when x.rs is one of the inputs.110 // A `mod x;` is satisfied when x.rs is one of the inputs.
105 lowerer.modules = inputs111 lowerer.modules = inputs
106 .iter()112 .iter()
modified tests/multifile.rs +29 -0
@@ -122,6 +122,35 @@ fn an_unevaluable_cfg_predicate_is_reported_not_assumed() {
122122 assert!(err.contains("not a predicate rustnim can evaluate"), "unexpected: {err}");
123123 }
124124
125+#[test]
126+fn a_custom_cfg_is_declared_not_guessed() {
127+ // A build script would normally set these. We do not run build scripts,
128+ // so the caller declares which are on -- as rustc's own `--cfg` does.
129+ let d = work("cfg-custom");
130+ let f = d.join("c.rs");
131+ fs::write(
132+ &f,
133+ "#[cfg(wayland_platform)]\nfn on_wayland() -> i32 { 1 }\n\
134+ #[cfg(some_other_flag)]\nfn not_declared() -> i32 { 2 }\n\
135+ fn main() {}\n",
136+ )
137+ .unwrap();
138+
139+ // Undeclared, so it cannot be evaluated and is reported.
140+ let (ok, _, err) = run(&d, &[f.to_str().unwrap()]);
141+ assert!(!ok, "an undeclared cfg must not be assumed");
142+ assert!(err.contains("not a predicate rustnim can evaluate"), "{err}");
143+
144+ // Declared: the gated item is kept, and the one still undeclared is gone.
145+ let (ok, out, err) = run(
146+ &d,
147+ &[f.to_str().unwrap(), "--cfg", "wayland_platform", "--cfg", "some_other_flag"],
148+ );
149+ assert!(ok, "{err}");
150+ assert!(out.contains("proc on_wayland"), "{out}");
151+ assert!(out.contains("proc not_declared"), "{out}");
152+}
153+
125154 #[test]
126155 fn host_facts_are_evaluated() {
127156 // These are determined by the machine the generated Nim is compiled for,
@@ -122,6 +122,35 @@ fn an_unevaluable_cfg_predicate_is_reported_not_assumed() {
122 assert!(err.contains("not a predicate rustnim can evaluate"), "unexpected: {err}");122 assert!(err.contains("not a predicate rustnim can evaluate"), "unexpected: {err}");
123 }123 }
124 124
125+#[test]
126+fn a_custom_cfg_is_declared_not_guessed() {
127+ // A build script would normally set these. We do not run build scripts,
128+ // so the caller declares which are on -- as rustc's own `--cfg` does.
129+ let d = work("cfg-custom");
130+ let f = d.join("c.rs");
131+ fs::write(
132+ &f,
133+ "#[cfg(wayland_platform)]\nfn on_wayland() -> i32 { 1 }\n\
134+ #[cfg(some_other_flag)]\nfn not_declared() -> i32 { 2 }\n\
135+ fn main() {}\n",
136+ )
137+ .unwrap();
138+
139+ // Undeclared, so it cannot be evaluated and is reported.
140+ let (ok, _, err) = run(&d, &[f.to_str().unwrap()]);
141+ assert!(!ok, "an undeclared cfg must not be assumed");
142+ assert!(err.contains("not a predicate rustnim can evaluate"), "{err}");
143+
144+ // Declared: the gated item is kept, and the one still undeclared is gone.
145+ let (ok, out, err) = run(
146+ &d,
147+ &[f.to_str().unwrap(), "--cfg", "wayland_platform", "--cfg", "some_other_flag"],
148+ );
149+ assert!(ok, "{err}");
150+ assert!(out.contains("proc on_wayland"), "{out}");
151+ assert!(out.contains("proc not_declared"), "{out}");
152+}
153+
125 #[test]154 #[test]
126 fn host_facts_are_evaluated() {155 fn host_facts_are_evaluated() {
127 // These are determined by the machine the generated Nim is compiled for,156 // These are determined by the machine the generated Nim is compiled for,