diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 27bd219526..53229d525b 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -7381,7 +7381,13 @@ impl Codegen { return; } - self.write(&format!("const {}", node.name)); + // A module-level `var` is MUTABLE. The parser records that in + // `extra_mutable`, and the Zig local path already branches on it + // (`var` vs `const`); this path never did, so a spec that assigns to a + // module-level name emitted `const` and Zig refused the assignment. + // gen-verilog got this right all along -- it emits a `reg`. + let kw = if node.extra_mutable { "var" } else { "const" }; + self.write(&format!("{} {}", kw, node.name)); if !node.extra_type.is_empty() { self.write(&format!(": {}", Self::t27_array_type_to_zig(&node.extra_type))); @@ -17304,6 +17310,29 @@ impl CCodegen { } // Regular constant with expression value + // A module-level `var` is MUTABLE, and neither spelling below can hold + // an assignment: `#define counter 0` turns `counter = counter + 1` + // into `0 = (0 + 1)`, which is not C at all, and `static const` is + // rejected by the compiler. The parser records mutability in + // `extra_mutable`; this path never read it. gen-verilog has always + // emitted a `reg` here. + let c_type = if !node.extra_type.is_empty() { + Self::type_to_c(&node.extra_type).to_string() + } else { + "int".to_string() + }; + if node.extra_mutable { + self.write(&format!("static {} {} = ", c_type, node.name)); + if let Some(child) = node.children.first() { + self.gen_c_expr(child); + } else if !node.value.is_empty() { + self.write(&node.value); + } else { + self.write("0"); + } + self.write_line(";"); + return; + } if !node.children.is_empty() { let child = &node.children[0]; // Simple literal → #define @@ -17311,11 +17340,6 @@ impl CCodegen { self.write_line(&format!("#define {} {}", node.name, child.value)); } else { // Complex expression → static const - let c_type = if !node.extra_type.is_empty() { - Self::type_to_c(&node.extra_type).to_string() - } else { - "int".to_string() - }; self.write(&format!("static const {} {} = ", c_type, node.name)); self.gen_c_expr(child); self.write_line(";"); diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index 80d60036eb..d268b7fbd8 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -eb54f8a9b83f5f1a7d0d2a4b216deb5f99f4d110471a6c724072df422b89fd2a +b4ed38b1d79afb67e4c2547042227175adc74b5a2c0597b60829c1968ae40661 diff --git a/docs/now/2026-08-28-a-module-level-var-was-a-constant-in-three-backends.md b/docs/now/2026-08-28-a-module-level-var-was-a-constant-in-three-backends.md new file mode 100644 index 0000000000..898f79d2dc --- /dev/null +++ b/docs/now/2026-08-28-a-module-level-var-was-a-constant-in-three-backends.md @@ -0,0 +1,8 @@ +# NOW -- A module-level var was a constant in three backends (2026-08-28) + +## A module-level var was a constant in three backends (Refs #2161) + +- Refs #2161. The parser records mutability in extra_mutable and both backends already branch on it for function LOCALS -- Zig var/const, Rust let mut/let. The module-level paths never read it, so `var counter: u32 = 0` came out as `#define counter 0` in C, which turns `counter = counter + 1` into `0 = (0 + 1)` -- not C at all -- and as `const` in Zig, which refuses the assignment. gen-verilog emitted a reg and was right all along +- Measured over the 42 specs whose C output changes: compile errors 537 -> 277, specs with clean C 0 -> 19. specs/fpga/bpsk.t27, the BPSK modem merged in #1250, goes 7 errors to 0. A probe spec compiles and RUNS: two calls incrementing a module-level counter print 1 2 +- gen-rust deliberately NOT fixed: Rust has no safe module-level mutable. static mut needs unsafe at every access, AtomicU32 changes the API to load/store, thread_local! changes the semantics -- three different generated interfaces, which is a datapath decision and not an oversight. Measured first that all 43 such specs already emit non-compiling Rust for other reasons, so nothing regresses by leaving it +- Mid-task the worktree stopped being a git repository -- a parallel session pruned it while I held uncommitted edits. The FILES survived; recovery was to copy compiler.rs aside, prune, re-add the worktree on the same branch, and restore. Worth noting because the first symptom was a scan that printed "0 changed": git ls-files had failed and the empty selection read as a clean result, which is the empty-scan lie one more time