fix: return inside inline if no longer drops trailing statements
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).
This commit is contained in:
39
examples/comptime/0651-comptime-inline-if-return.sx
Normal file
39
examples/comptime/0651-comptime-inline-if-return.sx
Normal file
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
classify: 100 200 300 -1
|
||||||
|
tag: 10 20 30 -1
|
||||||
@@ -250,6 +250,7 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
|
|||||||
const saved_target = self.target_type;
|
const saved_target = self.target_type;
|
||||||
if (is_value and result_type != .void) self.target_type = result_type;
|
if (is_value and result_type != .void) self.target_type = result_type;
|
||||||
var then_diverged = false;
|
var then_diverged = false;
|
||||||
|
var else_diverged = false;
|
||||||
var then_snap = self.narrowSnapshot();
|
var then_snap = self.narrowSnapshot();
|
||||||
self.applyNarrowing(present_true.items);
|
self.applyNarrowing(present_true.items);
|
||||||
if (is_value) {
|
if (is_value) {
|
||||||
@@ -278,6 +279,7 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
|
|||||||
self.applyNarrowing(present_false.items);
|
self.applyNarrowing(present_false.items);
|
||||||
if (is_value) {
|
if (is_value) {
|
||||||
var v = self.lowerExpr(ie.else_branch.?);
|
var v = self.lowerExpr(ie.else_branch.?);
|
||||||
|
else_diverged = self.currentBlockHasTerminator();
|
||||||
if (!self.currentBlockHasTerminator()) {
|
if (!self.currentBlockHasTerminator()) {
|
||||||
const v_ty = self.builder.getRefType(v);
|
const v_ty = self.builder.getRefType(v);
|
||||||
if (v_ty != result_type and v_ty != .void and result_type != .void) {
|
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 {
|
} else {
|
||||||
self.lowerBlock(ie.else_branch.?);
|
self.lowerBlock(ie.else_branch.?);
|
||||||
|
else_diverged = self.currentBlockHasTerminator();
|
||||||
if (!self.currentBlockHasTerminator()) {
|
if (!self.currentBlockHasTerminator()) {
|
||||||
self.builder.br(merge_bb, &.{});
|
self.builder.br(merge_bb, &.{});
|
||||||
}
|
}
|
||||||
@@ -302,6 +305,15 @@ pub fn lowerIfExpr(self: *Lowering, ie: *const ast.IfExpr) Ref {
|
|||||||
|
|
||||||
// Continue at merge
|
// Continue at merge
|
||||||
self.builder.switchToBlock(merge_bb);
|
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) {
|
if (is_value) {
|
||||||
return self.builder.blockParam(merge_bb, 0, result_type);
|
return self.builder.blockParam(merge_bb, 0, result_type);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user