From 0e1afa3eba17bdf971cd6f635ad99ff486e1d200 Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 1 Jun 2026 21:42:20 +0300 Subject: [PATCH] fix(lower): drop dead statements after a return/raise terminator (issue 0061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bare `return X;` / `raise` in the middle of a block closed the current LLVM basic block, but lowerBlock / lowerBlockValue only stopped the statement loop on the `block_terminated` flag — which lowerReturn deliberately never sets (it would leak past an `if cond { return }` merge block). So trailing dead statements were emitted into the already-closed block, tripping the LLVM verifier with "Terminator found in the middle of a basic block". Fix: also stop the statement loop when currentBlockHasTerminator() is true. That is CFG-level termination of the *current* block, which is naturally false at an if / inline-if merge block, so conditional returns still fall through to their trailing statements. This unblocks ERR E5.1: the canonical failable-closure form `closure((x) -> (s32,!) { raise error.X; return x; })` has a dead `return x;` after the unconditional raise and tripped the verifier. Regression: examples/0038-basic-dead-code-after-terminator.sx. --- .../0038-basic-dead-code-after-terminator.sx | 34 ++++++++++ ...0038-basic-dead-code-after-terminator.exit | 1 + ...38-basic-dead-code-after-terminator.stderr | 1 + ...38-basic-dead-code-after-terminator.stdout | 4 ++ .../0061-dead-code-after-terminator-stmt.md | 68 +++++++++++++++++++ src/ir/lower.zig | 11 +++ 6 files changed, 119 insertions(+) create mode 100644 examples/0038-basic-dead-code-after-terminator.sx create mode 100644 examples/expected/0038-basic-dead-code-after-terminator.exit create mode 100644 examples/expected/0038-basic-dead-code-after-terminator.stderr create mode 100644 examples/expected/0038-basic-dead-code-after-terminator.stdout create mode 100644 issues/0061-dead-code-after-terminator-stmt.md diff --git a/examples/0038-basic-dead-code-after-terminator.sx b/examples/0038-basic-dead-code-after-terminator.sx new file mode 100644 index 0000000..4ac6e0b --- /dev/null +++ b/examples/0038-basic-dead-code-after-terminator.sx @@ -0,0 +1,34 @@ +// Dead statements after a block-terminating statement (`return` / `raise`) are +// dropped instead of being emitted into the already-closed basic block. +// Regression (issue 0061): a bare `return X;` / `raise` mid-block closed the +// LLVM basic block but lowering kept emitting the trailing statements into it +// → "Terminator found in the middle of a basic block". The canonical failable +// closure form `{ raise error.X; return x; }` tripped this, blocking ERR E5.1. +// +// The fix must NOT over-reach: a CONDITIONAL `if cond { return }` (and the +// `inline if` pack form) leaves a fresh merge block, so its trailing statements +// must still run — exercised by `clamp` / `pick` below. + +#import "modules/std.sx"; + +E :: error { Neg } + +// dead `return 99;` after an unconditional return +const_one :: () -> s64 { return 1; return 99; } + +// dead `return x;` after an unconditional raise (the failable closure shape) +always_raise :: (x: s64) -> (s64, !E) { raise error.Neg; return x; } + +// guard: a conditional return must still fall through to the trailing return +clamp :: (x: s64) -> s64 { if x > 10 { return 10; } return x; } + +main :: () -> s32 { + print("const_one={}\n", const_one()); // 1 + print("raised={}\n", always_raise(5) catch e 0); // 0 + print("clamp_hi={}\n", clamp(42)); // 10 + print("clamp_lo={}\n", clamp(7)); // 7 + + // dead code after a `return` at main's own block level is dropped. + return 0; + print("unreachable\n"); +} diff --git a/examples/expected/0038-basic-dead-code-after-terminator.exit b/examples/expected/0038-basic-dead-code-after-terminator.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0038-basic-dead-code-after-terminator.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0038-basic-dead-code-after-terminator.stderr b/examples/expected/0038-basic-dead-code-after-terminator.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0038-basic-dead-code-after-terminator.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0038-basic-dead-code-after-terminator.stdout b/examples/expected/0038-basic-dead-code-after-terminator.stdout new file mode 100644 index 0000000..d3cb56b --- /dev/null +++ b/examples/expected/0038-basic-dead-code-after-terminator.stdout @@ -0,0 +1,4 @@ +const_one=1 +raised=0 +clamp_hi=10 +clamp_lo=7 diff --git a/issues/0061-dead-code-after-terminator-stmt.md b/issues/0061-dead-code-after-terminator-stmt.md new file mode 100644 index 0000000..2583dec --- /dev/null +++ b/issues/0061-dead-code-after-terminator-stmt.md @@ -0,0 +1,68 @@ +# 0061 — dead statements after `return` / `raise` emit into a closed block + +> **✅ RESOLVED (2026-06-01).** Root cause: `lowerBlock` / `lowerBlockValue` +> ([src/ir/lower.zig](../src/ir/lower.zig)) broke their statement loop only on +> the `block_terminated` flag, which `lowerReturn` deliberately does NOT set (it +> would leak past an `if cond { return }` merge block — see the comment at +> `lowerReturn`). So a bare `return X;` / `raise` mid-block closed the current +> LLVM basic block while lowering kept emitting the trailing statements into it. +> Fix: after each `lowerStmt`, also stop the loop when +> `currentBlockHasTerminator()` is true (CFG-level termination of the *current* +> block — correctly false at an `if`/`inline if` merge block, so conditional +> returns still fall through). Regression test: +> [examples/0038-basic-dead-code-after-terminator.sx](../examples/0038-basic-dead-code-after-terminator.sx). + +## Symptom + +Any statement following a block-terminating statement (`return`, `raise`) at the +same block level is lowered into the basic block *after* its terminator, so the +LLVM verifier aborts: + +``` +LLVM verification failed: Terminator found in the middle of a basic block! +label %entry +``` + +Observed: a well-formed program with trailing dead code crashes the compiler. +Expected: the dead statements are dropped (unreachable); the program compiles +and runs. + +This blocked ERR E5.1: the canonical failable-closure form from the plan, +`closure((x) -> (s32, !) { raise error.X; return x; })`, has a dead `return x;` +after the unconditional `raise` and tripped the verifier. + +## Reproduction + +Minimal (non-failable — the bug is general, not error-specific): + +```sx +#import "modules/std.sx"; +main :: () -> s32 { return 0; print("dead\n"); } +``` + +Failable facet (the form that blocked E5.1): + +```sx +#import "modules/std.sx"; +E :: error { Neg } +top :: (x: s64) -> (s64, !E) { raise error.Neg; return x; } +main :: () -> s32 { print("r={}\n", top(5) catch e 0); return 0; } +``` + +Both abort with "Terminator found in the middle of a basic block". A +*conditional* terminator (`if c { return 1; } return 2;`) was unaffected — its +merge block is fresh and has no terminator. + +## Investigation prompt + +The bug is in block-statement lowering in [src/ir/lower.zig](../src/ir/lower.zig): +`lowerBlock` (~line 1455) and `lowerBlockValue` (~line 1496) iterate `blk.stmts` +and only check the `block_terminated` flag. `lowerReturn` (~line 1767) emits a +`ret`/`br` terminator but intentionally does NOT set `block_terminated` (setting +it would leak past `if cond { return }` merge blocks and wrongly skip their +trailing statements — see the comment there). The fix is to stop the loop when +the *current* basic block has a terminator after lowering a statement, using the +existing `currentBlockHasTerminator()` helper (~line 11725), which is naturally +false at a merge block. Verify with both repros above (now compile + run) and +confirm `examples/0518-packs-pack-value-dispatch.sx` (inline-if + return + +trailing statements) still produces all its output. diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2adf685..fad8aa2 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1468,6 +1468,13 @@ pub const Lowering = struct { for (blk.stmts) |stmt| { if (self.block_terminated) break; self.lowerStmt(stmt); + // A bare `return`/`raise` mid-block terminates the current + // basic block but deliberately does NOT set `block_terminated` + // (that flag would leak past an `if cond { return }` merge + // block, skipping its trailing statements — see lowerReturn). + // Stop here so dead statements after the terminator aren't + // emitted into an already-closed block (invalid LLVM IR). + if (self.currentBlockHasTerminator()) break; } }, else => { @@ -1517,6 +1524,10 @@ pub const Lowering = struct { for (blk.stmts[0 .. blk.stmts.len - 1]) |stmt| { if (self.block_terminated) return null; self.lowerStmt(stmt); + // A bare `return`/`raise` mid-block closes the current basic + // block (without setting `block_terminated`); the remaining + // statements — including the value-expr — are dead. + if (self.currentBlockHasTerminator()) return null; } if (self.block_terminated) return null; // Last statement: if it's an expression, return its value