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

Collect items declared in function bodies, and unwrap derive const blocks

Two things stood between expanded libcosmic and serde's generated code.

`const _: () = { .. };` is what every derive macro emits, purely to give
`extern crate serde as _serde;` somewhere to live that cannot collide with the
user's names. rustnim read it as a const whose value was a block and gave up in
value position. The block is a scope in Rust, so nothing *declared* inside it
escapes -- what derive macros put there is a trait impl on an outer type, and
impls are not scoped. Unwrapped in both passes, with `extern crate` lowering to
nothing since paths are resolved by their last segment anyway.

`struct`/`enum` declared inside a function body: `serde`'s `deserialize` bodies
all declare an `enum __Field`. Rust scopes those to the body; Nim has no
function-local type namespace, so they are collected and declared at module
level instead, and `local_items` turns a genuine collision -- two different
types under one name -- into an error rather than a silent shadowing.

Also improves the error for an unsupported expression, which previously said
only "expression". It now names the kind and quotes the source text; "block
with statements -- `{ extern crate serde as _serde; ...`" is what found the
first of these.

tests/cases/051. Note the first draft of that case was wrong: it asserted a
struct declared inside `const _ = { .. }` was visible outside, which rustc
rejects. The case now tests what the construct actually does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nandithebull committed 2026-09-19T09:34:36-07:00 Browse files
32c48d0 parent: 2eee1c9
modified Cargo.lock +1 -0
@@ -140,6 +140,7 @@ dependencies = [
140140 "futures",
141141 "log",
142142 "proc-macro2",
143+ "quote",
143144 "syn",
144145 ]
145146
@@ -140,6 +140,7 @@ dependencies = [
140 "futures",140 "futures",
141 "log",141 "log",
142 "proc-macro2",142 "proc-macro2",
143+ "quote",
143 "syn",144 "syn",
144 ]145 ]
145 146
modified Cargo.toml +1 -0
@@ -4,6 +4,7 @@ version = "0.1.0"
44 edition = "2024"
55
66 [dependencies]
7+quote = "1"
78 proc-macro2 = "1"
89 syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] }
910
@@ -4,6 +4,7 @@ version = "0.1.0"
4 edition = "2024"4 edition = "2024"
5 5
6 [dependencies]6 [dependencies]
7+quote = "1"
7 proc-macro2 = "1"8 proc-macro2 = "1"
8 syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] }9 syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] }
9 10
modified src/lower.rs +167 -3
@@ -301,6 +301,13 @@ pub struct Lowerer {
301301 /// before the prelude is written.
302302 uses_sync: std::cell::Cell<bool>,
303303 uses_i128: std::cell::Cell<bool>,
304+ /// Function-local `struct`/`enum` declarations, by name and by the source
305+ /// text of the declaration. Nim has no function-local type namespace, so
306+ /// these land in the module's one namespace; two different types under one
307+ /// name would silently shadow, which is the failure this map exists to
308+ /// turn into an error.
309+ local_items: HashMap<String, String>,
310+ emitted_local: std::collections::HashSet<String>,
304311 /// Module names supplied as separate input files. A `mod x;` naming one
305312 /// of these is satisfied by that file having been passed in.
306313 pub modules: Vec<String>,
@@ -372,6 +379,8 @@ impl Lowerer {
372379 uses_async: false,
373380 uses_sync: std::cell::Cell::new(false),
374381 uses_i128: std::cell::Cell::new(false),
382+ local_items: HashMap::new(),
383+ emitted_local: std::collections::HashSet::new(),
375384 modules: Vec::new(),
376385 emitted: 0,
377386 features: Vec::new(),
@@ -616,6 +625,9 @@ impl Lowerer {
616625 }
617626 match item {
618627 Item::Fn(f) => {
628+ // A body may declare its own types, and a signature below may
629+ // name one, so these come first.
630+ self.collect_body_items(&f.block)?;
619631 let (params, ret) = self.signature(&f.sig)?;
620632 if matches!(ret, Nim::Future(_)) {
621633 self.uses_async = true;
@@ -775,6 +787,13 @@ impl Lowerer {
775787 EnumDef { name, simple, variants },
776788 );
777789 }
790+ Item::Const(c) => {
791+ if let Some(items) = derive_item_block(c) {
792+ for i in &items {
793+ self.collect(i)?;
794+ }
795+ }
796+ }
778797 Item::Impl(im) => {
779798 let outer_g =
780799 std::mem::replace(&mut self.impl_generics, Self::generics_of(&im.generics));
@@ -791,6 +810,13 @@ impl Lowerer {
791810 }
792811
793812 fn collect_impl(&mut self, im: &syn::ItemImpl, self_ty: &Nim) -> Result<(), String> {
813+ // Before anything else: a method body may declare its own types, and
814+ // the trait branches below return early.
815+ for it in &im.items {
816+ if let syn::ImplItem::Fn(m) = it {
817+ self.collect_body_items(&m.block)?;
818+ }
819+ }
794820 {
795821 let self_ty = self_ty.clone();
796822 let tyname = type_name(&self_ty);
@@ -1440,6 +1466,37 @@ impl Lowerer {
14401466 ty
14411467 }
14421468
1469+ /// Collect `struct` and `enum` declarations written inside a function
1470+ /// body. Rust scopes them to the body; Nim has no such scope, so they
1471+ /// become module-level types -- which is fine as long as the names do not
1472+ /// collide, and an error when they do. `serde`'s generated `deserialize`
1473+ /// bodies declare an `enum __Field`, which is what made this necessary.
1474+ fn collect_body_items(&mut self, b: &syn::Block) -> Result<(), String> {
1475+ for st in &b.stmts {
1476+ let Stmt::Item(i) = st else { continue };
1477+ let name = match i {
1478+ Item::Struct(s) => s.ident.to_string(),
1479+ Item::Enum(e) => e.ident.to_string(),
1480+ _ => continue,
1481+ };
1482+ let text = quote::quote!(#i).to_string();
1483+ if let Some(prev) = self.local_items.get(&name) {
1484+ if prev != &text {
1485+ return Err(format!(
1486+ "two different function-local types are both named \
1487+ `{name}`. Rust scopes each to its own body; Nim has \
1488+ no function-local type namespace, so one would \
1489+ silently shadow the other"
1490+ ));
1491+ }
1492+ continue;
1493+ }
1494+ self.local_items.insert(name, text);
1495+ self.collect(i)?;
1496+ }
1497+ Ok(())
1498+ }
1499+
14431500 fn signature(&self, sig: &syn::Signature) -> Result<(Vec<Nim>, Nim), String> {
14441501 // `unsafe fn` marks a contract for callers; it does not change what
14451502 // the body means, so it lowers like any other proc.
@@ -1489,9 +1546,26 @@ impl Lowerer {
14891546 return Ok(());
14901547 }
14911548 match item {
1549+ Item::Const(c) if derive_item_block(c).is_some() => {
1550+ for i in &derive_item_block(c).unwrap() {
1551+ self.item_types(i)?;
1552+ }
1553+ Ok(())
1554+ }
14921555 Item::Struct(_) | Item::Enum(_) | Item::Const(_) => self.item_inner(item),
14931556 Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => self.item_inner(item),
14941557 Item::ForeignMod(_) => self.item_inner(item),
1558+ // A function-local type becomes a module-level type, so it is
1559+ // declared in this pass with the rest of them, not in the body.
1560+ Item::Fn(f) => self.body_types(&f.block),
1561+ Item::Impl(im) => {
1562+ for it in &im.items {
1563+ if let syn::ImplItem::Fn(m) = it {
1564+ self.body_types(&m.block)?;
1565+ }
1566+ }
1567+ Ok(())
1568+ }
14951569 Item::Mod(m) if m.content.is_some() => {
14961570 let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default();
14971571 for i in &items {
@@ -1503,10 +1577,36 @@ impl Lowerer {
15031577 }
15041578 }
15051579
1580+ /// Emit the module-level declarations for types written inside a function
1581+ /// body. Guarded by `local_items`, which `collect_body_items` filled, so a
1582+ /// type reached from two places is declared once.
1583+ fn body_types(&mut self, b: &syn::Block) -> Result<(), String> {
1584+ for st in &b.stmts {
1585+ let Stmt::Item(i) = st else { continue };
1586+ let name = match i {
1587+ Item::Struct(s) => s.ident.to_string(),
1588+ Item::Enum(e) => e.ident.to_string(),
1589+ _ => continue,
1590+ };
1591+ if self.emitted_local.insert(name) {
1592+ self.item_inner(i)?;
1593+ }
1594+ }
1595+ Ok(())
1596+ }
1597+
15061598 fn item(&mut self, item: &Item) -> Result<(), String> {
15071599 if !self.cfg_keeps(item_attrs(item))? {
15081600 return Ok(());
15091601 }
1602+ if let Item::Const(c) = item {
1603+ if let Some(items) = derive_item_block(c) {
1604+ for i in &items {
1605+ self.item(i)?;
1606+ }
1607+ return Ok(());
1608+ }
1609+ }
15101610 // Types were emitted in their own pass.
15111611 if matches!(item, Item::Struct(_) | Item::Enum(_) | Item::Const(_)) {
15121612 return Ok(());
@@ -1568,10 +1668,22 @@ impl Lowerer {
15681668 Ok(())
15691669 }
15701670 Item::Enum(e) => {
1571- let def = self.enums[&e.ident.to_string()].clone();
1671+ // Collected in the first pass; if it is missing, the item was
1672+ // reached by a path the collector does not walk, and emitting
1673+ // a half-known enum would be worse than saying so.
1674+ let name = e.ident.to_string();
1675+ let def = self
1676+ .enums
1677+ .get(&name)
1678+ .cloned()
1679+ .ok_or_else(|| format!("`enum {name}` was never collected"))?;
15721680 self.emit_enum(&def);
15731681 Ok(())
15741682 }
1683+ // An `extern crate` is a name for path resolution only. Paths are
1684+ // resolved by their last segment here, so the alias carries no
1685+ // information and the declaration lowers to nothing.
1686+ Item::ExternCrate(_) => Ok(()),
15751687 Item::Const(c) => {
15761688 let t = self.map_ty(&c.ty)?.owned();
15771689 // The annotation types the initialiser, exactly as it does for
@@ -2608,6 +2720,10 @@ impl Lowerer {
26082720 // A `const` declared inside a function body is local to it, and
26092721 // must be emitted here rather than skipped as an already-emitted
26102722 // top-level type.
2723+ // A function-local `struct`/`enum` was already declared at module
2724+ // level by `body_types`; Nim would reject the `*` on a nested one
2725+ // anyway. Anything else declared in a body is emitted here.
2726+ Stmt::Item(Item::Struct(_) | Item::Enum(_)) => Ok(()),
26112727 Stmt::Item(i) => self.item_inner(i),
26122728 Stmt::Macro(m) => {
26132729 let line = self.macro_call(&m.mac)?;
@@ -4435,8 +4551,9 @@ impl Lowerer {
44354551 }
44364552 }
44374553 other => Err(format!(
4438- "unsupported expression in value position: {}",
4439- expr_kind(other)
4554+ "unsupported expression in value position: {} -- `{}`",
4555+ expr_kind(other),
4556+ snippet(&quote::quote!(#other).to_string()),
44404557 )),
44414558 }
44424559 }
@@ -6987,6 +7104,53 @@ fn expr_kind(e: &Expr) -> &'static str {
69877104 Expr::Let(_) => "`let` expression",
69887105 Expr::Unsafe(_) => "`unsafe` block",
69897106 Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => "loop (has no value in Nim)",
7107+ Expr::Closure(_) => "closure",
7108+ Expr::Block(_) => "block with statements",
7109+ Expr::Macro(_) => "macro invocation",
7110+ Expr::Const(_) => "`const` block",
7111+ Expr::TryBlock(_) => "`try` block",
7112+ Expr::Yield(_) => "`yield`",
7113+ Expr::Infer(_) => "`_`",
7114+ Expr::Verbatim(_) => "an expression syn could not parse",
7115+ Expr::Await(_) => "`.await`",
7116+ Expr::Break(_) | Expr::Continue(_) | Expr::Return(_) => "control flow",
7117+ Expr::RawAddr(_) => "`&raw`",
69907118 _ => "expression",
69917119 }
69927120 }
7121+
7122+/// A one-line excerpt of an expression, for error messages. `syn` gives no
7123+/// line numbers without a span-locating pass, so the text itself is the
7124+/// locator.
7125+fn snippet(s: &str) -> String {
7126+ let flat = s.split_whitespace().collect::<Vec<_>>().join(" ");
7127+ if flat.chars().count() > 120 {
7128+ format!("{}...", flat.chars().take(120).collect::<String>())
7129+ } else {
7130+ flat
7131+ }
7132+}
7133+
7134+/// `const _: () = { .. };` wrapping a set of items.
7135+///
7136+/// Every `derive` macro in the ecosystem emits this -- serde's is the one that
7137+/// forced it -- purely to give `extern crate serde as _serde;` somewhere to
7138+/// live that cannot collide with the user's names. There is no value here and
7139+/// no scope that matters: the items inside are module-level items, which is
7140+/// what Rust itself treats them as. Unwrapping is exact, not a shortcut.
7141+fn derive_item_block(c: &syn::ItemConst) -> Option<Vec<Item>> {
7142+ if c.ident != "_" || !matches!(&*c.ty, syn::Type::Tuple(t) if t.elems.is_empty()) {
7143+ return None;
7144+ }
7145+ let Expr::Block(b) = &*c.expr else { return None };
7146+ let items: Vec<Item> = b
7147+ .block
7148+ .stmts
7149+ .iter()
7150+ .filter_map(|s| match s {
7151+ Stmt::Item(i) => Some(i.clone()),
7152+ _ => None,
7153+ })
7154+ .collect();
7155+ if items.is_empty() { None } else { Some(items) }
7156+}
@@ -301,6 +301,13 @@ pub struct Lowerer {
301 /// before the prelude is written.301 /// before the prelude is written.
302 uses_sync: std::cell::Cell<bool>,302 uses_sync: std::cell::Cell<bool>,
303 uses_i128: std::cell::Cell<bool>,303 uses_i128: std::cell::Cell<bool>,
304+ /// Function-local `struct`/`enum` declarations, by name and by the source
305+ /// text of the declaration. Nim has no function-local type namespace, so
306+ /// these land in the module's one namespace; two different types under one
307+ /// name would silently shadow, which is the failure this map exists to
308+ /// turn into an error.
309+ local_items: HashMap<String, String>,
310+ emitted_local: std::collections::HashSet<String>,
304 /// Module names supplied as separate input files. A `mod x;` naming one311 /// Module names supplied as separate input files. A `mod x;` naming one
305 /// of these is satisfied by that file having been passed in.312 /// of these is satisfied by that file having been passed in.
306 pub modules: Vec<String>,313 pub modules: Vec<String>,
@@ -372,6 +379,8 @@ impl Lowerer {
372 uses_async: false,379 uses_async: false,
373 uses_sync: std::cell::Cell::new(false),380 uses_sync: std::cell::Cell::new(false),
374 uses_i128: std::cell::Cell::new(false),381 uses_i128: std::cell::Cell::new(false),
382+ local_items: HashMap::new(),
383+ emitted_local: std::collections::HashSet::new(),
375 modules: Vec::new(),384 modules: Vec::new(),
376 emitted: 0,385 emitted: 0,
377 features: Vec::new(),386 features: Vec::new(),
@@ -616,6 +625,9 @@ impl Lowerer {
616 }625 }
617 match item {626 match item {
618 Item::Fn(f) => {627 Item::Fn(f) => {
628+ // A body may declare its own types, and a signature below may
629+ // name one, so these come first.
630+ self.collect_body_items(&f.block)?;
619 let (params, ret) = self.signature(&f.sig)?;631 let (params, ret) = self.signature(&f.sig)?;
620 if matches!(ret, Nim::Future(_)) {632 if matches!(ret, Nim::Future(_)) {
621 self.uses_async = true;633 self.uses_async = true;
@@ -775,6 +787,13 @@ impl Lowerer {
775 EnumDef { name, simple, variants },787 EnumDef { name, simple, variants },
776 );788 );
777 }789 }
790+ Item::Const(c) => {
791+ if let Some(items) = derive_item_block(c) {
792+ for i in &items {
793+ self.collect(i)?;
794+ }
795+ }
796+ }
778 Item::Impl(im) => {797 Item::Impl(im) => {
779 let outer_g =798 let outer_g =
780 std::mem::replace(&mut self.impl_generics, Self::generics_of(&im.generics));799 std::mem::replace(&mut self.impl_generics, Self::generics_of(&im.generics));
@@ -791,6 +810,13 @@ impl Lowerer {
791 }810 }
792 811
793 fn collect_impl(&mut self, im: &syn::ItemImpl, self_ty: &Nim) -> Result<(), String> {812 fn collect_impl(&mut self, im: &syn::ItemImpl, self_ty: &Nim) -> Result<(), String> {
813+ // Before anything else: a method body may declare its own types, and
814+ // the trait branches below return early.
815+ for it in &im.items {
816+ if let syn::ImplItem::Fn(m) = it {
817+ self.collect_body_items(&m.block)?;
818+ }
819+ }
794 {820 {
795 let self_ty = self_ty.clone();821 let self_ty = self_ty.clone();
796 let tyname = type_name(&self_ty);822 let tyname = type_name(&self_ty);
@@ -1440,6 +1466,37 @@ impl Lowerer {
1440 ty1466 ty
1441 }1467 }
1442 1468
1469+ /// Collect `struct` and `enum` declarations written inside a function
1470+ /// body. Rust scopes them to the body; Nim has no such scope, so they
1471+ /// become module-level types -- which is fine as long as the names do not
1472+ /// collide, and an error when they do. `serde`'s generated `deserialize`
1473+ /// bodies declare an `enum __Field`, which is what made this necessary.
1474+ fn collect_body_items(&mut self, b: &syn::Block) -> Result<(), String> {
1475+ for st in &b.stmts {
1476+ let Stmt::Item(i) = st else { continue };
1477+ let name = match i {
1478+ Item::Struct(s) => s.ident.to_string(),
1479+ Item::Enum(e) => e.ident.to_string(),
1480+ _ => continue,
1481+ };
1482+ let text = quote::quote!(#i).to_string();
1483+ if let Some(prev) = self.local_items.get(&name) {
1484+ if prev != &text {
1485+ return Err(format!(
1486+ "two different function-local types are both named \
1487+ `{name}`. Rust scopes each to its own body; Nim has \
1488+ no function-local type namespace, so one would \
1489+ silently shadow the other"
1490+ ));
1491+ }
1492+ continue;
1493+ }
1494+ self.local_items.insert(name, text);
1495+ self.collect(i)?;
1496+ }
1497+ Ok(())
1498+ }
1499+
1443 fn signature(&self, sig: &syn::Signature) -> Result<(Vec<Nim>, Nim), String> {1500 fn signature(&self, sig: &syn::Signature) -> Result<(Vec<Nim>, Nim), String> {
1444 // `unsafe fn` marks a contract for callers; it does not change what1501 // `unsafe fn` marks a contract for callers; it does not change what
1445 // the body means, so it lowers like any other proc.1502 // the body means, so it lowers like any other proc.
@@ -1489,9 +1546,26 @@ impl Lowerer {
1489 return Ok(());1546 return Ok(());
1490 }1547 }
1491 match item {1548 match item {
1549+ Item::Const(c) if derive_item_block(c).is_some() => {
1550+ for i in &derive_item_block(c).unwrap() {
1551+ self.item_types(i)?;
1552+ }
1553+ Ok(())
1554+ }
1492 Item::Struct(_) | Item::Enum(_) | Item::Const(_) => self.item_inner(item),1555 Item::Struct(_) | Item::Enum(_) | Item::Const(_) => self.item_inner(item),
1493 Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => self.item_inner(item),1556 Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => self.item_inner(item),
1494 Item::ForeignMod(_) => self.item_inner(item),1557 Item::ForeignMod(_) => self.item_inner(item),
1558+ // A function-local type becomes a module-level type, so it is
1559+ // declared in this pass with the rest of them, not in the body.
1560+ Item::Fn(f) => self.body_types(&f.block),
1561+ Item::Impl(im) => {
1562+ for it in &im.items {
1563+ if let syn::ImplItem::Fn(m) = it {
1564+ self.body_types(&m.block)?;
1565+ }
1566+ }
1567+ Ok(())
1568+ }
1495 Item::Mod(m) if m.content.is_some() => {1569 Item::Mod(m) if m.content.is_some() => {
1496 let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default();1570 let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default();
1497 for i in &items {1571 for i in &items {
@@ -1503,10 +1577,36 @@ impl Lowerer {
1503 }1577 }
1504 }1578 }
1505 1579
1580+ /// Emit the module-level declarations for types written inside a function
1581+ /// body. Guarded by `local_items`, which `collect_body_items` filled, so a
1582+ /// type reached from two places is declared once.
1583+ fn body_types(&mut self, b: &syn::Block) -> Result<(), String> {
1584+ for st in &b.stmts {
1585+ let Stmt::Item(i) = st else { continue };
1586+ let name = match i {
1587+ Item::Struct(s) => s.ident.to_string(),
1588+ Item::Enum(e) => e.ident.to_string(),
1589+ _ => continue,
1590+ };
1591+ if self.emitted_local.insert(name) {
1592+ self.item_inner(i)?;
1593+ }
1594+ }
1595+ Ok(())
1596+ }
1597+
1506 fn item(&mut self, item: &Item) -> Result<(), String> {1598 fn item(&mut self, item: &Item) -> Result<(), String> {
1507 if !self.cfg_keeps(item_attrs(item))? {1599 if !self.cfg_keeps(item_attrs(item))? {
1508 return Ok(());1600 return Ok(());
1509 }1601 }
1602+ if let Item::Const(c) = item {
1603+ if let Some(items) = derive_item_block(c) {
1604+ for i in &items {
1605+ self.item(i)?;
1606+ }
1607+ return Ok(());
1608+ }
1609+ }
1510 // Types were emitted in their own pass.1610 // Types were emitted in their own pass.
1511 if matches!(item, Item::Struct(_) | Item::Enum(_) | Item::Const(_)) {1611 if matches!(item, Item::Struct(_) | Item::Enum(_) | Item::Const(_)) {
1512 return Ok(());1612 return Ok(());
@@ -1568,10 +1668,22 @@ impl Lowerer {
1568 Ok(())1668 Ok(())
1569 }1669 }
1570 Item::Enum(e) => {1670 Item::Enum(e) => {
1571- let def = self.enums[&e.ident.to_string()].clone();1671+ // Collected in the first pass; if it is missing, the item was
1672+ // reached by a path the collector does not walk, and emitting
1673+ // a half-known enum would be worse than saying so.
1674+ let name = e.ident.to_string();
1675+ let def = self
1676+ .enums
1677+ .get(&name)
1678+ .cloned()
1679+ .ok_or_else(|| format!("`enum {name}` was never collected"))?;
1572 self.emit_enum(&def);1680 self.emit_enum(&def);
1573 Ok(())1681 Ok(())
1574 }1682 }
1683+ // An `extern crate` is a name for path resolution only. Paths are
1684+ // resolved by their last segment here, so the alias carries no
1685+ // information and the declaration lowers to nothing.
1686+ Item::ExternCrate(_) => Ok(()),
1575 Item::Const(c) => {1687 Item::Const(c) => {
1576 let t = self.map_ty(&c.ty)?.owned();1688 let t = self.map_ty(&c.ty)?.owned();
1577 // The annotation types the initialiser, exactly as it does for1689 // The annotation types the initialiser, exactly as it does for
@@ -2608,6 +2720,10 @@ impl Lowerer {
2608 // A `const` declared inside a function body is local to it, and2720 // A `const` declared inside a function body is local to it, and
2609 // must be emitted here rather than skipped as an already-emitted2721 // must be emitted here rather than skipped as an already-emitted
2610 // top-level type.2722 // top-level type.
2723+ // A function-local `struct`/`enum` was already declared at module
2724+ // level by `body_types`; Nim would reject the `*` on a nested one
2725+ // anyway. Anything else declared in a body is emitted here.
2726+ Stmt::Item(Item::Struct(_) | Item::Enum(_)) => Ok(()),
2611 Stmt::Item(i) => self.item_inner(i),2727 Stmt::Item(i) => self.item_inner(i),
2612 Stmt::Macro(m) => {2728 Stmt::Macro(m) => {
2613 let line = self.macro_call(&m.mac)?;2729 let line = self.macro_call(&m.mac)?;
@@ -4435,8 +4551,9 @@ impl Lowerer {
4435 }4551 }
4436 }4552 }
4437 other => Err(format!(4553 other => Err(format!(
4438- "unsupported expression in value position: {}",4554+ "unsupported expression in value position: {} -- `{}`",
4439- expr_kind(other)4555+ expr_kind(other),
4556+ snippet(&quote::quote!(#other).to_string()),
4440 )),4557 )),
4441 }4558 }
4442 }4559 }
@@ -6987,6 +7104,53 @@ fn expr_kind(e: &Expr) -> &'static str {
6987 Expr::Let(_) => "`let` expression",7104 Expr::Let(_) => "`let` expression",
6988 Expr::Unsafe(_) => "`unsafe` block",7105 Expr::Unsafe(_) => "`unsafe` block",
6989 Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => "loop (has no value in Nim)",7106 Expr::Loop(_) | Expr::While(_) | Expr::ForLoop(_) => "loop (has no value in Nim)",
7107+ Expr::Closure(_) => "closure",
7108+ Expr::Block(_) => "block with statements",
7109+ Expr::Macro(_) => "macro invocation",
7110+ Expr::Const(_) => "`const` block",
7111+ Expr::TryBlock(_) => "`try` block",
7112+ Expr::Yield(_) => "`yield`",
7113+ Expr::Infer(_) => "`_`",
7114+ Expr::Verbatim(_) => "an expression syn could not parse",
7115+ Expr::Await(_) => "`.await`",
7116+ Expr::Break(_) | Expr::Continue(_) | Expr::Return(_) => "control flow",
7117+ Expr::RawAddr(_) => "`&raw`",
6990 _ => "expression",7118 _ => "expression",
6991 }7119 }
6992 }7120 }
7121+
7122+/// A one-line excerpt of an expression, for error messages. `syn` gives no
7123+/// line numbers without a span-locating pass, so the text itself is the
7124+/// locator.
7125+fn snippet(s: &str) -> String {
7126+ let flat = s.split_whitespace().collect::<Vec<_>>().join(" ");
7127+ if flat.chars().count() > 120 {
7128+ format!("{}...", flat.chars().take(120).collect::<String>())
7129+ } else {
7130+ flat
7131+ }
7132+}
7133+
7134+/// `const _: () = { .. };` wrapping a set of items.
7135+///
7136+/// Every `derive` macro in the ecosystem emits this -- serde's is the one that
7137+/// forced it -- purely to give `extern crate serde as _serde;` somewhere to
7138+/// live that cannot collide with the user's names. There is no value here and
7139+/// no scope that matters: the items inside are module-level items, which is
7140+/// what Rust itself treats them as. Unwrapping is exact, not a shortcut.
7141+fn derive_item_block(c: &syn::ItemConst) -> Option<Vec<Item>> {
7142+ if c.ident != "_" || !matches!(&*c.ty, syn::Type::Tuple(t) if t.elems.is_empty()) {
7143+ return None;
7144+ }
7145+ let Expr::Block(b) = &*c.expr else { return None };
7146+ let items: Vec<Item> = b
7147+ .block
7148+ .stmts
7149+ .iter()
7150+ .filter_map(|s| match s {
7151+ Stmt::Item(i) => Some(i.clone()),
7152+ _ => None,
7153+ })
7154+ .collect();
7155+ if items.is_empty() { None } else { Some(items) }
7156+}
added tests/cases/051-fn-local-items.rs +46 -0
new file mode 100644
@@ -0,0 +1,46 @@
1+// Items declared inside a function body. Rust scopes them to the body; Nim has
2+// no function-local type namespace, so they become module-level types. This is
3+// how every `serde` `deserialize` body is shaped.
4+fn classify(n: u32) -> String {
5+ enum Kind {
6+ Small,
7+ Large,
8+ }
9+
10+ struct Tagged {
11+ kind: Kind,
12+ value: u32,
13+ }
14+
15+ let t = Tagged {
16+ kind: if n < 10 { Kind::Small } else { Kind::Large },
17+ value: n,
18+ };
19+
20+ match t.kind {
21+ Kind::Small => format!("small({})", t.value),
22+ Kind::Large => format!("large({})", t.value),
23+ }
24+}
25+
26+// A `const _: () = { .. }` wrapper, which is what every derive macro emits so
27+// its `extern crate` alias cannot collide. There is no value here: the items
28+// inside are module-level items.
29+// The block is a scope in Rust too, so nothing declared *inside* it escapes.
30+// What derive macros put here is an `impl` on a type declared outside, which
31+// does escape -- trait impls are not scoped. That is the shape below.
32+struct Counter(u32);
33+
34+const _: () = {
35+ impl Counter {
36+ pub fn doubled(&self) -> u32 {
37+ self.0 * 2
38+ }
39+ }
40+};
41+
42+fn main() {
43+ println!("{}", classify(3));
44+ println!("{}", classify(30));
45+ println!("{}", Counter(21).doubled());
46+}
new file mode 100644
@@ -0,0 +1,46 @@
1+// Items declared inside a function body. Rust scopes them to the body; Nim has
2+// no function-local type namespace, so they become module-level types. This is
3+// how every `serde` `deserialize` body is shaped.
4+fn classify(n: u32) -> String {
5+ enum Kind {
6+ Small,
7+ Large,
8+ }
9+
10+ struct Tagged {
11+ kind: Kind,
12+ value: u32,
13+ }
14+
15+ let t = Tagged {
16+ kind: if n < 10 { Kind::Small } else { Kind::Large },
17+ value: n,
18+ };
19+
20+ match t.kind {
21+ Kind::Small => format!("small({})", t.value),
22+ Kind::Large => format!("large({})", t.value),
23+ }
24+}
25+
26+// A `const _: () = { .. }` wrapper, which is what every derive macro emits so
27+// its `extern crate` alias cannot collide. There is no value here: the items
28+// inside are module-level items.
29+// The block is a scope in Rust too, so nothing declared *inside* it escapes.
30+// What derive macros put here is an `impl` on a type declared outside, which
31+// does escape -- trait impls are not scoped. That is the shape below.
32+struct Counter(u32);
33+
34+const _: () = {
35+ impl Counter {
36+ pub fn doubled(&self) -> u32 {
37+ self.0 * 2
38+ }
39+ }
40+};
41+
42+fn main() {
43+ println!("{}", classify(3));
44+ println!("{}", classify(30));
45+ println!("{}", Counter(21).doubled());
46+}