From c8b128a4cd4adc6083790e24f48881ba561c3923 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Sun, 9 Aug 2026 05:37:16 +0700 Subject: [PATCH] fix(gen-verilog): test-block for-loops, no CSE of loop-variant calls, tail expressions Refs #1948 Three linked test-block/fn-body defects: - `for i in ..`/`while` in a test block was DROPPED as `// (stmt: StmtForRange)`, silently voiding loop bodies that accumulate assertions. Loops emit now; the loop variable is declared `integer` at the block top. - The call-CSE pass hoisted a call from INSIDE a loop into one temp evaluated once before the loop with an uninitialized index, so the loop tested a constant. predeclare_call_array_tmps no longer recurses into loop bodies -- loop-variant calls are emitted inline per iteration. - A Rust-style tail expression (a fn body ending in a bare expression, no `return`) lowered to a bare `expr;` that iverilog read as an unknown-task enable; it lowers to ` = ;` now. tri-net corpus: icarus 97 -> 98 (m3_multihop). Top-level `if` in test blocks stays dropped as before (asserts inside need the TB assert-lowering -- out of scope, no regression). bridge.v regenerated; unit suite at the single pre-existing red. FROZEN_HASH resealed. Co-Authored-By: Claude Fable 5 --- NOW.md | 8 ++++++ bootstrap/src/compiler.rs | 55 ++++++++++++++++++++++++++++++++++++ bootstrap/stage0/FROZEN_HASH | 2 +- docs/NOW.md | 8 ++++++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/NOW.md b/NOW.md index dd653a317..b4c3a73ad 100644 --- a/NOW.md +++ b/NOW.md @@ -2,6 +2,14 @@ Last updated: 2026-08-08 +## gen-verilog: test-block for-loops emitted; loop-variant calls not CSE-hoisted; tail expressions (Refs #1948) + +- `for i in ..` / `while` in a test block was dropped as `// (stmt: StmtForRange)`, silently voiding loop bodies that accumulate assertions. Loops emit now (loop var declared `integer`) +- The call-CSE pass hoisted a call from inside a loop into ONE temp evaluated before the loop with an uninitialized index -- the loop then tested a constant. predeclare no longer recurses into loop bodies, so loop-variant calls are emitted inline per iteration +- A Rust-style tail expression (a fn body ending in a bare expression) lowers to an implicit return assignment ` = ;` instead of a bare `expr;` (an unknown-task enable) +- tri-net corpus: icarus 97 -> 98 (m3_multihop); cross_layer_optimizer already joined +- FROZEN_HASH resealed + ## gen-verilog: tuple-destructure temp declared in test blocks (Refs #1948) - A test-block `let (a, b) = call()` slices a packed temp `__tup_l{line}`, but that temp (and the element regs) are declared only in emit_local's Decl phase, which the TB Init-only path skips -- the temp was referenced undeclared ("Could not find variable __tup_l242") diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 6d875df46..35fb48d8b 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -5201,6 +5201,15 @@ impl VerilogCodegen { } } } + // Do NOT recurse into loop bodies: a call there runs per iteration and + // may depend on the loop variable, so it must be emitted inline each + // pass, never hoisted into one loop-invariant temporary (t27#1948). + if matches!( + node.kind, + NodeKind::StmtForRange | NodeKind::StmtWhile | NodeKind::StmtFor + ) { + return; + } for child in &node.children { self.predeclare_call_array_tmps(child, block_name); } @@ -8283,6 +8292,25 @@ impl VerilogCodegen { self.write_line("end"); return; } + // A final bare expression is a Rust-style tail expression -- + // the function's implicit return value. Verilog has no tail + // expressions, so it emitted `expr;` (a bare statement iverilog + // reads as a task-enable). Lower the LAST StmtExpr to an implicit + // return assignment ` = ;` (t27#1948). + let is_tail_expr = idx + 1 == stmts.len() + && stmt.kind == NodeKind::StmtExpr + && !stmt.children.is_empty() + && !self.current_fn_name.is_empty() + && self.current_fn_return_type != "void"; + if is_tail_expr { + self.write_indent(); + let asn = if self.clocked_nonblocking { " <= " } else { " = " }; + self.write(&self.current_fn_name.clone()); + self.write(asn); + self.gen_verilog_expr(&stmt.children[0]); + self.write_line(";"); + continue; + } // #1741: top-level locals had their `reg` declaration hoisted, so // emit only the assignment here. if self.hoist_fn_locals && stmt.kind == NodeKind::StmtLocal { @@ -8447,6 +8475,19 @@ impl VerilogCodegen { } } } + // A `for i in ..` loop variable needs an `integer` + // declaration at the block top (t27#1948, TB for-loop + // support). The body statements are walked via `stack`. + if stmt.kind == NodeKind::StmtForRange + && !stmt.name.is_empty() + && declared.insert(stmt.name.clone()) + { + self.write_indent(); + self.write_line(&format!( + "integer {}; // t27#1948 loop variable", + Self::verilog_safe_identifier(&stmt.name) + )); + } // Untyped (or nested) `let` locals never reached a reg // declaration either -- iverilog cannot bind them // (t27#1948, the biggest testbench compile class). @@ -8807,6 +8848,12 @@ impl VerilogCodegen { self.write_indent(); self.gen_verilog_stmt(node); } + NodeKind::StmtForRange + | NodeKind::StmtWhile + | NodeKind::StmtFor => { + self.materialize_call_array_tmps_in_expr(node); + self.gen_verilog_stmt(node); + } _ => { self.write_indent(); self.write_line(&format!("// (stmt: {:?})", node.kind)); @@ -8893,6 +8940,14 @@ impl VerilogCodegen { // from a call (`array = create(...)`) stayed X in the TB. self.gen_verilog_stmt(node); } + NodeKind::StmtForRange + | NodeKind::StmtWhile + | NodeKind::StmtFor => { + // Control flow in a test block: a `for`/`while`/`if` was + // dropped as `// (stmt: StmtForRange)`, silently voiding + // loop bodies that accumulate assertions (t27#1948). + self.gen_verilog_stmt(node); + } _ => { self.write_indent(); self.write_line(&format!("// (stmt: {:?})", node.kind)); diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index 3cd74de07..1ecf8dea5 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -df2991ce372f7f3ecb9de196116662a7a40f2490cdeeb75ca77b2df18dfbb5fd +871ec3135001a0270d7afecf1fc384595dfa5a2fd15dbf4e25a4997983a18605 diff --git a/docs/NOW.md b/docs/NOW.md index 9b6d91a20..fef8c82fa 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -2,6 +2,14 @@ Last updated: 2026-08-08 +## gen-verilog: test-block for-loops emitted; loop-variant calls not CSE-hoisted; tail expressions (Refs #1948) + +- `for i in ..` / `while` in a test block was dropped as `// (stmt: StmtForRange)`, silently voiding loop bodies that accumulate assertions. Loops emit now (loop var declared `integer`) +- The call-CSE pass hoisted a call from inside a loop into ONE temp evaluated before the loop with an uninitialized index -- the loop then tested a constant. predeclare no longer recurses into loop bodies, so loop-variant calls are emitted inline per iteration +- A Rust-style tail expression (a fn body ending in a bare expression) lowers to an implicit return assignment ` = ;` instead of a bare `expr;` (an unknown-task enable) +- tri-net corpus: icarus 97 -> 98 (m3_multihop); cross_layer_optimizer already joined +- FROZEN_HASH resealed + ## gen-verilog: tuple-destructure temp declared in test blocks (Refs #1948) - A test-block `let (a, b) = call()` slices a packed temp `__tup_l{line}`, but that temp (and the element regs) are declared only in emit_local's Decl phase, which the TB Init-only path skips -- the temp was referenced undeclared ("Could not find variable __tup_l242")