From be9b42d7074077eadf18e8d934c6f22051397a72 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 11 Mar 2026 16:30:12 +0100 Subject: [PATCH 1/3] compiler: allow equality comparisons for packed unions This was already possible in practice by just wrapping a packed union into a packed struct. Now it's also possible without doing that. --- doc/langref.html.in | 7 +++++++ doc/langref/test_packed_union_equality.zig | 14 ++++++++++++++ src/Type.zig | 3 +-- src/codegen/llvm.zig | 2 +- test/behavior/packed-union.zig | 20 ++++++++++++++++++++ 5 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 doc/langref/test_packed_union_equality.zig diff --git a/doc/langref.html.in b/doc/langref.html.in index 7118d01c91..adac8f137f 100644 --- a/doc/langref.html.in +++ b/doc/langref.html.in @@ -2513,6 +2513,13 @@ or

A {#syntax#}packed union{#endsyntax#} has well-defined in-memory layout and is eligible to be in a {#link|packed struct#}.

All fields in a packed union must have the same {#link|@bitSizeOf#}.

+ +

+ Equating packed unions results in a comparison of the backing integer, + and only works for the {#syntax#}=={#endsyntax#} and {#syntax#}!={#endsyntax#} {#link|Operators#}. +

+ {#code|test_packed_union_equality.zig#} + {#header_close#} {#header_open|Anonymous Union Literals#} diff --git a/doc/langref/test_packed_union_equality.zig b/doc/langref/test_packed_union_equality.zig new file mode 100644 index 0000000000..5ba69110ab --- /dev/null +++ b/doc/langref/test_packed_union_equality.zig @@ -0,0 +1,14 @@ +const std = @import("std"); +const expectEqual = std.testing.expectEqual; + +test "packed union equality" { + const U = packed union { + a: u4, + b: i4, + }; + const x: U = .{ .a = 3 }; + const y: U = .{ .b = 3 }; + try expectEqual(x, y); +} + +// test diff --git a/src/Type.zig b/src/Type.zig index 841ca38b0e..475fb09edd 100644 --- a/src/Type.zig +++ b/src/Type.zig @@ -334,11 +334,10 @@ pub fn isSelfComparable(ty: Type, zcu: *const Zcu, is_equality_cmp: bool) bool { .undefined, .null, .error_union, - .@"union", .frame, => false, - .@"struct" => is_equality_cmp and ty.containerLayout(zcu) == .@"packed", + .@"struct", .@"union" => is_equality_cmp and ty.containerLayout(zcu) == .@"packed", .pointer => !ty.isSlice(zcu) and (is_equality_cmp or ty.isCPtr(zcu)), .optional => { if (!is_equality_cmp) return false; diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index 0907396630..7661d2fe52 100644 --- a/src/codegen/llvm.zig +++ b/src/codegen/llvm.zig @@ -5750,7 +5750,7 @@ pub const FuncGen = struct { return phi.toValue(); }, .float => return self.buildFloatCmp(fast, op, operand_ty, .{ lhs, rhs }), - .@"struct" => scalar_ty.bitpackBackingInt(zcu), + .@"struct", .@"union" => scalar_ty.bitpackBackingInt(zcu), else => unreachable, }; const is_signed = int_ty.isSignedInt(zcu); diff --git a/test/behavior/packed-union.zig b/test/behavior/packed-union.zig index 330a4853b1..fec1426657 100644 --- a/test/behavior/packed-union.zig +++ b/test/behavior/packed-union.zig @@ -199,3 +199,23 @@ test "packed union with explicit backing integer" { try U.check(.{ .raw = -2 }); try comptime U.check(.{ .raw = -2 }); } + +test "packed union equality" { + const Foo = packed union { + a: u4, + b: i4, + }; + + const S = struct { + fn doTest(x: Foo, y: Foo) !void { + try expect(x == y); + try expect(!(x != y)); + } + }; + + const x: Foo = .{ .a = 3 }; + const y: Foo = .{ .b = 3 }; + + try S.doTest(x, y); + comptime try S.doTest(x, y); +} From 28886ca9ec3170c3d97308c6e504cdcc50654b65 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 11 Mar 2026 13:51:31 +0100 Subject: [PATCH 2/3] Sema: implement switch for packed structs/unions Since packed containers are now represented by `bitpack` and can't include pointers anymore this has become a very easy change to make. This commit largely just reuses the logic already in place for integers. Also fixes a small bug where captures-by-ref of errors wouldn't cause a compile error for regular switch statements. There was already an astgen error in place for error handling switch statements (`switch_block_err_union`) capturing their error by reference. --- src/Sema.zig | 1093 +++++++++-------- src/codegen/llvm.zig | 2 +- test/behavior/switch.zig | 130 ++ test/behavior/switch_loop.zig | 55 + .../switch_capture_error_by_ref.zig | 25 + .../switch_capture_packed_union_tag.zig | 15 + .../switch_on_non_packed_struct.zig | 25 + .../switch_packed_exhaustion.zig | 41 + 8 files changed, 851 insertions(+), 535 deletions(-) create mode 100644 test/cases/compile_errors/switch_capture_error_by_ref.zig create mode 100644 test/cases/compile_errors/switch_capture_packed_union_tag.zig create mode 100644 test/cases/compile_errors/switch_on_non_packed_struct.zig create mode 100644 test/cases/compile_errors/switch_packed_exhaustion.zig diff --git a/src/Sema.zig b/src/Sema.zig index f2c52697b4..65381b8375 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -10211,15 +10211,17 @@ fn analyzeSwitchBlock( const operand_ty = sema.typeOf(val); operand_ty.assertHasLayout(zcu); const maybe_operand_opv = try operand_ty.onePossibleValue(pt); - const init_cond: Air.Inst.Ref, const item_ty: Type = switch (operand_ty.zigTypeTag(zcu)) { - .@"union" => tag: { + const init_cond: Air.Inst.Ref, const item_ty: Type = init: { + if (operand_ty.zigTypeTag(zcu) == .@"union" and + operand_ty.containerLayout(zcu) != .@"packed") + { const tag_val = try sema.unionToTag(block, val); - break :tag .{ tag_val, sema.typeOf(tag_val) }; - }, - else => .{ + break :init .{ tag_val, sema.typeOf(tag_val) }; + } + break :init .{ if (maybe_operand_opv) |operand_opv| .fromValue(operand_opv) else val, operand_ty, - }, + }; }; item_ty.assertHasLayout(zcu); @@ -10250,7 +10252,8 @@ fn analyzeSwitchBlock( const raw_operand_ty = sema.typeOf(raw_operand); - const union_originally = operand_ty.zigTypeTag(zcu) == .@"union"; + const tagged_union_originally = operand_ty.zigTypeTag(zcu) == .@"union" and + operand_ty.containerLayout(zcu) != .@"packed"; const err_set = operand_ty.zigTypeTag(zcu) == .error_set; if (item_ty.zigTypeTag(zcu) == .@"enum" and @@ -10305,7 +10308,7 @@ fn analyzeSwitchBlock( else .{ new_operand, .none }; - const new_cond_ref = if (union_originally) + const new_cond_ref = if (tagged_union_originally) try sema.unionToTag(child_block, new_val) else new_val; @@ -10327,9 +10330,26 @@ fn analyzeSwitchBlock( unreachable; } - if (try item_ty.onePossibleValue(pt)) |item_opv| { + const switch_ref: Air.Inst.Ref, const item_has_opv = switch_ref: { + const item_opv = try item_ty.onePossibleValue(pt) orelse { + assert(maybe_operand_opv == null); // `operand_ty` can only be an OPV type if `item_ty` is one too! + const air_ref = try sema.finishSwitchBr( + block, + child_block, + operand, + raw_operand_ty, + operand_is_ref, + merges, + switch_inst, + zir_switch, + validated_switch, + ); + break :switch_ref .{ air_ref, false }; + }; + // We simplify conditions with OPV to either a `loop` or a `block` since // we cannot switch on a value which doesn't exist at runtime. + assert(operand == .loop); // `simple` should have already been comptime-resolved above! var case_block = child_block.makeSubBlock(); @@ -10374,7 +10394,7 @@ fn analyzeSwitchBlock( unreachable; // malformed validated switch }; - const analyze_body = sema.wantSwitchProngBodyAnalysis(.fromValue(item_opv), operand_ty, union_originally, err_set, false); + const analyze_body = sema.wantSwitchProngBodyAnalysis(.fromValue(item_opv), operand_ty, tagged_union_originally, err_set, false); if (!analyze_body) return .unreachable_value; if (!(err_set and @@ -10384,47 +10404,46 @@ fn analyzeSwitchBlock( const payload_inst: Zir.Inst.Index = if (capture != .none) inst: { const payload_inst = zir_switch.payload_capture_placeholder.unwrap() orelse switch_inst; const payload_ref: Air.Inst.Ref = payload_ref: { - const item_val: Value = switch (operand_ty.zigTypeTag(zcu)) { - .@"union" => item_val: { - if (maybe_operand_opv) |operand_opv| { - break :item_val .fromInterned(zcu.intern_pool.indexToKey(operand_opv.toIntern()).un.val); - } - assert(union_originally); // operand type must be union, otherwise it would be an OPV type here - assert(zir_switch.any_maybe_runtime_capture); // there's a payload capture - const operand_val, const operand_ref = switch (operand) { - .simple => unreachable, - .loop => |l| load_operand: { - const loaded = try sema.analyzeLoad(block, src, l.operand_alloc, src); - if (l.operand_is_ref) { - const by_val = try sema.analyzeLoad(block, src, loaded, src); - break :load_operand .{ by_val, loaded }; - } else { - break :load_operand .{ loaded, .none }; - } - }, - }; - const prong_kind: SwitchProngKind = kind: { - if (is_inline) break :kind .{ .inline_ref = .fromValue(item_opv) }; - if (is_special) break :kind .special; - break :kind .{ .item_refs = &.{.fromValue(item_opv)} }; - }; - break :payload_ref try sema.analyzeSwitchPayloadCapture( - &case_block, - operand, - operand_val, - operand_ref, - operand_ty, - operand_src, - block.src(.{ .switch_capture = .{ - .switch_node_offset = src_node_offset, - .case_idx = index, - } }), - capture == .by_ref, - prong_kind, - validated_switch.else_err_ty, - ); - }, - else => item_opv, + const item_val: Value = item_val: { + if (!tagged_union_originally) { + break :item_val item_opv; + } + if (maybe_operand_opv) |operand_opv| { + break :item_val .fromInterned(zcu.intern_pool.indexToKey(operand_opv.toIntern()).un.val); + } + assert(zir_switch.any_maybe_runtime_capture); // there's a payload capture + const operand_val, const operand_ref = switch (operand) { + .simple => unreachable, + .loop => |l| load_operand: { + const loaded = try sema.analyzeLoad(block, src, l.operand_alloc, src); + if (l.operand_is_ref) { + const by_val = try sema.analyzeLoad(block, src, loaded, src); + break :load_operand .{ by_val, loaded }; + } else { + break :load_operand .{ loaded, .none }; + } + }, + }; + const prong_kind: SwitchProngKind = kind: { + if (is_inline) break :kind .{ .inline_ref = .fromValue(item_opv) }; + if (is_special) break :kind .special; + break :kind .{ .item_refs = &.{.fromValue(item_opv)} }; + }; + break :payload_ref try sema.analyzeSwitchPayloadCapture( + &case_block, + operand, + operand_val, + operand_ref, + operand_ty, + operand_src, + block.src(.{ .switch_capture = .{ + .switch_node_offset = src_node_offset, + .case_idx = index, + } }), + capture == .by_ref, + prong_kind, + validated_switch.else_err_ty, + ); }; break :payload_ref switch (capture) { .by_val => .fromValue(item_val), @@ -10462,40 +10481,100 @@ fn analyzeSwitchBlock( .loop else .block; - const air_loop_ref = try child_block.addInst(.{ + const air_ref = try child_block.addInst(.{ .tag = air_tag, .data = .{ .ty_pl = .{ .ty = .noreturn_type, .payload = payload_index, } }, }); - try sema.fixupSwitchContinues( - block, - src, - air_loop_ref, - operand, - operand_is_ref, - item_ty, - .opv, - zir_switch.any_maybe_runtime_capture, - merges, - ); - return null; + break :switch_ref .{ air_ref, true }; + }; + + const air_tag = sema.air_instructions.items(.tag)[@intFromEnum(switch_ref.toIndex().?)]; + switch (air_tag) { + .loop_switch_br, .switch_br => assert(!item_has_opv), + .loop, .block => assert(item_has_opv), + else => unreachable, + } + switch (air_tag) { + .loop_switch_br, .loop => assert(merges.extra_insts.items.len > 0), + .switch_br, .block => assert(merges.extra_insts.items.len == 0), + else => unreachable, } - assert(maybe_operand_opv == null); // `operand_ty` can only be an OPV type if `item_ty` is one too! + // We're done with analyzing the switch statement! Now all we have to do is + // replace the placeholder `br` insts inserted by `zirSwitchContinue`s with + // their respective finalized inst pointing back at `switch_ref`. + + for (merges.extra_insts.items, merges.extra_src_locs.items) |placeholder_inst, dispatch_src| { + var replacement_block = block.makeSubBlock(); + defer replacement_block.instructions.deinit(gpa); + + 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 (zir_switch.any_maybe_runtime_capture and !item_has_opv) { + _ = try replacement_block.addBinOp(.store, operand.loop.operand_alloc, new_operand_maybe_ref); + } + + const new_operand_val = if (operand_is_ref) + try sema.analyzeLoad(&replacement_block, dispatch_src, new_operand_maybe_ref, dispatch_src) + else + new_operand_maybe_ref; + + const new_cond = try sema.coerce(&replacement_block, item_ty, new_operand_val, dispatch_src); + + if (zcu.backendSupportsFeature(.is_named_enum_value) and block.wantSafety() and + item_ty.zigTypeTag(zcu) == .@"enum" and !item_ty.isNonexhaustiveEnum(zcu) and + !item_has_opv and !try sema.isComptimeKnown(new_cond)) + { + const ok = try replacement_block.addUnOp(.is_named_enum_value, new_cond); + try sema.addSafetyCheck(&replacement_block, src, ok, .corrupt_switch); + } + + if (item_has_opv) { + _ = try replacement_block.addInst(.{ + .tag = .repeat, + .data = .{ .repeat = .{ + .loop_inst = switch_ref.toIndex().?, + } }, + }); + } else { + _ = try replacement_block.addInst(.{ + .tag = .switch_dispatch, + .data = .{ .br = .{ + .block_inst = switch_ref.toIndex().?, + .operand = new_cond, + } }, + }); + } + + if (replacement_block.instructions.items.len == 1) { + // Optimization: we don't need a block! + sema.air_instructions.set( + @intFromEnum(placeholder_inst), + sema.air_instructions.get(@intFromEnum(replacement_block.instructions.items[0])), + ); + continue; + } + + // Replace placeholder with a block. + // No `br` is needed as the block is a switch dispatch so necessarily `noreturn`. + try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.Block).@"struct".fields.len + + replacement_block.instructions.items.len); + sema.air_instructions.set(@intFromEnum(placeholder_inst), .{ + .tag = .block, + .data = .{ .ty_pl = .{ + .ty = .noreturn_type, + .payload = sema.addExtraAssumeCapacity(Air.Block{ + .body_len = @intCast(replacement_block.instructions.items.len), + }), + } }, + }); + sema.air_extra.appendSliceAssumeCapacity(@ptrCast(replacement_block.instructions.items)); + } - try sema.finishSwitchBr( - block, - child_block, - operand, - raw_operand_ty, - operand_is_ref, - merges, - switch_inst, - zir_switch, - validated_switch, - ); return null; } @@ -10510,7 +10589,7 @@ fn finishSwitchBr( switch_inst: Zir.Inst.Index, zir_switch: *const Zir.UnwrappedSwitchBlock, validated_switch: *const ValidatedSwitchBlock, -) CompileError!void { +) CompileError!Air.Inst.Ref { const pt = sema.pt; const zcu = pt.zcu; const ip = &zcu.intern_pool; @@ -10544,13 +10623,15 @@ fn finishSwitchBr( const else_is_named_only = has_else and has_under; - const item_ty = switch (operand_ty.zigTypeTag(zcu)) { - .@"union" => operand_ty.unionTagType(zcu).?, - else => operand_ty, - }; - const union_originally = operand_ty.zigTypeTag(zcu) == .@"union"; + const tagged_union_originally = operand_ty.zigTypeTag(zcu) == .@"union" and + operand_ty.containerLayout(zcu) != .@"packed"; const err_set = operand_ty.zigTypeTag(zcu) == .error_set; + const item_ty = if (tagged_union_originally) + operand_ty.unionTagType(zcu).? + else + operand_ty; + const estimated_cases_len: u32 = scalar_cases_len + multi_cases_len + @intFromBool(has_else or has_under); @@ -10632,7 +10713,7 @@ fn finishSwitchBr( if (item_ref == .none) is_under_prong = true; if (item_info.bodyLen()) |body_len| extra_index += body_len; - const analyze_body = sema.wantSwitchProngBodyAnalysis(item_ref, operand_ty, union_originally, err_set, prong_info.is_comptime_unreach); + const analyze_body = sema.wantSwitchProngBodyAnalysis(item_ref, operand_ty, tagged_union_originally, err_set, prong_info.is_comptime_unreach); if (analyze_body) any_analyze_body = true; if (prong_info.is_inline) { @@ -10840,9 +10921,8 @@ fn finishSwitchBr( const else_prong_src = block.src(.{ .node_offset_switch_else_prong = src_node_offset }); 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) + !item_ty.isNonexhaustiveEnum(zcu) or tagged_union_originally) { try branch_hints.ensureUnusedCapacity(gpa, @intCast(validated_switch.seen_enum_fields.len)); break :check_enumerable .{ undefined, undefined }; @@ -10856,6 +10936,11 @@ fn finishSwitchBr( const min_int = try item_ty.minInt(pt, item_ty); break :check_enumerable .{ undefined, min_int }; }, + .@"union", .@"struct" => { + const backing_int_ty = item_ty.bitpackBackingInt(zcu); + const min_backing_int = try backing_int_ty.minInt(pt, backing_int_ty); + break :check_enumerable .{ undefined, min_backing_int }; + }, .bool, .void => break :check_enumerable .{ undefined, undefined }, else => {}, } @@ -10871,7 +10956,7 @@ fn finishSwitchBr( const item_ref: Air.Inst.Ref = .fromValue(item_val); - const analyze_body = sema.wantSwitchProngBodyAnalysis(item_ref, operand_ty, union_originally, err_set, false); + const analyze_body = sema.wantSwitchProngBodyAnalysis(item_ref, operand_ty, tagged_union_originally, err_set, false); if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src); emit_bb = true; @@ -10918,13 +11003,11 @@ fn finishSwitchBr( if (zcu.backendSupportsFeature(.is_named_enum_value) and (has_else or has_under) and block.wantSafety() and item_ty.zigTypeTag(zcu) == .@"enum" and - (!operand_ty.isNonexhaustiveEnum(zcu) or union_originally)) + (!operand_ty.isNonexhaustiveEnum(zcu) or tagged_union_originally)) { try sema.zirDbgStmt(&case_block, cond_dbg_node_index); const ok = try case_block.addUnOp(.is_named_enum_value, cond_ref); - if (else_is_named_only) {} else { - try sema.addSafetyCheck(&case_block, src, ok, .corrupt_switch); - } + try sema.addSafetyCheck(&case_block, src, ok, .corrupt_switch); } if (else_is_named_only and !else_case.is_inline) { @@ -10991,13 +11074,15 @@ fn finishSwitchBr( const analyze_catch_all_body = analyze_body: { if (has_under) { - break :analyze_body true; // can't be a union or an error set, never inlined + assert(!tagged_union_originally); + assert(!err_set); + break :analyze_body true; // can never be inline } else if (has_else) { if (else_case.is_inline) break :analyze_body false; // already handled above } else { break :analyze_body false; // we still may want a safety check! } - if (union_originally) { + if (tagged_union_originally) { const union_obj = zcu.typeToUnion(operand_ty).?; for (validated_switch.seen_enum_fields, 0..) |seen_field, field_i| { if (seen_field != null) continue; @@ -11072,126 +11157,14 @@ fn finishSwitchBr( .loop_switch_br else .switch_br; - const air_switch_ref = try child_block.addInst(.{ + const air_ref = try child_block.addInst(.{ .tag = air_tag, .data = .{ .pl_op = .{ .operand = cond_ref, .payload = payload_index, } }, }); - try sema.fixupSwitchContinues( - block, - src, - air_switch_ref, - operand, - operand_is_ref, - item_ty, - .normal, - zir_switch.any_maybe_runtime_capture, - merges, - ); -} - -/// This is the counterpart to `zirSwitchContinue`; replaces placeholder `br` insts -/// with their respective finalized inst pointing back at `switch_ref`. -fn fixupSwitchContinues( - sema: *Sema, - block: *Block, - switch_src: LazySrcLoc, - switch_ref: Air.Inst.Ref, - operand: SwitchOperand, - operand_is_ref: bool, - item_ty: Type, - mode: enum { normal, opv }, - any_maybe_runtime_capture: bool, - merges: *const Block.Merges, -) CompileError!void { - const pt = sema.pt; - const zcu = pt.zcu; - const gpa = sema.gpa; - - const air_tag = sema.air_instructions.items(.tag)[@intFromEnum(switch_ref.toIndex().?)]; - switch (air_tag) { - .loop_switch_br, .switch_br => assert(mode == .normal), - .loop, .block => assert(mode == .opv), - else => unreachable, - } - switch (air_tag) { - .loop_switch_br, .loop => assert(merges.extra_insts.items.len > 0), - .switch_br, .block => assert(merges.extra_insts.items.len == 0), - else => unreachable, - } - - for (merges.extra_insts.items, merges.extra_src_locs.items) |placeholder_inst, dispatch_src| { - var replacement_block = block.makeSubBlock(); - defer replacement_block.instructions.deinit(gpa); - - 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_maybe_runtime_capture and mode != .opv) { - _ = try replacement_block.addBinOp(.store, operand.loop.operand_alloc, new_operand_maybe_ref); - } - - const new_operand_val = if (operand_is_ref) - try sema.analyzeLoad(&replacement_block, dispatch_src, new_operand_maybe_ref, dispatch_src) - else - new_operand_maybe_ref; - - const new_cond = try sema.coerce(&replacement_block, item_ty, new_operand_val, dispatch_src); - - if (zcu.backendSupportsFeature(.is_named_enum_value) and block.wantSafety() and - item_ty.zigTypeTag(zcu) == .@"enum" and !item_ty.isNonexhaustiveEnum(zcu) and - mode == .normal and !try sema.isComptimeKnown(new_cond)) - { - const ok = try replacement_block.addUnOp(.is_named_enum_value, new_cond); - try sema.addSafetyCheck(&replacement_block, switch_src, ok, .corrupt_switch); - } - - switch (mode) { - .normal => { - _ = try replacement_block.addInst(.{ - .tag = .switch_dispatch, - .data = .{ .br = .{ - .block_inst = switch_ref.toIndex().?, - .operand = new_cond, - } }, - }); - }, - .opv => { - _ = try replacement_block.addInst(.{ - .tag = .repeat, - .data = .{ .repeat = .{ - .loop_inst = switch_ref.toIndex().?, - } }, - }); - }, - } - - if (replacement_block.instructions.items.len == 1) { - // Optimization: we don't need a block! - sema.air_instructions.set( - @intFromEnum(placeholder_inst), - sema.air_instructions.get(@intFromEnum(replacement_block.instructions.items[0])), - ); - continue; - } - - // Replace placeholder with a block. - // No `br` is needed as the block is a switch dispatch so necessarily `noreturn`. - try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.Block).@"struct".fields.len + - replacement_block.instructions.items.len); - sema.air_instructions.set(@intFromEnum(placeholder_inst), .{ - .tag = .block, - .data = .{ .ty_pl = .{ - .ty = .noreturn_type, - .payload = sema.addExtraAssumeCapacity(Air.Block{ - .body_len = @intCast(replacement_block.instructions.items.len), - }), - } }, - }); - sema.air_extra.appendSliceAssumeCapacity(@ptrCast(replacement_block.instructions.items)); - } + return air_ref; } const ValidatedSwitchBlock = struct { @@ -11242,7 +11215,6 @@ const ValidatedSwitchBlock = struct { const zcu = pt.zcu; const ip = &zcu.intern_pool; switch (item_ty.zigTypeTag(zcu)) { - .@"union" => unreachable, .@"enum" => { for (it.seen_enum_fields[it.next_idx..], it.next_idx..) |seen_field, field_i| { if (seen_field != null) continue; @@ -11262,26 +11234,35 @@ const ValidatedSwitchBlock = struct { } return null; }, - .int => { - var cur = it.next_val orelse return null; + .int, .@"union", .@"struct" => |type_tag| { + var cur_val = it.next_val orelse return null; + const int_ty = switch (type_tag) { + .int => item_ty, + .@"union", .@"struct" => item_ty.bitpackBackingInt(zcu), + else => unreachable, + }; while (it.next_idx < it.seen_ranges.len and - cur.eql(it.seen_ranges[it.next_idx].first, item_ty, zcu)) + cur_val.eql(it.seen_ranges[it.next_idx].first, int_ty, zcu)) { defer it.next_idx += 1; const incr = try arith.incrementDefinedInt( sema, - item_ty, + int_ty, it.seen_ranges[it.next_idx].last, ); if (incr.overflow) { it.next_val = null; return null; } - cur = incr.val; + cur_val = incr.val; } - const incr = try arith.incrementDefinedInt(sema, item_ty, cur); + const incr = try arith.incrementDefinedInt(sema, int_ty, cur_val); it.next_val = if (incr.overflow) null else incr.val; - return cur; + return switch (type_tag) { + .int => cur_val, + .@"union", .@"struct" => try pt.bitpackValue(item_ty, cur_val), + else => unreachable, + }; }, .bool => { if (!it.seen_true) { @@ -11366,17 +11347,43 @@ fn validateSwitchBlock( => break :item_ty operand_ty, .@"union" => { - const enum_ty = operand_ty.unionTagType(zcu) orelse { - return sema.failWithOwnedErrorMsg(block, msg: { - const msg = try sema.errMsg(operand_src, "switch on union with no attached enum", .{}); - errdefer msg.destroy(sema.gpa); - if (operand_ty.srcLocOrNull(zcu)) |union_src| { - try sema.errNote(union_src, msg, "consider 'union(enum)' here", .{}); + operand_ty.assertHasLayout(zcu); + const union_obj = ip.loadUnionType(operand_ty.toIntern()); + switch (union_obj.tag_usage) { + .tagged => { + break :item_ty .fromInterned(union_obj.enum_tag_type); + }, + .none => { + if (union_obj.layout == .@"packed") { + break :item_ty operand_ty; } - break :msg msg; - }); - }; - break :item_ty enum_ty; + }, + .safety => {}, + } + return sema.failWithOwnedErrorMsg(block, msg: { + const msg = try sema.errMsg(operand_src, "switch on union with no attached enum", .{}); + errdefer msg.destroy(sema.gpa); + if (operand_ty.srcLocOrNull(zcu)) |union_src| { + try sema.errNote(union_src, msg, "consider 'union(enum)' here", .{}); + } + break :msg msg; + }); + }, + + .@"struct" => { + operand_ty.assertHasLayout(zcu); + const layout = operand_ty.containerLayout(zcu); + if (layout == .@"packed") { + break :item_ty operand_ty; + } + return sema.failWithOwnedErrorMsg(block, msg: { + const msg = try sema.errMsg(operand_src, "switch on struct with {t} layout", .{layout}); + errdefer msg.destroy(sema.gpa); + if (operand_ty.srcLocOrNull(zcu)) |struct_src| { + try sema.errNote(struct_src, msg, "consider 'packed struct' here", .{}); + } + break :msg msg; + }); }, .pointer => { @@ -11424,7 +11431,6 @@ fn validateSwitchBlock( const else_case = zir_switch.else_case orelse undefined; switch (item_ty.zigTypeTag(zcu)) { - .@"union" => unreachable, .@"enum" => { seen_enum_fields = try arena.alloc(?LazySrcLoc, item_ty.enumFieldCount(zcu)); @memset(seen_enum_fields, null); @@ -11435,7 +11441,7 @@ fn validateSwitchBlock( .error_set => { try seen_errors.ensureUnusedCapacity(arena, zir_switch.totalItemsLen()); }, - .int, .comptime_int => { + .int, .comptime_int, .@"union", .@"struct" => { try range_set.ensureUnusedCapacity(arena, zir_switch.totalItemsLen()); }, .enum_literal, .@"fn", .pointer, .type => { @@ -11503,7 +11509,6 @@ fn validateSwitchBlock( } switch (item_ty.zigTypeTag(zcu)) { - .@"union" => unreachable, .int, .comptime_int => {}, else => if (zir_switch.anyRanges()) { const range_src = block.src(.{ .node_offset_switch_range = src_node_offset }); @@ -11528,7 +11533,6 @@ fn validateSwitchBlock( // Validate for missing special prongs. switch (item_ty.zigTypeTag(zcu)) { - .@"union" => unreachable, .@"enum" => { const all_tags_handled = for (seen_enum_fields) |seen_src| { if (seen_src == null) break false; @@ -11661,22 +11665,26 @@ fn validateSwitchBlock( }, }; }, - .int, .comptime_int => |type_tag| { + .int, .comptime_int, .@"union", .@"struct" => |type_tag| { check_range: { - if (type_tag == .int) { - const min_int = try item_ty.minInt(pt, item_ty); - const max_int = try item_ty.maxInt(pt, item_ty); - if (try range_set.spans(arena, min_int, max_int, item_ty, zcu)) { - if (has_else) { - return sema.fail( - block, - else_prong_src, - "unreachable else prong; all cases already handled", - .{}, - ); - } - break :check_range; + const int_ty = switch (type_tag) { + .comptime_int => break :check_range, // comptime_int has 'infinite' range + .int => item_ty, + .@"union", .@"struct" => item_ty.bitpackBackingInt(zcu), + else => unreachable, + }; + const min_int = try int_ty.minInt(pt, int_ty); + const max_int = try int_ty.maxInt(pt, int_ty); + if (try range_set.spans(arena, min_int, max_int, int_ty, zcu)) { + if (has_else) { + return sema.fail( + block, + else_prong_src, + "unreachable else prong; all cases already handled", + .{}, + ); } + break :check_range; } if (!has_else) { return sema.fail( @@ -11759,12 +11767,15 @@ fn resolveSwitchBlock( const switch_node_offset = zir_switch.switch_src_node_offset; const operand_ty = sema.typeOf(operand.simple.by_val); - const item_ty = switch (operand_ty.zigTypeTag(zcu)) { - .@"union" => operand_ty.unionTagType(zcu).?, - else => operand_ty, - }; - const union_originally = operand_ty.zigTypeTag(zcu) == .@"union"; - const err_set = item_ty.zigTypeTag(zcu) == .error_set; + + const tagged_union_originally = operand_ty.zigTypeTag(zcu) == .@"union" and + operand_ty.containerLayout(zcu) != .@"packed"; + const err_set = operand_ty.zigTypeTag(zcu) == .error_set; + + const item_ty = if (tagged_union_originally) + operand_ty.unionTagType(zcu).? + else + operand_ty; const cond_ref = operand.simple.cond; @@ -11807,7 +11818,7 @@ fn resolveSwitchBlock( const item_val = sema.resolveValue(item_ref).?; if (cond_val.eql(item_val, item_ty, zcu)) { if (err_set) try sema.maybeErrorUnwrapComptime(child_block, prong_body, cond_ref); - if (union_originally and operand_ty.unionFieldType(item_val, zcu).?.isNoReturn(zcu)) { + if (tagged_union_originally and operand_ty.unionFieldType(item_val, zcu).?.isNoReturn(zcu)) { // This prong should be unreachable! return .unreachable_value; } @@ -11908,7 +11919,7 @@ fn resolveSwitchBlock( else .{ else_case.index, else_case.body, else_case.capture, else_case.has_tag_capture, else_case.is_inline }; if (err_set) try sema.maybeErrorUnwrapComptime(child_block, body, cond_ref); - if (union_originally) { + if (tagged_union_originally) { for (validated_switch.seen_enum_fields, 0..) |maybe_seen, field_i| { if (maybe_seen != null) continue; if (!operand_ty.unionFieldTypeByIndex(field_i, zcu).isNoReturn(zcu)) break; @@ -12049,12 +12060,12 @@ fn wantSwitchProngBodyAnalysis( sema: *Sema, item_ref: Air.Inst.Ref, operand_ty: Type, - union_originally: bool, + tagged_union_originally: bool, err_set: bool, prong_is_comptime_unreach: bool, ) bool { const zcu = sema.pt.zcu; - if (union_originally) { + if (tagged_union_originally) { const item_val = sema.resolveValue(item_ref).?; const field_ty = operand_ty.unionFieldType(item_val, zcu).?; if (field_ty.isNoReturn(zcu)) return false; @@ -12106,8 +12117,10 @@ fn analyzeSwitchProng( // No need to load the operand for this prong! break :need_load false; } - if (capture != .none and operand_ty.zigTypeTag(zcu) == .@"union") { - // Non-OPV union payload captures are always runtime-known. + if (capture != .none and operand_ty.zigTypeTag(zcu) == .@"union" and + operand_ty.containerLayout(zcu) != .@"packed") + { + // Non-OPV tagged union payload captures are always runtime-known. break :need_load true; } if (kind == .inline_ref) { @@ -12202,6 +12215,9 @@ fn analyzeSwitchTagCapture( operand_ty.fmt(pt), }); } + if (operand_ty.containerLayout(zcu) == .@"packed") { + return sema.fail(case_block, tag_capture_src, "cannot capture tag of packed union", .{}); + } switch (kind) { .has_ranges => unreachable, .inline_ref => |ref| return ref, @@ -12215,9 +12231,9 @@ fn analyzeSwitchPayloadCapture( sema: *Sema, case_block: *Block, operand: SwitchOperand, - /// 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. + /// Always has to be not-`none` if this is a tagged union payload capture. + /// For non-tagged-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 `none` if `capture_by_ref` is `false` or if `operand_val` is also `none`. operand_ptr: Air.Inst.Ref, @@ -12234,9 +12250,22 @@ fn analyzeSwitchPayloadCapture( const switch_node_offset = operand_src.offset.node_offset_switch_operand; + const tagged_union_originally = operand_ty.zigTypeTag(zcu) == .@"union" and + operand_ty.containerLayout(zcu) != .@"packed"; + const err_set = operand_ty.zigTypeTag(zcu) == .error_set; + + if (err_set and capture_by_ref) { + return sema.fail( + case_block, + capture_src, + "error set cannot be captured by reference", + .{}, + ); + } + if (kind == .inline_ref) { const item_val = sema.resolveValue(kind.inline_ref).?; - if (operand_ty.zigTypeTag(zcu) == .@"union") { + if (tagged_union_originally) { const field_index: u32 = @intCast(operand_ty.unionTagFieldIndex(item_val, zcu).?); const union_obj = zcu.typeToUnion(operand_ty).?; const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_index]); @@ -12267,52 +12296,90 @@ fn analyzeSwitchPayloadCapture( } if (kind == .special) { - if (capture_by_ref) return operand_ptr; - return switch (operand_ty.zigTypeTag(zcu)) { - .error_set => e: { - if (else_err_ty) |err_ty| { - break :e sema.bitCast(case_block, err_ty, operand_val, operand_src, null); - } else { - try sema.analyzeUnreachable(case_block, operand_src, false); - break :e .unreachable_value; - } - }, - else => operand_val, - }; + if (err_set) { + if (else_err_ty) |err_ty| { + return sema.bitCast(case_block, err_ty, operand_val, operand_src, null); + } else { + try sema.analyzeUnreachable(case_block, operand_src, false); + return .unreachable_value; + } + } + if (capture_by_ref) { + return operand_ptr; + } + return operand_val; } - switch (operand_ty.zigTypeTag(zcu)) { - .@"union" => { - const case_vals = kind.item_refs; + if (tagged_union_originally) { + const case_vals = kind.item_refs; - const union_obj = zcu.typeToUnion(operand_ty).?; - const first_item_val = sema.resolveValue(case_vals[0]).?; + const union_obj = zcu.typeToUnion(operand_ty).?; + const first_item_val = sema.resolveValue(case_vals[0]).?; - const first_field_index: u32 = zcu.unionTagFieldIndex(union_obj, first_item_val).?; - const first_field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[first_field_index]); + const first_field_index: u32 = zcu.unionTagFieldIndex(union_obj, first_item_val).?; + const first_field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[first_field_index]); - const field_indices = try sema.arena.alloc(u32, case_vals.len); - for (case_vals, field_indices) |item, *field_idx| { - const item_val = sema.resolveValue(item).?; - field_idx.* = zcu.unionTagFieldIndex(union_obj, item_val).?; + const field_indices = try sema.arena.alloc(u32, case_vals.len); + for (case_vals, field_indices) |item, *field_idx| { + const item_val = sema.resolveValue(item).?; + field_idx.* = zcu.unionTagFieldIndex(union_obj, item_val).?; + } + + // Fast path: if all the operands are the same type already, we don't need to hit + // PTR! This will also allow us to emit simpler code. + const same_types = for (field_indices[1..]) |field_idx| { + const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); + if (!field_ty.eql(first_field_ty, zcu)) break false; + } else true; + + const capture_ty: Type = capture_ty: { + if (same_types) break :capture_ty first_field_ty; + // We need values to run PTR on, so make a bunch of undef constants. + const dummy_captures = try sema.arena.alloc(Air.Inst.Ref, case_vals.len); + for (dummy_captures, field_indices) |*dummy, field_idx| { + const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); + dummy.* = try pt.undefRef(field_ty); } - // Fast path: if all the operands are the same type already, we don't need to hit - // PTR! This will also allow us to emit simpler code. - const same_types = for (field_indices[1..]) |field_idx| { - const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); - if (!field_ty.eql(first_field_ty, zcu)) break false; - } else true; + const case_srcs = try sema.arena.alloc(?LazySrcLoc, case_vals.len); + for (case_srcs, 0..) |*case_src, item_i| { + case_src.* = .{ + .base_node_inst = capture_src.base_node_inst, + .offset = .{ .switch_case_item = .{ + .switch_node_offset = switch_node_offset, + .case_idx = capture_src.offset.switch_capture.case_idx, + .item_idx = .{ .kind = .single, .value = @intCast(item_i) }, + } }, + }; + } - const capture_ty: Type = capture_ty: { - if (same_types) break :capture_ty first_field_ty; + break :capture_ty sema.resolvePeerTypes( + case_block, + capture_src, + dummy_captures, + .{ .override = case_srcs }, + ) catch |err| switch (err) { + error.AnalysisFail => { + const msg = sema.err orelse return error.AnalysisFail; + try sema.reparentOwnedErrorMsg(capture_src, msg, "capture group with incompatible types", .{}); + return error.AnalysisFail; + }, + else => |e| return e, + }; + }; + + // By-reference captures have some further restrictions which make them easier to emit + if (capture_by_ref) { + const operand_ptr_ty = sema.typeOf(operand_ptr); + 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. // We need values to run PTR on, so make a bunch of undef constants. const dummy_captures = try sema.arena.alloc(Air.Inst.Ref, case_vals.len); - for (dummy_captures, field_indices) |*dummy, field_idx| { - const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); - dummy.* = try pt.undefRef(field_ty); + for (field_indices, dummy_captures) |field_index, *dummy| { + const field_ptr_ty = try operand_ptr_ty.fieldPtrType(field_index, pt); + dummy.* = try pt.undefRef(field_ptr_ty); } - const case_srcs = try sema.arena.alloc(?LazySrcLoc, case_vals.len); for (case_srcs, 0..) |*case_src, item_i| { case_src.* = .{ @@ -12325,7 +12392,7 @@ fn analyzeSwitchPayloadCapture( }; } - break :capture_ty sema.resolvePeerTypes( + break :resolve sema.resolvePeerTypes( case_block, capture_src, dummy_captures, @@ -12333,6 +12400,7 @@ fn analyzeSwitchPayloadCapture( ) catch |err| switch (err) { error.AnalysisFail => { const msg = sema.err orelse return error.AnalysisFail; + try sema.errNote(capture_src, msg, "this coercion is only possible when capturing by value", .{}); try sema.reparentOwnedErrorMsg(capture_src, msg, "capture group with incompatible types", .{}); return error.AnalysisFail; }, @@ -12340,272 +12408,222 @@ fn analyzeSwitchPayloadCapture( }; }; - // By-reference captures have some further restrictions which make them easier to emit - if (capture_by_ref) { - const operand_ptr_ty = sema.typeOf(operand_ptr); - 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. - // We need values to run PTR on, so make a bunch of undef constants. - const dummy_captures = try sema.arena.alloc(Air.Inst.Ref, case_vals.len); - for (field_indices, dummy_captures) |field_index, *dummy| { - const field_ptr_ty = try operand_ptr_ty.fieldPtrType(field_index, pt); - dummy.* = try pt.undefRef(field_ptr_ty); - } - const case_srcs = try sema.arena.alloc(?LazySrcLoc, case_vals.len); - for (case_srcs, 0..) |*case_src, item_i| { - case_src.* = .{ - .base_node_inst = capture_src.base_node_inst, - .offset = .{ .switch_case_item = .{ - .switch_node_offset = switch_node_offset, - .case_idx = capture_src.offset.switch_capture.case_idx, - .item_idx = .{ .kind = .single, .value = @intCast(item_i) }, - } }, - }; - } - - break :resolve sema.resolvePeerTypes( - case_block, - capture_src, - dummy_captures, - .{ .override = case_srcs }, - ) catch |err| switch (err) { - error.AnalysisFail => { - const msg = sema.err orelse return error.AnalysisFail; - try sema.errNote(capture_src, msg, "this coercion is only possible when capturing by value", .{}); - try sema.reparentOwnedErrorMsg(capture_src, msg, "capture group with incompatible types", .{}); - return error.AnalysisFail; - }, - else => |e| return e, - }; - }; - - if (try sema.resolveDefinedValue(case_block, operand_src, operand_ptr)) |op_ptr_val| { - if (op_ptr_val.isUndef(zcu)) return pt.undefRef(capture_ptr_ty); - const field_ptr_val = try op_ptr_val.ptrField(first_field_index, pt); - return .fromValue(try pt.getCoerced(field_ptr_val, capture_ptr_ty)); - } - - try sema.requireRuntimeBlock(case_block, operand_src, null); - return case_block.addStructFieldPtr(operand_ptr, first_field_index, capture_ptr_ty); - } - - if (try capture_ty.onePossibleValue(pt)) |opv| return .fromValue(opv); - - if (try sema.resolveDefinedValue(case_block, operand_src, operand_val)) |operand_val_val| { - if (operand_val_val.isUndef(zcu)) return pt.undefRef(capture_ty); - const union_val = ip.indexToKey(operand_val_val.toIntern()).un; - if (Value.fromInterned(union_val.tag).isUndef(zcu)) return pt.undefRef(capture_ty); - const uncoerced: Air.Inst.Ref = .fromIntern(union_val.val); - return sema.coerce(case_block, capture_ty, uncoerced, operand_src); + if (try sema.resolveDefinedValue(case_block, operand_src, operand_ptr)) |op_ptr_val| { + if (op_ptr_val.isUndef(zcu)) return pt.undefRef(capture_ptr_ty); + const field_ptr_val = try op_ptr_val.ptrField(first_field_index, pt); + return .fromValue(try pt.getCoerced(field_ptr_val, capture_ptr_ty)); } try sema.requireRuntimeBlock(case_block, operand_src, null); + return case_block.addStructFieldPtr(operand_ptr, first_field_index, capture_ptr_ty); + } - if (same_types) { - return case_block.addStructFieldVal(operand_val, first_field_index, capture_ty); - } + if (try capture_ty.onePossibleValue(pt)) |opv| return .fromValue(opv); - // We may have to emit a switch block which coerces the operand to the capture type. - // If we can, try to avoid that using in-memory coercions. - const first_non_imc = in_mem: { - for (field_indices, 0..) |field_idx, i| { - const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); - if (.ok != try sema.coerceInMemoryAllowed(case_block, capture_ty, field_ty, false, zcu.getTarget(), .unneeded, .unneeded, null)) { - break :in_mem i; - } - } - // All fields are in-memory coercible to the resolved type! - // Just take the first field and bitcast the result. - const uncoerced = try case_block.addStructFieldVal(operand_val, first_field_index, first_field_ty); - return case_block.addBitCast(capture_ty, uncoerced); - }; + if (try sema.resolveDefinedValue(case_block, operand_src, operand_val)) |operand_val_val| { + if (operand_val_val.isUndef(zcu)) return pt.undefRef(capture_ty); + const union_val = ip.indexToKey(operand_val_val.toIntern()).un; + if (Value.fromInterned(union_val.tag).isUndef(zcu)) return pt.undefRef(capture_ty); + const uncoerced: Air.Inst.Ref = .fromIntern(union_val.val); + return sema.coerce(case_block, capture_ty, uncoerced, operand_src); + } - // By-val capture with heterogeneous types which are not all in-memory coercible to - // the resolved capture type. We finally have to fall back to the ugly method. + try sema.requireRuntimeBlock(case_block, operand_src, null); - // However, let's first track which operands are in-memory coercible. There may well - // be several, and we can squash all of these cases into the same switch prong using - // a simple bitcast. We'll make this the 'else' prong. + if (same_types) { + return case_block.addStructFieldVal(operand_val, first_field_index, capture_ty); + } - var in_mem_coercible: std.DynamicBitSet = try .initFull(sema.arena, field_indices.len); - in_mem_coercible.unset(first_non_imc); - { - const next = first_non_imc + 1; - for (field_indices[next..], next..) |field_idx, i| { - const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); - if (.ok != try sema.coerceInMemoryAllowed(case_block, capture_ty, field_ty, false, zcu.getTarget(), .unneeded, .unneeded, null)) { - in_mem_coercible.unset(i); - } + // We may have to emit a switch block which coerces the operand to the capture type. + // If we can, try to avoid that using in-memory coercions. + const first_non_imc = in_mem: { + for (field_indices, 0..) |field_idx, i| { + const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); + if (.ok != try sema.coerceInMemoryAllowed(case_block, capture_ty, field_ty, false, zcu.getTarget(), .unneeded, .unneeded, null)) { + break :in_mem i; } } + // All fields are in-memory coercible to the resolved type! + // Just take the first field and bitcast the result. + const uncoerced = try case_block.addStructFieldVal(operand_val, first_field_index, first_field_ty); + return case_block.addBitCast(capture_ty, uncoerced); + }; - const capture_block_inst = try case_block.addInstAsIndex(.{ - .tag = .block, - .data = .{ - .ty_pl = .{ - .ty = .fromType(capture_ty), - .payload = undefined, // updated below - }, + // By-val capture with heterogeneous types which are not all in-memory coercible to + // the resolved capture type. We finally have to fall back to the ugly method. + + // However, let's first track which operands are in-memory coercible. There may well + // be several, and we can squash all of these cases into the same switch prong using + // a simple bitcast. We'll make this the 'else' prong. + + var in_mem_coercible: std.DynamicBitSet = try .initFull(sema.arena, field_indices.len); + in_mem_coercible.unset(first_non_imc); + { + const next = first_non_imc + 1; + for (field_indices[next..], next..) |field_idx, i| { + const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); + if (.ok != try sema.coerceInMemoryAllowed(case_block, capture_ty, field_ty, false, zcu.getTarget(), .unneeded, .unneeded, null)) { + in_mem_coercible.unset(i); + } + } + } + + const capture_block_inst = try case_block.addInstAsIndex(.{ + .tag = .block, + .data = .{ + .ty_pl = .{ + .ty = .fromType(capture_ty), + .payload = undefined, // updated below }, - }); + }, + }); - const prong_count = field_indices.len - in_mem_coercible.count(); + const prong_count = field_indices.len - in_mem_coercible.count(); - const estimated_extra = prong_count * 6 + (prong_count / 10); // 2 for Case, 1 item, probably 3 insts; plus hints - var cases_extra = try std.array_list.Managed(u32).initCapacity(sema.gpa, estimated_extra); - defer cases_extra.deinit(); + const estimated_extra = prong_count * 6 + (prong_count / 10); // 2 for Case, 1 item, probably 3 insts; plus hints + var cases_extra = try std.array_list.Managed(u32).initCapacity(sema.gpa, estimated_extra); + defer cases_extra.deinit(); - { - // All branch hints are `.none`, so just add zero elems. - comptime assert(@intFromEnum(std.builtin.BranchHint.none) == 0); - const need_elems = std.math.divCeil(usize, prong_count + 1, 10) catch unreachable; - try cases_extra.appendNTimes(0, need_elems); - } + { + // All branch hints are `.none`, so just add zero elems. + comptime assert(@intFromEnum(std.builtin.BranchHint.none) == 0); + const need_elems = std.math.divCeil(usize, prong_count + 1, 10) catch unreachable; + try cases_extra.appendNTimes(0, need_elems); + } - { - // Non-bitcast cases - var it = in_mem_coercible.iterator(.{ .kind = .unset }); - while (it.next()) |idx| { - var coerce_block = case_block.makeSubBlock(); - defer coerce_block.instructions.deinit(sema.gpa); - - const case_src: LazySrcLoc = .{ - .base_node_inst = capture_src.base_node_inst, - .offset = .{ .switch_case_item = .{ - .switch_node_offset = switch_node_offset, - .case_idx = capture_src.offset.switch_capture.case_idx, - .item_idx = .{ .kind = .single, .value = @intCast(idx) }, - } }, - }; - - const field_idx = field_indices[idx]; - const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); - const uncoerced = try coerce_block.addStructFieldVal(operand_val, field_idx, field_ty); - const coerced = try sema.coerce(&coerce_block, capture_ty, uncoerced, case_src); - _ = try coerce_block.addBr(capture_block_inst, coerced); - - try cases_extra.ensureUnusedCapacity(@typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - 1 + // `item`, no ranges - coerce_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = 1, - .ranges_len = 0, - .body_len = @intCast(coerce_block.instructions.items.len), - })); - cases_extra.appendAssumeCapacity(@intFromEnum(case_vals[idx])); // item - cases_extra.appendSliceAssumeCapacity(@ptrCast(coerce_block.instructions.items)); // body - } - } - const else_body_len = len: { - // 'else' prong uses a bitcast + { + // Non-bitcast cases + var it = in_mem_coercible.iterator(.{ .kind = .unset }); + while (it.next()) |idx| { var coerce_block = case_block.makeSubBlock(); defer coerce_block.instructions.deinit(sema.gpa); - const first_imc_item_idx = in_mem_coercible.findFirstSet().?; - const first_imc_field_idx = field_indices[first_imc_item_idx]; - const first_imc_field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[first_imc_field_idx]); - const uncoerced = try coerce_block.addStructFieldVal(operand_val, first_imc_field_idx, first_imc_field_ty); - const coerced = try coerce_block.addBitCast(capture_ty, uncoerced); + const case_src: LazySrcLoc = .{ + .base_node_inst = capture_src.base_node_inst, + .offset = .{ .switch_case_item = .{ + .switch_node_offset = switch_node_offset, + .case_idx = capture_src.offset.switch_capture.case_idx, + .item_idx = .{ .kind = .single, .value = @intCast(idx) }, + } }, + }; + + const field_idx = field_indices[idx]; + const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); + const uncoerced = try coerce_block.addStructFieldVal(operand_val, field_idx, field_ty); + const coerced = try sema.coerce(&coerce_block, capture_ty, uncoerced, case_src); _ = try coerce_block.addBr(capture_block_inst, coerced); - try cases_extra.appendSlice(@ptrCast(coerce_block.instructions.items)); - break :len coerce_block.instructions.items.len; - }; + try cases_extra.ensureUnusedCapacity(@typeInfo(Air.SwitchBr.Case).@"struct".fields.len + + 1 + // `item`, no ranges + coerce_block.instructions.items.len); + cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ + .items_len = 1, + .ranges_len = 0, + .body_len = @intCast(coerce_block.instructions.items.len), + })); + cases_extra.appendAssumeCapacity(@intFromEnum(case_vals[idx])); // item + cases_extra.appendSliceAssumeCapacity(@ptrCast(coerce_block.instructions.items)); // body + } + } + const else_body_len = len: { + // 'else' prong uses a bitcast + var coerce_block = case_block.makeSubBlock(); + defer coerce_block.instructions.deinit(sema.gpa); - try sema.air_extra.ensureUnusedCapacity(sema.gpa, @typeInfo(Air.SwitchBr).@"struct".fields.len + - cases_extra.items.len + - @typeInfo(Air.Block).@"struct".fields.len + - 1); + const first_imc_item_idx = in_mem_coercible.findFirstSet().?; + const first_imc_field_idx = field_indices[first_imc_item_idx]; + const first_imc_field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[first_imc_field_idx]); + const uncoerced = try coerce_block.addStructFieldVal(operand_val, first_imc_field_idx, first_imc_field_ty); + const coerced = try coerce_block.addBitCast(capture_ty, uncoerced); + _ = try coerce_block.addBr(capture_block_inst, coerced); - const switch_br_inst: u32 = @intCast(sema.air_instructions.len); - try sema.air_instructions.append(sema.gpa, .{ - .tag = .switch_br, - .data = .{ - .pl_op = .{ - .operand = undefined, // set by switch below - .payload = sema.addExtraAssumeCapacity(Air.SwitchBr{ - .cases_len = @intCast(prong_count), - .else_body_len = @intCast(else_body_len), - }), - }, + try cases_extra.appendSlice(@ptrCast(coerce_block.instructions.items)); + break :len coerce_block.instructions.items.len; + }; + + try sema.air_extra.ensureUnusedCapacity(sema.gpa, @typeInfo(Air.SwitchBr).@"struct".fields.len + + cases_extra.items.len + + @typeInfo(Air.Block).@"struct".fields.len + + 1); + + const switch_br_inst: u32 = @intCast(sema.air_instructions.len); + try sema.air_instructions.append(sema.gpa, .{ + .tag = .switch_br, + .data = .{ + .pl_op = .{ + .operand = undefined, // set by switch below + .payload = sema.addExtraAssumeCapacity(Air.SwitchBr{ + .cases_len = @intCast(prong_count), + .else_body_len = @intCast(else_body_len), + }), }, - }); - sema.air_extra.appendSliceAssumeCapacity(cases_extra.items); + }, + }); + sema.air_extra.appendSliceAssumeCapacity(cases_extra.items); - // Set up block body - switch (operand) { - .simple => |s| { - const air_datas = sema.air_instructions.items(.data); - air_datas[switch_br_inst].pl_op.operand = s.cond; - air_datas[@intFromEnum(capture_block_inst)].ty_pl.payload = - sema.addExtraAssumeCapacity(Air.Block{ .body_len = 1 }); - sema.air_extra.appendAssumeCapacity(switch_br_inst); - }, - .loop => { - // The block must first extract the tag from the loaded union. - const tag_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len); - try sema.air_instructions.append(sema.gpa, .{ - .tag = .get_union_tag, - .data = .{ .ty_op = .{ - .ty = .fromIntern(union_obj.enum_tag_type), - .operand = operand_val, - } }, - }); - const air_datas = sema.air_instructions.items(.data); - air_datas[switch_br_inst].pl_op.operand = tag_inst.toRef(); - air_datas[@intFromEnum(capture_block_inst)].ty_pl.payload = - sema.addExtraAssumeCapacity(Air.Block{ .body_len = 2 }); - sema.air_extra.appendAssumeCapacity(@intFromEnum(tag_inst)); - sema.air_extra.appendAssumeCapacity(switch_br_inst); - }, - } + // Set up block body + switch (operand) { + .simple => |s| { + const air_datas = sema.air_instructions.items(.data); + air_datas[switch_br_inst].pl_op.operand = s.cond; + air_datas[@intFromEnum(capture_block_inst)].ty_pl.payload = + sema.addExtraAssumeCapacity(Air.Block{ .body_len = 1 }); + sema.air_extra.appendAssumeCapacity(switch_br_inst); + }, + .loop => { + // The block must first extract the tag from the loaded union. + const tag_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len); + try sema.air_instructions.append(sema.gpa, .{ + .tag = .get_union_tag, + .data = .{ .ty_op = .{ + .ty = .fromIntern(union_obj.enum_tag_type), + .operand = operand_val, + } }, + }); + const air_datas = sema.air_instructions.items(.data); + air_datas[switch_br_inst].pl_op.operand = tag_inst.toRef(); + air_datas[@intFromEnum(capture_block_inst)].ty_pl.payload = + sema.addExtraAssumeCapacity(Air.Block{ .body_len = 2 }); + sema.air_extra.appendAssumeCapacity(@intFromEnum(tag_inst)); + sema.air_extra.appendAssumeCapacity(switch_br_inst); + }, + } - return capture_block_inst.toRef(); - }, - .error_set => { - if (capture_by_ref) { - return sema.fail( - case_block, - capture_src, - "error set cannot be captured by reference", - .{}, - ); - } - - const case_vals = kind.item_refs; - if (case_vals.len == 1) { - const item_val = sema.resolveValue(case_vals[0]).?; - const item_ty = try pt.singleErrorSetType(item_val.getErrorName(zcu).unwrap().?); - return sema.bitCast(case_block, item_ty, .fromValue(item_val), operand_src, null); - } - - var names: InferredErrorSet.NameMap = .{}; - try names.ensureUnusedCapacity(sema.arena, case_vals.len); - for (case_vals) |err| { - const err_val = sema.resolveValue(err).?; - names.putAssumeCapacityNoClobber(err_val.getErrorName(zcu).unwrap().?, {}); - } - const error_ty = try pt.errorSetFromUnsortedNames(names.keys()); - return sema.bitCast(case_block, error_ty, operand_val, operand_src, null); - }, - else => { - // In this case the capture value is just the passed-through value of the - // switch condition. It is comptime-known if there is only one item. - if (capture_by_ref) { - return operand_ptr; - } - switch (kind) { - .inline_ref, .special => unreachable, - .item_refs => |case_vals| { - // If there's only a single item, the capture is comptime-known! - if (case_vals.len == 1) return case_vals[0]; - }, - .has_ranges => {}, - } - return operand_val; - }, + return capture_block_inst.toRef(); } + + if (err_set) { + const case_vals = kind.item_refs; + if (case_vals.len == 1) { + const item_val = sema.resolveValue(case_vals[0]).?; + const item_ty = try pt.singleErrorSetType(item_val.getErrorName(zcu).unwrap().?); + return sema.bitCast(case_block, item_ty, .fromValue(item_val), operand_src, null); + } + + var names: InferredErrorSet.NameMap = .{}; + try names.ensureUnusedCapacity(sema.arena, case_vals.len); + for (case_vals) |err| { + const err_val = sema.resolveValue(err).?; + names.putAssumeCapacityNoClobber(err_val.getErrorName(zcu).unwrap().?, {}); + } + const error_ty = try pt.errorSetFromUnsortedNames(names.keys()); + return sema.bitCast(case_block, error_ty, operand_val, operand_src, null); + } + + // In this case the capture value is just the passed-through value of the + // switch condition. It is comptime-known if there is only one item. + if (capture_by_ref) { + return operand_ptr; + } + switch (kind) { + .inline_ref, .special => unreachable, + .item_refs => |case_vals| { + // If there's only a single item, the capture is comptime-known! + if (case_vals.len == 1) return case_vals[0]; + }, + .has_ranges => {}, + } + return operand_val; } const ResolvedSwitchItem = struct { @@ -12714,7 +12732,6 @@ fn validateSwitchItemOrRange( const zcu = pt.zcu; const ip = &zcu.intern_pool; const maybe_prev_src: ?LazySrcLoc = maybe_prev_src: switch (item_ty.zigTypeTag(zcu)) { - .@"union" => unreachable, .@"enum" => { const int = ip.indexToKey(item_val.toIntern()).enum_tag.int; if (ip.loadEnumType(item_ty.toIntern()).tagValueIndex(ip, int)) |field_index| { @@ -12755,6 +12772,14 @@ fn validateSwitchItemOrRange( }, item_ty, zcu); } }, + .@"union", .@"struct" => { + const backing_int_val = ip.indexToKey(item_val.toIntern()).bitpack.backing_int_val; + break :maybe_prev_src range_set.addAssumeCapacity(.{ + .first = .fromInterned(backing_int_val), + .last = .fromInterned(backing_int_val), + .src = item_src, + }, item_ty.bitpackBackingInt(zcu), zcu); + }, .enum_literal, .@"fn", .pointer, .type => { break :maybe_prev_src if (seen_sparse_values.fetchPutAssumeCapacity(item_val.toIntern(), item_src)) |prev| prev.value diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index 7661d2fe52..a81991be77 100644 --- a/src/codegen/llvm.zig +++ b/src/codegen/llvm.zig @@ -6252,7 +6252,7 @@ pub const FuncGen = struct { const cond_ty = self.typeOf(switch_br.operand); switch (cond_ty.zigTypeTag(zcu)) { .bool, .pointer => break :jmp_table null, - .@"enum", .int, .error_set => {}, + .@"enum", .int, .error_set, .@"struct", .@"union" => {}, else => unreachable, } diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index 784305bca7..bc46b56d17 100644 --- a/test/behavior/switch.zig +++ b/test/behavior/switch.zig @@ -1327,3 +1327,133 @@ test "single range switch prong capture" { try S.doTheTest(2); try comptime S.doTheTest(2); } + +test "switch on packed struct" { + const P = packed struct { + a: u1, + b: u1, + + fn doTheTest(p: @This()) !void { + switch (p) { + .{ .a = 0, .b = 1 } => {}, + else => return error.TestFailed, + } + + switch (p) { + .{ .a = 0, .b = 1 } => {}, + .{ .a = 0, .b = 0 }, + .{ .a = 1, .b = 0 }, + .{ .a = 1, .b = 1 }, + => return error.TestFailed, + } + + switch (p) { + inline else => |val| { + if (val != @This(){ .a = 0, .b = 1 }) return error.TestFailed; + }, + } + } + }; + try P.doTheTest(.{ .a = 0, .b = 1 }); + try comptime P.doTheTest(.{ .a = 0, .b = 1 }); +} + +test "switch on packed union" { + const P = packed union(u2) { + a: u2, + b: i2, + c: packed struct(u2) { x: u1, y: i1 }, + + fn doTheTest(p: @This()) !void { + switch (p) { + .{ .a = 1 } => {}, + else => return error.TestFailed, + } + + switch (p) { + .{ .a = 1 } => {}, + .{ .a = 0 }, + .{ .a = 2 }, + .{ .a = 3 }, + => return error.TestFailed, + } + + switch (p) { + .{ .a = 1 } => {}, + .{ .a = 0 }, + .{ .b = -2 }, + .{ .b = -1 }, + => return error.TestFailed, + } + + switch (p) { + .{ .c = .{ .x = 1, .y = 0 } } => {}, + .{ .b = 0 }, + .{ .a = 2 }, + .{ .c = .{ .x = 1, .y = -1 } }, + => return error.TestFailed, + } + + switch (p) { + inline else => |val| { + if (val != @This(){ .c = .{ .x = 1, .y = 0 } }) return error.TestFailed; + }, + } + } + }; + try P.doTheTest(.{ .a = 1 }); + try comptime P.doTheTest(.{ .a = 1 }); +} + +test "switch on nested packed containers" { + const P = packed struct { + iu: u17, + is: i31, + b: bool, + e: enum(u5) { a = 5, b = 3, c = 12 }, + un: packed union { + a: i9, + b: u9, + c: packed struct(u9) { a: i5, b: u4 }, + }, + p: packed struct(u9) { a: u3, b: u6 }, + + fn doTheTest(p: @This()) !void { + switch (p) { + .{ + .iu = 72, + .is = 124, + .b = false, + .e = .c, + .un = .{ .b = 13 }, + .p = .{ .a = 0, .b = 12 }, + } => return error.TestFailed, + .{ + .iu = 129, + .is = -162784612, + .b = true, + .e = .a, + .un = .{ .c = .{ .a = -3, .b = 9 } }, + .p = .{ .a = 2, .b = 17 }, + } => {}, + else => return error.TestFailed, + } + } + }; + try P.doTheTest(.{ + .iu = 129, + .is = -162784612, + .b = true, + .e = .a, + .un = .{ .c = .{ .a = -3, .b = 9 } }, + .p = .{ .a = 2, .b = 17 }, + }); + try comptime P.doTheTest(.{ + .iu = 129, + .is = -162784612, + .b = true, + .e = .a, + .un = .{ .c = .{ .a = -3, .b = 9 } }, + .p = .{ .a = 2, .b = 17 }, + }); +} diff --git a/test/behavior/switch_loop.zig b/test/behavior/switch_loop.zig index bbf9c86d50..a653b6a638 100644 --- a/test/behavior/switch_loop.zig +++ b/test/behavior/switch_loop.zig @@ -509,3 +509,58 @@ test "switch loop for error handling" { try S.doTheTest(); try comptime S.doTheTest(); } + +test "switch loop with packed structs" { + const P = packed struct { + a: u7, + b: u20, + + fn doTheTest(p: @This()) !void { + const result = s: switch (p) { + .{ .a = 5, .b = 10 } => |x| x, + else => |x| continue :s .{ .a = x.a, .b = x.b + 1 }, + }; + try expect(result == @This(){ .a = 5, .b = 10 }); + } + }; + try P.doTheTest(.{ .a = 5, .b = 0 }); + try comptime P.doTheTest(.{ .a = 5, .b = 0 }); +} + +test "switch loop with packed unions" { + const P = packed union { + a: u7, + b: i7, + + fn doTheTest(p: @This()) !void { + const result = s: switch (p) { + .{ .a = 10 } => |x| x, + else => |x| continue :s .{ .b = @intCast(x.a + 1) }, + }; + try expect(result == @This(){ .b = 10 }); + } + }; + try P.doTheTest(.{ .a = 5 }); + try comptime P.doTheTest(.{ .a = 5 }); +} + +test "switch loop with packed unions with OPV" { + const P = packed union { + a: u0, + b: i0, + + fn doTheTest(p: @This()) !void { + var looped = false; + s: switch (p) { + .{ .b = 0 } => |x| { + comptime assert(x.a == 0); + if (looped) break :s; + looped = true; + continue :s .{ .a = 0 }; + }, + } + } + }; + try P.doTheTest(.{ .a = 0 }); + try comptime P.doTheTest(.{ .a = 0 }); +} diff --git a/test/cases/compile_errors/switch_capture_error_by_ref.zig b/test/cases/compile_errors/switch_capture_error_by_ref.zig new file mode 100644 index 0000000000..e432159d2c --- /dev/null +++ b/test/cases/compile_errors/switch_capture_error_by_ref.zig @@ -0,0 +1,25 @@ +export fn entry1() void { + switch (@as(anyerror, error.MyError)) { + error.MyError, error.MyOtherError => |*err| _ = err, + else => {}, + } +} + +export fn entry2() void { + switch (@as(anyerror, error.MyError)) { + inline error.MyError, error.MyOtherError => |*err| _ = err, + else => {}, + } +} + +export fn entry3() void { + switch (@as(anyerror, error.MyError)) { + else => |*err| _ = err, + } +} + +// error +// +// :3:47: error: error set cannot be captured by reference +// :10:54: error: error set cannot be captured by reference +// :17:18: error: error set cannot be captured by reference diff --git a/test/cases/compile_errors/switch_capture_packed_union_tag.zig b/test/cases/compile_errors/switch_capture_packed_union_tag.zig new file mode 100644 index 0000000000..71991ceb3a --- /dev/null +++ b/test/cases/compile_errors/switch_capture_packed_union_tag.zig @@ -0,0 +1,15 @@ +const P = packed union(u8) { + a: u8, + b: i8, +}; + +export fn foo(p: P) void { + switch (p) { + .{ .a = 123 } => |_, tag| _ = tag, + else => {}, + } +} + +// error +// +// :8:30: error: cannot capture tag of packed union diff --git a/test/cases/compile_errors/switch_on_non_packed_struct.zig b/test/cases/compile_errors/switch_on_non_packed_struct.zig new file mode 100644 index 0000000000..ef17bb3ca5 --- /dev/null +++ b/test/cases/compile_errors/switch_on_non_packed_struct.zig @@ -0,0 +1,25 @@ +const Auto = struct { + a: u8, +}; +export fn entry1(a: u8) void { + const s: Auto = .{ .a = a }; + switch (s) { + else => {}, + } +} + +const Extern = extern struct { + a: u8, +}; +export fn entry2(s: Extern) void { + switch (s) { + else => {}, + } +} + +// error +// +// :6:13: error: switch on struct with auto layout +// :1:14: note: consider 'packed struct' here +// :15:13: error: switch on struct with extern layout +// :11:23: note: consider 'packed struct' here diff --git a/test/cases/compile_errors/switch_packed_exhaustion.zig b/test/cases/compile_errors/switch_packed_exhaustion.zig new file mode 100644 index 0000000000..98b91c9137 --- /dev/null +++ b/test/cases/compile_errors/switch_packed_exhaustion.zig @@ -0,0 +1,41 @@ +const S = packed struct(u2) { + a: u2, +}; +export fn entry1(x: u8) void { + const s: S = .{ .a = @intCast(x) }; + switch (s) { + .{ .a = 0b00 }, .{ .a = 0b01 }, .{ .a = 0b10 }, .{ .a = 0b11 } => {}, + else => {}, + } +} +export fn entry2(x: u8) void { + const s: S = .{ .a = @intCast(x) }; + switch (s) { + .{ .a = 0b00 }, .{ .a = 0b01 }, .{ .a = 0b11 } => {}, + } +} + +const U = packed union(u2) { + a: u2, + b: i2, +}; +export fn entry3(x: u8) void { + const u: U = .{ .a = @intCast(x) }; + switch (u) { + .{ .a = 0b00 }, .{ .a = 0b01 }, .{ .a = 0b10 }, .{ .a = 0b11 } => {}, + else => {}, + } +} +export fn entry4(x: u8) void { + const u: U = .{ .a = @intCast(x) }; + switch (u) { + .{ .a = 0b00 }, .{ .a = 0b01 }, .{ .a = 0b11 } => {}, + } +} + +// error +// +// :8:14: error: unreachable else prong; all cases already handled +// :13:5: error: switch must handle all possibilities +// :26:14: error: unreachable else prong; all cases already handled +// :31:5: error: switch must handle all possibilities From e91654b1e730ad8082eeb8bb0ae90a17ca69f679 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 11 Mar 2026 21:04:32 +0100 Subject: [PATCH 3/3] test: disable switch behavior test switching on type >64bits for cbe workaround for #31467 --- test/behavior/switch.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index bc46b56d17..d95a5733f1 100644 --- a/test/behavior/switch.zig +++ b/test/behavior/switch.zig @@ -1406,6 +1406,8 @@ test "switch on packed union" { } test "switch on nested packed containers" { + if (builtin.object_format == .c) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/31467 + const P = packed struct { iu: u17, is: i31,