nandi/rustnimpublic Fork
⑂ 8f6401f
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

Run libcosmic through --expand; it now halts on the founding rule, not a gap

cargo rustc -- -Zunpretty=expanded over the whole crate: 439 dependency
crates built, 44,649 lines of expanded Rust with every macro resolved --
derives, println!, bitflags!, all of it. One file, no mod declarations,
nothing left to guess about.

rustnim needed two things to get through that.

Destructuring parameters, which iced writes several times as
`fn from((column, row): (u16, u16))`. The parameter is named here and taken
apart at the top of the body, which is what Rust does. tests/cases/048 pins
it, and two smaller bugs surfaced with it: a tuple literal did not take its
element types from the expected tuple, so `Size::from((7, 8))` defaulted to
i32 and matched no impl; and a From impl whose source is a tuple generated a
proc name containing parens and commas, which is not a Nim identifier.

Then u128, where it stops deliberately. 16 occurrences, from enumflags2's bit
arithmetic and an iced DragId(pub u128). That is the founding rule refusing
to truncate rather than a missing feature.

DESIGN.md records what this does not mean as well as what it does: the crate
is 44,649 lines of which rustnim has type-checked a prefix, and halting at
line N says nothing about N+1 onward. One blocker at a time is the only
honest reading.

54 differential cases, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-19T08:56:41-07:00 Browse files
8f6401f parent: e7dbe75
modified DESIGN.md +23 -0
@@ -388,6 +388,29 @@ 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 a third time, with `--expand`
392+
393+`cargo rustc -- -Zunpretty=expanded` over the whole crate: 439 dependency
394+crates built, **44,649 lines** of expanded Rust with every macro already
395+resolved — derives, `println!`, `bitflags!`, the lot. One file, no `mod`
396+declarations, nothing left to guess about.
397+
398+rustnim on that took **two** blockers to get through:
399+
400+1. destructuring parameters — `fn from((column, row): (u16, u16))`, which
401+ `iced` writes several times. Named here and taken apart at the top of the
402+ body, as Rust does. Now `tests/cases/048`.
403+2. `u128` — and here it stops, **deliberately**. 16 occurrences, from
404+ `enumflags2`'s bit arithmetic and an `iced` `DragId(pub u128)`. That is the
405+ founding rule refusing to truncate, not a missing feature.
406+
407+So the expanded crate clears the language and halts at the project's own
408+first principle. That is a much more interesting place to stop than "generics
409+are not implemented yet", and it is worth saying plainly what it does *not*
410+mean: the crate is 44,649 lines of which rustnim has type-checked a prefix,
411+and stopping at line N tells you nothing about lines N+1 onward. It is one
412+blocker at a time, and that is the only honest way to read it.
413+
391414 ### Measured again, after the language features
392415
393416 Re-run once generics, associated types, `impl Trait` in both positions,
@@ -388,6 +388,29 @@ 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 a third time, with `--expand`
392+
393+`cargo rustc -- -Zunpretty=expanded` over the whole crate: 439 dependency
394+crates built, **44,649 lines** of expanded Rust with every macro already
395+resolved — derives, `println!`, `bitflags!`, the lot. One file, no `mod`
396+declarations, nothing left to guess about.
397+
398+rustnim on that took **two** blockers to get through:
399+
400+1. destructuring parameters — `fn from((column, row): (u16, u16))`, which
401+ `iced` writes several times. Named here and taken apart at the top of the
402+ body, as Rust does. Now `tests/cases/048`.
403+2. `u128` — and here it stops, **deliberately**. 16 occurrences, from
404+ `enumflags2`'s bit arithmetic and an `iced` `DragId(pub u128)`. That is the
405+ founding rule refusing to truncate, not a missing feature.
406+
407+So the expanded crate clears the language and halts at the project's own
408+first principle. That is a much more interesting place to stop than "generics
409+are not implemented yet", and it is worth saying plainly what it does *not*
410+mean: the crate is 44,649 lines of which rustnim has type-checked a prefix,
411+and stopping at line N tells you nothing about lines N+1 onward. It is one
412+blocker at a time, and that is the only honest way to read it.
413+
391 ### Measured again, after the language features414 ### Measured again, after the language features
392 415
393 Re-run once generics, associated types, `impl Trait` in both positions,416 Re-run once generics, associated types, `impl Trait` in both positions,
modified src/lower.rs +84 -9
@@ -260,6 +260,8 @@ pub struct Lowerer {
260260 fmt_impls: HashMap<(String, String), ()>,
261261 /// `(from, to)` conversions declared by `impl From<A> for B`.
262262 from_impls: HashMap<(String, String), String>,
263+ /// The full source type of each `From` impl, which its key only names.
264+ from_types: HashMap<(String, String), Nim>,
263265 /// Operator traits implemented for a type, so `a += b` on a user type can
264266 /// be dispatched to the impl rather than to Nim's built-in operator.
265267 op_impls: HashMap<(String, String), ()>,
@@ -355,6 +357,7 @@ impl Lowerer {
355357 methods: HashMap::new(),
356358 fmt_impls: HashMap::new(),
357359 from_impls: HashMap::new(),
360+ from_types: HashMap::new(),
358361 op_impls: HashMap::new(),
359362 statics: HashMap::new(),
360363 fmt_param: None,
@@ -797,8 +800,12 @@ impl Lowerer {
797800 .first()
798801 .ok_or("`fn from` takes one argument")?
799802 .clone();
800- let name = format!("rsFrom{}{}", tyname, type_name(&src));
803+ // The source type may be a tuple, whose rendering has
804+ // parens and commas in it; a Nim identifier may not.
805+ let name = format!("rsFrom{}{}", mangle(&tyname), mangle(&type_name(&src)));
801806 self.forwards.push(self.head_of(&name, &m.sig, None)?);
807+ self.from_types
808+ .insert((type_name(&src), tyname.clone()), src.clone());
802809 self.from_impls
803810 .insert((type_name(&src), tyname), name);
804811 return Ok(());
@@ -1297,9 +1304,10 @@ impl Lowerer {
12971304 let pname = match &*p.pat {
12981305 Pat::Ident(id) => id.ident.to_string(),
12991306 Pat::Wild(_) => format!("unused{}", parts.len()),
1300- _ => return Err("only plain identifier parameters are supported".into()),
1307+ // A destructuring parameter takes a name here and is taken
1308+ // apart at the top of the body, which is what Rust does too.
1309+ _ => format!("rsArg{}", i),
13011310 };
1302- let _ = i;
13031311 parts.push(format!("{}: {}", ident(&pname), t.render()));
13041312 }
13051313 Ok(if ret == Nim::Unit {
@@ -2233,14 +2241,20 @@ impl Lowerer {
22332241 _ => None,
22342242 })
22352243 .collect();
2244+ let mut destructures: Vec<(String, Pat, Nim)> = Vec::new();
22362245 for (p, t) in typed.iter().zip(ptys.iter()) {
2237- let pname = match &*p.pat {
2238- Pat::Ident(i) => i.ident.to_string(),
2246+ let (pname, destructure) = match &*p.pat {
2247+ Pat::Ident(i) => (i.ident.to_string(), None),
22392248 // `fn from(_: Error) -> ..` — the parameter is unused, but Nim
22402249 // still needs a name for it.
2241- Pat::Wild(_) => format!("unused{}", rendered.len()),
2242- _ => return Err("only plain identifier parameters are supported".into()),
2250+ Pat::Wild(_) => (format!("unused{}", rendered.len()), None),
2251+ // `fn f((a, b): (i32, i32))` — named here, taken apart at the
2252+ // top of the body, as Rust does.
2253+ other => (format!("rsArg{}", rendered.len()), Some(other.clone())),
22432254 };
2255+ if let Some(d) = destructure {
2256+ destructures.push((pname.clone(), d, t.clone()));
2257+ }
22442258 rendered.push(format!("{}: {}", ident(&pname), t.render()));
22452259 // Inside the body a `var T` parameter is used exactly like a `T`.
22462260 self.bind(&pname, t.clone().owned());
@@ -2270,6 +2284,31 @@ impl Lowerer {
22702284 };
22712285 self.line(&head);
22722286 self.indent += 1;
2287+ // Take a destructuring parameter apart before the body runs, which is
2288+ // what Rust does with `fn f((a, b): (i32, i32))`.
2289+ for (arg, pat, t) in &destructures {
2290+ let elems = match pat {
2291+ Pat::Tuple(tp) => tp.elems.clone(),
2292+ _ => return Err("only tuple patterns are supported as parameters".into()),
2293+ };
2294+ let tys = match t {
2295+ Nim::Tuple(ts) if ts.len() == elems.len() => ts.clone(),
2296+ _ => return Err("a destructuring parameter needs a tuple type".into()),
2297+ };
2298+ for (i, (e, et)) in elems.iter().zip(tys).enumerate() {
2299+ let n = match e {
2300+ Pat::Ident(id) => id.ident.to_string(),
2301+ Pat::Wild(_) => continue,
2302+ _ => {
2303+ return Err(
2304+ "nested destructuring parameters are not implemented yet".into()
2305+ )
2306+ }
2307+ };
2308+ self.line(&format!("let {}: {} = {}[{}]", ident(&n), et.render(), arg, i));
2309+ self.bind(&n, et);
2310+ }
2311+ }
22732312 let outer_ret = self.ret.replace(ret.clone());
22742313
22752314 // A Rust fn's trailing expression is its return value. Naming Nim's
@@ -4203,10 +4242,16 @@ impl Lowerer {
42034242 }
42044243 Expr::Tuple(t) if t.elems.is_empty() => Ok(Val::new("", Some(Nim::Unit))),
42054244 Expr::Tuple(t) => {
4245+ // Each element takes its type from the expected tuple, the
4246+ // same way a `let` annotation types a scalar literal.
4247+ let want: Option<&Vec<Nim>> = match expect {
4248+ Some(Nim::Tuple(ts)) if ts.len() == t.elems.len() => Some(ts),
4249+ _ => None,
4250+ };
42064251 let mut parts = Vec::new();
42074252 let mut tys = Vec::new();
4208- for e in &t.elems {
4209- let v = self.expr(e)?;
4253+ for (i, e) in t.elems.iter().enumerate() {
4254+ let v = self.expr_at(e, want.and_then(|w| w.get(i)))?;
42104255 tys.push(v.ty.clone());
42114256 parts.push(v.code);
42124257 }
@@ -5133,6 +5178,27 @@ impl Lowerer {
51335178 ));
51345179 }
51355180 }
5181+ // The argument's own type may not match yet -- a tuple literal
5182+ // defaults its elements the way any literal does. If exactly
5183+ // one `From` targets this type, its declared source says what
5184+ // the argument should be.
5185+ let cands: Vec<(String, String)> = self
5186+ .from_impls
5187+ .iter()
5188+ .filter(|((_, to), _)| *to == q)
5189+ .map(|((from, _), f)| (from.clone(), f.clone()))
5190+ .collect();
5191+ if cands.len() == 1 {
5192+ let want = self
5193+ .from_types
5194+ .get(&(cands[0].0.clone(), q.clone()))
5195+ .cloned();
5196+ let v = self.expr_at(&c.args[0], want.as_ref())?;
5197+ return Ok(Val::new(
5198+ format!("{}({})", cands[0].1, v.code),
5199+ Some(Nim::Named(q, vec![])),
5200+ ));
5201+ }
51365202 }
51375203 }
51385204
@@ -6425,6 +6491,15 @@ fn elem_of(t: &Option<Nim>) -> Option<Nim> {
64256491 }
64266492 }
64276493
6494+/// Reduce a rendered type to something usable inside an identifier.
6495+fn mangle(s: &str) -> String {
6496+ s.chars()
6497+ .map(|c| if c.is_alphanumeric() { c } else { '_' })
6498+ .collect::<String>()
6499+ .trim_matches('_')
6500+ .replace("__", "_")
6501+}
6502+
64286503 /// The short name a Nim type is known by, for keying method tables.
64296504 fn type_name(t: &Nim) -> String {
64306505 match t {
@@ -260,6 +260,8 @@ pub struct Lowerer {
260 fmt_impls: HashMap<(String, String), ()>,260 fmt_impls: HashMap<(String, String), ()>,
261 /// `(from, to)` conversions declared by `impl From<A> for B`.261 /// `(from, to)` conversions declared by `impl From<A> for B`.
262 from_impls: HashMap<(String, String), String>,262 from_impls: HashMap<(String, String), String>,
263+ /// The full source type of each `From` impl, which its key only names.
264+ from_types: HashMap<(String, String), Nim>,
263 /// Operator traits implemented for a type, so `a += b` on a user type can265 /// Operator traits implemented for a type, so `a += b` on a user type can
264 /// be dispatched to the impl rather than to Nim's built-in operator.266 /// be dispatched to the impl rather than to Nim's built-in operator.
265 op_impls: HashMap<(String, String), ()>,267 op_impls: HashMap<(String, String), ()>,
@@ -355,6 +357,7 @@ impl Lowerer {
355 methods: HashMap::new(),357 methods: HashMap::new(),
356 fmt_impls: HashMap::new(),358 fmt_impls: HashMap::new(),
357 from_impls: HashMap::new(),359 from_impls: HashMap::new(),
360+ from_types: HashMap::new(),
358 op_impls: HashMap::new(),361 op_impls: HashMap::new(),
359 statics: HashMap::new(),362 statics: HashMap::new(),
360 fmt_param: None,363 fmt_param: None,
@@ -797,8 +800,12 @@ impl Lowerer {
797 .first()800 .first()
798 .ok_or("`fn from` takes one argument")?801 .ok_or("`fn from` takes one argument")?
799 .clone();802 .clone();
800- let name = format!("rsFrom{}{}", tyname, type_name(&src));803+ // The source type may be a tuple, whose rendering has
804+ // parens and commas in it; a Nim identifier may not.
805+ let name = format!("rsFrom{}{}", mangle(&tyname), mangle(&type_name(&src)));
801 self.forwards.push(self.head_of(&name, &m.sig, None)?);806 self.forwards.push(self.head_of(&name, &m.sig, None)?);
807+ self.from_types
808+ .insert((type_name(&src), tyname.clone()), src.clone());
802 self.from_impls809 self.from_impls
803 .insert((type_name(&src), tyname), name);810 .insert((type_name(&src), tyname), name);
804 return Ok(());811 return Ok(());
@@ -1297,9 +1304,10 @@ impl Lowerer {
1297 let pname = match &*p.pat {1304 let pname = match &*p.pat {
1298 Pat::Ident(id) => id.ident.to_string(),1305 Pat::Ident(id) => id.ident.to_string(),
1299 Pat::Wild(_) => format!("unused{}", parts.len()),1306 Pat::Wild(_) => format!("unused{}", parts.len()),
1300- _ => return Err("only plain identifier parameters are supported".into()),1307+ // A destructuring parameter takes a name here and is taken
1308+ // apart at the top of the body, which is what Rust does too.
1309+ _ => format!("rsArg{}", i),
1301 };1310 };
1302- let _ = i;
1303 parts.push(format!("{}: {}", ident(&pname), t.render()));1311 parts.push(format!("{}: {}", ident(&pname), t.render()));
1304 }1312 }
1305 Ok(if ret == Nim::Unit {1313 Ok(if ret == Nim::Unit {
@@ -2233,14 +2241,20 @@ impl Lowerer {
2233 _ => None,2241 _ => None,
2234 })2242 })
2235 .collect();2243 .collect();
2244+ let mut destructures: Vec<(String, Pat, Nim)> = Vec::new();
2236 for (p, t) in typed.iter().zip(ptys.iter()) {2245 for (p, t) in typed.iter().zip(ptys.iter()) {
2237- let pname = match &*p.pat {2246+ let (pname, destructure) = match &*p.pat {
2238- Pat::Ident(i) => i.ident.to_string(),2247+ Pat::Ident(i) => (i.ident.to_string(), None),
2239 // `fn from(_: Error) -> ..` — the parameter is unused, but Nim2248 // `fn from(_: Error) -> ..` — the parameter is unused, but Nim
2240 // still needs a name for it.2249 // still needs a name for it.
2241- Pat::Wild(_) => format!("unused{}", rendered.len()),2250+ Pat::Wild(_) => (format!("unused{}", rendered.len()), None),
2242- _ => return Err("only plain identifier parameters are supported".into()),2251+ // `fn f((a, b): (i32, i32))` — named here, taken apart at the
2252+ // top of the body, as Rust does.
2253+ other => (format!("rsArg{}", rendered.len()), Some(other.clone())),
2243 };2254 };
2255+ if let Some(d) = destructure {
2256+ destructures.push((pname.clone(), d, t.clone()));
2257+ }
2244 rendered.push(format!("{}: {}", ident(&pname), t.render()));2258 rendered.push(format!("{}: {}", ident(&pname), t.render()));
2245 // Inside the body a `var T` parameter is used exactly like a `T`.2259 // Inside the body a `var T` parameter is used exactly like a `T`.
2246 self.bind(&pname, t.clone().owned());2260 self.bind(&pname, t.clone().owned());
@@ -2270,6 +2284,31 @@ impl Lowerer {
2270 };2284 };
2271 self.line(&head);2285 self.line(&head);
2272 self.indent += 1;2286 self.indent += 1;
2287+ // Take a destructuring parameter apart before the body runs, which is
2288+ // what Rust does with `fn f((a, b): (i32, i32))`.
2289+ for (arg, pat, t) in &destructures {
2290+ let elems = match pat {
2291+ Pat::Tuple(tp) => tp.elems.clone(),
2292+ _ => return Err("only tuple patterns are supported as parameters".into()),
2293+ };
2294+ let tys = match t {
2295+ Nim::Tuple(ts) if ts.len() == elems.len() => ts.clone(),
2296+ _ => return Err("a destructuring parameter needs a tuple type".into()),
2297+ };
2298+ for (i, (e, et)) in elems.iter().zip(tys).enumerate() {
2299+ let n = match e {
2300+ Pat::Ident(id) => id.ident.to_string(),
2301+ Pat::Wild(_) => continue,
2302+ _ => {
2303+ return Err(
2304+ "nested destructuring parameters are not implemented yet".into()
2305+ )
2306+ }
2307+ };
2308+ self.line(&format!("let {}: {} = {}[{}]", ident(&n), et.render(), arg, i));
2309+ self.bind(&n, et);
2310+ }
2311+ }
2273 let outer_ret = self.ret.replace(ret.clone());2312 let outer_ret = self.ret.replace(ret.clone());
2274 2313
2275 // A Rust fn's trailing expression is its return value. Naming Nim's2314 // A Rust fn's trailing expression is its return value. Naming Nim's
@@ -4203,10 +4242,16 @@ impl Lowerer {
4203 }4242 }
4204 Expr::Tuple(t) if t.elems.is_empty() => Ok(Val::new("", Some(Nim::Unit))),4243 Expr::Tuple(t) if t.elems.is_empty() => Ok(Val::new("", Some(Nim::Unit))),
4205 Expr::Tuple(t) => {4244 Expr::Tuple(t) => {
4245+ // Each element takes its type from the expected tuple, the
4246+ // same way a `let` annotation types a scalar literal.
4247+ let want: Option<&Vec<Nim>> = match expect {
4248+ Some(Nim::Tuple(ts)) if ts.len() == t.elems.len() => Some(ts),
4249+ _ => None,
4250+ };
4206 let mut parts = Vec::new();4251 let mut parts = Vec::new();
4207 let mut tys = Vec::new();4252 let mut tys = Vec::new();
4208- for e in &t.elems {4253+ for (i, e) in t.elems.iter().enumerate() {
4209- let v = self.expr(e)?;4254+ let v = self.expr_at(e, want.and_then(|w| w.get(i)))?;
4210 tys.push(v.ty.clone());4255 tys.push(v.ty.clone());
4211 parts.push(v.code);4256 parts.push(v.code);
4212 }4257 }
@@ -5133,6 +5178,27 @@ impl Lowerer {
5133 ));5178 ));
5134 }5179 }
5135 }5180 }
5181+ // The argument's own type may not match yet -- a tuple literal
5182+ // defaults its elements the way any literal does. If exactly
5183+ // one `From` targets this type, its declared source says what
5184+ // the argument should be.
5185+ let cands: Vec<(String, String)> = self
5186+ .from_impls
5187+ .iter()
5188+ .filter(|((_, to), _)| *to == q)
5189+ .map(|((from, _), f)| (from.clone(), f.clone()))
5190+ .collect();
5191+ if cands.len() == 1 {
5192+ let want = self
5193+ .from_types
5194+ .get(&(cands[0].0.clone(), q.clone()))
5195+ .cloned();
5196+ let v = self.expr_at(&c.args[0], want.as_ref())?;
5197+ return Ok(Val::new(
5198+ format!("{}({})", cands[0].1, v.code),
5199+ Some(Nim::Named(q, vec![])),
5200+ ));
5201+ }
5136 }5202 }
5137 }5203 }
5138 5204
@@ -6425,6 +6491,15 @@ fn elem_of(t: &Option<Nim>) -> Option<Nim> {
6425 }6491 }
6426 }6492 }
6427 6493
6494+/// Reduce a rendered type to something usable inside an identifier.
6495+fn mangle(s: &str) -> String {
6496+ s.chars()
6497+ .map(|c| if c.is_alphanumeric() { c } else { '_' })
6498+ .collect::<String>()
6499+ .trim_matches('_')
6500+ .replace("__", "_")
6501+}
6502+
6428 /// The short name a Nim type is known by, for keying method tables.6503 /// The short name a Nim type is known by, for keying method tables.
6429 fn type_name(t: &Nim) -> String {6504 fn type_name(t: &Nim) -> String {
6430 match t {6505 match t {
added tests/cases/048-destructuring-params.rs +28 -0
new file mode 100644
@@ -0,0 +1,28 @@
1+// `fn f((a, b): (i32, i32))` names the parameter and takes it apart at the top
2+// of the body, which is what Rust does too. Found in libcosmic's expanded
3+// source, where `impl From<(u16, u16)>` is written that way.
4+struct Size {
5+ w: u16,
6+ h: u16,
7+}
8+
9+impl From<(u16, u16)> for Size {
10+ fn from((w, h): (u16, u16)) -> Size {
11+ Size { w, h }
12+ }
13+}
14+
15+fn area((w, h): (i32, i32)) -> i32 {
16+ w * h
17+}
18+
19+fn first((a, _): (i32, i32)) -> i32 {
20+ a
21+}
22+
23+fn main() {
24+ println!("{}", area((3, 4)));
25+ println!("{}", first((9, 1)));
26+ let s = Size::from((7, 8));
27+ println!("{} {}", s.w, s.h);
28+}
new file mode 100644
@@ -0,0 +1,28 @@
1+// `fn f((a, b): (i32, i32))` names the parameter and takes it apart at the top
2+// of the body, which is what Rust does too. Found in libcosmic's expanded
3+// source, where `impl From<(u16, u16)>` is written that way.
4+struct Size {
5+ w: u16,
6+ h: u16,
7+}
8+
9+impl From<(u16, u16)> for Size {
10+ fn from((w, h): (u16, u16)) -> Size {
11+ Size { w, h }
12+ }
13+}
14+
15+fn area((w, h): (i32, i32)) -> i32 {
16+ w * h
17+}
18+
19+fn first((a, _): (i32, i32)) -> i32 {
20+ a
21+}
22+
23+fn main() {
24+ println!("{}", area((3, 4)));
25+ println!("{}", first((9, 1)));
26+ let s = Size::from((7, 8));
27+ println!("{} {}", s.w, s.h);
28+}