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>
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 | ||
| 1246 | 1246 | rustnim was not reaching for. And the last two only appear at scale: a single |
| 1247 | 1247 | crate never puts two different `__Field`s in one namespace, so the 3M-line run |
| 1248 | 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. | |
| @@ -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 single | 1246 | 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 run | 1247 | 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 { | ||
| 763 | 763 | for i in &items { |
| 764 | 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 | 776 | Item::Enum(e) => { |
| @@ -5243,7 +5251,13 @@ impl Lowerer { | ||
| 5243 | 5251 | let code = format!("{}({}'u64, {}'u64)", f, (v >> 64) as u64, v as u64); |
| 5244 | 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 | 5262 | Lit::Float(f) => { |
| 5249 | 5263 | let t = match f.suffix() { |
| @@ -5403,10 +5417,12 @@ impl Lowerer { | ||
| 5403 | 5417 | fn cast(&mut self, c: &syn::ExprCast) -> Result<Val, String> { |
| 5404 | 5418 | let v = self.expr(&c.expr)?; |
| 5405 | 5419 | let to = self.map_ty(&c.ty)?; |
| 5420 | + let src = &c.expr; | |
| 5406 | 5421 | let from = v.ty.clone().ok_or_else(|| { |
| 5407 | 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 | 5424 | truncates, so the source width decides the result", |
| 5425 | + snippet("e::quote!(#src).to_string()), | |
| 5410 | 5426 | to.render() |
| 5411 | 5427 | ) |
| 5412 | 5428 | })?; |
| @@ -7660,7 +7676,9 @@ fn nim_suffix(t: &Nim) -> Result<&'static str, String> { | ||
| 7660 | 7676 | "int16" => "i16", |
| 7661 | 7677 | "int32" => "i32", |
| 7662 | 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 | 7682 | "uint8" => "u8", |
| 7665 | 7683 | "uint16" => "u16", |
| 7666 | 7684 | "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("e::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 | +} | ||