Sema: harden switch logic against undef IB

Most places where `undefined` was previously (intentionally) passed across
function calls now use `Air.Inst.Ref.none` instead to ensure that these
`undefined` references don't accidentally outlive the `switch` logic they
belong to.
This commit is contained in:
Justus Klausecker 2026-01-28 11:27:10 +01:00 committed by Alex Rønne Petersen
parent fa988e88ed
commit c7c4e8d802

View file

@ -10761,7 +10761,7 @@ fn analyzeSwitchBlock(
const val, const ref = if (operand_is_ref)
.{ try sema.analyzeLoad(block, src, raw_operand, operand_src), raw_operand }
else
.{ raw_operand, undefined };
.{ raw_operand, .none };
const operand_ty = sema.typeOf(val);
const maybe_operand_opv = try sema.typeHasOnePossibleValue(operand_ty);
@ -10785,7 +10785,7 @@ fn analyzeSwitchBlock(
const operand_alloc = try block.addTy(.alloc, operand_ptr_ty);
_ = try block.addBinOp(.store, operand_alloc, raw_operand);
break :alloc operand_alloc;
} else undefined;
} else .none;
break :operand .{ .{ .loop = .{
.operand_alloc = operand_alloc,
.operand_is_ref = operand_is_ref,
@ -10857,7 +10857,7 @@ fn analyzeSwitchBlock(
const new_val, const new_ref = if (operand_is_ref)
.{ try sema.analyzeLoad(child_block, src, new_operand, new_operand_src), new_operand }
else
.{ new_operand, undefined };
.{ new_operand, .none };
const new_cond_ref = if (union_originally)
try sema.unionToTag(child_block, item_ty, new_val, src)
@ -10953,7 +10953,7 @@ fn analyzeSwitchBlock(
const by_val = try sema.analyzeLoad(block, src, loaded, src);
break :load_operand .{ by_val, loaded };
} else {
break :load_operand .{ loaded, undefined };
break :load_operand .{ loaded, .none };
}
},
};
@ -11393,33 +11393,31 @@ fn finishSwitchBr(
var emit_bb = false;
if (has_else and else_case.is_inline) {
const else_prong_src = block.src(.{ .node_offset_switch_else_prong = src_node_offset });
var error_names: InternPool.NullTerminatedString.Slice = undefined;
var min_int: Value = undefined;
check_enumerable: {
const error_names, const min_int = check_enumerable: {
switch (item_ty.zigTypeTag(zcu)) {
.@"union" => unreachable,
.@"enum" => if (else_is_named_only or
!item_ty.isNonexhaustiveEnum(zcu) or union_originally)
{
try branch_hints.ensureUnusedCapacity(gpa, @intCast(validated_switch.seen_enum_fields.len));
break :check_enumerable;
break :check_enumerable .{ undefined, undefined };
},
.error_set => if (!operand_ty.isAnyError(zcu)) {
error_names = item_ty.errorSetNames(zcu);
const error_names = item_ty.errorSetNames(zcu);
try branch_hints.ensureUnusedCapacity(gpa, error_names.len);
break :check_enumerable;
break :check_enumerable .{ error_names, undefined };
},
.int => {
min_int = try item_ty.minInt(pt, item_ty);
break :check_enumerable;
const min_int = try item_ty.minInt(pt, item_ty);
break :check_enumerable .{ undefined, min_int };
},
.bool, .void => break :check_enumerable,
.bool, .void => break :check_enumerable .{ undefined, undefined },
else => {},
}
return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{
item_ty.fmt(pt),
});
}
};
var unhandled_it = validated_switch.iterateUnhandledItems(error_names, min_int);
while (try unhandled_it.next(sema, item_ty)) |item_val| {
cases_len += 1;
@ -11660,7 +11658,7 @@ fn fixupSwitchContinues(
operand_is_ref: bool,
item_ty: Type,
mode: enum { normal, opv },
any_non_inline_capture: bool,
any_maybe_runtime_capture: bool,
merges: *const Block.Merges,
) CompileError!void {
const pt = sema.pt;
@ -11686,7 +11684,7 @@ fn fixupSwitchContinues(
assert(sema.air_instructions.items(.tag)[@intFromEnum(placeholder_inst)] == .br);
const new_operand_maybe_ref = sema.air_instructions.items(.data)[@intFromEnum(placeholder_inst)].br.operand;
if (any_non_inline_capture and mode != .opv) {
if (any_maybe_runtime_capture and mode != .opv) {
_ = try replacement_block.addBinOp(.store, operand.loop.operand_alloc, new_operand_maybe_ref);
}
@ -12431,6 +12429,8 @@ fn resolveSwitchBlock(
}
}
assert(zir_switch.else_case != null or under_prong != null); // switch exhaustion check wrong
const else_case = validated_switch.else_case;
const else_is_named_only = zir_switch.else_case != null and under_prong != null;
@ -12507,8 +12507,8 @@ const SwitchOperand = union(enum) {
simple: struct {
/// The raw switch operand value. Always defined.
by_val: Air.Inst.Ref,
/// The switch operand *pointer*. Defined only if there is a prong
/// with a by-ref capture.
/// The switch operand *pointer*. `none` if there are no prongs with a
/// by-ref capture.
by_ref: Air.Inst.Ref,
/// The switch condition value. For unions, `operand` is the union
/// and `cond` is its enum tag value.
@ -12519,7 +12519,7 @@ const SwitchOperand = union(enum) {
loop: struct {
/// The `alloc` containing the `switch` operand for the active dispatch.
/// Each prong must load from this `alloc` to get captures.
/// If there are no captures, this may be undefined.
/// If there are no captures, this may be `none`.
operand_alloc: Air.Inst.Ref,
/// Whether `operand_alloc` contains a by-val operand or a by-ref
/// operand.
@ -12665,19 +12665,31 @@ fn analyzeSwitchProng(
}
}
const operand_val, const operand_ptr = load_operand: {
const need_load: bool = need_load: {
if (capture == .none and !has_tag_capture) {
// No need to load the operand for this prong!
break :load_operand .{ undefined, undefined };
break :need_load false;
}
if (kind == .inline_ref and
!(capture != .none and operand_ty.zigTypeTag(zcu) == .@"union"))
{
// We only need to load the operand if there's a union payload capture
// since it's always runtime-known; only the tag is comptime-known here.
break :load_operand .{ undefined, undefined };
if (capture != .none and operand_ty.zigTypeTag(zcu) == .@"union") {
// Non-OPV union payload captures are always runtime-known.
break :need_load true;
}
if (kind == .inline_ref) {
// `inline_ref` *is* the (comptime-known) capture.
break :need_load false;
}
assert(zir_switch.any_maybe_runtime_capture); // should have caught everything else by now
if (capture != .by_ref and
kind == .item_refs and kind.item_refs.len == 1)
{
// Capture is comptime-known because it's the only prong item
break :need_load false;
}
break :need_load true;
};
const operand_val: Air.Inst.Ref, const operand_ptr: Air.Inst.Ref = load_operand: {
if (!need_load) break :load_operand .{ .none, .none };
switch (operand) {
.simple => |s| break :load_operand .{ s.by_val, s.by_ref },
.loop => |l| {
@ -12686,7 +12698,7 @@ fn analyzeSwitchProng(
const by_val = try sema.analyzeLoad(case_block, operand_src, loaded, operand_src);
break :load_operand .{ by_val, loaded };
} else {
break :load_operand .{ loaded, undefined };
break :load_operand .{ loaded, .none };
}
},
}
@ -12735,7 +12747,7 @@ fn analyzeSwitchProng(
fn analyzeSwitchTagCapture(
sema: *Sema,
case_block: *Block,
/// May be `undefined` if `inline_case_capture` is not `.none`.
/// May be `none` if this is an inline capture or if `kind.item_refs.len == 1`.
operand_val: Air.Inst.Ref,
operand_ty: Type,
capture_src: LazySrcLoc,
@ -12768,9 +12780,11 @@ fn analyzeSwitchPayloadCapture(
sema: *Sema,
case_block: *Block,
operand: SwitchOperand,
/// May be `undefined` if this is an inline capture and operand is not a union.
/// Always has to be not-`none` if this is a union payload capture.
/// For non-union captures, this may be `none` if this is an inline capture
/// or if `kind.item_refs.len == 1` and capture is by val.
operand_val: Air.Inst.Ref,
/// May be `undefined` if `capture_by_ref` is `false` or if `operand_val` is also `undefined`.
/// May be `none` if `capture_by_ref` is `false` or if `operand_val` is also `none`.
operand_ptr: Air.Inst.Ref,
operand_ty: Type,
operand_src: LazySrcLoc,
@ -12816,8 +12830,6 @@ fn analyzeSwitchPayloadCapture(
}
}
const operand_ptr_ty = if (capture_by_ref) sema.typeOf(operand_ptr) else undefined;
if (kind == .special) {
if (capture_by_ref) return operand_ptr;
return switch (operand_ty.zigTypeTag(zcu)) {
@ -12894,7 +12906,7 @@ fn analyzeSwitchPayloadCapture(
// By-reference captures have some further restrictions which make them easier to emit
if (capture_by_ref) {
const operand_ptr_info = operand_ptr_ty.ptrInfo(zcu);
const operand_ptr_info = sema.typeOf(operand_ptr).ptrInfo(zcu);
const capture_ptr_ty = resolve: {
// By-ref captures of hetereogeneous types are only allowed if all field
// pointer types are peer resolvable to each other.
@ -13136,7 +13148,7 @@ fn analyzeSwitchPayloadCapture(
if (case_vals.len == 1) {
const item_val = sema.resolveConstDefinedValue(case_block, .unneeded, case_vals[0], undefined) catch unreachable;
const item_ty = try pt.singleErrorSetType(item_val.getErrorName(zcu).unwrap().?);
return sema.bitCast(case_block, item_ty, operand_val, operand_src, null);
return sema.bitCast(case_block, item_ty, .fromValue(item_val), operand_src, null);
}
var names: InferredErrorSet.NameMap = .{};