From 84c2ae4f22ff7b4811ed8b813a117c60d05f36fa Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 26 Jun 2026 15:32:32 +0300 Subject: [PATCH] fix: `return` inside `inline if` no longer drops trailing statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `return` inside an `inline if` / comptime `case` branch — itself inside an `inline for`, under a runtime `if` — made the compiler wrongly reject the function as "body produces no value" and DROP its trailing statements (e.g. a trailing `return -1`). Root cause: the inline-if branch lowering sets the global `block_terminated` flag when its taken arm returns (control_flow.zig / stmt.zig `lowerInlineBranch`), unlike a bare `return` STATEMENT which deliberately never sets it (precisely to avoid leaking past an `if cond { return }` merge — see the comment at stmt.zig:37-42). The enclosing runtime-`if`'s merge never reset the flag, so it leaked past the merge and `lowerBlock` skipped the following statements as dead. Fix: after the runtime-`if` switches to its merge block, set `block_terminated = then_diverged and has_else and else_diverged` — the `if` leaves the block terminated ONLY when both arms diverged with an `else` covering the cond-false edge (otherwise the merge is reachable and the flag must be false). Adds `else_diverged` tracking alongside the existing `then_diverged`. Adversarially reviewed (SHIP): the reachability predicate is correct across all arm/else/divergence cases; the both-arms-diverge case still sets true (preserving prior behavior); value-ternary and inline-for-unroll paths are unaffected. Regression: examples/comptime/0651-comptime-inline-if-return.sx (nested inline-if and comptime `case`, both with per-arm returns, asserting the trailing return is emitted). Suite green (822/0). --- .../0651-comptime-inline-if-return.sx | 39 +++++++++++++++++++ .../0651-comptime-inline-if-return.exit | 1 + .../0651-comptime-inline-if-return.stderr | 1 + .../0651-comptime-inline-if-return.stdout | 2 + src/ir/lower/control_flow.zig | 12 ++++++ 5 files changed, 55 insertions(+) create mode 100644 examples/comptime/0651-comptime-inline-if-return.sx create mode 100644 examples/comptime/expected/0651-comptime-inline-if-return.exit create mode 100644 examples/comptime/expected/0651-comptime-inline-if-return.stderr create mode 100644 examples/comptime/expected/0651-comptime-inline-if-return.stdout diff --git a/examples/comptime/0651-comptime-inline-if-return.sx b/examples/comptime/0651-comptime-inline-if-return.sx new file mode 100644 index 00000000..e7ec2480 --- /dev/null +++ b/examples/comptime/0651-comptime-inline-if-return.sx @@ -0,0 +1,39 @@ +// Regression: a `return` inside an `inline if` (a comptime-folded branch), +// itself inside an `inline for`, must NOT make the compiler drop the function's +// trailing statements. The `inline if`/`case` branch sets a "block terminated" +// flag when its taken arm returns; that flag used to leak past the enclosing +// runtime `if`'s merge block, so the trailing `return -1` was skipped and the +// function was wrongly rejected as "produces no value". Now the runtime-`if` +// merge resets the flag to the merge's actual reachability. +#import "modules/std.sx"; + +// nested inline-if/else with returns, inside an inline-for, under a runtime if: +classify :: (idx: i64) -> i64 { + inline for 0..3 (i) { + if idx == i { + inline if i == 0 { return 100; } + else { inline if i == 1 { return 200; } else { return 300; } } + } + } + return -1; // trailing statement — must still be emitted (idx out of range) +} + +// the comptime `case` match form, also with per-arm returns: +tag :: (idx: i64) -> i64 { + inline for 0..3 (i) { + if idx == i { + inline if i == { + case 0: { return 10; } + case 1: { return 20; } + else: { return 30; } + } + } + } + return -1; +} + +main :: () -> i32 { + print("classify: {} {} {} {}\n", classify(0), classify(1), classify(2), classify(9)); + print("tag: {} {} {} {}\n", tag(0), tag(1), tag(2), tag(9)); + return 0; +} diff --git a/examples/comptime/expected/0651-comptime-inline-if-return.exit b/examples/comptime/expected/0651-comptime-inline-if-return.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/comptime/expected/0651-comptime-inline-if-return.exit @@ -0,0 +1 @@ +0 diff --git a/examples/comptime/expected/0651-comptime-inline-if-return.stderr b/examples/comptime/expected/0651-comptime-inline-if-return.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/comptime/expected/0651-comptime-inline-if-return.stderr @@ -0,0 +1 @@ + diff --git a/examples/comptime/expected/0651-comptime-inline-if-return.stdout b/examples/comptime/expected/0651-comptime-inline-if-return.stdout new file mode 100644 index 00000000..b3a7e55f --- /dev/null +++ b/examples/comptime/expected/0651-comptime-inline-if-return.stdout @@ -0,0 +1,2 @@ +classify: 100 200 300 -1 +tag: 10 20 30 -1 diff --git a/src/ir/lower/control_flow.zig b/src/ir/lower/control_flow.zig index 4a0b84c3..9da8214c 100644 --- a/src/ir/lower/control_flow.zig +++ b/src/ir/lower/control_flow.zig @@ -250,6 +250,7 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref { const saved_target = self.target_type; if (is_value and result_type != .void) self.target_type = result_type; var then_diverged = false; + var else_diverged = false; var then_snap = self.narrowSnapshot(); self.applyNarrowing(present_true.items); if (is_value) { @@ -278,6 +279,7 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref { self.applyNarrowing(present_false.items); if (is_value) { var v = self.lowerExpr(ie.else_branch.?); + else_diverged = self.currentBlockHasTerminator(); if (!self.currentBlockHasTerminator()) { const v_ty = self.builder.getRefType(v); if (v_ty != result_type and v_ty != .void and result_type != .void) { @@ -287,6 +289,7 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref { } } else { self.lowerBlock(ie.else_branch.?); + else_diverged = self.currentBlockHasTerminator(); if (!self.currentBlockHasTerminator()) { self.builder.br(merge_bb, &.{}); } @@ -302,6 +305,15 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref { // Continue at merge self.builder.switchToBlock(merge_bb); + // The merge block is REACHABLE (control falls through this `if`) unless BOTH + // arms diverged with an explicit `else` covering the cond-false edge. Reset + // `block_terminated` to that reachability so the flag — which an inline-if in + // either arm may have set (control_flow.zig / stmt.zig `lowerInlineBranch`) + // when its taken branch returned — does NOT leak past this merge and wrongly + // drop the enclosing block's trailing statements. Mirrors the bare-`return`- + // statement rule documented in `lowerBlock` (a return terminates its block but + // never sets the flag). + self.block_terminated = then_diverged and has_else and else_diverged; if (is_value) { return self.builder.blockParam(merge_bb, 0, result_type); }