authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-01-28 11:27:10+01:00
committergravatar for alex@alexrp.comAlex Rønne Petersen <alex@alexrp.com> 2026-01-31 06:36:07+01:00
logc7c4e8d802c5c8ab2dc9d064c3985836e6677115
tree2350f65e220a00aae2b0dcdb020592775e72d2b1
parentfa988e88ed21485830a70276b5c7567efb122f80

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.

1 files changed, 47 insertions(+), 35 deletions(-)

src/Sema.zig+47-35
......@@ -10761,7 +10761,7 @@ fn analyzeSwitchBlock(
1076110761 const val, const ref = if (operand_is_ref)
1076210762 .{ try sema.analyzeLoad(block, src, raw_operand, operand_src), raw_operand }
1076310763 else
10764 .{ raw_operand, undefined };
10764 .{ raw_operand, .none };
1076510765
1076610766 const operand_ty = sema.typeOf(val);
1076710767 const maybe_operand_opv = try sema.typeHasOnePossibleValue(operand_ty);
......@@ -10785,7 +10785,7 @@ fn analyzeSwitchBlock(
1078510785 const operand_alloc = try block.addTy(.alloc, operand_ptr_ty);
1078610786 _ = try block.addBinOp(.store, operand_alloc, raw_operand);
1078710787 break :alloc operand_alloc;
10788 } else undefined;
10788 } else .none;
1078910789 break :operand .{ .{ .loop = .{
1079010790 .operand_alloc = operand_alloc,
1079110791 .operand_is_ref = operand_is_ref,
......@@ -10857,7 +10857,7 @@ fn analyzeSwitchBlock(
1085710857 const new_val, const new_ref = if (operand_is_ref)
1085810858 .{ try sema.analyzeLoad(child_block, src, new_operand, new_operand_src), new_operand }
1085910859 else
10860 .{ new_operand, undefined };
10860 .{ new_operand, .none };
1086110861
1086210862 const new_cond_ref = if (union_originally)
1086310863 try sema.unionToTag(child_block, item_ty, new_val, src)
......@@ -10953,7 +10953,7 @@ fn analyzeSwitchBlock(
1095310953 const by_val = try sema.analyzeLoad(block, src, loaded, src);
1095410954 break :load_operand .{ by_val, loaded };
1095510955 } else {
10956 break :load_operand .{ loaded, undefined };
10956 break :load_operand .{ loaded, .none };
1095710957 }
1095810958 },
1095910959 };
......@@ -11393,33 +11393,31 @@ fn finishSwitchBr(
1139311393 var emit_bb = false;
1139411394 if (has_else and else_case.is_inline) {
1139511395 const else_prong_src = block.src(.{ .node_offset_switch_else_prong = src_node_offset });
11396 var error_names: InternPool.NullTerminatedString.Slice = undefined;
11397 var min_int: Value = undefined;
11398 check_enumerable: {
11396 const error_names, const min_int = check_enumerable: {
1139911397 switch (item_ty.zigTypeTag(zcu)) {
1140011398 .@"union" => unreachable,
1140111399 .@"enum" => if (else_is_named_only or
1140211400 !item_ty.isNonexhaustiveEnum(zcu) or union_originally)
1140311401 {
1140411402 try branch_hints.ensureUnusedCapacity(gpa, @intCast(validated_switch.seen_enum_fields.len));
11405 break :check_enumerable;
11403 break :check_enumerable .{ undefined, undefined };
1140611404 },
1140711405 .error_set => if (!operand_ty.isAnyError(zcu)) {
11408 error_names = item_ty.errorSetNames(zcu);
11406 const error_names = item_ty.errorSetNames(zcu);
1140911407 try branch_hints.ensureUnusedCapacity(gpa, error_names.len);
11410 break :check_enumerable;
11408 break :check_enumerable .{ error_names, undefined };
1141111409 },
1141211410 .int => {
11413 min_int = try item_ty.minInt(pt, item_ty);
11414 break :check_enumerable;
11411 const min_int = try item_ty.minInt(pt, item_ty);
11412 break :check_enumerable .{ undefined, min_int };
1141511413 },
11416 .bool, .void => break :check_enumerable,
11414 .bool, .void => break :check_enumerable .{ undefined, undefined },
1141711415 else => {},
1141811416 }
1141911417 return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{
1142011418 item_ty.fmt(pt),
1142111419 });
11422 }
11420 };
1142311421 var unhandled_it = validated_switch.iterateUnhandledItems(error_names, min_int);
1142411422 while (try unhandled_it.next(sema, item_ty)) |item_val| {
1142511423 cases_len += 1;
......@@ -11660,7 +11658,7 @@ fn fixupSwitchContinues(
1166011658 operand_is_ref: bool,
1166111659 item_ty: Type,
1166211660 mode: enum { normal, opv },
11663 any_non_inline_capture: bool,
11661 any_maybe_runtime_capture: bool,
1166411662 merges: *const Block.Merges,
1166511663) CompileError!void {
1166611664 const pt = sema.pt;
......@@ -11686,7 +11684,7 @@ fn fixupSwitchContinues(
1168611684 assert(sema.air_instructions.items(.tag)[@intFromEnum(placeholder_inst)] == .br);
1168711685 const new_operand_maybe_ref = sema.air_instructions.items(.data)[@intFromEnum(placeholder_inst)].br.operand;
1168811686
11689 if (any_non_inline_capture and mode != .opv) {
11687 if (any_maybe_runtime_capture and mode != .opv) {
1169011688 _ = try replacement_block.addBinOp(.store, operand.loop.operand_alloc, new_operand_maybe_ref);
1169111689 }
1169211690
......@@ -12431,6 +12429,8 @@ fn resolveSwitchBlock(
1243112429 }
1243212430 }
1243312431
12432 assert(zir_switch.else_case != null or under_prong != null); // switch exhaustion check wrong
12433
1243412434 const else_case = validated_switch.else_case;
1243512435 const else_is_named_only = zir_switch.else_case != null and under_prong != null;
1243612436
......@@ -12507,8 +12507,8 @@ const SwitchOperand = union(enum) {
1250712507 simple: struct {
1250812508 /// The raw switch operand value. Always defined.
1250912509 by_val: Air.Inst.Ref,
12510 /// The switch operand *pointer*. Defined only if there is a prong
12511 /// with a by-ref capture.
12510 /// The switch operand *pointer*. `none` if there are no prongs with a
12511 /// by-ref capture.
1251212512 by_ref: Air.Inst.Ref,
1251312513 /// The switch condition value. For unions, `operand` is the union
1251412514 /// and `cond` is its enum tag value.
......@@ -12519,7 +12519,7 @@ const SwitchOperand = union(enum) {
1251912519 loop: struct {
1252012520 /// The `alloc` containing the `switch` operand for the active dispatch.
1252112521 /// Each prong must load from this `alloc` to get captures.
12522 /// If there are no captures, this may be undefined.
12522 /// If there are no captures, this may be `none`.
1252312523 operand_alloc: Air.Inst.Ref,
1252412524 /// Whether `operand_alloc` contains a by-val operand or a by-ref
1252512525 /// operand.
......@@ -12665,19 +12665,31 @@ fn analyzeSwitchProng(
1266512665 }
1266612666 }
1266712667
12668 const operand_val, const operand_ptr = load_operand: {
12668 const need_load: bool = need_load: {
1266912669 if (capture == .none and !has_tag_capture) {
1267012670 // No need to load the operand for this prong!
12671 break :load_operand .{ undefined, undefined };
12671 break :need_load false;
1267212672 }
12673 if (kind == .inline_ref and
12674 !(capture != .none and operand_ty.zigTypeTag(zcu) == .@"union"))
12675 {
12676 // We only need to load the operand if there's a union payload capture
12677 // since it's always runtime-known; only the tag is comptime-known here.
12678 break :load_operand .{ undefined, undefined };
12673 if (capture != .none and operand_ty.zigTypeTag(zcu) == .@"union") {
12674 // Non-OPV union payload captures are always runtime-known.
12675 break :need_load true;
12676 }
12677 if (kind == .inline_ref) {
12678 // `inline_ref` *is* the (comptime-known) capture.
12679 break :need_load false;
1267912680 }
1268012681 assert(zir_switch.any_maybe_runtime_capture); // should have caught everything else by now
12682 if (capture != .by_ref and
12683 kind == .item_refs and kind.item_refs.len == 1)
12684 {
12685 // Capture is comptime-known because it's the only prong item
12686 break :need_load false;
12687 }
12688 break :need_load true;
12689 };
12690
12691 const operand_val: Air.Inst.Ref, const operand_ptr: Air.Inst.Ref = load_operand: {
12692 if (!need_load) break :load_operand .{ .none, .none };
1268112693 switch (operand) {
1268212694 .simple => |s| break :load_operand .{ s.by_val, s.by_ref },
1268312695 .loop => |l| {
......@@ -12686,7 +12698,7 @@ fn analyzeSwitchProng(
1268612698 const by_val = try sema.analyzeLoad(case_block, operand_src, loaded, operand_src);
1268712699 break :load_operand .{ by_val, loaded };
1268812700 } else {
12689 break :load_operand .{ loaded, undefined };
12701 break :load_operand .{ loaded, .none };
1269012702 }
1269112703 },
1269212704 }
......@@ -12735,7 +12747,7 @@ fn analyzeSwitchProng(
1273512747fn analyzeSwitchTagCapture(
1273612748 sema: *Sema,
1273712749 case_block: *Block,
12738 /// May be `undefined` if `inline_case_capture` is not `.none`.
12750 /// May be `none` if this is an inline capture or if `kind.item_refs.len == 1`.
1273912751 operand_val: Air.Inst.Ref,
1274012752 operand_ty: Type,
1274112753 capture_src: LazySrcLoc,
......@@ -12768,9 +12780,11 @@ fn analyzeSwitchPayloadCapture(
1276812780 sema: *Sema,
1276912781 case_block: *Block,
1277012782 operand: SwitchOperand,
12771 /// May be `undefined` if this is an inline capture and operand is not a union.
12783 /// Always has to be not-`none` if this is a union payload capture.
12784 /// For non-union captures, this may be `none` if this is an inline capture
12785 /// or if `kind.item_refs.len == 1` and capture is by val.
1277212786 operand_val: Air.Inst.Ref,
12773 /// May be `undefined` if `capture_by_ref` is `false` or if `operand_val` is also `undefined`.
12787 /// May be `none` if `capture_by_ref` is `false` or if `operand_val` is also `none`.
1277412788 operand_ptr: Air.Inst.Ref,
1277512789 operand_ty: Type,
1277612790 operand_src: LazySrcLoc,
......@@ -12816,8 +12830,6 @@ fn analyzeSwitchPayloadCapture(
1281612830 }
1281712831 }
1281812832
12819 const operand_ptr_ty = if (capture_by_ref) sema.typeOf(operand_ptr) else undefined;
12820
1282112833 if (kind == .special) {
1282212834 if (capture_by_ref) return operand_ptr;
1282312835 return switch (operand_ty.zigTypeTag(zcu)) {
......@@ -12894,7 +12906,7 @@ fn analyzeSwitchPayloadCapture(
1289412906
1289512907 // By-reference captures have some further restrictions which make them easier to emit
1289612908 if (capture_by_ref) {
12897 const operand_ptr_info = operand_ptr_ty.ptrInfo(zcu);
12909 const operand_ptr_info = sema.typeOf(operand_ptr).ptrInfo(zcu);
1289812910 const capture_ptr_ty = resolve: {
1289912911 // By-ref captures of hetereogeneous types are only allowed if all field
1290012912 // pointer types are peer resolvable to each other.
......@@ -13136,7 +13148,7 @@ fn analyzeSwitchPayloadCapture(
1313613148 if (case_vals.len == 1) {
1313713149 const item_val = sema.resolveConstDefinedValue(case_block, .unneeded, case_vals[0], undefined) catch unreachable;
1313813150 const item_ty = try pt.singleErrorSetType(item_val.getErrorName(zcu).unwrap().?);
13139 return sema.bitCast(case_block, item_ty, operand_val, operand_src, null);
13151 return sema.bitCast(case_block, item_ty, .fromValue(item_val), operand_src, null);
1314013152 }
1314113153
1314213154 var names: InferredErrorSet.NameMap = .{};