From 28b18f812a7db0cc0b5abc1c133ce40aca2c4545 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 31 May 2026 21:04:06 +0300 Subject: [PATCH] fix(issue-0057): all-diverging match arms no longer fail LLVM verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A match (`if subject == { case ... }`) whose arms all diverge (each `return`s / `raise`s) failed LLVM verification with a `void` phi plus "Terminator found in the middle of a basic block". Two causes in lowerMatch: - The value-arm path did `lowerBlockValue(arm.body) orelse constInt(0, …)`, emitting the fallback `const` into a block the body had ALREADY terminated (a diverging arm), so `currentBlockHasTerminator()` then saw the const (not the `ret`) and emitted a `br merge` after the terminator. Fix: materialize the fallback value + branch only when the block hasn't terminated. - A fully-diverging match infers `result_type == .noreturn` yet still built a value-merge phi. Fix: `has_value_merge` excludes `.noreturn`, so such a match builds no phi; its arms terminate and the merge block is unreachable. Also: inferMatchResultType now skips `.noreturn` arms (a diverging arm doesn't decide the result type) and reports `.noreturn` only when EVERY arm diverges — so a mixed match (some arms yield values, some diverge) infers the value type. This unblocks ERR E1.5's `catch` match-body form (`x catch e == { case .A: return …; else: raise e; }`), which desugars to an all-diverging match. Regression: examples/225-match-diverging-arms.sx (all-diverging + mixed, exit 134). Gates: zig build, zig build test, 263/263 examples. --- examples/225-match-diverging-arms.sx | 40 +++++++++++++++++++ src/ir/lower.zig | 41 ++++++++++++++------ tests/expected/225-match-diverging-arms.exit | 1 + tests/expected/225-match-diverging-arms.txt | 1 + 4 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 examples/225-match-diverging-arms.sx create mode 100644 tests/expected/225-match-diverging-arms.exit create mode 100644 tests/expected/225-match-diverging-arms.txt diff --git a/examples/225-match-diverging-arms.sx b/examples/225-match-diverging-arms.sx new file mode 100644 index 0000000..48e2c32 --- /dev/null +++ b/examples/225-match-diverging-arms.sx @@ -0,0 +1,40 @@ +// Regression for issue 0057: a match (`if subject == { case ... }`) whose arms +// ALL diverge (each `return`s) used to fail LLVM verification (a `void` phi + +// "terminator in the middle of a basic block") — lowerMatch emitted a +// value-merge phi and a fallback `const` into arm blocks that had already +// terminated. Now a fully-diverging match produces no merge phi, and a mixed +// match (some arms diverge, some yield values) materializes the merge only +// from the value-producing arms. + +#import "modules/std.sx"; + +// All arms diverge — the match is `noreturn`, no merge phi. +classify :: (n: s32) -> s32 { + if n == { + case 0: return 10; + case 1: return 20; + else: return 90; + } + return 0; // unreachable +} + +// Mixed: value arms + a diverging arm. +pick :: (n: s32) -> s32 { + v := if n == { + case 0: 1; + case 1: return 100; // diverging arm — no fallback const after its `ret` + else: 3; + }; + return v + 5; +} + +main :: () -> s32 { + r : s32 = 0; + r = r + classify(0); // 10 + r = r + classify(1); // 20 + r = r + classify(7); // 90 + r = r + pick(0); // 1 + 5 = 6 + r = r + pick(9); // 3 + 5 = 8 (else arm) + print("match result: {}\n", r); // 10+20+90+6+8 = 134 + return r; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index b90d6ac..a862c5a 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -3722,7 +3722,11 @@ pub const Lowering = struct { } const is_value = if (is_type_match) self.force_block_value else (self.force_block_value or (inferred_result != .void and inferred_result != .unresolved)); const result_type: TypeId = if (is_value) inferred_result else .void; - const merge_params: []const TypeId = if (is_value and result_type != .void) &.{result_type} else &.{}; + // A fully-diverging match (`result_type == .noreturn` — every arm + // `return`s / `raise`s / etc.) produces no value, so it builds no + // merge phi; its arms terminate and the merge block is unreachable. + const has_value_merge = is_value and result_type != .void and result_type != .noreturn; + const merge_params: []const TypeId = if (has_value_merge) &.{result_type} else &.{}; const merge_bb = self.freshBlockWithParams("match.merge", merge_params); // Build arm blocks @@ -3918,16 +3922,20 @@ pub const Lowering = struct { self.current_match_tags = arm_tag_values.items[i]; } - if (is_value and result_type != .void) { - var v = self.lowerBlockValue(arm.body) orelse if (result_type == .string or !result_type.isBuiltin()) - self.builder.constUndef(result_type) - else - self.builder.constInt(0, result_type); + if (has_value_merge) { + const maybe_v = self.lowerBlockValue(arm.body); self.current_match_tags = saved_match_tags; self.scope = old_scope; arm_scope.deinit(); + // Only materialize a value + branch to the merge when the arm + // body did NOT diverge. A diverging arm (e.g. `return x`) has + // already terminated its block; emitting the fallback const + // here would land AFTER the terminator (the issue-0057 bug). if (!self.currentBlockHasTerminator()) { - // Coerce arm value to match result type + var v = maybe_v orelse if (result_type == .string or !result_type.isBuiltin()) + self.builder.constUndef(result_type) + else + self.builder.constInt(0, result_type); const v_ty = self.builder.getRefType(v); v = self.coerceToType(v, v_ty, result_type); self.builder.br(merge_bb, &.{v}); @@ -3954,7 +3962,7 @@ pub const Lowering = struct { if (is_type_match) { // For type-category matches, unrecognized tags should skip to merge // (e.g., optional types not covered by any_to_string categories) - if (is_value and result_type != .void) { + if (has_value_merge) { const default_val = self.builder.constUndef(result_type); self.builder.br(merge_bb, &.{default_val}); } else { @@ -3976,7 +3984,7 @@ pub const Lowering = struct { }; if (is_exhaustive) { self.builder.emitUnreachable(); - } else if (is_value and result_type != .void) { + } else if (has_value_merge) { const default_val = self.builder.constUndef(result_type); self.builder.br(merge_bb, &.{default_val}); } else { @@ -3987,7 +3995,7 @@ pub const Lowering = struct { } self.builder.switchToBlock(merge_bb); - if (is_value and result_type != .void) { + if (has_value_merge) { return self.builder.blockParam(merge_bb, 0, result_type); } return self.builder.constInt(0, .void); @@ -10803,6 +10811,7 @@ pub const Lowering = struct { // were null arms, the result is ?T (optional). var has_null = false; var saw_unresolved = false; + var saw_noreturn = false; for (me.arms) |arm| { const last_node = if (arm.body.data == .block) blk: { if (arm.body.data.block.stmts.len > 0) { @@ -10821,6 +10830,14 @@ pub const Lowering = struct { // enum literal) doesn't decide — keep looking; the caller falls back // to the contextual target type if none of the arms resolve. const arm_ty = self.inferExprType(last_node); + // A diverging arm (`noreturn` — `return` / `raise` / `break` / + // `continue`) doesn't produce a value, so it doesn't decide the + // result type; keep looking. The match is `noreturn` only if EVERY + // arm diverges (handled after the loop). + if (arm_ty == .noreturn) { + saw_noreturn = true; + continue; + } if (arm_ty == .unresolved) { saw_unresolved = true; continue; @@ -10830,7 +10847,9 @@ pub const Lowering = struct { } return arm_ty; } - return if (saw_unresolved) .unresolved else .void; + if (saw_unresolved) return .unresolved; + if (saw_noreturn) return .noreturn; // all arms diverge + return .void; } fn isTypeCategoryMatch(me: *const ast.MatchExpr) bool { diff --git a/tests/expected/225-match-diverging-arms.exit b/tests/expected/225-match-diverging-arms.exit new file mode 100644 index 0000000..405e2af --- /dev/null +++ b/tests/expected/225-match-diverging-arms.exit @@ -0,0 +1 @@ +134 diff --git a/tests/expected/225-match-diverging-arms.txt b/tests/expected/225-match-diverging-arms.txt new file mode 100644 index 0000000..32b2f0d --- /dev/null +++ b/tests/expected/225-match-diverging-arms.txt @@ -0,0 +1 @@ +match result: 134