fix(issue-0057): all-diverging match arms no longer fail LLVM verification

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.
This commit is contained in:
agra
2026-05-31 21:04:06 +03:00
parent 696a749bd5
commit 28b18f812a
4 changed files with 72 additions and 11 deletions

View File

@@ -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 {