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(...@@ -10761,7 +10761,7 @@ fn analyzeSwitchBlock(
10761 const val, const ref = if (operand_is_ref)10761 const val, const ref = if (operand_is_ref)
10762 .{ try sema.analyzeLoad(block, src, raw_operand, operand_src), raw_operand }10762 .{ try sema.analyzeLoad(block, src, raw_operand, operand_src), raw_operand }
10763 else10763 else
10764 .{ raw_operand, undefined };10764 .{ raw_operand, .none };
1076510765
10766 const operand_ty = sema.typeOf(val);10766 const operand_ty = sema.typeOf(val);
10767 const maybe_operand_opv = try sema.typeHasOnePossibleValue(operand_ty);10767 const maybe_operand_opv = try sema.typeHasOnePossibleValue(operand_ty);
...@@ -10785,7 +10785,7 @@ fn analyzeSwitchBlock(...@@ -10785,7 +10785,7 @@ fn analyzeSwitchBlock(
10785 const operand_alloc = try block.addTy(.alloc, operand_ptr_ty);10785 const operand_alloc = try block.addTy(.alloc, operand_ptr_ty);
10786 _ = try block.addBinOp(.store, operand_alloc, raw_operand);10786 _ = try block.addBinOp(.store, operand_alloc, raw_operand);
10787 break :alloc operand_alloc;10787 break :alloc operand_alloc;
10788 } else undefined;10788 } else .none;
10789 break :operand .{ .{ .loop = .{10789 break :operand .{ .{ .loop = .{
10790 .operand_alloc = operand_alloc,10790 .operand_alloc = operand_alloc,
10791 .operand_is_ref = operand_is_ref,10791 .operand_is_ref = operand_is_ref,
...@@ -10857,7 +10857,7 @@ fn analyzeSwitchBlock(...@@ -10857,7 +10857,7 @@ fn analyzeSwitchBlock(
10857 const new_val, const new_ref = if (operand_is_ref)10857 const new_val, const new_ref = if (operand_is_ref)
10858 .{ try sema.analyzeLoad(child_block, src, new_operand, new_operand_src), new_operand }10858 .{ try sema.analyzeLoad(child_block, src, new_operand, new_operand_src), new_operand }
10859 else10859 else
10860 .{ new_operand, undefined };10860 .{ new_operand, .none };
1086110861
10862 const new_cond_ref = if (union_originally)10862 const new_cond_ref = if (union_originally)
10863 try sema.unionToTag(child_block, item_ty, new_val, src)10863 try sema.unionToTag(child_block, item_ty, new_val, src)
...@@ -10953,7 +10953,7 @@ fn analyzeSwitchBlock(...@@ -10953,7 +10953,7 @@ fn analyzeSwitchBlock(
10953 const by_val = try sema.analyzeLoad(block, src, loaded, src);10953 const by_val = try sema.analyzeLoad(block, src, loaded, src);
10954 break :load_operand .{ by_val, loaded };10954 break :load_operand .{ by_val, loaded };
10955 } else {10955 } else {
10956 break :load_operand .{ loaded, undefined };10956 break :load_operand .{ loaded, .none };
10957 }10957 }
10958 },10958 },
10959 };10959 };
...@@ -11393,33 +11393,31 @@ fn finishSwitchBr(...@@ -11393,33 +11393,31 @@ fn finishSwitchBr(
11393 var emit_bb = false;11393 var emit_bb = false;
11394 if (has_else and else_case.is_inline) {11394 if (has_else and else_case.is_inline) {
11395 const else_prong_src = block.src(.{ .node_offset_switch_else_prong = src_node_offset });11395 const else_prong_src = block.src(.{ .node_offset_switch_else_prong = src_node_offset });
11396 var error_names: InternPool.NullTerminatedString.Slice = undefined;11396 const error_names, const min_int = check_enumerable: {
11397 var min_int: Value = undefined;
11398 check_enumerable: {
11399 switch (item_ty.zigTypeTag(zcu)) {11397 switch (item_ty.zigTypeTag(zcu)) {
11400 .@"union" => unreachable,11398 .@"union" => unreachable,
11401 .@"enum" => if (else_is_named_only or11399 .@"enum" => if (else_is_named_only or
11402 !item_ty.isNonexhaustiveEnum(zcu) or union_originally)11400 !item_ty.isNonexhaustiveEnum(zcu) or union_originally)
11403 {11401 {
11404 try branch_hints.ensureUnusedCapacity(gpa, @intCast(validated_switch.seen_enum_fields.len));11402 try branch_hints.ensureUnusedCapacity(gpa, @intCast(validated_switch.seen_enum_fields.len));
11405 break :check_enumerable;11403 break :check_enumerable .{ undefined, undefined };
11406 },11404 },
11407 .error_set => if (!operand_ty.isAnyError(zcu)) {11405 .error_set => if (!operand_ty.isAnyError(zcu)) {
11408 error_names = item_ty.errorSetNames(zcu);11406 const error_names = item_ty.errorSetNames(zcu);
11409 try branch_hints.ensureUnusedCapacity(gpa, error_names.len);11407 try branch_hints.ensureUnusedCapacity(gpa, error_names.len);
11410 break :check_enumerable;11408 break :check_enumerable .{ error_names, undefined };
11411 },11409 },
11412 .int => {11410 .int => {
11413 min_int = try item_ty.minInt(pt, item_ty);11411 const min_int = try item_ty.minInt(pt, item_ty);
11414 break :check_enumerable;11412 break :check_enumerable .{ undefined, min_int };
11415 },11413 },
11416 .bool, .void => break :check_enumerable,11414 .bool, .void => break :check_enumerable .{ undefined, undefined },
11417 else => {},11415 else => {},
11418 }11416 }
11419 return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{11417 return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{
11420 item_ty.fmt(pt),11418 item_ty.fmt(pt),
11421 });11419 });
11422 }11420 };
11423 var unhandled_it = validated_switch.iterateUnhandledItems(error_names, min_int);11421 var unhandled_it = validated_switch.iterateUnhandledItems(error_names, min_int);
11424 while (try unhandled_it.next(sema, item_ty)) |item_val| {11422 while (try unhandled_it.next(sema, item_ty)) |item_val| {
11425 cases_len += 1;11423 cases_len += 1;
...@@ -11660,7 +11658,7 @@ fn fixupSwitchContinues(...@@ -11660,7 +11658,7 @@ fn fixupSwitchContinues(
11660 operand_is_ref: bool,11658 operand_is_ref: bool,
11661 item_ty: Type,11659 item_ty: Type,
11662 mode: enum { normal, opv },11660 mode: enum { normal, opv },
11663 any_non_inline_capture: bool,11661 any_maybe_runtime_capture: bool,
11664 merges: *const Block.Merges,11662 merges: *const Block.Merges,
11665) CompileError!void {11663) CompileError!void {
11666 const pt = sema.pt;11664 const pt = sema.pt;
...@@ -11686,7 +11684,7 @@ fn fixupSwitchContinues(...@@ -11686,7 +11684,7 @@ fn fixupSwitchContinues(
11686 assert(sema.air_instructions.items(.tag)[@intFromEnum(placeholder_inst)] == .br);11684 assert(sema.air_instructions.items(.tag)[@intFromEnum(placeholder_inst)] == .br);
11687 const new_operand_maybe_ref = sema.air_instructions.items(.data)[@intFromEnum(placeholder_inst)].br.operand;11685 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) {
11690 _ = try replacement_block.addBinOp(.store, operand.loop.operand_alloc, new_operand_maybe_ref);11688 _ = try replacement_block.addBinOp(.store, operand.loop.operand_alloc, new_operand_maybe_ref);
11691 }11689 }
1169211690
...@@ -12431,6 +12429,8 @@ fn resolveSwitchBlock(...@@ -12431,6 +12429,8 @@ fn resolveSwitchBlock(
12431 }12429 }
12432 }12430 }
1243312431
12432 assert(zir_switch.else_case != null or under_prong != null); // switch exhaustion check wrong
12433
12434 const else_case = validated_switch.else_case;12434 const else_case = validated_switch.else_case;
12435 const else_is_named_only = zir_switch.else_case != null and under_prong != null;12435 const else_is_named_only = zir_switch.else_case != null and under_prong != null;
1243612436
...@@ -12507,8 +12507,8 @@ const SwitchOperand = union(enum) {...@@ -12507,8 +12507,8 @@ const SwitchOperand = union(enum) {
12507 simple: struct {12507 simple: struct {
12508 /// The raw switch operand value. Always defined.12508 /// The raw switch operand value. Always defined.
12509 by_val: Air.Inst.Ref,12509 by_val: Air.Inst.Ref,
12510 /// The switch operand *pointer*. Defined only if there is a prong12510 /// The switch operand *pointer*. `none` if there are no prongs with a
12511 /// with a by-ref capture.12511 /// by-ref capture.
12512 by_ref: Air.Inst.Ref,12512 by_ref: Air.Inst.Ref,
12513 /// The switch condition value. For unions, `operand` is the union12513 /// The switch condition value. For unions, `operand` is the union
12514 /// and `cond` is its enum tag value.12514 /// and `cond` is its enum tag value.
...@@ -12519,7 +12519,7 @@ const SwitchOperand = union(enum) {...@@ -12519,7 +12519,7 @@ const SwitchOperand = union(enum) {
12519 loop: struct {12519 loop: struct {
12520 /// The `alloc` containing the `switch` operand for the active dispatch.12520 /// The `alloc` containing the `switch` operand for the active dispatch.
12521 /// Each prong must load from this `alloc` to get captures.12521 /// 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`.
12523 operand_alloc: Air.Inst.Ref,12523 operand_alloc: Air.Inst.Ref,
12524 /// Whether `operand_alloc` contains a by-val operand or a by-ref12524 /// Whether `operand_alloc` contains a by-val operand or a by-ref
12525 /// operand.12525 /// operand.
...@@ -12665,19 +12665,31 @@ fn analyzeSwitchProng(...@@ -12665,19 +12665,31 @@ fn analyzeSwitchProng(
12665 }12665 }
12666 }12666 }
1266712667
12668 const operand_val, const operand_ptr = load_operand: {12668 const need_load: bool = need_load: {
12669 if (capture == .none and !has_tag_capture) {12669 if (capture == .none and !has_tag_capture) {
12670 // No need to load the operand for this prong!12670 // No need to load the operand for this prong!
12671 break :load_operand .{ undefined, undefined };12671 break :need_load false;
12672 }12672 }
12673 if (kind == .inline_ref and12673 if (capture != .none and operand_ty.zigTypeTag(zcu) == .@"union") {
12674 !(capture != .none and operand_ty.zigTypeTag(zcu) == .@"union"))12674 // Non-OPV union payload captures are always runtime-known.
12675 {12675 break :need_load true;
12676 // We only need to load the operand if there's a union payload capture12676 }
12677 // since it's always runtime-known; only the tag is comptime-known here.12677 if (kind == .inline_ref) {
12678 break :load_operand .{ undefined, undefined };12678 // `inline_ref` *is* the (comptime-known) capture.
12679 break :need_load false;
12679 }12680 }
12680 assert(zir_switch.any_maybe_runtime_capture); // should have caught everything else by now12681 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 };
12681 switch (operand) {12693 switch (operand) {
12682 .simple => |s| break :load_operand .{ s.by_val, s.by_ref },12694 .simple => |s| break :load_operand .{ s.by_val, s.by_ref },
12683 .loop => |l| {12695 .loop => |l| {
...@@ -12686,7 +12698,7 @@ fn analyzeSwitchProng(...@@ -12686,7 +12698,7 @@ fn analyzeSwitchProng(
12686 const by_val = try sema.analyzeLoad(case_block, operand_src, loaded, operand_src);12698 const by_val = try sema.analyzeLoad(case_block, operand_src, loaded, operand_src);
12687 break :load_operand .{ by_val, loaded };12699 break :load_operand .{ by_val, loaded };
12688 } else {12700 } else {
12689 break :load_operand .{ loaded, undefined };12701 break :load_operand .{ loaded, .none };
12690 }12702 }
12691 },12703 },
12692 }12704 }
...@@ -12735,7 +12747,7 @@ fn analyzeSwitchProng(...@@ -12735,7 +12747,7 @@ fn analyzeSwitchProng(
12735fn analyzeSwitchTagCapture(12747fn analyzeSwitchTagCapture(
12736 sema: *Sema,12748 sema: *Sema,
12737 case_block: *Block,12749 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`.
12739 operand_val: Air.Inst.Ref,12751 operand_val: Air.Inst.Ref,
12740 operand_ty: Type,12752 operand_ty: Type,
12741 capture_src: LazySrcLoc,12753 capture_src: LazySrcLoc,
...@@ -12768,9 +12780,11 @@ fn analyzeSwitchPayloadCapture(...@@ -12768,9 +12780,11 @@ fn analyzeSwitchPayloadCapture(
12768 sema: *Sema,12780 sema: *Sema,
12769 case_block: *Block,12781 case_block: *Block,
12770 operand: SwitchOperand,12782 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.
12772 operand_val: Air.Inst.Ref,12786 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`.
12774 operand_ptr: Air.Inst.Ref,12788 operand_ptr: Air.Inst.Ref,
12775 operand_ty: Type,12789 operand_ty: Type,
12776 operand_src: LazySrcLoc,12790 operand_src: LazySrcLoc,
...@@ -12816,8 +12830,6 @@ fn analyzeSwitchPayloadCapture(...@@ -12816,8 +12830,6 @@ fn analyzeSwitchPayloadCapture(
12816 }12830 }
12817 }12831 }
1281812832
12819 const operand_ptr_ty = if (capture_by_ref) sema.typeOf(operand_ptr) else undefined;
12820
12821 if (kind == .special) {12833 if (kind == .special) {
12822 if (capture_by_ref) return operand_ptr;12834 if (capture_by_ref) return operand_ptr;
12823 return switch (operand_ty.zigTypeTag(zcu)) {12835 return switch (operand_ty.zigTypeTag(zcu)) {
...@@ -12894,7 +12906,7 @@ fn analyzeSwitchPayloadCapture(...@@ -12894,7 +12906,7 @@ fn analyzeSwitchPayloadCapture(
1289412906
12895 // By-reference captures have some further restrictions which make them easier to emit12907 // By-reference captures have some further restrictions which make them easier to emit
12896 if (capture_by_ref) {12908 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);
12898 const capture_ptr_ty = resolve: {12910 const capture_ptr_ty = resolve: {
12899 // By-ref captures of hetereogeneous types are only allowed if all field12911 // By-ref captures of hetereogeneous types are only allowed if all field
12900 // pointer types are peer resolvable to each other.12912 // pointer types are peer resolvable to each other.
...@@ -13136,7 +13148,7 @@ fn analyzeSwitchPayloadCapture(...@@ -13136,7 +13148,7 @@ fn analyzeSwitchPayloadCapture(
13136 if (case_vals.len == 1) {13148 if (case_vals.len == 1) {
13137 const item_val = sema.resolveConstDefinedValue(case_block, .unneeded, case_vals[0], undefined) catch unreachable;13149 const item_val = sema.resolveConstDefinedValue(case_block, .unneeded, case_vals[0], undefined) catch unreachable;
13138 const item_ty = try pt.singleErrorSetType(item_val.getErrorName(zcu).unwrap().?);13150 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);
13140 }13152 }
1314113153
13142 var names: InferredErrorSet.NameMap = .{};13154 var names: InferredErrorSet.NameMap = .{};