From ee18729f702523ad74e34a6582041e39a664ebf8 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Fri, 28 Aug 2026 01:38:02 +0700 Subject: [PATCH 1/3] parser: a hyphen is part of the name in a `use` path too (Refs #2161) Module NAMES have accepted hyphens since the module-declaration parser was written. `use` paths never did, so use tritype-base::Trit; read the import as `tritype` and left `-base::Trit` behind as a module-level expression statement. That phantom reached gen-verilog as -base_Trit; a line the simulator rejects, and no diagnostic mentions it because from the parser's side nothing went wrong: the `use` parsed, the leftover parsed, and both were accepted. `read_hyphenated_ident` factors the loop the module parser already has and is called at both places a `use` segment is read -- the first one and each one after `::`. The braced form `use a-b::{X, Y}` works for the same reason: `full_path` now ends in `::` before the `{`, which is exactly the precondition the W630 braced-import block already tests. Measured over 746 tracked specs, master binary vs this one: phantom `-name;` statements in generated Verilog 28 -> 0 specs emitting one 11 -> 0 specs whose Verilog output changes 11 specs that parse 620 -> 620 t27c tests 1629/6 -> 1629/6 The parse count does not move: 18 specs carry a hyphenated `use`, and the three that fail `parse` fail on defects behind this one. What this fixes is the silent half -- eleven specs that parsed, generated, and shipped invalid Verilog. FROZEN_HASH resealed in the same commit (M5). --- bootstrap/src/compiler.rs | 34 ++++++++++++++++++++++++++++++---- bootstrap/stage0/FROZEN_HASH | 2 +- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 53229d525b..deef78d39d 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -1614,9 +1614,8 @@ impl Parser { let mut full_path = String::new(); let mut alias_name = String::new(); if self.current.kind == TokenKind::Ident { - let first_ident = self.current.lexeme.clone(); + let first_ident = self.read_hyphenated_ident(); full_path.push_str(&first_ident); - self.advance(); // Check for aliased import: using name: @import("path"); if self.current.kind == TokenKind::Colon && self.peek.kind != TokenKind::Colon { @@ -1662,8 +1661,7 @@ impl Parser { break; } if self.current.kind == TokenKind::Ident { - full_path.push_str(&self.current.lexeme); - self.advance(); + full_path.push_str(&self.read_hyphenated_ident()); } else { break; } @@ -3917,6 +3915,34 @@ impl Parser { /// Parse for statement: for (iterable) |capture| { body } /// Also: for i in start..end { body } (range for) + /// Read an identifier that may carry hyphens: `tritype-base`. + /// + /// Module NAMES have accepted this since the module-declaration parser was + /// written; `use` paths never did. `use tritype-base::Trit;` therefore read + /// the import as `tritype`, left `-base::Trit` behind as a module-level + /// expression statement, and that phantom reached gen-verilog as + /// `-base_Trit;` -- a line the simulator rejects and that no diagnostic + /// mentions, because from the parser's side nothing went wrong. + /// + /// Assumes `self.current` is the leading Ident and consumes it. + fn read_hyphenated_ident(&mut self) -> String { + let mut name = String::new(); + if self.current.kind != TokenKind::Ident { + return name; + } + name.push_str(&self.current.lexeme); + self.advance(); + while self.current.kind == TokenKind::Minus + && matches!(self.peek.kind, TokenKind::Ident | TokenKind::Number) + { + name.push('-'); + self.advance(); // consume '-' + name.push_str(&self.current.lexeme); + self.advance(); + } + name + } + fn parse_for_stmt(&mut self) -> Result { self.advance(); // consume 'for' diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index d268b7fbd8..25a0dad32b 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -b4ed38b1d79afb67e4c2547042227175adc74b5a2c0597b60829c1968ae40661 +9ab1510688f2e88b4fd16069a8f3bce3e7faec6287dcdd61eb0fe6aa48bca72b From 810946fdd824ccf81c7e1aff2a5af1cab04fd2e1 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Fri, 28 Aug 2026 01:46:00 +0700 Subject: [PATCH 2/3] gen-rust: a module-level `var` is `static mut`, and its readers are unsafe (Refs #2161, closes #2731) I filed #2731 saying this needed an owner decision because Rust has no safe mutable global. Re-reading the other three backends settles it: they already agree. gen-verilog lowers a module-level `var` to a `reg`, gen-c to a `static`, Zig to a `var` -- all three mean SHARED mutable state. Of the three Rust candidates, only `static mut` means that; `AtomicU32` changes the API and `thread_local!` changes the semantics to per-thread, which would make Rust the one backend disagreeing about what the source says. So the decision was already in the tree, in the form of what the other three do. #2731 asked a question the repository had answered. pub static mut counter: u32 = 0; pub fn bump() -> u32 { unsafe { counter = (counter + 1); return counter; } } Every access to a `static mut` is unsafe. Wrapping the whole body is the smallest correct answer -- the alternative needs the expression emitter to know the name set at each site. `static_mut_names` is collected in a PRE-PASS, because a function can be emitted before the declaration it reads. Measured over the 43 specs whose Rust output this changes: rustc errors 921 -> 760 specs clean 0 -> 0 The second row is the honest one: not one of these specs compiles yet, because they carry other defects -- Zig builtins leaking into the Rust output chief among them. What this fixes is the declaration and its readers, which were wrong on their own terms. A function that touches no module-level mutable is emitted byte for byte as before -- checked. No regressions: parse 620/746 unchanged, tests 1629 passed / 6 failed unchanged, RATCHET: CLEAN. FROZEN_HASH resealed in the same commit (M5). --- bootstrap/src/compiler.rs | 55 ++++++++++++++++++++++++++++++++++-- bootstrap/stage0/FROZEN_HASH | 2 +- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index deef78d39d..7729c49034 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -19001,6 +19001,7 @@ impl Compiler { // and sufficient here. Verilog/Zig/C backends keep their own pipelines. // Fixes gHashTag/t27#1455. let mut codegen = RustCodegen::new(); + codegen.collect_static_muts(&ast); codegen.gen_rust(&ast); Ok(codegen.into_string()) } @@ -21104,6 +21105,14 @@ pub struct RustCodegen { /// `Verdict::escalate`. Without knowing which identifiers name enums the /// emitter cannot tell that access apart from a struct field. enum_names: std::collections::HashSet, + /// Module-level names declared `var`, i.e. MUTABLE. + /// + /// Rust has no safe mutable global. gen-verilog lowers this to a `reg`, + /// gen-c to a `static`, Zig to a `var` -- all three mean SHARED mutable + /// state, so Rust's answer has to be `static mut`, and every access to one + /// needs `unsafe`. Collected in a pre-pass because a function may be + /// emitted before the declaration it reads. + static_mut_names: std::collections::HashSet, } #[allow(dead_code)] @@ -21120,6 +21129,7 @@ impl RustCodegen { const_types: std::collections::HashMap::new(), fn_ret_types: std::collections::HashMap::new(), enum_names: std::collections::HashSet::new(), + static_mut_names: std::collections::HashSet::new(), } } @@ -21265,6 +21275,25 @@ impl RustCodegen { self.blank_line(); } + /// Record every module-level `var` before anything is emitted. + fn collect_static_muts(&mut self, node: &Node) { + if node.kind == NodeKind::ConstDecl && node.extra_mutable && !node.name.is_empty() { + self.static_mut_names.insert(node.name.clone()); + } + for c in &node.children { + self.collect_static_muts(c); + } + } + + /// Does this subtree name a module-level mutable? Then its enclosing + /// function body has to be `unsafe` in Rust. + fn touches_static_mut(&self, node: &Node) -> bool { + if self.static_mut_names.contains(&node.name) { + return true; + } + node.children.iter().any(|c| self.touches_static_mut(c)) + } + fn gen_enum(&mut self, node: &Node) { // Recorded before the body is written, so a member referenced inside // the same module resolves however the declarations are ordered. @@ -21298,9 +21327,18 @@ impl RustCodegen { } else { self.expr_to_rust(&node.children[0]) }; + // A module-level `var` is MUTABLE. gen-verilog lowers it to a `reg`, + // gen-c to a `static`, Zig to a `var` -- all three mean shared mutable + // state, so Rust's answer is `static mut`, and accesses need `unsafe` + // (added around the body of any function that touches one). + let kw = if node.extra_mutable { + "pub static mut" + } else { + "pub const" + }; self.write_line(&format!( - "pub const {}: {} = {};", - node.name, const_type, value + "{} {}: {} = {};", + kw, node.name, const_type, value )); self.blank_line(); } @@ -21358,9 +21396,18 @@ impl RustCodegen { // type always wins over an inferred one. self.record_inferred_locals(&node.children); + // Any access to a `static mut` is unsafe in Rust. Wrapping the whole + // body is the smallest correct answer: the alternative is an `unsafe` + // block around every read and write, which needs the expression + // emitter to know the set at each site. + let body_unsafe = node.children.iter().any(|c| self.touches_static_mut(c)); if has_body { self.output.push('\n'); self.indent += 1; + if body_unsafe { + self.write_line("unsafe {"); + self.indent += 1; + } for child in &node.children { match child.kind { NodeKind::ExprReturn => { @@ -21525,6 +21572,10 @@ impl RustCodegen { _ => {} } } + if body_unsafe { + self.indent -= 1; + self.write_line("}"); + } self.indent -= 1; self.write_line("}"); } else { diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index 25a0dad32b..f475699756 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -9ab1510688f2e88b4fd16069a8f3bce3e7faec6287dcdd61eb0fe6aa48bca72b +ff31ebbf36eb1b0ec4a370c8132f27f5d6c79b8d0b2d2c4f8e504b2d42f0f9c2 From 41fd0b5129c4ca7c0d789418e28c0038927902df Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Fri, 28 Aug 2026 01:46:48 +0700 Subject: [PATCH 3/3] docs/now: the decision was already in the tree (Refs #2161) --- ...ion-was-already-in-the-tree-in-what-the-other-three.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 docs/now/2026-08-28-the-decision-was-already-in-the-tree-in-what-the-other-three.md diff --git a/docs/now/2026-08-28-the-decision-was-already-in-the-tree-in-what-the-other-three.md b/docs/now/2026-08-28-the-decision-was-already-in-the-tree-in-what-the-other-three.md new file mode 100644 index 0000000000..e94e874c40 --- /dev/null +++ b/docs/now/2026-08-28-the-decision-was-already-in-the-tree-in-what-the-other-three.md @@ -0,0 +1,8 @@ +# NOW -- The decision was already in the tree, in what the other three backends do (2026-08-28) + +## The decision was already in the tree, in what the other three backends do (Refs #2161) + +- Refs #2161, #2731. I filed #2731 saying gen-rust needed an owner decision because Rust has no safe mutable global. Re-reading the other three settles it: gen-verilog lowers a module-level var to a reg, gen-c to a static, Zig to a var -- all three mean SHARED mutable state. Of the three Rust candidates only `static mut` means that; AtomicU32 changes the API and thread_local! changes the semantics to per-thread. The repository had already answered the question I asked it +- Every access to a static mut is unsafe, so the function body is wrapped. static_mut_names is collected in a PRE-PASS because a function can be emitted before the declaration it reads. Verified end to end: the generated Rust compiles and prints 1 2, the same answer the C does +- Measured over the 43 specs whose Rust changes: rustc errors 921 -> 760, specs fully clean 0 -> 0. The second number is the honest one -- none of them compiles yet because of other defects, Zig builtins leaking into Rust chief among them. What this fixes is the declaration and its readers, which were wrong on their own terms +- Separately: a hyphen is now part of the name in a `use` path, as it already was in a module declaration. `use tritype-base::Trit;` read the import as `tritype` and left `-base::Trit` as a module-level statement that reached gen-verilog as `-base_Trit;`. Phantom statements in generated Verilog 28 -> 0 across 11 specs; no diagnostic had ever mentioned them because from the parser side nothing went wrong