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

Bind constant types before discriminants are lowered, and stop emitting `10'i`

`None = ffi::MZ_NO_FLUSH as isize` needs the constant's type, and constants
were not bound until the emission pass -- which runs after discriminants are
lowered. The pre-pass that already registers length-bearing names now binds
module-level constant types too. Only the type: the value is not needed to
lower a cast, and is not available that early anyway.

Writing the test found a second bug behind it. `nim_suffix` returned `"i"` for
`int`, so an `isize` literal came out as `10'i` -- Nim has no bare `'i` suffix
and read it as a custom literal on a string. A plain integer literal already
is `int` there. `'u` does exist, which is why `usize` never showed this.

DESIGN.md records a third thing found on the way and deliberately not fixed:
Nim compares identifiers ignoring case after the first character, so an
idiomatic Rust pair like `enum Step` and `const STEP` is one name in Nim. Nim
rejects it rather than confusing them, so it blocks translations instead of
corrupting them -- but it needs a whole-program renaming to solve, which is
not a change to make in passing.

tests/cases/057.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-19T13:31:42-07:00 Browse files
8194456 parent: 08bebb7
modified DESIGN.md +22 -0
@@ -1246,3 +1246,25 @@ have not mapped yet" — every one is a construct with a Nim equivalent that
12461246 rustnim was not reaching for. And the last two only appear at scale: a single
12471247 crate never puts two different `__Field`s in one namespace, so the 3M-line run
12481248 is not a stunt, it is the only configuration in which those bugs exist.
1249+
1250+### Nim identifier equality is not Rust's — an open hazard
1251+
1252+Nim compares identifiers ignoring case after the first character, and ignoring
1253+underscores. Rust does not. So two names Rust keeps apart can be one name in
1254+Nim, and the pairing is entirely idiomatic:
1255+
1256+```rust
1257+enum Step { .. }
1258+const STEP: isize = 5; // `Step` and `STEP` are the same identifier in Nim
1259+```
1260+
1261+Found while writing `tests/cases/057`, where that exact pair appeared by
1262+accident. Nim rejects it as a redefinition, so nothing is silently wrong --
1263+this is a *blocked* translation, not a corrupted one, which is the right side
1264+of the founding rule to fail on. But it blocks valid Rust.
1265+
1266+`ident()` handles Nim keywords and the underscore rules; it does not handle
1267+this, because handling it needs a whole-program view: the disambiguation has
1268+to be consistent across every use, and which of a colliding pair gets renamed
1269+cannot be decided one identifier at a time. Untouched for now, and recorded so
1270+it is not rediscovered as a mystery.
@@ -1246,3 +1246,25 @@ have not mapped yet" — every one is a construct with a Nim equivalent that
1246 rustnim was not reaching for. And the last two only appear at scale: a single1246 rustnim was not reaching for. And the last two only appear at scale: a single
1247 crate never puts two different `__Field`s in one namespace, so the 3M-line run1247 crate never puts two different `__Field`s in one namespace, so the 3M-line run
1248 is not a stunt, it is the only configuration in which those bugs exist.1248 is not a stunt, it is the only configuration in which those bugs exist.
1249+
1250+### Nim identifier equality is not Rust's — an open hazard
1251+
1252+Nim compares identifiers ignoring case after the first character, and ignoring
1253+underscores. Rust does not. So two names Rust keeps apart can be one name in
1254+Nim, and the pairing is entirely idiomatic:
1255+
1256+```rust
1257+enum Step { .. }
1258+const STEP: isize = 5; // `Step` and `STEP` are the same identifier in Nim
1259+```
1260+
1261+Found while writing `tests/cases/057`, where that exact pair appeared by
1262+accident. Nim rejects it as a redefinition, so nothing is silently wrong --
1263+this is a *blocked* translation, not a corrupted one, which is the right side
1264+of the founding rule to fail on. But it blocks valid Rust.
1265+
1266+`ident()` handles Nim keywords and the underscore rules; it does not handle
1267+this, because handling it needs a whole-program view: the disambiguation has
1268+to be consistent across every use, and which of a colliding pair gets renamed
1269+cannot be decided one identifier at a time. Untouched for now, and recorded so
1270+it is not rediscovered as a mystery.
modified src/lower.rs +21 -3
@@ -763,6 +763,14 @@ impl Lowerer {
763763 for i in &items {
764764 self.precollect(i);
765765 }
766+ return;
767+ }
768+ // A module-level constant's *type*, which is all an `as` on it
769+ // needs. Enum discriminants are lowered before constants are
770+ // emitted -- `None = ffi::MZ_NO_FLUSH as isize` is one -- so
771+ // waiting for the emission to bind it is too late.
772+ if let Ok(t) = self.map_ty(&c.ty) {
773+ self.bind(&c.ident.to_string(), t.owned());
766774 }
767775 }
768776 Item::Enum(e) => {
@@ -5243,7 +5251,13 @@ impl Lowerer {
52435251 let code = format!("{}({}'u64, {}'u64)", f, (v >> 64) as u64, v as u64);
52445252 return Ok(Val::new(code, Some(t)));
52455253 }
5246- Ok(Val::new(format!("{}'{}", digits, nim_suffix(&t)?), Some(t)))
5254+ let suf = nim_suffix(&t)?;
5255+ let code = if suf.is_empty() {
5256+ digits
5257+ } else {
5258+ format!("{digits}'{suf}")
5259+ };
5260+ Ok(Val::new(code, Some(t)))
52475261 }
52485262 Lit::Float(f) => {
52495263 let t = match f.suffix() {
@@ -5403,10 +5417,12 @@ impl Lowerer {
54035417 fn cast(&mut self, c: &syn::ExprCast) -> Result<Val, String> {
54045418 let v = self.expr(&c.expr)?;
54055419 let to = self.map_ty(&c.ty)?;
5420+ let src = &c.expr;
54065421 let from = v.ty.clone().ok_or_else(|| {
54075422 format!(
5408- "cannot lower `as {}`: the source type is unknown, and `as` \
5423+ "cannot lower `{} as {}`: the source type is unknown, and `as` \
54095424 truncates, so the source width decides the result",
5425+ snippet(&quote::quote!(#src).to_string()),
54105426 to.render()
54115427 )
54125428 })?;
@@ -7660,7 +7676,9 @@ fn nim_suffix(t: &Nim) -> Result<&'static str, String> {
76607676 "int16" => "i16",
76617677 "int32" => "i32",
76627678 "int64" => "i64",
7663- "int" => "i",
7679+ // Nim has no `'i` suffix -- a bare integer literal already is `int`.
7680+ // `'u` does exist, so `uint` below keeps one.
7681+ "int" => "",
76647682 "uint8" => "u8",
76657683 "uint16" => "u16",
76667684 "uint32" => "u32",
@@ -763,6 +763,14 @@ impl Lowerer {
763 for i in &items {763 for i in &items {
764 self.precollect(i);764 self.precollect(i);
765 }765 }
766+ return;
767+ }
768+ // A module-level constant's *type*, which is all an `as` on it
769+ // needs. Enum discriminants are lowered before constants are
770+ // emitted -- `None = ffi::MZ_NO_FLUSH as isize` is one -- so
771+ // waiting for the emission to bind it is too late.
772+ if let Ok(t) = self.map_ty(&c.ty) {
773+ self.bind(&c.ident.to_string(), t.owned());
766 }774 }
767 }775 }
768 Item::Enum(e) => {776 Item::Enum(e) => {
@@ -5243,7 +5251,13 @@ impl Lowerer {
5243 let code = format!("{}({}'u64, {}'u64)", f, (v >> 64) as u64, v as u64);5251 let code = format!("{}({}'u64, {}'u64)", f, (v >> 64) as u64, v as u64);
5244 return Ok(Val::new(code, Some(t)));5252 return Ok(Val::new(code, Some(t)));
5245 }5253 }
5246- Ok(Val::new(format!("{}'{}", digits, nim_suffix(&t)?), Some(t)))5254+ let suf = nim_suffix(&t)?;
5255+ let code = if suf.is_empty() {
5256+ digits
5257+ } else {
5258+ format!("{digits}'{suf}")
5259+ };
5260+ Ok(Val::new(code, Some(t)))
5247 }5261 }
5248 Lit::Float(f) => {5262 Lit::Float(f) => {
5249 let t = match f.suffix() {5263 let t = match f.suffix() {
@@ -5403,10 +5417,12 @@ impl Lowerer {
5403 fn cast(&mut self, c: &syn::ExprCast) -> Result<Val, String> {5417 fn cast(&mut self, c: &syn::ExprCast) -> Result<Val, String> {
5404 let v = self.expr(&c.expr)?;5418 let v = self.expr(&c.expr)?;
5405 let to = self.map_ty(&c.ty)?;5419 let to = self.map_ty(&c.ty)?;
5420+ let src = &c.expr;
5406 let from = v.ty.clone().ok_or_else(|| {5421 let from = v.ty.clone().ok_or_else(|| {
5407 format!(5422 format!(
5408- "cannot lower `as {}`: the source type is unknown, and `as` \5423+ "cannot lower `{} as {}`: the source type is unknown, and `as` \
5409 truncates, so the source width decides the result",5424 truncates, so the source width decides the result",
5425+ snippet(&quote::quote!(#src).to_string()),
5410 to.render()5426 to.render()
5411 )5427 )
5412 })?;5428 })?;
@@ -7660,7 +7676,9 @@ fn nim_suffix(t: &Nim) -> Result<&'static str, String> {
7660 "int16" => "i16",7676 "int16" => "i16",
7661 "int32" => "i32",7677 "int32" => "i32",
7662 "int64" => "i64",7678 "int64" => "i64",
7663- "int" => "i",7679+ // Nim has no `'i` suffix -- a bare integer literal already is `int`.
7680+ // `'u` does exist, so `uint` below keeps one.
7681+ "int" => "",
7664 "uint8" => "u8",7682 "uint8" => "u8",
7665 "uint16" => "u16",7683 "uint16" => "u16",
7666 "uint32" => "u32",7684 "uint32" => "u32",
added tests/cases/057-const-before-discriminant.rs +16 -0
new file mode 100644
@@ -0,0 +1,16 @@
1+// An enum discriminant naming a constant declared elsewhere, with a cast.
2+// Discriminants are lowered before constants are emitted, so the constant's
3+// type has to be known earlier than its value.
4+mod ffi {
5+ pub const BASE: isize = 10;
6+ pub const STRIDE: isize = 5;
7+}
8+
9+enum Step {
10+ First = ffi::BASE as isize,
11+ Second = (ffi::BASE + ffi::STRIDE) as isize,
12+}
13+
14+fn main() {
15+ println!("{} {}", Step::First as isize, Step::Second as isize);
16+}
new file mode 100644
@@ -0,0 +1,16 @@
1+// An enum discriminant naming a constant declared elsewhere, with a cast.
2+// Discriminants are lowered before constants are emitted, so the constant's
3+// type has to be known earlier than its value.
4+mod ffi {
5+ pub const BASE: isize = 10;
6+ pub const STRIDE: isize = 5;
7+}
8+
9+enum Step {
10+ First = ffi::BASE as isize,
11+ Second = (ffi::BASE + ffi::STRIDE) as isize,
12+}
13+
14+fn main() {
15+ println!("{} {}", Step::First as isize, Step::Second as isize);
16+}