From 73b760e03361a86c2121b0e8b6e62de98721ee0d Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Tue, 5 May 2026 01:15:05 +0200 Subject: [PATCH] Sema: simplify switch capture logic Previously all switch capture logic was in a single gigantic function and had lots of weird rules around its args depending on each other which were only encoded in doc comments and not actually enforced by the compiler. That's odd since the 'happy path' of a switch capture is actually super simple; it's literally just loading the switch operand inside of the scope of the prong. Error sets introduce a little complexity since the type of an error capture must be narrowed to the set of values that can possibly be captured by their prong, but the real source of complexity here are tagged union captures. First of, they support *two* captures - a payload and a tag capture. The captured payload has to be set up correctly, and payload captures are not guaranteed to have the same type so Sema has to make sure that all possible payloads of a capture are compatible. Instead of throwing all of this logic into one large function with lots of special casing interspersed it is now split into three main paths: - tagged unions (+ a potential tag capture) - errors (including error set narrowing) - everything else This should greatly simplify reasoning about this logic in the future if there's a bug or a change to be made. There's also no more dependencies of args on each other; instead everything is a bit more modular, dependencies are now encoded either as control flow or in the type system. This has additionally lead to the OPV path being able to reuse some more of the capture logic. This commit also fixes inline prongs not narrowing error sets. --- src/Sema.zig | 1074 +++++++++-------- test/behavior/switch.zig | 33 + .../switch_capture_packed_union_tag.zig | 15 - .../switch_invalid_tag_capture.zig | 38 + 4 files changed, 631 insertions(+), 529 deletions(-) delete mode 100644 test/cases/compile_errors/switch_capture_packed_union_tag.zig create mode 100644 test/cases/compile_errors/switch_invalid_tag_capture.zig diff --git a/src/Sema.zig b/src/Sema.zig index b2ba7c8643df8b61a5065a4a38e6f6ab168ec76d..8796b9b91cdbdaa0354c65e04ea93d249bb970d2 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -10216,7 +10216,7 @@ fn analyzeSwitchBlock( const case_vals = validated_switch.case_vals; - const index, const body, const capture, const has_tag_capture, const is_inline, const is_special = find_prong: { + const body, const capture, const has_tag_capture = find_prong: { var case_val_idx: usize = 0; var case_it = zir_switch.iterateCases(); var extra_index = zir_switch.end; @@ -10239,12 +10239,12 @@ fn analyzeSwitchBlock( } continue; } - break :find_prong .{ case.index, prong_body, prong_info.capture, prong_info.has_tag_capture, prong_info.is_inline, false }; + break :find_prong .{ prong_body, prong_info.capture, prong_info.has_tag_capture }; } if (has_else) { // This *has* to be checked after iterating all regular cases because // we allow simple noreturn else prongs when switching on error sets! - break :find_prong .{ else_case.index, else_case.body, else_case.capture, else_case.has_tag_capture, else_case.is_inline, true }; + break :find_prong .{ else_case.body, else_case.capture, else_case.has_tag_capture }; } unreachable; // malformed validated switch }; @@ -10255,58 +10255,33 @@ fn analyzeSwitchBlock( if (!(err_set and try sema.maybeErrorUnwrap(&case_block, body, cond_ref, operand_src, true))) { - // Set up captures manually to avoid special cases in the main logic. - const payload_inst: Zir.Inst.Index = if (capture != .none) inst: { + const payload_inst = 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 = item_val: { + const captured_opv: Value = captured_opv: { if (!tagged_union_originally) { - break :item_val item_opv; + break :captured_opv item_opv; } if (maybe_operand_opv) |operand_opv| { - break :item_val .fromInterned(zcu.intern_pool.indexToKey(operand_opv.toIntern()).un.val); + break :captured_opv .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( + const loaded_operand = try sema.analyzeSwitchOperandLoad(&case_block, operand, operand_src, capture == .by_ref); + break :payload_ref try sema.resolveSwitchPayloadCaptureTaggedUnion( &case_block, - operand, - operand_val, - operand_ref, - operand_ty, + loaded_operand, operand_src, - block.src(.{ .switch_capture = .{ - .switch_node_offset = src_node_offset, - .case_idx = index, - } }), + operand_ty, + item_opv, capture == .by_ref, - prong_kind, - validated_switch.else_err_ty, ); }; break :payload_ref switch (capture) { - .by_val => .fromValue(item_val), - .by_ref => try sema.uavRef(item_val), + .by_val => .fromValue(captured_opv), + .by_ref => try sema.uavRef(captured_opv), .none => unreachable, }; }; - assert(!sema.typeOf(payload_ref).isNoReturn(sema.pt.zcu)); sema.inst_map.putAssumeCapacity(payload_inst, payload_ref); break :inst payload_inst; } else undefined; @@ -10599,7 +10574,7 @@ fn finishSwitchBr( } }), prong_info.capture, prong_info.has_tag_capture, - .{ .inline_ref = item_ref }, + .{ .@"inline" = item_ref }, validated_switch.else_err_ty, switch_inst, zir_switch, @@ -10689,7 +10664,7 @@ fn finishSwitchBr( } }), prong_info.capture, prong_info.has_tag_capture, - .{ .inline_ref = item_ref }, + .{ .@"inline" = item_ref }, validated_switch.else_err_ty, switch_inst, zir_switch, @@ -10829,7 +10804,7 @@ fn finishSwitchBr( } }), else_case.capture, else_case.has_tag_capture, - .{ .inline_ref = item_ref }, + .{ .@"inline" = item_ref }, validated_switch.else_err_ty, switch_inst, zir_switch, @@ -11269,8 +11244,7 @@ fn validateSwitchBlock( const has_else = zir_switch.else_case != null; const has_under = zir_switch.has_under; - var case_vals: std.ArrayList(Air.Inst.Ref) = .empty; - try case_vals.ensureUnusedCapacity(arena, zir_switch.item_infos.len); + var case_vals: std.ArrayList(Air.Inst.Ref) = try .initCapacity(arena, zir_switch.item_infos.len); // Duplicate checking variables later also used for `inline else`. var seen_enum_fields: []?LazySrcLoc = &.{}; @@ -11676,10 +11650,10 @@ fn resolveSwitchBlock( // This prong should be unreachable! return .unreachable_value; } - const prong_kind: SwitchProngKind = kind: { - if (prong_info.is_inline) break :kind .{ .inline_ref = cond_ref }; - if (range_refs.len > 0) break :kind .has_ranges; - break :kind .{ .item_refs = item_refs }; + const prong_items: SwitchProngItems = prong_items: { + if (prong_info.is_inline) break :prong_items .{ .@"inline" = cond_ref }; + if (range_refs.len > 0) break :prong_items .has_ranges; + break :prong_items .{ .item_refs = item_refs }; }; return sema.resolveSwitchProng( block, @@ -11693,7 +11667,7 @@ fn resolveSwitchBlock( } }), prong_info.capture, prong_info.has_tag_capture, - prong_kind, + prong_items, validated_switch.else_err_ty, merges, switch_inst, @@ -11707,8 +11681,8 @@ fn resolveSwitchBlock( if ((try sema.compareAll(cond_val, .gte, first_val, item_ty)) and (try sema.compareAll(cond_val, .lte, last_val, item_ty))) { - const prong_kind: SwitchProngKind = if (prong_info.is_inline) - .{ .inline_ref = cond_ref } + const prong_items: SwitchProngItems = if (prong_info.is_inline) + .{ .@"inline" = cond_ref } else .has_ranges; return sema.resolveSwitchProng( @@ -11723,7 +11697,7 @@ fn resolveSwitchBlock( } }), prong_info.capture, prong_info.has_tag_capture, - prong_kind, + prong_items, validated_switch.else_err_ty, merges, switch_inst, @@ -11742,8 +11716,8 @@ fn resolveSwitchBlock( if (else_is_named_only and item_ty.enumTagFieldIndex(cond_val, zcu) != null) { assert(item_ty.isNonexhaustiveEnum(zcu)); - const prong_kind: SwitchProngKind = if (else_case.is_inline) - .{ .inline_ref = cond_ref } + const prong_items: SwitchProngItems = if (else_case.is_inline) + .{ .@"inline" = cond_ref } else .special; return sema.resolveSwitchProng( @@ -11758,7 +11732,7 @@ fn resolveSwitchBlock( } }), else_case.capture, else_case.has_tag_capture, - prong_kind, + prong_items, validated_switch.else_err_ty, merges, switch_inst, @@ -11782,8 +11756,8 @@ fn resolveSwitchBlock( return .unreachable_value; } } - const prong_kind: SwitchProngKind = if (is_inline) - .{ .inline_ref = cond_ref } + const prong_items: SwitchProngItems = if (is_inline) + .{ .@"inline" = cond_ref } else .special; return sema.resolveSwitchProng( @@ -11798,7 +11772,7 @@ fn resolveSwitchBlock( } }), capture, has_tag_capture, - prong_kind, + prong_items, validated_switch.else_err_ty, merges, switch_inst, @@ -11834,14 +11808,59 @@ const SwitchOperand = union(enum) { }, }; -const SwitchProngKind = union(enum) { - /// Prefer populating this field over the others, if possible. - inline_ref: Air.Inst.Ref, +fn analyzeSwitchOperandLoad( + sema: *Sema, + block: *Block, + operand: SwitchOperand, + operand_src: LazySrcLoc, + by_ref: bool, +) CompileError!Air.Inst.Ref { + switch (operand) { + .simple => |s| { + if (by_ref) { + assert(s.by_ref != .none); + return s.by_ref; + } else { + return s.by_val; + } + }, + .loop => |l| { + const loaded = try sema.analyzeLoad(block, operand_src, l.operand_alloc, operand_src); + assert(loaded != .none); // there are no captures, so no need to load the switch operand + if (by_ref) { + assert(l.operand_is_ref); + return loaded; + } + return if (l.operand_is_ref) + try sema.analyzeLoad(block, operand_src, loaded, operand_src) + else + loaded; + }, + } +} + +const SwitchProngItems = union(enum) { + @"inline": Air.Inst.Ref, item_refs: []const Air.Inst.Ref, has_ranges, special, }; +/// A switch capture is comptime-known if it is `inline` and/or it is a by-value +/// capture of a prong with a single item. +fn resolveSwitchCaptureFromProngItems( + sema: *Sema, + prong_items: SwitchProngItems, + by_ref: bool, +) ?Value { + const ref: Air.Inst.Ref = switch (prong_items) { + .@"inline" => |ref| ref, + .item_refs => |refs| if (refs.len == 1 and !by_ref) refs[0] else return null, + .has_ranges, .special => return null, + }; + return sema.resolveValue(ref).?; +} + /// Resolve a switch prong which is determined at comptime to have no peers. /// Sets up captures as needed. Uses `analyzeBodyRuntimeBreak`. fn resolveSwitchProng( @@ -11855,7 +11874,7 @@ fn resolveSwitchProng( capture_src: LazySrcLoc, capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, has_tag_capture: bool, - kind: SwitchProngKind, + prong_items: SwitchProngItems, else_err_ty: ?Type, merges: *Block.Merges, switch_inst: Zir.Inst.Index, @@ -11870,36 +11889,28 @@ fn resolveSwitchProng( const parent_hint = sema.branch_hint; defer sema.branch_hint = parent_hint orelse if (sema.branch_hint == .cold) .cold else null; - const payload_inst: Zir.Inst.Index = if (capture != .none) inst: { + const analyzed_captures = try sema.analyzeSwitchCaptures( + child_block, + operand, + operand_src, + sema.typeOf(operand.simple.by_val), + capture_src, + capture, + has_tag_capture, + prong_items, + else_err_ty, + ); + + const payload_inst = if (capture != .none) inst: { const payload_inst = zir_switch.payload_capture_placeholder.unwrap() orelse switch_inst; - const payload_ref = try sema.analyzeSwitchPayloadCapture( - child_block, - operand, - operand.simple.by_val, - operand.simple.by_ref, - sema.typeOf(operand.simple.by_val), - operand_src, - capture_src, - capture == .by_ref, - kind, - else_err_ty, - ); - assert(!sema.typeOf(payload_ref).isNoReturn(sema.pt.zcu)); - sema.inst_map.putAssumeCapacity(payload_inst, payload_ref); + sema.inst_map.putAssumeCapacity(payload_inst, analyzed_captures.payload_ref); break :inst payload_inst; } else undefined; defer if (capture != .none) assert(sema.inst_map.remove(payload_inst)); const tag_inst: Zir.Inst.Index = if (has_tag_capture) inst: { const tag_inst = zir_switch.tag_capture_placeholder.unwrap() orelse switch_inst; - const tag_ref = try sema.analyzeSwitchTagCapture( - child_block, - operand.simple.by_val, - sema.typeOf(operand.simple.by_val), - capture_src, - kind, - ); - sema.inst_map.putAssumeCapacity(tag_inst, tag_ref); + sema.inst_map.putAssumeCapacity(tag_inst, analyzed_captures.tag_ref); break :inst tag_inst; } else undefined; defer if (has_tag_capture) assert(sema.inst_map.remove(tag_inst)); @@ -11945,7 +11956,7 @@ fn analyzeSwitchProng( capture_src: LazySrcLoc, capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, has_tag_capture: bool, - kind: SwitchProngKind, + prong_items: SwitchProngItems, else_err_ty: ?Type, switch_inst: Zir.Inst.Index, zir_switch: *const Zir.UnwrappedSwitchBlock, @@ -11966,77 +11977,28 @@ fn analyzeSwitchProng( } } - const need_load: bool = need_load: { - if (capture == .none and !has_tag_capture) { - // No need to load the operand for this prong! - break :need_load false; - } - 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) { - // `inline_ref` *is* the (comptime-known) capture. - break :need_load false; - } - assert(zir_switch.any_maybe_runtime_capture); // should have caught everything else by now - if (capture != .by_ref and - kind == .item_refs and kind.item_refs.len == 1) - { - // Capture is comptime-known because it's the only prong item - break :need_load false; - } - break :need_load true; - }; + const analyzed_captures = try sema.analyzeSwitchCaptures( + case_block, + operand, + operand_src, + operand_ty, + capture_src, + capture, + has_tag_capture, + prong_items, + else_err_ty, + ); - const operand_val: Air.Inst.Ref, const operand_ptr: Air.Inst.Ref = load_operand: { - if (!need_load) break :load_operand .{ .none, .none }; - switch (operand) { - .simple => |s| break :load_operand .{ s.by_val, s.by_ref }, - .loop => |l| { - const loaded = try sema.analyzeLoad(case_block, operand_src, l.operand_alloc, operand_src); - if (l.operand_is_ref) { - const by_val = try sema.analyzeLoad(case_block, operand_src, loaded, operand_src); - break :load_operand .{ by_val, loaded }; - } else { - break :load_operand .{ loaded, .none }; - } - }, - } - }; - - const payload_inst: Zir.Inst.Index = if (capture != .none) inst: { + const payload_inst = if (capture != .none) inst: { const payload_inst = zir_switch.payload_capture_placeholder.unwrap() orelse switch_inst; - const payload_ref = try sema.analyzeSwitchPayloadCapture( - case_block, - operand, - operand_val, - operand_ptr, - operand_ty, - operand_src, - capture_src, - capture == .by_ref, - kind, - else_err_ty, - ); - assert(!sema.typeOf(payload_ref).isNoReturn(sema.pt.zcu)); - sema.inst_map.putAssumeCapacity(payload_inst, payload_ref); + sema.inst_map.putAssumeCapacity(payload_inst, analyzed_captures.payload_ref); break :inst payload_inst; } else undefined; defer if (capture != .none) assert(sema.inst_map.remove(payload_inst)); const tag_inst: Zir.Inst.Index = if (has_tag_capture) inst: { const tag_inst = zir_switch.tag_capture_placeholder.unwrap() orelse switch_inst; - const tag_ref = try sema.analyzeSwitchTagCapture( - case_block, - operand_val, - operand_ty, - capture_src, - kind, - ); - sema.inst_map.putAssumeCapacity(tag_inst, tag_ref); + sema.inst_map.putAssumeCapacity(tag_inst, analyzed_captures.tag_ref); break :inst tag_inst; } else undefined; defer if (has_tag_capture) assert(sema.inst_map.remove(tag_inst)); @@ -12047,157 +12009,314 @@ fn analyzeSwitchProng( return sema.analyzeBodyRuntimeBreak(case_block, prong_body); } -fn analyzeSwitchTagCapture( - sema: *Sema, - case_block: *Block, - /// May be `none` if this is an inline capture or if `kind.item_refs.len == 1`. - operand_val: Air.Inst.Ref, - operand_ty: Type, - capture_src: LazySrcLoc, - kind: SwitchProngKind, -) CompileError!Air.Inst.Ref { - const pt = sema.pt; - const zcu = pt.zcu; - - const tag_capture_src: LazySrcLoc = .{ - .base_node_inst = capture_src.base_node_inst, - .offset = .{ .switch_tag_capture = capture_src.offset.switch_capture }, - }; - - if (operand_ty.zigTypeTag(zcu) != .@"union") { - return sema.fail(case_block, tag_capture_src, "cannot capture tag of non-union type '{f}'", .{ - 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, - .item_refs => |refs| if (refs.len == 1) return refs[0], - .special => {}, - } - return sema.unionToTag(case_block, operand_val); -} - -fn analyzeSwitchPayloadCapture( +fn analyzeSwitchCaptures( sema: *Sema, case_block: *Block, operand: SwitchOperand, - /// 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, - operand_ty: Type, operand_src: LazySrcLoc, + operand_ty: Type, capture_src: LazySrcLoc, - capture_by_ref: bool, - kind: SwitchProngKind, + capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, + has_tag_capture: bool, + prong_items: SwitchProngItems, else_err_ty: ?Type, +) CompileError!struct { + payload_ref: Air.Inst.Ref, + tag_ref: Air.Inst.Ref, +} { + const pt = sema.pt; + const zcu = pt.zcu; + + if (operand_ty.zigTypeTag(zcu) == .@"union" and + operand_ty.containerLayout(zcu) != .@"packed") + { + if (capture == .none) { + const tag_ref: Air.Inst.Ref = tag_ref: { + if (!has_tag_capture) break :tag_ref .none; + if (sema.resolveSwitchCaptureFromProngItems(prong_items, false)) |tag_val| { + break :tag_ref .fromValue(tag_val); + } + const loaded_operand = try sema.analyzeSwitchOperandLoad(case_block, operand, operand_src, false); + break :tag_ref try sema.unionToTag(case_block, loaded_operand); + }; + return .{ .payload_ref = .none, .tag_ref = tag_ref }; + } + + // We always have to load the operand for tagged union payload captures + // since we can't derive the payload value from the tag (except for OPV + // types, for which the load is always basically a noop anyway). + + const loaded_operand = try sema.analyzeSwitchOperandLoad(case_block, operand, operand_src, capture == .by_ref); + + if (sema.resolveSwitchCaptureFromProngItems(prong_items, capture == .by_ref)) |tag_val| { + const payload_ref = try sema.resolveSwitchPayloadCaptureTaggedUnion( + case_block, + loaded_operand, + operand_src, + operand_ty, + tag_val, + capture == .by_ref, + ); + const tag_ref: Air.Inst.Ref = if (has_tag_capture) .fromValue(tag_val) else .none; + return .{ .payload_ref = payload_ref, .tag_ref = tag_ref }; + } + + const payload_ref = try sema.analyzeSwitchPayloadCaptureTaggedUnion( + case_block, + operand, + loaded_operand, + operand_src, + operand_ty, + capture == .by_ref, + capture_src, + prong_items, + ); + + const tag_ref: Air.Inst.Ref = tag_ref: { + if (!has_tag_capture) break :tag_ref .none; + const operand_val = switch (capture) { + .none => unreachable, // handled above + .by_val => loaded_operand, + .by_ref => try sema.analyzeLoad(case_block, operand_src, loaded_operand, operand_src), + }; + break :tag_ref try sema.unionToTag(case_block, operand_val); + }; + + assert(!sema.typeOf(payload_ref).isNoReturn(zcu)); + return .{ .payload_ref = payload_ref, .tag_ref = tag_ref }; + } + + const payload_ref: Air.Inst.Ref = payload_ref: { + if (capture == .none) break :payload_ref .none; + + if (operand_ty.zigTypeTag(zcu) == .error_set) { + // Error captures need to have their type narrowed! + + if (capture == .by_ref) { + return sema.fail( + case_block, + capture_src, + "error set cannot be captured by reference", + .{}, + ); + } + assert(capture == .by_val); + + if (sema.resolveSwitchCaptureFromProngItems(prong_items, false)) |err_val| { + const err_name = err_val.getErrorName(zcu).unwrap().?; + break :payload_ref .fromIntern((try pt.intern(.{ .err = .{ + .ty = (try pt.singleErrorSetType(err_name)).toIntern(), + .name = err_name, + } }))); + } + + const loaded_operand = try sema.analyzeSwitchOperandLoad(case_block, operand, operand_src, false); + + switch (prong_items) { + .@"inline" => unreachable, // handled above + .has_ranges => unreachable, // not possible for error set + .special => { + if (else_err_ty) |err_ty| { + break :payload_ref try sema.bitCast(case_block, err_ty, loaded_operand, operand_src, null); + } else { + try sema.analyzeUnreachable(case_block, operand_src, false); + break :payload_ref .unreachable_value; + } + }, + .item_refs => |item_refs| { + var names: InferredErrorSet.NameMap = .{}; + try names.ensureUnusedCapacity(sema.arena, item_refs.len); + for (item_refs) |item_ref| { + const item_val = sema.resolveValue(item_ref).?; + names.putAssumeCapacityNoClobber(item_val.getErrorName(zcu).unwrap().?, {}); + } + const narrowed_ty = try pt.errorSetFromUnsortedNames(names.keys()); + break :payload_ref try sema.bitCast(case_block, narrowed_ty, loaded_operand, operand_src, null); + }, + } + } + + // We try to make the capture comptime-known based on `prong_items` first: + + if (sema.resolveSwitchCaptureFromProngItems(prong_items, capture == .by_ref)) |item_val| { + break :payload_ref switch (capture) { + .none => unreachable, // handled above + .by_val => .fromValue(item_val), + .by_ref => try sema.uavRef(item_val), + }; + } + + // Otherwise the capture value is just the passed-through value of the + // switch condition (which we might have to load first). + + break :payload_ref try sema.analyzeSwitchOperandLoad(case_block, operand, operand_src, capture == .by_ref); + }; + + if (has_tag_capture) { + const tag_capture_src: LazySrcLoc = .{ + .base_node_inst = capture_src.base_node_inst, + .offset = .{ .switch_tag_capture = capture_src.offset.switch_capture }, + }; + if (operand_ty.zigTypeTag(zcu) == .@"union") { + assert(operand_ty.containerLayout(zcu) == .@"packed"); + return sema.failWithOwnedErrorMsg(case_block, msg: { + const msg = try sema.errMsg(tag_capture_src, "cannot capture tag of packed union", .{}); + errdefer msg.destroy(sema.gpa); + try sema.addDeclaredHereNote(msg, operand_ty); + if (operand_ty.srcLocOrNull(zcu)) |ty_src| { + try sema.errNote(ty_src, msg, "consider using a tagged union", .{}); + } + break :msg msg; + }); + } + return sema.failWithOwnedErrorMsg(case_block, msg: { + const msg = try sema.errMsg(tag_capture_src, "cannot capture tag of non-union type '{f}'", .{ + operand_ty.fmt(pt), + }); + errdefer msg.destroy(sema.gpa); + try sema.addDeclaredHereNote(msg, operand_ty); + break :msg msg; + }); + } + + return .{ .payload_ref = payload_ref, .tag_ref = .none }; +} + +fn resolveSwitchPayloadCaptureTaggedUnion( + sema: *Sema, + case_block: *Block, + loaded_operand: Air.Inst.Ref, + operand_src: LazySrcLoc, + operand_ty: Type, + tag_val: Value, + capture_by_ref: bool, +) CompileError!Air.Inst.Ref { + const pt = sema.pt; + const zcu = pt.zcu; + const ip = &zcu.intern_pool; + + const field_index: u32 = @intCast(operand_ty.unionTagFieldIndex(tag_val, zcu).?); + const union_obj = zcu.typeToUnion(operand_ty).?; + const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_index]); + const payload_ref: Air.Inst.Ref = payload_ref: { + if (capture_by_ref) { + const operand_ptr_info = sema.typeOf(loaded_operand).ptrInfo(zcu); + const ptr_field_ty = try pt.ptrType(.{ + .child = field_ty.toIntern(), + .flags = .{ + .is_const = operand_ptr_info.flags.is_const, + .is_volatile = operand_ptr_info.flags.is_volatile, + .address_space = operand_ptr_info.flags.address_space, + }, + }); + break :payload_ref try case_block.addStructFieldPtr(loaded_operand, field_index, ptr_field_ty); + } + if (try sema.resolveDefinedValue(case_block, operand_src, loaded_operand)) |union_val| { + const tag_and_val = ip.indexToKey(union_val.toIntern()).un; + break :payload_ref .fromIntern(tag_and_val.val); + } + if (try field_ty.onePossibleValue(pt)) |opv| break :payload_ref .fromValue(opv); + break :payload_ref try case_block.addStructFieldVal(loaded_operand, field_index, field_ty); + }; + assert(!sema.typeOf(payload_ref).isNoReturn(zcu)); + return payload_ref; +} + +fn analyzeSwitchPayloadCaptureTaggedUnion( + sema: *Sema, + case_block: *Block, + operand: SwitchOperand, + loaded_operand: Air.Inst.Ref, + operand_src: LazySrcLoc, + operand_ty: Type, + capture_by_ref: bool, + capture_src: LazySrcLoc, + prong_items: SwitchProngItems, ) CompileError!Air.Inst.Ref { const pt = sema.pt; const zcu = pt.zcu; const ip = &zcu.intern_pool; + const gpa = sema.gpa; + + const item_refs: []const Air.Inst.Ref = switch (prong_items) { + .@"inline" => unreachable, // handled above + .has_ranges => unreachable, // not possible for tagged union + .special => return loaded_operand, + .item_refs => |item_refs| item_refs, + }; 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; + const union_obj = zcu.typeToUnion(operand_ty).?; - if (err_set and capture_by_ref) { - return sema.fail( + const first_item_val = sema.resolveValue(item_refs[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 field_indices = try sema.arena.alloc(u32, item_refs.len); + for (item_refs, field_indices) |item_ref, *field_idx| { + const item_val = sema.resolveValue(item_ref).?; + 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, item_refs.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); + } + + const item_srcs = try sema.arena.alloc(?LazySrcLoc, item_refs.len); + for (item_srcs, 0..) |*item_src, item_i| { + item_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 :capture_ty sema.resolvePeerTypes( case_block, capture_src, - "error set cannot be captured by reference", - .{}, - ); - } - - if (kind == .inline_ref) { - const item_val = sema.resolveValue(kind.inline_ref).?; - 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]); - if (capture_by_ref) { - const operand_ptr_info = sema.typeOf(operand_ptr).ptrInfo(zcu); - const ptr_field_ty = try pt.ptrType(.{ - .child = field_ty.toIntern(), - .flags = .{ - .is_const = operand_ptr_info.flags.is_const, - .is_volatile = operand_ptr_info.flags.is_volatile, - .address_space = operand_ptr_info.flags.address_space, - }, - }); - return case_block.addStructFieldPtr(operand_ptr, field_index, ptr_field_ty); - } else { - if (try sema.resolveDefinedValue(case_block, operand_src, operand_val)) |union_val| { - const tag_and_val = ip.indexToKey(union_val.toIntern()).un; - return .fromIntern(tag_and_val.val); - } - if (try field_ty.onePossibleValue(pt)) |opv| return .fromValue(opv); - return case_block.addStructFieldVal(operand_val, field_index, field_ty); - } - } else if (capture_by_ref) { - return sema.uavRef(item_val); - } else { - return kind.inline_ref; - } - } - - if (kind == .special) { - 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; - } - - 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 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).?; - } - - // 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; + dummy_captures, + .{ .override = item_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(loaded_operand); + 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); + const dummy_captures = try sema.arena.alloc(Air.Inst.Ref, item_refs.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.* = .{ + const item_srcs = try sema.arena.alloc(?LazySrcLoc, item_refs.len); + for (item_srcs, 0..) |*item_src, item_i| { + item_src.* = .{ .base_node_inst = capture_src.base_node_inst, .offset = .{ .switch_case_item = .{ .switch_node_offset = switch_node_offset, @@ -12207,14 +12326,15 @@ fn analyzeSwitchPayloadCapture( }; } - break :capture_ty sema.resolvePeerTypes( + break :resolve sema.resolvePeerTypes( case_block, capture_src, dummy_captures, - .{ .override = case_srcs }, + .{ .override = item_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; }, @@ -12222,262 +12342,188 @@ 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, loaded_operand)) |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(loaded_operand, 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); + + if (try sema.resolveDefinedValue(case_block, operand_src, loaded_operand)) |operand_val| { + if (operand_val.isUndef(zcu)) return pt.undefRef(capture_ty); + const union_val = ip.indexToKey(operand_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); + } + + try sema.requireRuntimeBlock(case_block, operand_src, null); + + if (same_types) { + return case_block.addStructFieldVal(loaded_operand, first_field_index, capture_ty); + } - // 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; - } + // 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); - }; - - // 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.bit_set.Dynamic = 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); - } + } + // 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(loaded_operand, first_field_index, first_field_ty); + return case_block.addBitCast(capture_ty, uncoerced); + }; + + // 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.bit_set.Dynamic = 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 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.ArrayList(u32).initCapacity(gpa, estimated_extra); + defer cases_extra.deinit(gpa); - { - // All branch hints are `.none`, so just add zero elems. - comptime assert(@intFromEnum(std.lang.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.lang.BranchHint.none) == 0); + const need_elems = std.math.divCeil(usize, prong_count + 1, 10) catch unreachable; + try cases_extra.appendNTimes(gpa, 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".field_names.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(loaded_operand, 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 sema.air_extra.ensureUnusedCapacity(sema.gpa, @typeInfo(Air.SwitchBr).@"struct".field_names.len + - cases_extra.items.len + - @typeInfo(Air.Block).@"struct".field_names.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); - - // 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); - }, + try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".field_names.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(item_refs[idx])); // item + cases_extra.appendSliceAssumeCapacity(@ptrCast(coerce_block.instructions.items)); // body } - - return capture_block_inst.toRef(); } + const else_body_len = len: { + // 'else' prong uses a bitcast + var coerce_block = case_block.makeSubBlock(); + defer coerce_block.instructions.deinit(sema.gpa); - 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); - } + 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(loaded_operand, 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); - 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); - } + try cases_extra.appendSlice(gpa, @ptrCast(coerce_block.instructions.items)); + break :len coerce_block.instructions.items.len; + }; - // 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]; + try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr).@"struct".field_names.len + + cases_extra.items.len + + @typeInfo(Air.Block).@"struct".field_names.len + + 1); + + const switch_br_inst: u32 = @intCast(sema.air_instructions.len); + try sema.air_instructions.append(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); + + // 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 = loaded_operand, + } }, + }); + 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); }, - .has_ranges => {}, } - return operand_val; + + return capture_block_inst.toRef(); } const ResolvedSwitchItem = struct { diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index b832f1aeed3b9b84f3de7319ebfaca43966213f2..43286ea0fcefdd3ca1371243ea81a5f483e96349 100644 --- a/test/behavior/switch.zig +++ b/test/behavior/switch.zig @@ -1486,3 +1486,36 @@ test "switch on large types" { try S.doTheTest(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_1234, 0xFFFF_1234); try comptime S.doTheTest(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_1234, 0xFFFF_1234); } + +test "error captures narrow error sets" { + const S = struct { + fn doTheTest(err: error{ A, B, C, D }) !void { + switch (err) { + error.A, error.B => |e| comptime assert(@TypeOf(e) == error{ A, B }), + else => |e| comptime assert(@TypeOf(e) == error{ C, D }), + } + switch (err) { + inline error.A, error.B => |e| comptime { + if (e == error.A) + assert(@TypeOf(e) == error{A}) + else if (e == error.B) + assert(@TypeOf(e) == error{B}) + else + unreachable; + }, + inline else => |e| comptime { + if (e == error.C) + assert(@TypeOf(e) == error{C}) + else if (e == error.D) + assert(@TypeOf(e) == error{D}) + else + unreachable; + }, + } + } + }; + + try S.doTheTest(error.B); + try comptime S.doTheTest(error.B); + try comptime S.doTheTest(error.C); +} diff --git a/test/cases/compile_errors/switch_capture_packed_union_tag.zig b/test/cases/compile_errors/switch_capture_packed_union_tag.zig deleted file mode 100644 index 71991ceb3ad27a6747fde59085502ff05c3b1745..0000000000000000000000000000000000000000 --- a/test/cases/compile_errors/switch_capture_packed_union_tag.zig +++ /dev/null @@ -1,15 +0,0 @@ -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_invalid_tag_capture.zig b/test/cases/compile_errors/switch_invalid_tag_capture.zig new file mode 100644 index 0000000000000000000000000000000000000000..a054810a9535f0ed2a42f0c2d4f3f9c569acd49e --- /dev/null +++ b/test/cases/compile_errors/switch_invalid_tag_capture.zig @@ -0,0 +1,38 @@ +const P = packed union(u8) { + a: u8, + b: i8, +}; +export fn entry1(p: P) void { + switch (p) { + .{ .a = 123 } => |_, tag| _ = tag, + else => {}, + } +} + +const E = enum(u8) { a, b }; +export fn entry3(e: E) void { + switch (e) { + .a => |_, tag| _ = tag, + else => {}, + } +} + +const Error = error{ MyError, MyOtherError }; +export fn entry2(ok: bool) void { + switch (foo(ok)) { + error.MyError => |_, tag| _ = tag, + else => {}, + } +} +fn foo(ok: bool) Error { + return if (ok) error.MyError else error.MyOtherError; +} + +// error +// +// :7:30: error: cannot capture tag of packed union +// :1:18: note: union declared here +// :1:18: note: consider using a tagged union +// :15:19: error: cannot capture tag of non-union type 'tmp.E' +// :12:11: note: enum declared here +// :23:30: error: cannot capture tag of non-union type 'error{MyError,MyOtherError}' -- 2.54.0