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>32c48d0 parent: 2eee1c9 modified
Cargo.lock +1 -0 | @@ -140,6 +140,7 @@ dependencies = [ | ||
| 140 | 140 | "futures", |
| 141 | 141 | "log", |
| 142 | 142 | "proc-macro2", |
| 143 | + "quote", | |
| 143 | 144 | "syn", |
| 144 | 145 | ] |
| 145 | 146 | |
| @@ -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" | ||
| 4 | 4 | edition = "2024" |
| 5 | 5 | |
| 6 | 6 | [dependencies] |
| 7 | +quote = "1" | |
| 7 | 8 | proc-macro2 = "1" |
| 8 | 9 | syn = { version = "3.0.5", features = ["full", "extra-traits", "visit"] } |
| 9 | 10 | |
| @@ -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 { | ||
| 301 | 301 | /// before the prelude is written. |
| 302 | 302 | uses_sync: std::cell::Cell<bool>, |
| 303 | 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 | 311 | /// Module names supplied as separate input files. A `mod x;` naming one |
| 305 | 312 | /// of these is satisfied by that file having been passed in. |
| 306 | 313 | pub modules: Vec<String>, |
| @@ -372,6 +379,8 @@ impl Lowerer { | ||
| 372 | 379 | uses_async: false, |
| 373 | 380 | uses_sync: std::cell::Cell::new(false), |
| 374 | 381 | uses_i128: std::cell::Cell::new(false), |
| 382 | + local_items: HashMap::new(), | |
| 383 | + emitted_local: std::collections::HashSet::new(), | |
| 375 | 384 | modules: Vec::new(), |
| 376 | 385 | emitted: 0, |
| 377 | 386 | features: Vec::new(), |
| @@ -616,6 +625,9 @@ impl Lowerer { | ||
| 616 | 625 | } |
| 617 | 626 | match item { |
| 618 | 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 | 631 | let (params, ret) = self.signature(&f.sig)?; |
| 620 | 632 | if matches!(ret, Nim::Future(_)) { |
| 621 | 633 | self.uses_async = true; |
| @@ -775,6 +787,13 @@ impl Lowerer { | ||
| 775 | 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 | 797 | Item::Impl(im) => { |
| 779 | 798 | let outer_g = |
| 780 | 799 | std::mem::replace(&mut self.impl_generics, Self::generics_of(&im.generics)); |
| @@ -791,6 +810,13 @@ impl Lowerer { | ||
| 791 | 810 | } |
| 792 | 811 | |
| 793 | 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 | 821 | let self_ty = self_ty.clone(); |
| 796 | 822 | let tyname = type_name(&self_ty); |
| @@ -1440,6 +1466,37 @@ impl Lowerer { | ||
| 1440 | 1466 | 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 | 1500 | fn signature(&self, sig: &syn::Signature) -> Result<(Vec<Nim>, Nim), String> { |
| 1444 | 1501 | // `unsafe fn` marks a contract for callers; it does not change what |
| 1445 | 1502 | // the body means, so it lowers like any other proc. |
| @@ -1489,9 +1546,26 @@ impl Lowerer { | ||
| 1489 | 1546 | return Ok(()); |
| 1490 | 1547 | } |
| 1491 | 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 | 1555 | Item::Struct(_) | Item::Enum(_) | Item::Const(_) => self.item_inner(item), |
| 1493 | 1556 | Item::Macro(m) if path_name(&m.mac.path) == "bitflags" => self.item_inner(item), |
| 1494 | 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 | 1569 | Item::Mod(m) if m.content.is_some() => { |
| 1496 | 1570 | let items = m.content.as_ref().map(|(_, i)| i.clone()).unwrap_or_default(); |
| 1497 | 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 | 1598 | fn item(&mut self, item: &Item) -> Result<(), String> { |
| 1507 | 1599 | if !self.cfg_keeps(item_attrs(item))? { |
| 1508 | 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 | 1610 | // Types were emitted in their own pass. |
| 1511 | 1611 | if matches!(item, Item::Struct(_) | Item::Enum(_) | Item::Const(_)) { |
| 1512 | 1612 | return Ok(()); |
| @@ -1568,10 +1668,22 @@ impl Lowerer { | ||
| 1568 | 1668 | Ok(()) |
| 1569 | 1669 | } |
| 1570 | 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 | 1680 | self.emit_enum(&def); |
| 1573 | 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 | 1687 | Item::Const(c) => { |
| 1576 | 1688 | let t = self.map_ty(&c.ty)?.owned(); |
| 1577 | 1689 | // The annotation types the initialiser, exactly as it does for |
| @@ -2608,6 +2720,10 @@ impl Lowerer { | ||
| 2608 | 2720 | // A `const` declared inside a function body is local to it, and |
| 2609 | 2721 | // must be emitted here rather than skipped as an already-emitted |
| 2610 | 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 | 2727 | Stmt::Item(i) => self.item_inner(i), |
| 2612 | 2728 | Stmt::Macro(m) => { |
| 2613 | 2729 | let line = self.macro_call(&m.mac)?; |
| @@ -4435,8 +4551,9 @@ impl Lowerer { | ||
| 4435 | 4551 | } |
| 4436 | 4552 | } |
| 4437 | 4553 | 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("e::quote!(#other).to_string()), | |
| 4440 | 4557 | )), |
| 4441 | 4558 | } |
| 4442 | 4559 | } |
| @@ -6987,6 +7104,53 @@ fn expr_kind(e: &Expr) -> &'static str { | ||
| 6987 | 7104 | Expr::Let(_) => "`let` expression", |
| 6988 | 7105 | Expr::Unsafe(_) => "`unsafe` block", |
| 6989 | 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 | 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 | +} | |
| @@ -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 one | 311 | /// 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 | ty | 1466 | 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 what | 1501 | // `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 for | 1689 | // 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, and | 2720 | // A `const` declared inside a function body is local to it, and |
| 2609 | // must be emitted here rather than skipped as an already-emitted | 2721 | // 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("e::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 | +} | ||