From aa2b178029fc466f847c0cb663c3b32c7809a357 Mon Sep 17 00:00:00 2001 From: David Rubin Date: Sat, 4 Oct 2025 12:04:42 -0700 Subject: [PATCH 01/23] disallow switch case capture discards Previously Zig allowed you to write something like, ```zig switch (x) { .y => |_| { ``` This seems a bit strange because in other cases, such as when capturing the tag in a switch case, ```zig switch (x) { .y => |_, _| { ``` this produces an error. The only usecase I can think of for the previous behaviour is if you wanted to assert that all union payloads are able to coerce, ```zig const X = union(enum) { y: u8, z: f32 }; switch (x) { .y, .z => |_| { ``` This will compile-error with the `|_|` and pass without it. I don't believe this usecase is strong enough to keep the current behaviour; it was never used in the Zig codebase and I cannot find a single usage of this behaviour in the real world, searching through Sourcegraph. --- lib/std/testing.zig | 2 +- lib/std/zig/AstGen.zig | 12 ++++++--- src/Sema.zig | 4 ++- test/behavior/switch_loop.zig | 27 +++++++++++++++++++ .../switch_loop_discarded_capture.zig | 16 +++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 test/cases/compile_errors/switch_loop_discarded_capture.zig diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 03e1d06c18..24287f65aa 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -813,7 +813,7 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe } }, - .array => |_| { + .array => { if (expected.len != actual.len) { print("Array len not the same, expected {d}, found {d}\n", .{ expected.len, actual.len }); return error.TestExpectedEqual; diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index 2a6423a60d..38fa5f84d7 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -7882,8 +7882,10 @@ fn switchExpr( var payload_sub_scope: *Scope = undefined; if (mem.eql(u8, ident_slice, "_")) { if (capture_is_ref) { + // |*_, tag| is invalid, so we can fail early return astgen.failTok(payload_token, "pointer modifier invalid on discard", .{}); } + capture = .none; payload_sub_scope = &case_scope.base; } else { const capture_name = try astgen.identAsString(ident); @@ -7903,11 +7905,15 @@ fn switchExpr( const tag_token = if (tree.tokenTag(ident + 1) == .comma) ident + 2 - else - break :blk payload_sub_scope; + else if (capture == .none) { + // discarding the capture is only valid iff the tag is captured + // whether the tag capture is discarded is handled below + return astgen.failTok(payload_token, "discard of capture; omit it instead", .{}); + } else break :blk payload_sub_scope; + const tag_slice = tree.tokenSlice(tag_token); if (mem.eql(u8, tag_slice, "_")) { - try astgen.appendErrorTok(tag_token, "discard of tag capture; omit it instead", .{}); + return astgen.failTok(tag_token, "discard of tag capture; omit it instead", .{}); } else if (case.inline_token == null) { return astgen.failTok(tag_token, "tag capture on non-inline prong", .{}); } diff --git a/src/Sema.zig b/src/Sema.zig index 63e923ad19..88e70ff889 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -11546,7 +11546,9 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r }); } try sema.validateRuntimeValue(block, operand_src, maybe_ptr); - const operand_alloc = if (extra.data.bits.any_non_inline_capture) a: { + const operand_alloc = if (extra.data.bits.any_non_inline_capture or + extra.data.bits.any_has_tag_capture) + a: { const operand_ptr_ty = try pt.singleMutPtrType(sema.typeOf(maybe_ptr)); const operand_alloc = try block.addTy(.alloc, operand_ptr_ty); _ = try block.addBinOp(.store, operand_alloc, maybe_ptr); diff --git a/test/behavior/switch_loop.zig b/test/behavior/switch_loop.zig index 8dfbf9775c..0df3383342 100644 --- a/test/behavior/switch_loop.zig +++ b/test/behavior/switch_loop.zig @@ -270,3 +270,30 @@ test "switch loop on non-exhaustive enum" { try S.doTheTest(); try comptime S.doTheTest(); } + +test "switch loop with discarded tag capture" { + if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; + if (builtin.zig_backend == .stage2_c) return error.SkipZigTest; + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; + + const S = struct { + const U = union(enum) { + a: u32, + b: u32, + c: u32, + }; + + fn doTheTest() void { + const a: U = .{ .a = 10 }; + blk: switch (a) { + inline .b => |_, tag| { + _ = tag; + continue :blk .{ .c = 20 }; + }, + else => {}, + } + } + }; + S.doTheTest(); + comptime S.doTheTest(); +} diff --git a/test/cases/compile_errors/switch_loop_discarded_capture.zig b/test/cases/compile_errors/switch_loop_discarded_capture.zig new file mode 100644 index 0000000000..bca1d9e85c --- /dev/null +++ b/test/cases/compile_errors/switch_loop_discarded_capture.zig @@ -0,0 +1,16 @@ +export fn foo() void { + const S = struct { + fn doTheTest() void { + blk: switch (@as(u8, 'a')) { + '1' => |_| continue :blk '1', + else => {}, + } + } + }; + S.doTheTest(); + comptime S.doTheTest(); +} + +// error +// +// :5:25: error: discard of capture; omit it instead From e0108dec54deaf044e869bb76aa4a40813913277 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 17 Dec 2025 16:43:28 +0100 Subject: [PATCH 02/23] AstGen: allow labels to provide separate `break` and `continue` targets Enhances `GenZir` to allow labels to provide separate `break` and `continue` target blocks and adds some more information on continue targets to communicate whether the target is a switch block or cannot be targeted by `continue` at all. The main motivation is enabling this: ``` const result: u32 = operand catch |err| label: switch (err) { else => continue :label error.MyError, error.MyError => break :label 1, }; ``` to be lowered to something like this: ``` %1 = block({ %2 = is_non_err(%operand) %3 = condbr(%2, { %4 = err_union_payload_unsafe(%operand) %5 = break(%1, result) // targets enclosing `block` }, { %6 = err_union_code(%operand) %7 = switch_block(%6, else => { %8 = switch_continue(%7, "error.MyError") // targets `switch_block` }, "error.MyError" => { %9 = break(%1, @one) // targets enclosing `block` }, ) %10 = break(%1, @void_value) }) }) ``` which makes the non-error case and all breaks from switch prongs, but not continues from switch prongs, peers. This is required to avoid the problems described in gh#11957 for labeled switches without having to introduce a fairly complex special case to the `switch_block_err_union` logic. Since this construct is very rare in practice, introducing this additional complexity just to save a few ZIR bytes is likely not worth it, so the simplified lowering described above will be used instead. As a nice bonus, AstGen can now also detect a `continue` trying to target a labeled block and emit an appropriate error message. --- lib/std/zig/AstGen.zig | 413 ++++++++++-------- .../compile_errors/labeled_block_continue.zig | 10 + 2 files changed, 235 insertions(+), 188 deletions(-) create mode 100644 test/cases/compile_errors/labeled_block_continue.zig diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index 38fa5f84d7..dd02c9fe4e 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -2162,92 +2162,101 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn // Look for the label in the scope. var scope = parent_scope; - while (true) { - switch (scope.tag) { - .gen_zir => { - const block_gz = scope.cast(GenZir).?; + find_scope: switch (scope.tag) { + .gen_zir => { + const gen_zir = scope.cast(GenZir).?; - if (block_gz.cur_defer_node.unwrap()) |cur_defer_node| { - // We are breaking out of a `defer` block. - return astgen.failNodeNotes(node, "cannot break out of defer expression", .{}, &.{ - try astgen.errNoteNode( - cur_defer_node, - "defer expression here", - .{}, - ), - }); - } + if (gen_zir.cur_defer_node.unwrap()) |cur_defer_node| { + // We are breaking out of a `defer` block. + return astgen.failNodeNotes(node, "cannot break out of defer expression", .{}, &.{ + try astgen.errNoteNode( + cur_defer_node, + "defer expression here", + .{}, + ), + }); + } - const block_inst = blk: { - if (opt_break_label.unwrap()) |break_label| { - if (block_gz.label) |*label| { - if (try astgen.tokenIdentEql(label.token, break_label)) { - label.used = true; - break :blk label.block_inst; - } - } - } else if (block_gz.break_block.unwrap()) |i| { - break :blk i; + if (opt_break_label.unwrap()) |break_label| labeled: { + if (gen_zir.label) |*label| { + if (try astgen.tokenIdentEql(label.token, break_label)) { + label.used = true; + break :labeled; } - // If not the target, start over with the parent - scope = block_gz.parent; - continue; - }; - // If we made it here, this block is the target of the break expr + } + // gz without or with different label, continue to parent scopes. + scope = gen_zir.parent; + continue :find_scope scope.tag; + } else if (!gen_zir.allow_unlabeled_control_flow) { + // This `break` is unlabeled and the gz we've found doesn't allow + // unlabeled control flow. Continue to parent scopes. + scope = gen_zir.parent; + continue :find_scope scope.tag; + } - const break_tag: Zir.Inst.Tag = if (block_gz.is_inline) - .break_inline - else - .@"break"; + const break_tag: Zir.Inst.Tag = if (gen_zir.is_inline) + .break_inline + else + .@"break"; - const rhs = opt_rhs.unwrap() orelse { - _ = try rvalue(parent_gz, block_gz.break_result_info, .void_value, node); - - try genDefers(parent_gz, scope, parent_scope, .normal_only); - - // As our last action before the break, "pop" the error trace if needed - if (!block_gz.is_comptime) - _ = try parent_gz.addRestoreErrRetIndex(.{ .block = block_inst }, .always, node); - - _ = try parent_gz.addBreak(break_tag, block_inst, .void_value); - return Zir.Inst.Ref.unreachable_value; - }; - - const operand = try reachableExpr(parent_gz, parent_scope, block_gz.break_result_info, rhs, node); + if (opt_rhs.unwrap()) |rhs| { + // We have a `break` operand. + const operand = try reachableExpr(parent_gz, parent_scope, gen_zir.break_result_info, rhs, node); try genDefers(parent_gz, scope, parent_scope, .normal_only); // As our last action before the break, "pop" the error trace if needed - if (!block_gz.is_comptime) - try restoreErrRetIndex(parent_gz, .{ .block = block_inst }, block_gz.break_result_info, rhs, operand); - - switch (block_gz.break_result_info.rl) { + if (!gen_zir.is_comptime) { + try restoreErrRetIndex(parent_gz, .{ .block = gen_zir.break_target }, gen_zir.break_result_info, rhs, operand); + } + switch (gen_zir.break_result_info.rl) { .ptr => { // In this case we don't have any mechanism to intercept it; // we assume the result location is written, and we break with void. - _ = try parent_gz.addBreak(break_tag, block_inst, .void_value); + _ = try parent_gz.addBreak(break_tag, gen_zir.break_target, .void_value); }, .discard => { - _ = try parent_gz.addBreak(break_tag, block_inst, .void_value); + _ = try parent_gz.addBreak(break_tag, gen_zir.break_target, .void_value); }, else => { - _ = try parent_gz.addBreakWithSrcNode(break_tag, block_inst, operand, rhs); + _ = try parent_gz.addBreakWithSrcNode(break_tag, gen_zir.break_target, operand, rhs); }, } - return Zir.Inst.Ref.unreachable_value; - }, - .local_val => scope = scope.cast(Scope.LocalVal).?.parent, - .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent, - .namespace => break, - .defer_normal, .defer_error => scope = scope.cast(Scope.Defer).?.parent, - .top => unreachable, - } - } - if (opt_break_label.unwrap()) |break_label| { - const label_name = try astgen.identifierTokenString(break_label); - return astgen.failTok(break_label, "label not found: '{s}'", .{label_name}); - } else { - return astgen.failNode(node, "break expression outside loop", .{}); + return .unreachable_value; + } else { + _ = try rvalue(parent_gz, gen_zir.break_result_info, .void_value, node); + + try genDefers(parent_gz, scope, parent_scope, .normal_only); + + // As our last action before the break, "pop" the error trace if needed + if (!gen_zir.is_comptime) + _ = try parent_gz.addRestoreErrRetIndex(.{ .block = gen_zir.break_target }, .always, node); + + _ = try parent_gz.addBreak(break_tag, gen_zir.break_target, .void_value); + return .unreachable_value; + } + }, + .local_val => { + scope = scope.cast(Scope.LocalVal).?.parent; + continue :find_scope scope.tag; + }, + .local_ptr => { + scope = scope.cast(Scope.LocalPtr).?.parent; + continue :find_scope scope.tag; + }, + .defer_normal, .defer_error => { + scope = scope.cast(Scope.Defer).?.parent; + continue :find_scope scope.tag; + }, + .namespace => { + if (opt_break_label.unwrap()) |break_label| { + const label_name = try astgen.identifierTokenString(break_label); + return astgen.failTok(break_label, "label not found: '{s}'", .{label_name}); + } else { + return astgen.failNode(node, "break expression outside loop", .{}); + } + }, + .top => unreachable, } } @@ -2262,100 +2271,116 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) // Look for the label in the scope. var scope = parent_scope; - while (true) { - switch (scope.tag) { - .gen_zir => { - const gen_zir = scope.cast(GenZir).?; + find_scope: switch (scope.tag) { + .gen_zir => { + const gen_zir = scope.cast(GenZir).?; - if (gen_zir.cur_defer_node.unwrap()) |cur_defer_node| { - return astgen.failNodeNotes(node, "cannot continue out of defer expression", .{}, &.{ - try astgen.errNoteNode( - cur_defer_node, - "defer expression here", - .{}, - ), - }); - } - const continue_block = gen_zir.continue_block.unwrap() orelse { - scope = gen_zir.parent; - continue; - }; - if (opt_break_label.unwrap()) |break_label| blk: { - if (gen_zir.label) |*label| { - if (try astgen.tokenIdentEql(label.token, break_label)) { - const maybe_switch_tag = astgen.instructions.items(.tag)[@intFromEnum(label.block_inst)]; - if (opt_rhs != .none) switch (maybe_switch_tag) { - .switch_block, .switch_block_ref => {}, - else => return astgen.failNode(node, "cannot continue loop with operand", .{}), - } else switch (maybe_switch_tag) { - .switch_block, .switch_block_ref => return astgen.failNode(node, "cannot continue switch without operand", .{}), - else => {}, - } + if (gen_zir.cur_defer_node.unwrap()) |cur_defer_node| { + return astgen.failNodeNotes(node, "cannot continue out of defer expression", .{}, &.{ + try astgen.errNoteNode( + cur_defer_node, + "defer expression here", + .{}, + ), + }); + } - label.used = true; - label.used_for_continue = true; - break :blk; + if (opt_break_label.unwrap()) |break_label| labeled: { + if (gen_zir.label) |*label| { + if (try astgen.tokenIdentEql(label.token, break_label)) { + switch (gen_zir.continue_target) { + .none => { + return astgen.failNode(node, "continue cannot target labeled block", .{}); + }, + .@"break" => if (opt_rhs != .none) { + return astgen.failNode(node, "cannot continue loop with operand", .{}); + }, + .switch_continue => if (opt_rhs == .none) { + return astgen.failNode(node, "cannot continue switch without operand", .{}); + }, } - } - // found continue but either it has a different label, or no label - scope = gen_zir.parent; - continue; - } else if (gen_zir.label) |label| { - // This `continue` is unlabeled. If the gz we've found corresponds to a labeled - // `switch`, ignore it and continue to parent scopes. - switch (astgen.instructions.items(.tag)[@intFromEnum(label.block_inst)]) { - .switch_block, .switch_block_ref => { - scope = gen_zir.parent; - continue; - }, - else => {}, + label.used = true; + label.used_for_continue = true; + break :labeled; } } + // gz without or with different label, continue to parent scopes. + scope = gen_zir.parent; + continue :find_scope scope.tag; + } else if (gen_zir.allow_unlabeled_control_flow) { + // This `continue` is unlabeled. If the gz we've found doesn't + // provide a `continue` target or corresponds to a labeled + // `switch`, ignore it and continue to parent scopes. + switch (gen_zir.continue_target) { + .none, .switch_continue => { + scope = gen_zir.parent; + continue :find_scope scope.tag; + }, + .@"break" => {}, + } + } else { + // We don't have a break label and the gz we found doesn't allow + // unlabeled control flow, so we continue to its parent scopes. + scope = gen_zir.parent; + continue :find_scope scope.tag; + } - if (opt_rhs.unwrap()) |rhs| { - // We need to figure out the result info to use. - // The type should match + switch (gen_zir.continue_target) { + .none => unreachable, // should have failed or continued to parent scopes by now + .@"break" => |block| { + try genDefers(parent_gz, scope, parent_scope, .normal_only); + + const break_tag: Zir.Inst.Tag = if (gen_zir.is_inline) + .break_inline + else + .@"break"; + if (break_tag == .break_inline) { + _ = try parent_gz.addUnNode(.check_comptime_control_flow, block.toRef(), node); + } + + // As our last action before the continue, "pop" the error trace if needed + if (!gen_zir.is_comptime) { + _ = try parent_gz.addRestoreErrRetIndex(.{ .block = block }, .always, node); + } + _ = try parent_gz.addBreak(break_tag, block, .void_value); + return .unreachable_value; + }, + .switch_continue => |switch_block| { + const rhs = opt_rhs.unwrap().?; // checked above const operand = try reachableExpr(parent_gz, parent_scope, gen_zir.continue_result_info, rhs, node); try genDefers(parent_gz, scope, parent_scope, .normal_only); // As our last action before the continue, "pop" the error trace if needed - if (!gen_zir.is_comptime) - _ = try parent_gz.addRestoreErrRetIndex(.{ .block = continue_block }, .always, node); - - _ = try parent_gz.addBreakWithSrcNode(.switch_continue, continue_block, operand, rhs); - return Zir.Inst.Ref.unreachable_value; - } - - try genDefers(parent_gz, scope, parent_scope, .normal_only); - - const break_tag: Zir.Inst.Tag = if (gen_zir.is_inline) - .break_inline - else - .@"break"; - if (break_tag == .break_inline) { - _ = try parent_gz.addUnNode(.check_comptime_control_flow, continue_block.toRef(), node); - } - - // As our last action before the continue, "pop" the error trace if needed - if (!gen_zir.is_comptime) - _ = try parent_gz.addRestoreErrRetIndex(.{ .block = continue_block }, .always, node); - - _ = try parent_gz.addBreak(break_tag, continue_block, .void_value); - return Zir.Inst.Ref.unreachable_value; - }, - .local_val => scope = scope.cast(Scope.LocalVal).?.parent, - .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent, - .defer_normal, .defer_error => scope = scope.cast(Scope.Defer).?.parent, - .namespace => break, - .top => unreachable, - } - } - if (opt_break_label.unwrap()) |break_label| { - const label_name = try astgen.identifierTokenString(break_label); - return astgen.failTok(break_label, "label not found: '{s}'", .{label_name}); - } else { - return astgen.failNode(node, "continue expression outside loop", .{}); + if (!gen_zir.is_comptime) { + _ = try parent_gz.addRestoreErrRetIndex(.{ .block = switch_block }, .always, node); + } + _ = try parent_gz.addBreakWithSrcNode(.switch_continue, switch_block, operand, rhs); + return .unreachable_value; + }, + } + }, + .local_val => { + scope = scope.cast(Scope.LocalVal).?.parent; + continue :find_scope scope.tag; + }, + .local_ptr => { + scope = scope.cast(Scope.LocalPtr).?.parent; + continue :find_scope scope.tag; + }, + .defer_normal, .defer_error => { + scope = scope.cast(Scope.Defer).?.parent; + continue :find_scope scope.tag; + }, + .namespace => { + if (opt_break_label.unwrap()) |break_label| { + const label_name = try astgen.identifierTokenString(break_label); + return astgen.failTok(break_label, "label not found: '{s}'", .{label_name}); + } else { + return astgen.failNode(node, "continue expression outside loop", .{}); + } + }, + .top => unreachable, } } @@ -2509,10 +2534,9 @@ fn labeledBlockExpr( try gz.instructions.append(astgen.gpa, block_inst); var block_scope = gz.makeSubBlock(parent_scope); block_scope.is_inline = force_comptime; - block_scope.label = GenZir.Label{ - .token = label_token, - .block_inst = block_inst, - }; + block_scope.label = .{ .token = label_token }; + block_scope.break_target = block_inst; + block_scope.continue_target = .none; block_scope.setBreakResultInfo(block_ri); if (force_comptime) block_scope.is_comptime = true; defer block_scope.unstack(); @@ -6574,7 +6598,6 @@ fn whileExpr( var loop_scope = parent_gz.makeSubBlock(scope); loop_scope.is_inline = is_inline; - loop_scope.setBreakResultInfo(block_ri); defer loop_scope.unstack(); var cond_scope = parent_gz.makeSubBlock(&loop_scope.base); @@ -6707,14 +6730,13 @@ fn whileExpr( _ = try loop_scope.addNode(repeat_tag, node); try loop_scope.setBlockBody(loop_block); - loop_scope.break_block = loop_block.toOptional(); - loop_scope.continue_block = continue_block.toOptional(); if (while_full.label_token) |label_token| { - loop_scope.label = .{ - .token = label_token, - .block_inst = loop_block, - }; + loop_scope.label = .{ .token = label_token }; } + loop_scope.allow_unlabeled_control_flow = true; + loop_scope.break_target = loop_block; + loop_scope.continue_target = .{ .@"break" = continue_block }; + loop_scope.setBreakResultInfo(block_ri); // done adding instructions to loop_scope, can now stack then_scope then_scope.instructions_top = then_scope.instructions.items.len; @@ -6787,10 +6809,12 @@ fn whileExpr( break :s &else_scope.base; } }; - // Remove the continue block and break block so that `continue` and `break` - // control flow apply to outer loops; not this one. - loop_scope.continue_block = .none; - loop_scope.break_block = .none; + // Remove label and forbid unlabeled control flow to this scope so that + // `continue` and `break` control flow apply to outer loops; not this one. + loop_scope.label = null; + loop_scope.allow_unlabeled_control_flow = false; + loop_scope.continue_target = undefined; + loop_scope.break_target = undefined; const else_result = try fullBodyExpr(&else_scope, sub_scope, loop_scope.break_result_info, else_node, .allow_branch_hint); if (is_statement) { _ = try addEnsureResult(&else_scope, else_result, else_node); @@ -6979,14 +7003,12 @@ fn forExpr( const cond_block = try loop_scope.makeBlockInst(block_tag, node); try cond_scope.setBlockBody(cond_block); - loop_scope.break_block = loop_block.toOptional(); - loop_scope.continue_block = cond_block.toOptional(); if (for_full.label_token) |label_token| { - loop_scope.label = .{ - .token = label_token, - .block_inst = loop_block, - }; + loop_scope.label = .{ .token = label_token }; } + loop_scope.allow_unlabeled_control_flow = true; + loop_scope.break_target = loop_block; + loop_scope.continue_target = .{ .@"break" = cond_block }; const then_node = for_full.ast.then_expr; var then_scope = parent_gz.makeSubBlock(&cond_scope.base); @@ -7077,10 +7099,12 @@ fn forExpr( if (for_full.ast.else_expr.unwrap()) |else_node| { const sub_scope = &else_scope.base; - // Remove the continue block and break block so that `continue` and `break` - // control flow apply to outer loops; not this one. - loop_scope.continue_block = .none; - loop_scope.break_block = .none; + // Remove label and forbid unlabeled control flow to this scope so that + // `continue` and `break` control flow apply to outer loops; not this one. + loop_scope.label = null; + loop_scope.allow_unlabeled_control_flow = false; + loop_scope.continue_target = undefined; + loop_scope.break_target = undefined; const else_result = try fullBodyExpr(&else_scope, sub_scope, loop_scope.break_result_info, else_node, .allow_branch_hint); if (is_statement) { _ = try addEnsureResult(&else_scope, else_result, else_node); @@ -7818,7 +7842,9 @@ fn switchExpr( const switch_block = try parent_gz.makeBlockInst(switch_tag, node); if (switch_full.label_token) |label_token| { - block_scope.continue_block = switch_block.toOptional(); + block_scope.label = .{ .token = label_token }; + block_scope.break_target = switch_block; + block_scope.continue_target = .{ .switch_continue = switch_block }; block_scope.continue_result_info = .{ .rl = if (any_payload_is_ref) .{ .ref_coerced_ty = raw_operand_ty_ref } @@ -7826,12 +7852,7 @@ fn switchExpr( .{ .coerced_ty = raw_operand_ty_ref }, }; - block_scope.label = .{ - .token = label_token, - .block_inst = switch_block, - }; - // `break` can target this via `label.block_inst` - // `break_result_info` already set by `setBreakResultInfo` + // `break_result_info` already set by `setBreakResultInfo` above. } // We re-use this same scope for all cases, including the special prong, if any. @@ -11916,8 +11937,8 @@ const GenZir = struct { /// whenever we know Sema will analyze the current block with `is_comptime`, /// for instance when we're within a `struct_decl` or a `block_comptime`. is_comptime: bool, - /// Whether we're in an expression within a `@TypeOf` operand. In this case, closure of runtime - /// variables is permitted where it is usually not. + /// Whether we're in an expression within a `@TypeOf` operand. In this case, + /// closure of runtime variables is permitted where it is usually not. is_typeof: bool = false, /// This is set to true for a `GenZir` of a `block_inline`, indicating that /// exits from this block should use `break_inline` rather than `break`. @@ -11938,10 +11959,27 @@ const GenZir = struct { /// if use is strictly nested. This saves prior size of list for unstacking. instructions_top: usize, label: ?Label = null, - break_block: Zir.Inst.OptionalIndex = .none, - continue_block: Zir.Inst.OptionalIndex = .none, + /// If `true`, unlabeled `break` and `continue` exprs can target this `GenZir`. + allow_unlabeled_control_flow: bool = false, + /// If `label` is `null` and `unlabeled_control_flow_target` is `false`, + /// this is unused and may be `undefined`. + /// Otherwise, this is the target for a `break` instruction when a `break` + /// targets this `GenZir`. + break_target: Zir.Inst.Index = undefined, + /// If `label` is `null` and `unlabeled_control_flow_target` is `false`, + /// this is unused and may be `undefined`. + continue_target: union(enum) { + /// A `continue` cannot target this `GenZir`; emit an error. + none, + /// Emit a `break` instruction targeting this block. + @"break": Zir.Inst.Index, + /// Emit a `switch_continue` instruction targeting this `switch_block`. + switch_continue: Zir.Inst.Index, + } = undefined, /// Only valid when setBreakResultInfo is called. break_result_info: AstGen.ResultInfo = undefined, + /// If `continue_target` is *not* `switch_continue`, this is unused and may + /// be `undefined`. continue_result_info: AstGen.ResultInfo = undefined, suspend_node: Ast.Node.OptionalIndex = .none, @@ -12008,7 +12046,6 @@ const GenZir = struct { const Label = struct { token: Ast.TokenIndex, - block_inst: Zir.Inst.Index, used: bool = false, used_for_continue: bool = false, }; diff --git a/test/cases/compile_errors/labeled_block_continue.zig b/test/cases/compile_errors/labeled_block_continue.zig new file mode 100644 index 0000000000..80bf2ba979 --- /dev/null +++ b/test/cases/compile_errors/labeled_block_continue.zig @@ -0,0 +1,10 @@ +export fn foo() void { + const result: u32 = b: { + continue :b 123; + }; + _ = result; +} + +// error +// +// :3:9: error: continue cannot target labeled block From d840bb511839464c852699acce471efccdd67f3e Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 17 Dec 2025 20:01:36 +0100 Subject: [PATCH 03/23] AstGen: improve ergonomics of `Scope` Adds `Scope.Unwrapped`, a simple union of pointers to already-casted scopes with `Scope.Tag` as its tag enum. This pairs very nicely with labeled switch and gets rid of almost every `scope.cast(...).?`, improving developer QOL :) --- lib/std/zig/AstGen.zig | 277 ++++++++++++++++++----------------------- 1 file changed, 122 insertions(+), 155 deletions(-) diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index dd02c9fe4e..f7d5b38681 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -2161,10 +2161,9 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn const opt_break_label, const opt_rhs = tree.nodeData(node).opt_token_and_opt_node; // Look for the label in the scope. - var scope = parent_scope; - find_scope: switch (scope.tag) { - .gen_zir => { - const gen_zir = scope.cast(GenZir).?; + find_scope: switch (parent_scope.unwrap()) { + .gen_zir => |gen_zir| { + const scope = &gen_zir.base; if (gen_zir.cur_defer_node.unwrap()) |cur_defer_node| { // We are breaking out of a `defer` block. @@ -2185,13 +2184,11 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn } } // gz without or with different label, continue to parent scopes. - scope = gen_zir.parent; - continue :find_scope scope.tag; + continue :find_scope gen_zir.parent.unwrap(); } else if (!gen_zir.allow_unlabeled_control_flow) { // This `break` is unlabeled and the gz we've found doesn't allow // unlabeled control flow. Continue to parent scopes. - scope = gen_zir.parent; - continue :find_scope scope.tag; + continue :find_scope gen_zir.parent.unwrap(); } const break_tag: Zir.Inst.Tag = if (gen_zir.is_inline) @@ -2236,18 +2233,9 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn return .unreachable_value; } }, - .local_val => { - scope = scope.cast(Scope.LocalVal).?.parent; - continue :find_scope scope.tag; - }, - .local_ptr => { - scope = scope.cast(Scope.LocalPtr).?.parent; - continue :find_scope scope.tag; - }, - .defer_normal, .defer_error => { - scope = scope.cast(Scope.Defer).?.parent; - continue :find_scope scope.tag; - }, + .local_val => |local_val| continue :find_scope local_val.parent.unwrap(), + .local_ptr => |local_ptr| continue :find_scope local_ptr.parent.unwrap(), + .defer_normal, .defer_error => |defer_scope| continue :find_scope defer_scope.parent.unwrap(), .namespace => { if (opt_break_label.unwrap()) |break_label| { const label_name = try astgen.identifierTokenString(break_label); @@ -2270,10 +2258,9 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) } // Look for the label in the scope. - var scope = parent_scope; - find_scope: switch (scope.tag) { - .gen_zir => { - const gen_zir = scope.cast(GenZir).?; + find_scope: switch (parent_scope.unwrap()) { + .gen_zir => |gen_zir| { + const scope = &gen_zir.base; if (gen_zir.cur_defer_node.unwrap()) |cur_defer_node| { return astgen.failNodeNotes(node, "cannot continue out of defer expression", .{}, &.{ @@ -2305,24 +2292,21 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) } } // gz without or with different label, continue to parent scopes. - scope = gen_zir.parent; - continue :find_scope scope.tag; + continue :find_scope gen_zir.parent.unwrap(); } else if (gen_zir.allow_unlabeled_control_flow) { // This `continue` is unlabeled. If the gz we've found doesn't // provide a `continue` target or corresponds to a labeled // `switch`, ignore it and continue to parent scopes. switch (gen_zir.continue_target) { .none, .switch_continue => { - scope = gen_zir.parent; - continue :find_scope scope.tag; + continue :find_scope gen_zir.parent.unwrap(); }, .@"break" => {}, } } else { // We don't have a break label and the gz we found doesn't allow // unlabeled control flow, so we continue to its parent scopes. - scope = gen_zir.parent; - continue :find_scope scope.tag; + continue :find_scope gen_zir.parent.unwrap(); } switch (gen_zir.continue_target) { @@ -2360,18 +2344,9 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) }, } }, - .local_val => { - scope = scope.cast(Scope.LocalVal).?.parent; - continue :find_scope scope.tag; - }, - .local_ptr => { - scope = scope.cast(Scope.LocalPtr).?.parent; - continue :find_scope scope.tag; - }, - .defer_normal, .defer_error => { - scope = scope.cast(Scope.Defer).?.parent; - continue :find_scope scope.tag; - }, + .local_val => |local_val| continue :find_scope local_val.parent.unwrap(), + .local_ptr => |local_ptr| continue :find_scope local_ptr.parent.unwrap(), + .defer_normal, .defer_error => |defer_scope| continue :find_scope defer_scope.parent.unwrap(), .namespace => { if (opt_break_label.unwrap()) |break_label| { const label_name = try astgen.identifierTokenString(break_label); @@ -2466,33 +2441,29 @@ fn blockExpr( fn checkLabelRedefinition(astgen: *AstGen, parent_scope: *Scope, label: Ast.TokenIndex) !void { // Look for the label in the scope. - var scope = parent_scope; - while (true) { - switch (scope.tag) { - .gen_zir => { - const gen_zir = scope.cast(GenZir).?; - if (gen_zir.label) |prev_label| { - if (try astgen.tokenIdentEql(label, prev_label.token)) { - const label_name = try astgen.identifierTokenString(label); - return astgen.failTokNotes(label, "redefinition of label '{s}'", .{ - label_name, - }, &[_]u32{ - try astgen.errNoteTok( - prev_label.token, - "previous definition here", - .{}, - ), - }); - } + find_scope: switch (parent_scope.unwrap()) { + .gen_zir => |gen_zir| { + if (gen_zir.label) |prev_label| { + if (try astgen.tokenIdentEql(label, prev_label.token)) { + const label_name = try astgen.identifierTokenString(label); + return astgen.failTokNotes(label, "redefinition of label '{s}'", .{ + label_name, + }, &[_]u32{ + try astgen.errNoteTok( + prev_label.token, + "previous definition here", + .{}, + ), + }); } - scope = gen_zir.parent; - }, - .local_val => scope = scope.cast(Scope.LocalVal).?.parent, - .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent, - .defer_normal, .defer_error => scope = scope.cast(Scope.Defer).?.parent, - .namespace => break, - .top => unreachable, - } + } + continue :find_scope gen_zir.parent.unwrap(); + }, + .local_val => |local_val| continue :find_scope local_val.parent.unwrap(), + .local_ptr => |local_ptr| continue :find_scope local_ptr.parent.unwrap(), + .defer_normal, .defer_error => |defer_scope| continue :find_scope defer_scope.parent.unwrap(), + .namespace => break :find_scope, + .top => unreachable, } } @@ -3007,18 +2978,16 @@ fn countDefers(outer_scope: *Scope, inner_scope: *Scope) struct { var need_err_code = false; var scope = inner_scope; while (scope != outer_scope) { - switch (scope.tag) { - .gen_zir => scope = scope.cast(GenZir).?.parent, - .local_val => scope = scope.cast(Scope.LocalVal).?.parent, - .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent, - .defer_normal => { - const defer_scope = scope.cast(Scope.Defer).?; + switch (scope.unwrap()) { + .gen_zir => |gen_zir| scope = gen_zir.parent, + .local_val => |local_val| scope = local_val.parent, + .local_ptr => |local_ptr| scope = local_ptr.parent, + .defer_normal => |defer_scope| { scope = defer_scope.parent; have_normal = true; }, - .defer_error => { - const defer_scope = scope.cast(Scope.Defer).?; + .defer_error => |defer_scope| { scope = defer_scope.parent; have_err = true; @@ -3054,17 +3023,15 @@ fn genDefers( var scope = inner_scope; while (scope != outer_scope) { - switch (scope.tag) { - .gen_zir => scope = scope.cast(GenZir).?.parent, - .local_val => scope = scope.cast(Scope.LocalVal).?.parent, - .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent, - .defer_normal => { - const defer_scope = scope.cast(Scope.Defer).?; + switch (scope.unwrap()) { + .gen_zir => |gen_zir| scope = gen_zir.parent, + .local_val => |local_val| scope = local_val.parent, + .local_ptr => |local_ptr| scope = local_ptr.parent, + .defer_normal => |defer_scope| { scope = defer_scope.parent; try gz.addDefer(defer_scope.index, defer_scope.len); }, - .defer_error => { - const defer_scope = scope.cast(Scope.Defer).?; + .defer_error => |defer_scope| { scope = defer_scope.parent; switch (which_ones) { .both_sans_err => { @@ -3107,10 +3074,9 @@ fn checkUsed(gz: *GenZir, outer_scope: *Scope, inner_scope: *Scope) InnerError!v var scope = inner_scope; while (scope != outer_scope) { - switch (scope.tag) { - .gen_zir => scope = scope.cast(GenZir).?.parent, - .local_val => { - const s = scope.cast(Scope.LocalVal).?; + switch (scope.unwrap()) { + .gen_zir => |gen_zir| scope = gen_zir.parent, + .local_val => |s| { if (s.used == .none and s.discarded == .none) { try astgen.appendErrorTok(s.token_src, "unused {s}", .{@tagName(s.id_cat)}); } else if (s.used != .none and s.discarded != .none) { @@ -3120,8 +3086,7 @@ fn checkUsed(gz: *GenZir, outer_scope: *Scope, inner_scope: *Scope) InnerError!v } scope = s.parent; }, - .local_ptr => { - const s = scope.cast(Scope.LocalPtr).?; + .local_ptr => |s| { if (s.used == .none and s.discarded == .none) { try astgen.appendErrorTok(s.token_src, "unused {s}", .{@tagName(s.id_cat)}); } else { @@ -3136,10 +3101,9 @@ fn checkUsed(gz: *GenZir, outer_scope: *Scope, inner_scope: *Scope) InnerError!v }); } } - scope = s.parent; }, - .defer_normal, .defer_error => scope = scope.cast(Scope.Defer).?.parent, + .defer_normal, .defer_error => |defer_scope| scope = defer_scope.parent, .namespace => unreachable, .top => unreachable, } @@ -4805,13 +4769,11 @@ fn testDecl( // Local variables, including function parameters. const name_str_index = try astgen.identAsString(test_name_token); - var s = scope; var found_already: ?Ast.Node.Index = null; // we have found a decl with the same name already var num_namespaces_out: u32 = 0; var capturing_namespace: ?*Scope.Namespace = null; - while (true) switch (s.tag) { - .local_val => { - const local_val = s.cast(Scope.LocalVal).?; + find_scope: switch (scope.unwrap()) { + .local_val => |local_val| { if (local_val.name == name_str_index) { local_val.used = .fromToken(test_name_token); return astgen.failTokNotes(test_name_token, "cannot test a {s}", .{ @@ -4822,10 +4784,9 @@ fn testDecl( }), }); } - s = local_val.parent; + continue :find_scope local_val.parent.unwrap(); }, - .local_ptr => { - const local_ptr = s.cast(Scope.LocalPtr).?; + .local_ptr => |local_ptr| { if (local_ptr.name == name_str_index) { local_ptr.used = .fromToken(test_name_token); return astgen.failTokNotes(test_name_token, "cannot test a {s}", .{ @@ -4836,12 +4797,11 @@ fn testDecl( }), }); } - s = local_ptr.parent; + continue :find_scope local_ptr.parent.unwrap(); }, - .gen_zir => s = s.cast(GenZir).?.parent, - .defer_normal, .defer_error => s = s.cast(Scope.Defer).?.parent, - .namespace => { - const ns = s.cast(Scope.Namespace).?; + .gen_zir => |gen_zir| continue :find_scope gen_zir.parent.unwrap(), + .defer_normal, .defer_error => |defer_scope| continue :find_scope defer_scope.parent.unwrap(), + .namespace => |ns| { if (ns.decls.get(name_str_index)) |i| { if (found_already) |f| { return astgen.failTokNotes(test_name_token, "ambiguous reference", .{}, &.{ @@ -4854,10 +4814,10 @@ fn testDecl( } num_namespaces_out += 1; capturing_namespace = ns; - s = ns.parent; + continue :find_scope ns.parent.unwrap(); }, - .top => break, - }; + .top => break :find_scope, + } if (found_already == null) { const ident_name = try astgen.identifierTokenString(test_name_token); return astgen.failTok(test_name_token, "use of undeclared identifier '{s}'", .{ident_name}); @@ -8409,7 +8369,6 @@ fn localVarRef( ) InnerError!Zir.Inst.Ref { const astgen = gz.astgen; const name_str_index = try astgen.identAsString(ident_token); - var s = scope; var found_already: ?Ast.Node.Index = null; // we have found a decl with the same name already var found_needs_tunnel: bool = undefined; // defined when `found_already != null` var found_namespaces_out: u32 = undefined; // defined when `found_already != null` @@ -8419,10 +8378,8 @@ fn localVarRef( // defined by `num_namespaces_out != 0` var capturing_namespace: *Scope.Namespace = undefined; - while (true) switch (s.tag) { - .local_val => { - const local_val = s.cast(Scope.LocalVal).?; - + find_scope: switch (scope.unwrap()) { + .local_val => |local_val| { if (local_val.name == name_str_index) { // Locals cannot shadow anything, so we do not need to look for ambiguous // references in this case. @@ -8445,10 +8402,9 @@ fn localVarRef( return rvalueNoCoercePreRef(gz, ri, value_inst, ident); } - s = local_val.parent; + continue :find_scope local_val.parent.unwrap(); }, - .local_ptr => { - const local_ptr = s.cast(Scope.LocalPtr).?; + .local_ptr => |local_ptr| { if (local_ptr.name == name_str_index) { if (ri.rl == .discard and ri.ctx == .assignment) { local_ptr.discarded = .fromToken(ident_token); @@ -8497,12 +8453,11 @@ fn localVarRef( }, } } - s = local_ptr.parent; + continue :find_scope local_ptr.parent.unwrap(); }, - .gen_zir => s = s.cast(GenZir).?.parent, - .defer_normal, .defer_error => s = s.cast(Scope.Defer).?.parent, - .namespace => { - const ns = s.cast(Scope.Namespace).?; + .gen_zir => |gen_zir| continue :find_scope gen_zir.parent.unwrap(), + .defer_normal, .defer_error => |defer_scope| continue :find_scope defer_scope.parent.unwrap(), + .namespace => |ns| { if (ns.decls.get(name_str_index)) |i| { if (found_already) |f| { return astgen.failNodeNotes(ident, "ambiguous reference", .{}, &.{ @@ -8517,10 +8472,10 @@ fn localVarRef( } num_namespaces_out += 1; capturing_namespace = ns; - s = ns.parent; + continue :find_scope ns.parent.unwrap(); }, - .top => break, - }; + .top => break :find_scope, + } if (found_already == null) { const ident_name = try astgen.identifierTokenString(ident_token); return astgen.failNode(ident, "use of undeclared identifier '{s}'", .{ident_name}); @@ -11812,6 +11767,26 @@ const Scope = struct { }; } + fn unwrap(base: *Scope) Unwrapped { + return switch (base.tag) { + inline else => |tag| @unionInit( + Unwrapped, + @tagName(tag), + @alignCast(@fieldParentPtr("base", base)), + ), + }; + } + + const Unwrapped = union(Tag) { + gen_zir: *GenZir, + local_val: *LocalVal, + local_ptr: *LocalPtr, + defer_normal: *Defer, + defer_error: *Defer, + namespace: *Namespace, + top: *Top, + }; + const Tag = enum { gen_zir, local_val, @@ -13408,11 +13383,9 @@ fn detectLocalShadowing( }); } - var s = scope; var outer_scope = false; - while (true) switch (s.tag) { - .local_val => { - const local_val = s.cast(Scope.LocalVal).?; + find_scope: switch (scope.unwrap()) { + .local_val => |local_val| { if (local_val.name == ident_name) { const name_slice = mem.span(astgen.nullTerminatedString(ident_name)); const name = try gpa.dupe(u8, name_slice); @@ -13438,10 +13411,9 @@ fn detectLocalShadowing( ), }); } - s = local_val.parent; + continue :find_scope local_val.parent.unwrap(); }, - .local_ptr => { - const local_ptr = s.cast(Scope.LocalPtr).?; + .local_ptr => |local_ptr| { if (local_ptr.name == ident_name) { const name_slice = mem.span(astgen.nullTerminatedString(ident_name)); const name = try gpa.dupe(u8, name_slice); @@ -13467,14 +13439,12 @@ fn detectLocalShadowing( ), }); } - s = local_ptr.parent; + continue :find_scope local_ptr.parent.unwrap(); }, - .namespace => { + .namespace => |ns| { outer_scope = true; - const ns = s.cast(Scope.Namespace).?; const decl_node = ns.decls.get(ident_name) orelse { - s = ns.parent; - continue; + continue :find_scope ns.parent.unwrap(); }; const name_slice = mem.span(astgen.nullTerminatedString(ident_name)); const name = try gpa.dupe(u8, name_slice); @@ -13485,13 +13455,13 @@ fn detectLocalShadowing( try astgen.errNoteNode(decl_node, "declared here", .{}), }); }, - .gen_zir => { - s = s.cast(GenZir).?.parent; + .gen_zir => |gen_zir| { outer_scope = true; + continue :find_scope gen_zir.parent.unwrap(); }, - .defer_normal, .defer_error => s = s.cast(Scope.Defer).?.parent, - .top => break, - }; + .defer_normal, .defer_error => |defer_scope| continue :find_scope defer_scope.parent.unwrap(), + .top => break :find_scope, + } } const LineColumn = struct { u32, u32 }; @@ -13728,10 +13698,8 @@ fn scanContainer( continue; } - var s = namespace.parent; - while (true) switch (s.tag) { - .local_val => { - const local_val = s.cast(Scope.LocalVal).?; + find_scope: switch (namespace.parent.unwrap()) { + .local_val => |local_val| { if (local_val.name == name_str_index) { try astgen.appendErrorTokNotes(name_token, "declaration '{s}' shadows {s} from outer scope", .{ token_bytes, @tagName(local_val.id_cat), @@ -13743,12 +13711,11 @@ fn scanContainer( ), }); any_invalid_declarations = true; - break; + break :find_scope; } - s = local_val.parent; + continue :find_scope local_val.parent.unwrap(); }, - .local_ptr => { - const local_ptr = s.cast(Scope.LocalPtr).?; + .local_ptr => |local_ptr| { if (local_ptr.name == name_str_index) { try astgen.appendErrorTokNotes(name_token, "declaration '{s}' shadows {s} from outer scope", .{ token_bytes, @tagName(local_ptr.id_cat), @@ -13760,15 +13727,15 @@ fn scanContainer( ), }); any_invalid_declarations = true; - break; + break :find_scope; } - s = local_ptr.parent; + continue :find_scope local_ptr.parent.unwrap(); }, - .namespace => s = s.cast(Scope.Namespace).?.parent, - .gen_zir => s = s.cast(GenZir).?.parent, - .defer_normal, .defer_error => s = s.cast(Scope.Defer).?.parent, - .top => break, - }; + .namespace => |ns| continue :find_scope ns.parent.unwrap(), + .gen_zir => |gen_zir| continue :find_scope gen_zir.parent.unwrap(), + .defer_normal, .defer_error => |defer_scope| continue :find_scope defer_scope.parent.unwrap(), + .top => break :find_scope, + } } if (!any_duplicates) { From 42dea36ce91db4d79711a8005ae9124bfb1364b3 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Tue, 30 Dec 2025 23:47:46 +0100 Subject: [PATCH 04/23] llvm: fix jump table gen for labeled switch with single `else` prong Avoids a null unwrap if there are no cases with explicit values present while trying to construct a jump table for a labeled switch statement. --- src/codegen/llvm.zig | 8 ++++-- test/behavior/switch_loop.zig | 49 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index fca89ea4fc..358d4ac469 100644 --- a/src/codegen/llvm.zig +++ b/src/codegen/llvm.zig @@ -6432,7 +6432,7 @@ pub const FuncGen = struct { // Don't worry about the size of the type -- it's irrelevant, because the prong values could be fairly dense. // If they are, then we will construct a jump table. - const min, const max = self.switchCaseItemRange(switch_br); + const min, const max = self.switchCaseItemRange(switch_br) orelse break :jmp_table null; const min_int = min.getUnsignedInt(zcu) orelse break :jmp_table null; const max_int = max.getUnsignedInt(zcu) orelse break :jmp_table null; const table_len = max_int - min_int + 1; @@ -6595,7 +6595,7 @@ pub const FuncGen = struct { } } - fn switchCaseItemRange(self: *FuncGen, switch_br: Air.UnwrappedSwitch) [2]Value { + fn switchCaseItemRange(self: *FuncGen, switch_br: Air.UnwrappedSwitch) ?[2]Value { const zcu = self.ng.pt.zcu; var it = switch_br.iterateCases(); var min: ?Value = null; @@ -6619,6 +6619,10 @@ pub const FuncGen = struct { if (high) max = vals[1]; } } + if (min == null) { + assert(max == null); + return null; + } return .{ min.?, max.? }; } diff --git a/test/behavior/switch_loop.zig b/test/behavior/switch_loop.zig index 0df3383342..5c973d9e1b 100644 --- a/test/behavior/switch_loop.zig +++ b/test/behavior/switch_loop.zig @@ -297,3 +297,52 @@ test "switch loop with discarded tag capture" { S.doTheTest(); comptime S.doTheTest(); } + +test "switch loop with single catch-all prong" { + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; + + const S = struct { + const E = enum { a, b, c }; + const U = union(E) { a: u32, b: u16, c: u8 }; + + fn doTheTest() !void { + var x: usize = 0; + label: switch (E.a) { + else => { + x += 1; + if (x >= 5) continue :label .b; + if (x == 10) break :label; + continue :label .c; + }, + } + try expect(x == 10); + + label: switch (E.a) { + .a, .b, .c => { + x += 1; + if (x >= 15) continue :label .b; + if (x == 20) break :label; + continue :label .c; + }, + } + try expect(x == 20); + + label: switch (E.a) { + else => if (false) continue :label true, + } + + const ok = label: switch (U{ .a = 123 }) { + else => |u| { + const y: u32 = switch (u) { + inline else => |y| y, + }; + if (y == 456) break :label true; + continue :label .{ .b = 456 }; + }, + }; + try expect(ok); + } + }; + try S.doTheTest(); + try comptime S.doTheTest(); +} From 00d4f3c00188e2b1fcb2669ba6346831787828c2 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 7 Jan 2026 17:07:24 +0100 Subject: [PATCH 05/23] Liveness: improve logging --- src/Air/Liveness.zig | 51 ++++++++++++++++++++----------------- src/Air/Liveness/Verify.zig | 27 ++++++++++++-------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/src/Air/Liveness.zig b/src/Air/Liveness.zig index d90a5a1c3f..051472a97a 100644 --- a/src/Air/Liveness.zig +++ b/src/Air/Liveness.zig @@ -176,7 +176,10 @@ pub fn analyze(zcu: *Zcu, air: Air, intern_pool: *InternPool) Allocator.Error!Li data.old_extra = a.extra; a.extra = .{}; try analyzeBody(&a, .main_analysis, &data, main_body); - assert(data.live_set.count() == 0); + if (std.debug.runtime_safety and data.live_set.count() != 0) { + log.debug("instructions still in live set after analysis: {f}", .{fmtInstSet(&data.live_set)}); + @panic("liveness analysis failed"); + } } return .{ @@ -825,10 +828,10 @@ fn analyzeOperands( // This logic must synchronize with `will_die_immediately` in `AnalyzeBigOperands.init`. const immediate_death = if (data.live_set.remove(inst)) blk: { - log.debug("[{}] %{d}: removed from live set", .{ pass, @intFromEnum(inst) }); + log.debug("[{t}] {f}: removed from live set", .{ pass, inst }); break :blk false; } else blk: { - log.debug("[{}] %{d}: immediate death", .{ pass, @intFromEnum(inst) }); + log.debug("[{t}] {f}: immediate death", .{ pass, inst }); break :blk true; }; @@ -849,7 +852,7 @@ fn analyzeOperands( const mask = @as(Bpi, 1) << @as(OperandInt, @intCast(i)); if ((try data.live_set.fetchPut(gpa, operand, {})) == null) { - log.debug("[{}] %{d}: added %{d} to live set (operand dies here)", .{ pass, @intFromEnum(inst), operand }); + log.debug("[{t}] {f}: added {f} to live set (operand dies here)", .{ pass, inst, operand }); tomb_bits |= mask; } } @@ -988,19 +991,19 @@ fn analyzeInstBlock( }, .main_analysis => { - log.debug("[{}] %{f}: block live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) }); + log.debug("[{t}] {f}: block live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) }); // We can move the live set because the body should have a noreturn // instruction which overrides the set. try data.block_scopes.put(gpa, inst, .{ .live_set = data.live_set.move(), }); defer { - log.debug("[{}] %{f}: popped block scope", .{ pass, inst }); + log.debug("[{t}] {f}: popped block scope", .{ pass, inst }); var scope = data.block_scopes.fetchRemove(inst).?.value; scope.live_set.deinit(gpa); } - log.debug("[{}] %{f}: pushed new block scope", .{ pass, inst }); + log.debug("[{t}] {f}: pushed new block scope", .{ pass, inst }); try analyzeBody(a, pass, data, body); // If the block is noreturn, block deaths not only aren't useful, they're impossible to @@ -1027,7 +1030,7 @@ fn analyzeInstBlock( } assert(measured_num == num_deaths); // post-live-set should be a subset of pre-live-set try a.special.put(gpa, inst, extra_index); - log.debug("[{}] %{f}: block deaths are {f}", .{ + log.debug("[{t}] {f}: block deaths are {f}", .{ pass, inst, fmtInstList(@ptrCast(a.extra.items[extra_index + 1 ..][0..num_deaths])), @@ -1064,7 +1067,7 @@ fn writeLoopInfo( const block_inst = key.*; a.extra.appendAssumeCapacity(@intFromEnum(block_inst)); } - log.debug("[{}] %{f}: includes breaks to {f}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.breaks) }); + log.debug("[{t}] {f}: includes breaks to {f}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.breaks) }); // Now we put the live operands from the loop body in too const num_live = data.live_set.count(); @@ -1076,7 +1079,7 @@ fn writeLoopInfo( const alive = key.*; a.extra.appendAssumeCapacity(@intFromEnum(alive)); } - log.debug("[{}] %{f}: maintain liveness of {f}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.live_set) }); + log.debug("[{t}] {f}: maintain liveness of {f}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.live_set) }); try a.special.put(gpa, inst, extra_index); @@ -1117,7 +1120,7 @@ fn resolveLoopLiveSet( try data.live_set.ensureUnusedCapacity(gpa, @intCast(loop_live.len)); for (loop_live) |alive| data.live_set.putAssumeCapacity(alive, {}); - log.debug("[{}] %{f}: block live set is {f}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) }); + log.debug("[{t}] {f}: block live set is {f}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) }); for (breaks) |block_inst| { // We might break to this block, so include every operand that the block needs alive @@ -1130,7 +1133,7 @@ fn resolveLoopLiveSet( } } - log.debug("[{}] %{f}: loop live set is {f}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) }); + log.debug("[{t}] {f}: loop live set is {f}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) }); } fn analyzeInstLoop( @@ -1168,7 +1171,7 @@ fn analyzeInstLoop( .live_set = data.live_set.move(), }); defer { - log.debug("[{}] %{f}: popped loop block scop", .{ pass, inst }); + log.debug("[{t}] {f}: popped loop block scop", .{ pass, inst }); var scope = data.block_scopes.fetchRemove(inst).?.value; scope.live_set.deinit(gpa); } @@ -1269,13 +1272,13 @@ fn analyzeInstCondBr( } } - log.debug("[{}] %{f}: 'then' branch mirrored deaths are {f}", .{ pass, inst, fmtInstList(then_mirrored_deaths.items) }); - log.debug("[{}] %{f}: 'else' branch mirrored deaths are {f}", .{ pass, inst, fmtInstList(else_mirrored_deaths.items) }); + log.debug("[{t}] {f}: 'then' branch mirrored deaths are {f}", .{ pass, inst, fmtInstList(then_mirrored_deaths.items) }); + log.debug("[{t}] {f}: 'else' branch mirrored deaths are {f}", .{ pass, inst, fmtInstList(else_mirrored_deaths.items) }); data.live_set.deinit(gpa); data.live_set = then_live.move(); // Really the union of both live sets - log.debug("[{}] %{f}: new live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) }); + log.debug("[{t}] {f}: new live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) }); // Write the mirrored deaths to `extra` const then_death_count = @as(u32, @intCast(then_mirrored_deaths.items.len)); @@ -1343,7 +1346,7 @@ fn analyzeInstSwitchBr( }); } defer if (is_dispatch_loop) { - log.debug("[{}] %{f}: popped loop block scop", .{ pass, inst }); + log.debug("[{t}] {f}: popped loop block scope", .{ pass, inst }); var scope = data.block_scopes.fetchRemove(inst).?.value; scope.live_set.deinit(gpa); }; @@ -1401,13 +1404,13 @@ fn analyzeInstSwitchBr( } for (mirrored_deaths, 0..) |mirrored, i| { - log.debug("[{}] %{f}: case {} mirrored deaths are {f}", .{ pass, inst, i, fmtInstList(mirrored.items) }); + log.debug("[{t}] {f}: case {} mirrored deaths are {f}", .{ pass, inst, i, fmtInstList(mirrored.items) }); } data.live_set.deinit(gpa); data.live_set = all_alive.move(); - log.debug("[{}] %{f}: new live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) }); + log.debug("[{t}] {f}: new live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) }); } const else_death_count = @as(u32, @intCast(mirrored_deaths[ncases].items.len)); @@ -1506,7 +1509,7 @@ fn AnalyzeBigOperands(comptime pass: LivenessPass) type { .main_analysis => { if ((try big.data.live_set.fetchPut(gpa, operand, {})) == null) { - log.debug("[{}] %{f}: added %{f} to live set (operand dies here)", .{ pass, big.inst, operand }); + log.debug("[{t}] {f}: added {f} to live set (operand dies here)", .{ pass, big.inst, operand }); big.extra_tombs[extra_byte] |= @as(u32, 1) << extra_bit; } }, @@ -1568,9 +1571,9 @@ const FmtInstSet = struct { return; } var it = val.set.keyIterator(); - try w.print("%{f}", .{it.next().?.*}); + try w.print("{f}", .{it.next().?.*}); while (it.next()) |key| { - try w.print(" %{f}", .{key.*}); + try w.print(" {f}", .{key.*}); } } }; @@ -1587,9 +1590,9 @@ const FmtInstList = struct { try w.writeAll("[no instructions]"); return; } - try w.print("%{f}", .{val.list[0]}); + try w.print("{f}", .{val.list[0]}); for (val.list[1..]) |inst| { - try w.print(" %{f}", .{inst}); + try w.print(" {f}", .{inst}); } } }; diff --git a/src/Air/Liveness/Verify.zig b/src/Air/Liveness/Verify.zig index 617ad5eaac..cdb5786921 100644 --- a/src/Air/Liveness/Verify.zig +++ b/src/Air/Liveness/Verify.zig @@ -73,7 +73,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void { .trap, .unreach => { try self.verifyInstOperands(inst, .{ .none, .none, .none }); // This instruction terminates the function, so everything should be dead - if (self.live.count() > 0) return invalid("%{f}: instructions still alive", .{inst}); + if (self.live.count() > 0) return invalid("{f}: instructions still alive", .{inst}); }, // unary @@ -166,7 +166,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void { const un_op = data[@intFromEnum(inst)].un_op; try self.verifyInstOperands(inst, .{ un_op, .none, .none }); // This instruction terminates the function, so everything should be dead - if (self.live.count() > 0) return invalid("%{f}: instructions still alive", .{inst}); + if (self.live.count() > 0) return invalid("{f}: instructions still alive", .{inst}); }, .dbg_var_ptr, .dbg_var_val, @@ -441,7 +441,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void { .repeat => { const repeat = data[@intFromEnum(inst)].repeat; const expected_live = self.loops.get(repeat.loop_inst) orelse - return invalid("%{d}: loop %{d} not in scope", .{ @intFromEnum(inst), @intFromEnum(repeat.loop_inst) }); + return invalid("{f}: loop {f} not in scope", .{ inst, repeat.loop_inst }); try self.verifyMatchingLiveness(repeat.loop_inst, expected_live); }, @@ -451,7 +451,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void { try self.verifyOperand(inst, br.operand, self.liveness.operandDies(inst, 0)); const expected_live = self.loops.get(br.block_inst) orelse - return invalid("%{d}: loop %{d} not in scope", .{ @intFromEnum(inst), @intFromEnum(br.block_inst) }); + return invalid("{f}: loop {f} not in scope", .{ inst, br.block_inst }); try self.verifyMatchingLiveness(br.block_inst, expected_live); }, @@ -487,7 +487,12 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void { if (ip.isNoReturn(block_ty.toIntern())) { assert(!self.blocks.contains(inst)); } else { - var live = self.blocks.fetchRemove(inst).?.value; + var live = if (self.blocks.fetchRemove(inst)) |kv| kv.value else { + return invalid( + "{f}: block of type '{f}' not terminated correctly", + .{ inst, block_ty.fmtDebug() }, + ); + }; defer live.deinit(self.gpa); try self.verifyMatchingLiveness(inst, live); @@ -502,7 +507,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void { // The same stuff should be alive after the loop as before it. const gop = try self.loops.getOrPut(self.gpa, inst); - if (gop.found_existing) return invalid("%{d}: loop already exists", .{@intFromEnum(inst)}); + if (gop.found_existing) return invalid("{f}: loop already exists", .{inst}); defer { var live = self.loops.fetchRemove(inst).?; live.value.deinit(self.gpa); @@ -551,7 +556,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void { // after the loop as before it. { const gop = try self.loops.getOrPut(self.gpa, inst); - if (gop.found_existing) return invalid("%{d}: loop already exists", .{@intFromEnum(inst)}); + if (gop.found_existing) return invalid("{f}: loop already exists", .{inst}); gop.value_ptr.* = self.live.move(); } defer { @@ -606,11 +611,11 @@ fn verifyOperand(self: *Verify, inst: Air.Inst.Index, op_ref: Air.Inst.Ref, dies return; }; if (dies) { - if (!self.live.remove(operand)) return invalid("%{f}: dead operand %{f} reused and killed again", .{ + if (!self.live.remove(operand)) return invalid("{f}: dead operand {f} reused and killed again", .{ inst, operand, }); } else { - if (!self.live.contains(operand)) return invalid("%{f}: dead operand %{f} reused", .{ inst, operand }); + if (!self.live.contains(operand)) return invalid("{f}: dead operand {f} reused", .{ inst, operand }); } } @@ -635,9 +640,9 @@ fn verifyInst(self: *Verify, inst: Air.Inst.Index) Error!void { } fn verifyMatchingLiveness(self: *Verify, block: Air.Inst.Index, live: LiveMap) Error!void { - if (self.live.count() != live.count()) return invalid("%{f}: different deaths across branches", .{block}); + if (self.live.count() != live.count()) return invalid("{f}: different deaths across branches", .{block}); var live_it = self.live.keyIterator(); - while (live_it.next()) |live_inst| if (!live.contains(live_inst.*)) return invalid("%{f}: different deaths across branches", .{block}); + while (live_it.next()) |live_inst| if (!live.contains(live_inst.*)) return invalid("{f}: different deaths across branches", .{block}); } fn invalid(comptime fmt: []const u8, args: anytype) error{LivenessInvalid} { From 5b00e24b6e28c24d7fa2417e9b7b88e13e75741e Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 7 Jan 2026 17:07:34 +0100 Subject: [PATCH 06/23] frontend: rework switch ZIR Moved to a more linear layout which lends itself well to exposing an iterator. Consumers of this iterator now just have to keep track of an index into a homogenous sequence of bodies. The new ZIR layout also enables giving switch prong items result locations by storing the bodies of all items inside of the switch encoding itself. There are some deliberate exceptions to this: enum literals and error values are directly encoded as strings and number literals are resolved to comptime values outside of the switch block. These special encodings exist to save space and can easily be resolved during semantic analysis. This commit also re-implements `AstGen` and `print_zir` for switch based on the new layout and adds some additional information to the ZIR text repr. Notably `switchExprErrUnion` has been merged into `switchExpr` to reduce code duplication. The rules around allowing an unreachable `else` prong in error switches are also refined by this commit, and enforced properly based on the actual AST. The special cases are listed exhaustively below: `else => unreachable,` `else => return,` `else => |e| return e,` (where `e` is any identifier) Additionally `{...} => comptime unreachable,` prongs are marked to support future features (refer to next couple of commits). Also fixes 'value with comptime-only type depends on runtime control flow' error for labeled error switch statements by surrounding the entire expr with a common block to break to (see previous commits for details). --- lib/std/zig/AstGen.zig | 1653 ++++++++++++++++++++-------------------- lib/std/zig/Zir.zig | 775 ++++++++++++------- src/print_zir.zig | 401 +++------- 3 files changed, 1435 insertions(+), 1394 deletions(-) diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index f7d5b38681..124543b883 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -115,7 +115,6 @@ fn setExtra(astgen: *AstGen, index: usize, extra: anytype) void { Zir.Inst.Call.Flags, Zir.Inst.BuiltinCall.Flags, Zir.Inst.SwitchBlock.Bits, - Zir.Inst.SwitchBlockErrUnion.Bits, Zir.Inst.FuncFancy.Bits, Zir.Inst.Param.Type, Zir.Inst.Func.RetTy, @@ -858,11 +857,11 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE no_switch_on_err: { const error_token = if_full.error_token orelse break :no_switch_on_err; const else_node = if_full.ast.else_expr.unwrap() orelse break :no_switch_on_err; - const full_switch = tree.fullSwitch(else_node) orelse break :no_switch_on_err; - if (full_switch.label_token != null) break :no_switch_on_err; - if (tree.nodeTag(full_switch.ast.condition) != .identifier) break :no_switch_on_err; - if (!mem.eql(u8, tree.tokenSlice(error_token), tree.tokenSlice(tree.nodeMainToken(full_switch.ast.condition)))) break :no_switch_on_err; - return switchExprErrUnion(gz, scope, ri.br(), node, .@"if"); + const switch_full = tree.fullSwitch(else_node) orelse break :no_switch_on_err; + if (switch_full.label_token != null) break :no_switch_on_err; // handled in `ifExpr` + if (tree.nodeTag(switch_full.ast.condition) != .identifier) break :no_switch_on_err; + if (!try astgen.tokenIdentEql(error_token, tree.nodeMainToken(switch_full.ast.condition))) break :no_switch_on_err; + return switchExpr(gz, scope, ri.br(), node, switch_full, .{ .@"if" = if_full }); } return ifExpr(gz, scope, ri.br(), node, if_full); }, @@ -1024,11 +1023,11 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE null; no_switch_on_err: { const capture_token = payload_token orelse break :no_switch_on_err; - const full_switch = tree.fullSwitch(tree.nodeData(node).node_and_node[1]) orelse break :no_switch_on_err; - if (full_switch.label_token != null) break :no_switch_on_err; - if (tree.nodeTag(full_switch.ast.condition) != .identifier) break :no_switch_on_err; - if (!mem.eql(u8, tree.tokenSlice(capture_token), tree.tokenSlice(tree.nodeMainToken(full_switch.ast.condition)))) break :no_switch_on_err; - return switchExprErrUnion(gz, scope, ri.br(), node, .@"catch"); + const switch_full = tree.fullSwitch(tree.nodeData(node).node_and_node[1]) orelse break :no_switch_on_err; + if (switch_full.label_token != null) break :no_switch_on_err; // handled in `orelseCatchExpr` + if (tree.nodeTag(switch_full.ast.condition) != .identifier) break :no_switch_on_err; + if (!try astgen.tokenIdentEql(capture_token, tree.nodeMainToken(switch_full.ast.condition))) break :no_switch_on_err; + return switchExpr(gz, scope, ri.br(), node, switch_full, .@"catch"); } switch (ri.rl) { .ref, .ref_coerced_ty => return orelseCatchExpr( @@ -1108,7 +1107,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE .error_set_decl => return errorSetDecl(gz, ri, node), .array_access => return arrayAccess(gz, scope, ri, node), .@"comptime" => return comptimeExprAst(gz, scope, ri, node), - .@"switch", .switch_comma => return switchExpr(gz, scope, ri.br(), node, tree.fullSwitch(node).?), + .@"switch", .switch_comma => return switchExpr(gz, scope, ri.br(), node, tree.fullSwitch(node).?, .none), .@"nosuspend" => return nosuspendExpr(gz, scope, ri, node), .@"suspend" => return suspendExpr(gz, scope, node), @@ -3134,14 +3133,7 @@ fn deferStmt( } const remapped_err_code: Zir.Inst.Index = @enumFromInt(gz.astgen.instructions.len); opt_remapped_err_code = remapped_err_code.toOptional(); - try gz.astgen.instructions.append(gz.astgen.gpa, .{ - .tag = .extended, - .data = .{ .extended = .{ - .opcode = .value_placeholder, - .small = undefined, - .operand = undefined, - } }, - }); + _ = try gz.astgen.appendPlaceholder(); const remapped_err_code_ref = remapped_err_code.toRef(); local_val_scope = .{ .parent = &defer_gen.base, @@ -6115,7 +6107,30 @@ fn orelseCatchExpr( break :blk &err_val_scope.base; }; - const else_result = try fullBodyExpr(&else_scope, else_sub_scope, block_scope.break_result_info, rhs, .allow_branch_hint); + const else_result = else_result: { + if (tree.fullSwitch(rhs)) |switch_full| no_switch_on_err: { + if (tree.nodeTag(node) != .@"catch") break :no_switch_on_err; + const catch_token = tree.nodeMainToken(node); + const capture_token = if (tree.tokenTag(catch_token + 1) == .pipe) token: { + break :token catch_token + 2; + } else break :no_switch_on_err; + if (switch_full.label_token == null) break :no_switch_on_err; // must use `switchExpr` with `non_err = .@"if"` + if (tree.nodeTag(switch_full.ast.condition) != .identifier) break :no_switch_on_err; + if (!try astgen.tokenIdentEql(capture_token, tree.nodeMainToken(switch_full.ast.condition))) break :no_switch_on_err; + break :else_result try switchExpr( + &else_scope, + else_sub_scope, + block_scope.break_result_info, + rhs, + switch_full, + .{ .peer_break_target = .{ + .block_inst = block, + .block_ri = block_ri, + } }, + ); + } + break :else_result try fullBodyExpr(&else_scope, else_sub_scope, block_scope.break_result_info, rhs, .allow_branch_hint); + }; if (!else_scope.endsWithNoReturn()) { // As our last action before the break, "pop" the error trace if needed if (do_err_trace) @@ -6468,7 +6483,26 @@ fn ifExpr( break :s &else_scope.base; } }; - const else_result = try fullBodyExpr(&else_scope, sub_scope, block_scope.break_result_info, else_node, .allow_branch_hint); + const else_result = else_result: { + if (tree.fullSwitch(else_node)) |switch_full| no_switch_on_err: { + const error_token = if_full.error_token orelse break :no_switch_on_err; + if (switch_full.label_token == null) break :no_switch_on_err; // must use `switchExpr` with `non_err = .@"if"` + if (tree.nodeTag(switch_full.ast.condition) != .identifier) break :no_switch_on_err; + if (!try astgen.tokenIdentEql(error_token, tree.nodeMainToken(switch_full.ast.condition))) break :no_switch_on_err; + break :else_result try switchExpr( + &else_scope, + sub_scope, + block_scope.break_result_info, + else_node, + switch_full, + .{ .peer_break_target = .{ + .block_inst = block, + .block_ri = block_ri, + } }, + ); + } + break :else_result try fullBodyExpr(&else_scope, sub_scope, block_scope.break_result_info, else_node, .allow_branch_hint); + }; if (!else_scope.endsWithNoReturn()) { // As our last action before the break, "pop" the error trace if needed if (do_err_trace) @@ -7117,503 +7151,34 @@ fn forExpr( return result; } -fn switchExprErrUnion( - parent_gz: *GenZir, - scope: *Scope, - ri: ResultInfo, - catch_or_if_node: Ast.Node.Index, - node_ty: enum { @"catch", @"if" }, -) InnerError!Zir.Inst.Ref { - const astgen = parent_gz.astgen; - const gpa = astgen.gpa; - const tree = astgen.tree; - - const if_full = switch (node_ty) { - .@"catch" => undefined, - .@"if" => tree.fullIf(catch_or_if_node).?, - }; - - const switch_node, const operand_node, const error_payload = switch (node_ty) { - .@"catch" => .{ - tree.nodeData(catch_or_if_node).node_and_node[1], - tree.nodeData(catch_or_if_node).node_and_node[0], - tree.nodeMainToken(catch_or_if_node) + 2, - }, - .@"if" => .{ - if_full.ast.else_expr.unwrap().?, - if_full.ast.cond_expr, - if_full.error_token.?, - }, - }; - const switch_full = tree.fullSwitch(switch_node).?; - - const do_err_trace = astgen.fn_block != null; - const need_rl = astgen.nodes_need_rl.contains(catch_or_if_node); - const block_ri: ResultInfo = if (need_rl) ri else .{ - .rl = switch (ri.rl) { - .ptr => .{ .ty = (try ri.rl.resultType(parent_gz, catch_or_if_node)).? }, - .inferred_ptr => .none, - else => ri.rl, - }, - .ctx = ri.ctx, - }; - - const payload_is_ref = switch (node_ty) { - .@"if" => if_full.payload_token != null and tree.tokenTag(if_full.payload_token.?) == .asterisk, - .@"catch" => ri.rl == .ref or ri.rl == .ref_coerced_ty, - }; - - // We need to call `rvalue` to write through to the pointer only if we had a - // result pointer and aren't forwarding it. - const LocTag = @typeInfo(ResultInfo.Loc).@"union".tag_type.?; - const need_result_rvalue = @as(LocTag, block_ri.rl) != @as(LocTag, ri.rl); - var scalar_cases_len: u32 = 0; - var multi_cases_len: u32 = 0; - var inline_cases_len: u32 = 0; - var has_else = false; - var else_node: Ast.Node.OptionalIndex = .none; - var else_src: ?Ast.TokenIndex = null; - for (switch_full.ast.cases) |case_node| { - const case = tree.fullSwitchCase(case_node).?; - - if (case.ast.values.len == 0) { - const case_src = case.ast.arrow_token - 1; - if (else_src) |src| { - return astgen.failTokNotes( - case_src, - "multiple else prongs in switch expression", - .{}, - &[_]u32{ - try astgen.errNoteTok( - src, - "previous else prong here", - .{}, - ), - }, - ); - } - has_else = true; - else_node = case_node.toOptional(); - else_src = case_src; - continue; - } else if (case.ast.values.len == 1 and - tree.nodeTag(case.ast.values[0]) == .identifier and - mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(case.ast.values[0])), "_")) - { - const case_src = case.ast.arrow_token - 1; - return astgen.failTokNotes( - case_src, - "'_' prong is not allowed when switching on errors", - .{}, - &[_]u32{ - try astgen.errNoteTok( - case_src, - "consider using 'else'", - .{}, - ), - }, - ); - } - - for (case.ast.values) |val| { - if (tree.nodeTag(val) == .string_literal) - return astgen.failNode(val, "cannot switch on strings", .{}); - } - - if (case.ast.values.len == 1 and tree.nodeTag(case.ast.values[0]) != .switch_range) { - scalar_cases_len += 1; - } else { - multi_cases_len += 1; - } - if (case.inline_token != null) { - inline_cases_len += 1; - } - } - - const operand_ri: ResultInfo = .{ - .rl = if (payload_is_ref) .ref else .none, - .ctx = .error_handling_expr, - }; - - astgen.advanceSourceCursorToNode(operand_node); - const operand_lc: LineColumn = .{ astgen.source_line - parent_gz.decl_line, astgen.source_column }; - - const raw_operand = try reachableExpr(parent_gz, scope, operand_ri, operand_node, switch_node); - const item_ri: ResultInfo = .{ .rl = .none }; - - // This contains the data that goes into the `extra` array for the SwitchBlockErrUnion, except - // the first cases_nodes.len slots are a table that indexes payloads later in the array, - // with the non-error and else case indices coming first, then scalar_cases_len indexes, then - // multi_cases_len indexes - const payloads = &astgen.scratch; - const scratch_top = astgen.scratch.items.len; - const case_table_start = scratch_top; - const scalar_case_table = case_table_start + 1 + @intFromBool(has_else); - const multi_case_table = scalar_case_table + scalar_cases_len; - const case_table_end = multi_case_table + multi_cases_len; - - try astgen.scratch.resize(gpa, case_table_end); - defer astgen.scratch.items.len = scratch_top; - - var block_scope = parent_gz.makeSubBlock(scope); - // block_scope not used for collecting instructions - block_scope.instructions_top = GenZir.unstacked_top; - block_scope.setBreakResultInfo(block_ri); - - // Sema expects a dbg_stmt immediately before switch_block_err_union - try emitDbgStmtForceCurrentIndex(parent_gz, operand_lc); - // This gets added to the parent block later, after the item expressions. - const switch_block = try parent_gz.makeBlockInst(.switch_block_err_union, switch_node); - - // We re-use this same scope for all cases, including the special prong, if any. - var case_scope = parent_gz.makeSubBlock(&block_scope.base); - case_scope.instructions_top = GenZir.unstacked_top; - - { - const body_len_index: u32 = @intCast(payloads.items.len); - payloads.items[case_table_start] = body_len_index; - try payloads.resize(gpa, body_len_index + 1); // body_len - - case_scope.instructions_top = parent_gz.instructions.items.len; - defer case_scope.unstack(); - - const unwrap_payload_tag: Zir.Inst.Tag = if (payload_is_ref) - .err_union_payload_unsafe_ptr - else - .err_union_payload_unsafe; - - const unwrapped_payload = try case_scope.addUnNode( - unwrap_payload_tag, - raw_operand, - catch_or_if_node, - ); - - switch (node_ty) { - .@"catch" => { - const case_result = switch (ri.rl) { - .ref, .ref_coerced_ty => unwrapped_payload, - else => try rvalue( - &case_scope, - block_scope.break_result_info, - unwrapped_payload, - catch_or_if_node, - ), - }; - _ = try case_scope.addBreakWithSrcNode( - .@"break", - switch_block, - case_result, - catch_or_if_node, - ); - }, - .@"if" => { - var payload_val_scope: Scope.LocalVal = undefined; - - const then_node = if_full.ast.then_expr; - const then_sub_scope = s: { - assert(if_full.error_token != null); - if (if_full.payload_token) |payload_token| { - const token_name_index = payload_token + @intFromBool(payload_is_ref); - const ident_name = try astgen.identAsString(token_name_index); - const token_name_str = tree.tokenSlice(token_name_index); - if (mem.eql(u8, "_", token_name_str)) - break :s &case_scope.base; - try astgen.detectLocalShadowing( - &case_scope.base, - ident_name, - token_name_index, - token_name_str, - .capture, - ); - payload_val_scope = .{ - .parent = &case_scope.base, - .gen_zir = &case_scope, - .name = ident_name, - .inst = unwrapped_payload, - .token_src = token_name_index, - .id_cat = .capture, - }; - try case_scope.addDbgVar(.dbg_var_val, ident_name, unwrapped_payload); - break :s &payload_val_scope.base; - } else { - _ = try case_scope.addUnNode( - .ensure_err_union_payload_void, - raw_operand, - catch_or_if_node, - ); - break :s &case_scope.base; - } - }; - const then_result = try expr( - &case_scope, - then_sub_scope, - block_scope.break_result_info, - then_node, - ); - try checkUsed(parent_gz, &case_scope.base, then_sub_scope); - if (!case_scope.endsWithNoReturn()) { - _ = try case_scope.addBreakWithSrcNode( - .@"break", - switch_block, - then_result, - then_node, - ); - } - }, - } - - const case_slice = case_scope.instructionsSlice(); - const body_len = astgen.countBodyLenAfterFixupsExtraRefs(case_slice, &.{switch_block}); - try payloads.ensureUnusedCapacity(gpa, body_len); - const capture: Zir.Inst.SwitchBlock.ProngInfo.Capture = switch (node_ty) { - .@"catch" => .none, - .@"if" => if (if_full.payload_token == null) - .none - else if (payload_is_ref) - .by_ref - else - .by_val, - }; - payloads.items[body_len_index] = @bitCast(Zir.Inst.SwitchBlock.ProngInfo{ - .body_len = @intCast(body_len), - .capture = capture, - .is_inline = false, - .has_tag_capture = false, - }); - appendBodyWithFixupsExtraRefsArrayList(astgen, payloads, case_slice, &.{switch_block}); - } - - const err_name = blk: { - const err_str = tree.tokenSlice(error_payload); - if (mem.eql(u8, err_str, "_")) { - // This is fatal because we already know we're switching on the captured error. - return astgen.failTok(error_payload, "discard of error capture; omit it instead", .{}); - } - const err_name = try astgen.identAsString(error_payload); - try astgen.detectLocalShadowing(scope, err_name, error_payload, err_str, .capture); - - break :blk err_name; - }; - - // allocate a shared dummy instruction for the error capture - const err_inst = err_inst: { - const inst: Zir.Inst.Index = @enumFromInt(astgen.instructions.len); - try astgen.instructions.append(astgen.gpa, .{ - .tag = .extended, - .data = .{ .extended = .{ - .opcode = .value_placeholder, - .small = undefined, - .operand = undefined, - } }, - }); - break :err_inst inst; - }; - - // In this pass we generate all the item and prong expressions for error cases. - var multi_case_index: u32 = 0; - var scalar_case_index: u32 = 0; - var any_uses_err_capture = false; - for (switch_full.ast.cases) |case_node| { - const case = tree.fullSwitchCase(case_node).?; - - const is_multi_case = case.ast.values.len > 1 or - (case.ast.values.len == 1 and tree.nodeTag(case.ast.values[0]) == .switch_range); - - var dbg_var_name: Zir.NullTerminatedString = .empty; - var dbg_var_inst: Zir.Inst.Ref = undefined; - var err_scope: Scope.LocalVal = undefined; - var capture_scope: Scope.LocalVal = undefined; - - const sub_scope = blk: { - err_scope = .{ - .parent = &case_scope.base, - .gen_zir = &case_scope, - .name = err_name, - .inst = err_inst.toRef(), - .token_src = error_payload, - .id_cat = .capture, - }; - - const capture_token = case.payload_token orelse break :blk &err_scope.base; - if (tree.tokenTag(capture_token) != .identifier) { - return astgen.failTok(capture_token + 1, "error set cannot be captured by reference", .{}); - } - - const capture_slice = tree.tokenSlice(capture_token); - if (mem.eql(u8, capture_slice, "_")) { - try astgen.appendErrorTok(capture_token, "discard of error capture; omit it instead", .{}); - } - const tag_name = try astgen.identAsString(capture_token); - try astgen.detectLocalShadowing(&case_scope.base, tag_name, capture_token, capture_slice, .capture); - - capture_scope = .{ - .parent = &case_scope.base, - .gen_zir = &case_scope, - .name = tag_name, - .inst = switch_block.toRef(), - .token_src = capture_token, - .id_cat = .capture, - }; - dbg_var_name = tag_name; - dbg_var_inst = switch_block.toRef(); - - err_scope.parent = &capture_scope.base; - - break :blk &err_scope.base; - }; - - const header_index: u32 = @intCast(payloads.items.len); - const body_len_index = if (is_multi_case) blk: { - payloads.items[multi_case_table + multi_case_index] = header_index; - multi_case_index += 1; - try payloads.resize(gpa, header_index + 3); // items_len, ranges_len, body_len - - // items - var items_len: u32 = 0; - for (case.ast.values) |item_node| { - if (tree.nodeTag(item_node) == .switch_range) continue; - items_len += 1; - - const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node, .switch_item); - try payloads.append(gpa, @intFromEnum(item_inst)); - } - - // ranges - var ranges_len: u32 = 0; - for (case.ast.values) |range| { - if (tree.nodeTag(range) != .switch_range) continue; - ranges_len += 1; - - const first_node, const last_node = tree.nodeData(range).node_and_node; - const first = try comptimeExpr(parent_gz, scope, item_ri, first_node, .switch_item); - const last = try comptimeExpr(parent_gz, scope, item_ri, last_node, .switch_item); - try payloads.appendSlice(gpa, &[_]u32{ - @intFromEnum(first), @intFromEnum(last), - }); - } - - payloads.items[header_index] = items_len; - payloads.items[header_index + 1] = ranges_len; - break :blk header_index + 2; - } else if (case_node.toOptional() == else_node) blk: { - payloads.items[case_table_start + 1] = header_index; - try payloads.resize(gpa, header_index + 1); // body_len - break :blk header_index; - } else blk: { - payloads.items[scalar_case_table + scalar_case_index] = header_index; - scalar_case_index += 1; - try payloads.resize(gpa, header_index + 2); // item, body_len - const item_node = case.ast.values[0]; - const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node, .switch_item); - payloads.items[header_index] = @intFromEnum(item_inst); - break :blk header_index + 1; - }; - - { - // temporarily stack case_scope on parent_gz - case_scope.instructions_top = parent_gz.instructions.items.len; - defer case_scope.unstack(); - - if (do_err_trace and nodeMayAppendToErrorTrace(tree, operand_node)) - _ = try case_scope.addSaveErrRetIndex(.always); - - if (dbg_var_name != .empty) { - try case_scope.addDbgVar(.dbg_var_val, dbg_var_name, dbg_var_inst); - } - - const target_expr_node = case.ast.target_expr; - const case_result = try fullBodyExpr(&case_scope, sub_scope, block_scope.break_result_info, target_expr_node, .allow_branch_hint); - // check capture_scope, not err_scope to avoid false positive unused error capture - try checkUsed(parent_gz, &case_scope.base, err_scope.parent); - const uses_err = err_scope.used != .none or err_scope.discarded != .none; - if (uses_err) { - try case_scope.addDbgVar(.dbg_var_val, err_name, err_inst.toRef()); - any_uses_err_capture = true; - } - - if (!parent_gz.refIsNoReturn(case_result)) { - if (do_err_trace) - try restoreErrRetIndex( - &case_scope, - .{ .block = switch_block }, - block_scope.break_result_info, - target_expr_node, - case_result, - ); - - _ = try case_scope.addBreakWithSrcNode(.@"break", switch_block, case_result, target_expr_node); - } - - const case_slice = case_scope.instructionsSlice(); - const extra_insts: []const Zir.Inst.Index = if (uses_err) &.{ switch_block, err_inst } else &.{switch_block}; - const body_len = astgen.countBodyLenAfterFixupsExtraRefs(case_slice, extra_insts); - try payloads.ensureUnusedCapacity(gpa, body_len); - payloads.items[body_len_index] = @bitCast(Zir.Inst.SwitchBlock.ProngInfo{ - .body_len = @intCast(body_len), - .capture = if (case.payload_token != null) .by_val else .none, - .is_inline = case.inline_token != null, - .has_tag_capture = false, - }); - appendBodyWithFixupsExtraRefsArrayList(astgen, payloads, case_slice, extra_insts); - } - } - // Now that the item expressions are generated we can add this. - try parent_gz.instructions.append(gpa, switch_block); - - try astgen.extra.ensureUnusedCapacity(gpa, @typeInfo(Zir.Inst.SwitchBlockErrUnion).@"struct".fields.len + - @intFromBool(multi_cases_len != 0) + - payloads.items.len - case_table_end + - (case_table_end - case_table_start) * @typeInfo(Zir.Inst.As).@"struct".fields.len); - - const payload_index = astgen.addExtraAssumeCapacity(Zir.Inst.SwitchBlockErrUnion{ - .operand = raw_operand, - .bits = Zir.Inst.SwitchBlockErrUnion.Bits{ - .has_multi_cases = multi_cases_len != 0, - .has_else = has_else, - .scalar_cases_len = @intCast(scalar_cases_len), - .any_uses_err_capture = any_uses_err_capture, - .payload_is_ref = payload_is_ref, - }, - .main_src_node_offset = parent_gz.nodeIndexToRelative(catch_or_if_node), - }); - - if (multi_cases_len != 0) { - astgen.extra.appendAssumeCapacity(multi_cases_len); - } - - if (any_uses_err_capture) { - astgen.extra.appendAssumeCapacity(@intFromEnum(err_inst)); - } - - const zir_datas = astgen.instructions.items(.data); - zir_datas[@intFromEnum(switch_block)].pl_node.payload_index = payload_index; - - for (payloads.items[case_table_start..case_table_end], 0..) |start_index, i| { - var body_len_index = start_index; - var end_index = start_index; - const table_index = case_table_start + i; - if (table_index < scalar_case_table) { - end_index += 1; - } else if (table_index < multi_case_table) { - body_len_index += 1; - end_index += 2; - } else { - body_len_index += 2; - const items_len = payloads.items[start_index]; - const ranges_len = payloads.items[start_index + 1]; - end_index += 3 + items_len + 2 * ranges_len; - } - const prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(payloads.items[body_len_index]); - end_index += prong_info.body_len; - astgen.extra.appendSliceAssumeCapacity(payloads.items[start_index..end_index]); - } - - if (need_result_rvalue) { - return rvalue(parent_gz, ri, switch_block.toRef(), switch_node); - } else { - return switch_block.toRef(); - } -} +const SwitchNonErr = union(enum) { + /// A regular switch expression. + /// Emits `switch_block[_ref]`. + none, + /// `eu catch |err| switch (err) { ... }` + /// + /// `switch` must not be labeled. + /// Emits `switch_block_err_union`. + @"catch", + /// `if (eu) |payload| { ... } else |err| switch (err) { ... }` + /// + /// `switch` must not be labeled. + /// Emits `switch_block_err_union`. + @"if": Ast.full.If, + /// `eu catch |err| label: switch (err) { ... }` + /// `if (eu) |payload| { ... } else |err| label: switch (err) { ... }` + /// + /// `switch` must be labeled. + /// Emits a `condbr` on the non-error body and a regular switch, though the + /// non-error prong and all `break`s from switch prongs are peers. + /// Exists to avoid a rather complex special case of `switch_block_err_union`. + peer_break_target: struct { + /// Refers to the enclosing block of the entire switch-on-err expression. + block_inst: Zir.Inst.Index, + /// Belongs to `block_inst`. + block_ri: ResultInfo, + }, +}; fn switchExpr( parent_gz: *GenZir, @@ -7621,13 +7186,38 @@ fn switchExpr( ri: ResultInfo, node: Ast.Node.Index, switch_full: Ast.full.Switch, + non_err: SwitchNonErr, ) InnerError!Zir.Inst.Ref { const astgen = parent_gz.astgen; const gpa = astgen.gpa; const tree = astgen.tree; - const operand_node = switch_full.ast.condition; + + const switch_node, const operand_node, const err_token = switch (non_err) { + .none, .peer_break_target => .{ + node, + switch_full.ast.condition, + undefined, + }, + .@"catch" => .{ + tree.nodeData(node).node_and_node[1], + tree.nodeData(node).node_and_node[0], + tree.nodeMainToken(node) + 2, + }, + .@"if" => |if_full| .{ + if_full.ast.else_expr.unwrap().?, + if_full.ast.cond_expr, + if_full.error_token.?, + }, + }; const case_nodes = switch_full.ast.cases; + const is_err_switch = non_err != .none; + const needs_non_err_handling = switch (non_err) { + .none => false, + .peer_break_target => false, // handled by parent expression + .@"catch", .@"if" => true, + }; + const need_rl = astgen.nodes_need_rl.contains(node); const block_ri: ResultInfo = if (need_rl) ri else .{ .rl = switch (ri.rl) { @@ -7637,48 +7227,80 @@ fn switchExpr( }, .ctx = ri.ctx, }; + // We need to call `rvalue` to write through to the pointer only if we had a // result pointer and aren't forwarding it. const LocTag = @typeInfo(ResultInfo.Loc).@"union".tag_type.?; const need_result_rvalue = @as(LocTag, block_ri.rl) != @as(LocTag, ri.rl); + const catch_or_if_node = if (needs_non_err_handling) node else undefined; + const do_err_trace = needs_non_err_handling and astgen.fn_block != null; + const non_err_is_ref: bool = switch (non_err) { + .none, .peer_break_target => undefined, + .@"catch" => ri.rl == .ref or ri.rl == .ref_coerced_ty, + .@"if" => |if_full| if_full.payload_token != null and + tree.tokenTag(if_full.payload_token.?) == .asterisk, + }; + if (switch_full.label_token) |label_token| { try astgen.checkLabelRedefinition(scope, label_token); } + const err_capture_name: Zir.NullTerminatedString = if (needs_non_err_handling) blk: { + const err_str = tree.tokenSlice(err_token); + if (mem.eql(u8, err_str, "_")) { + // This is fatal because we already know we're switching on the captured error. + return astgen.failTok(err_token, "discard of error capture; omit it instead", .{}); + } + const err_name = try astgen.identAsString(err_token); + try astgen.detectLocalShadowing(scope, err_name, err_token, err_str, .capture); + break :blk err_name; + } else undefined; + // We perform two passes over the AST. This first pass is to collect information - // for the following variables, make note of the special prong AST node index, - // and bail out with a compile error if there are multiple special prongs present. + // for the following variables, make note of the special prong AST node indices, + // and bail out with a compile error if there are incompatible special prongs present. var any_payload_is_ref = false; + var any_has_payload_capture = false; var any_has_tag_capture = false; - var any_non_inline_capture = false; + var any_maybe_runtime_capture = false; var scalar_cases_len: u32 = 0; var multi_cases_len: u32 = 0; - var inline_cases_len: u32 = 0; + var total_items_len: usize = 0; + var total_ranges_len: usize = 0; var else_case_node: Ast.Node.OptionalIndex = .none; var else_src: ?Ast.TokenIndex = null; - var underscore_case_node: Ast.Node.OptionalIndex = .none; + var under_case_node: Ast.Node.OptionalIndex = .none; var underscore_node: Ast.Node.OptionalIndex = .none; var underscore_src: ?Ast.TokenIndex = null; - var underscore_additional_items: Zir.SpecialProngs.AdditionalItems = .none; + var under_is_bare = false; for (case_nodes) |case_node| { const case = tree.fullSwitchCase(case_node).?; if (case.payload_token) |payload_token| { const ident = if (tree.tokenTag(payload_token) == .asterisk) blk: { + // Capturing errors by reference is never allowed, but as we will + // check for this again later we will fail as late as possible. any_payload_is_ref = true; break :blk payload_token + 1; } else payload_token; + + if (!mem.eql(u8, tree.tokenSlice(ident), "_")) { + any_has_payload_capture = true; + + // If we're capturing a union, its payload value cannot always be + // comptime-known, even if its prong is inlined as inlining only + // affects its enum tag. + // This check isn't perfect, because for things like enums, the + // entire capture *is* comptime-known for inline prongs! But such + // knowledge requires semantic analysis. + any_maybe_runtime_capture = true; + } if (tree.tokenTag(ident + 1) == .comma) { any_has_tag_capture = true; - } - // If the first capture is ignored, then there is no runtime-known - // capture, as the tag capture must be for an inline prong. - // This check isn't perfect, because for things like enums, the - // first prong *is* comptime-known for inline prongs! But such - // knowledge requires semantic analysis. - if (!mem.eql(u8, tree.tokenSlice(ident), "_")) { - any_non_inline_capture = true; + if (case.inline_token == null) { + any_maybe_runtime_capture = true; + } } } @@ -7690,13 +7312,7 @@ fn switchExpr( case_src, "multiple else prongs in switch expression", .{}, - &[_]u32{ - try astgen.errNoteTok( - src, - "previous else prong here", - .{}, - ), - }, + &.{try astgen.errNoteTok(src, "previous else prong here", .{})}, ); } else_case_node = case_node.toOptional(); @@ -7704,156 +7320,492 @@ fn switchExpr( continue; } - // Check for '_' prong. + // Check for '_' prong and ranges. var case_has_underscore = false; + var case_has_ranges = false; for (case.ast.values) |val| { switch (tree.nodeTag(val)) { - .identifier => if (mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(val)), "_")) { - const val_src = tree.nodeMainToken(val); - if (underscore_src) |src| { - return astgen.failTokNotes( - val_src, - "multiple '_' prongs in switch expression", - .{}, - &[_]u32{ - try astgen.errNoteTok( - src, - "previous '_' prong here", - .{}, - ), - }, - ); - } - if (case.inline_token != null) { - return astgen.failTok(val_src, "cannot inline '_' prong", .{}); - } - underscore_case_node = case_node.toOptional(); - underscore_src = val_src; - underscore_node = val.toOptional(); - underscore_additional_items = switch (case.ast.values.len) { - 0 => unreachable, - 1 => .none, - 2 => .one, - else => .many, - }; - case_has_underscore = true; + .switch_range => { + total_ranges_len += 1; + case_has_ranges = true; }, .string_literal => return astgen.failNode(val, "cannot switch on strings", .{}), - else => {}, + else => |tag| { + if (tag == .identifier and + mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(val)), "_")) + { + const val_src = tree.nodeMainToken(val); + if (is_err_switch) { + const case_src = case.ast.arrow_token - 1; + return astgen.failTokNotes( + case_src, + "'_' prong is not allowed when switching on errors", + .{}, + &.{ + try astgen.errNoteTok( + case_src, + "consider using 'else'", + .{}, + ), + }, + ); + } + if (underscore_src) |src| { + return astgen.failTokNotes( + val_src, + "multiple '_' prongs in switch expression", + .{}, + &.{try astgen.errNoteTok(src, "previous '_' prong here", .{})}, + ); + } + if (case.inline_token != null) { + return astgen.failTok(val_src, "cannot inline '_' prong", .{}); + } + under_case_node = case_node.toOptional(); + underscore_src = val_src; + underscore_node = val.toOptional(); + under_is_bare = case.ast.values.len == 1; + case_has_underscore = true; + } else { + total_items_len += 1; + } + }, } } - if (case_has_underscore) continue; - if (case.ast.values.len == 1 and tree.nodeTag(case.ast.values[0]) != .switch_range) { + const case_len = case.ast.values.len - @intFromBool(case_has_underscore); + if (case_len == 1 and !case_has_ranges) { scalar_cases_len += 1; - } else { + } else if (case_len >= 1) { multi_cases_len += 1; } - if (case.inline_token != null) { - inline_cases_len += 1; - } } - const special_prongs: Zir.SpecialProngs = .init( - else_src != null, - underscore_src != null, - underscore_additional_items, - ); - const has_else = special_prongs.hasElse(); - const has_under = special_prongs.hasUnder(); + const has_else = else_src != null; + const has_under = underscore_src != null; + if (under_is_bare) assert(has_under); // make sure that the former implies the latter + if (is_err_switch) assert(!has_under); // should have failed by now + const any_ranges = total_ranges_len > 0; - const operand_ri: ResultInfo = .{ .rl = if (any_payload_is_ref) .ref else .none }; - - astgen.advanceSourceCursorToNode(operand_node); - const operand_lc: LineColumn = .{ astgen.source_line - parent_gz.decl_line, astgen.source_column }; - - const raw_operand = try expr(parent_gz, scope, operand_ri, operand_node); - const item_ri: ResultInfo = .{ .rl = .none }; - - // If this switch is labeled, it may have `continue`s targeting it, and thus we need the operand type - // to provide a result type. - const raw_operand_ty_ref = if (switch_full.label_token != null) t: { - break :t try parent_gz.addUnNode(.typeof, raw_operand, operand_node); - } else undefined; - - // This contains the data that goes into the `extra` array for the SwitchBlock/SwitchBlockMulti, - // except the first cases_nodes.len slots are a table that indexes payloads later in the array, with - // the special case index coming first, then scalar_case_len indexes, then multi_cases_len indexes + // This contains all of the body lengths (already in the correct order) and + // the bodies they belong to that go into the `extra` array later, except the + // first item_table_end slots are a table that indexes the item bodies (and + // also indirectly the prong bodies, as they are always trailing after their + // item bodies). const payloads = &astgen.scratch; const scratch_top = astgen.scratch.items.len; - const case_table_start = scratch_top; - const else_case_index = if (has_else) case_table_start else undefined; - const under_case_index = if (has_under) case_table_start + @intFromBool(has_else) else undefined; - const scalar_case_table = case_table_start + @intFromBool(has_else) + @intFromBool(has_under); - const multi_case_table = scalar_case_table + scalar_cases_len; - const case_table_end = multi_case_table + multi_cases_len; - try astgen.scratch.resize(gpa, case_table_end); + var payloads_end = scratch_top; + + // Since range item body pairs are always contiguous we don't technically + // have to keep track of the position of the second body. However handling + // all of the several indices and offsets is complicated enough as it is, + // so for the sake of keeping this function a little bit more simple we do + // it anyway. + + const scalar_body_table = payloads_end; + payloads_end += scalar_cases_len; + const multi_item_body_table = payloads_end; + payloads_end += total_items_len + 2 * total_ranges_len - scalar_cases_len; + const multi_prong_body_table = payloads_end; + payloads_end += multi_cases_len; + const body_table_end = payloads_end; + + const scalar_prong_infos_start = payloads_end; + payloads_end += scalar_cases_len; + const multi_prong_infos_start = payloads_end; + payloads_end += multi_cases_len; + const multi_case_items_lens_start = payloads_end; + payloads_end += multi_cases_len; + const multi_case_ranges_lens_start = if (any_ranges) blk: { + const multi_case_ranges_lens_start = payloads_end; + payloads_end += multi_cases_len; + break :blk multi_case_ranges_lens_start; + } else undefined; + const scalar_item_infos_start = payloads_end; + payloads_end += scalar_cases_len; + const multi_items_infos_start = payloads_end; + payloads_end += total_items_len - scalar_cases_len + 2 * total_ranges_len; + const bodies_start = payloads_end; + + try payloads.resize(gpa, bodies_start); defer astgen.scratch.items.len = scratch_top; + var non_err_prong_body_start: u32 = undefined; + var else_prong_body_start: u32 = undefined; + var bare_under_prong_body_start: u32 = undefined; + var non_err_info: Zir.Inst.SwitchBlock.ProngInfo.NonErr = undefined; + var else_info: Zir.Inst.SwitchBlock.ProngInfo.Else = undefined; + var under_extra: u32 = undefined; + var block_scope = parent_gz.makeSubBlock(scope); // block_scope not used for collecting instructions block_scope.instructions_top = GenZir.unstacked_top; - block_scope.setBreakResultInfo(block_ri); - // Sema expects a dbg_stmt immediately before switch_block(_ref) + const operand_ri: ResultInfo = .{ + .rl = if (any_payload_is_ref or + (needs_non_err_handling and non_err_is_ref)) .ref else .none, + .ctx = if (do_err_trace) .error_handling_expr else .none, + }; + + astgen.advanceSourceCursorToNode(operand_node); + const operand_lc: LineColumn = .{ astgen.source_line - parent_gz.decl_line, astgen.source_column }; + + const raw_operand: Zir.Inst.Ref = if (needs_non_err_handling) + try reachableExpr(parent_gz, scope, operand_ri, operand_node, switch_node) + else + try expr(parent_gz, scope, operand_ri, operand_node); + + // Sema expects a dbg_stmt immediately before any kind of switch_block inst. try emitDbgStmtForceCurrentIndex(parent_gz, operand_lc); // This gets added to the parent block later, after the item expressions. - const switch_tag: Zir.Inst.Tag = if (any_payload_is_ref) .switch_block_ref else .switch_block; - const switch_block = try parent_gz.makeBlockInst(switch_tag, node); + const switch_tag: Zir.Inst.Tag = switch (non_err) { + .none, .peer_break_target => if (any_payload_is_ref) .switch_block_ref else .switch_block, + .@"if", .@"catch" => .switch_block_err_union, + }; + const switch_block = try parent_gz.makeBlockInst(switch_tag, switch_node); + + // Set `break` target if applicable; `continue` target may differ! + switch (non_err) { + .none => { + if (switch_full.label_token != null) { + block_scope.break_target = switch_block; + } + block_scope.setBreakResultInfo(block_ri); + }, + .@"catch", .@"if" => { + assert(switch_full.label_token == null); // use `peer_break_target` code path instead! + block_scope.setBreakResultInfo(block_ri); + }, + .peer_break_target => |peer_break_target| { + + // Special case; we have an error switch + label situation and we + // want to generate this: + // ``` + // %1 = block({ + // %2 = is_non_err(%operand) + // %3 = condbr(%2, { + // %4 = err_union_payload_unsafe(%operand) + // %5 = break(%1, result) // targets enclosing `block` + // }, { + // %6 = err_union_code(%operand) + // %7 = switch_block(%6, + // { ... } => { + // %8 = break(%1, result) // targets enclosing `block` + // }, + // { ... } => { + // %9 = switch_continue(%7, result) // targets `switch_block` + // }, + // ) + // %10 = break(%1, @void_value) + // }) + // }) + // ``` + // to ensure that the non-err case and the switch are only peers when + // breaking from either, but not when continuing the switch. We use + // this lowering to avoiding a rather complex special case in Sema. + + assert(switch_full.label_token != null); // use `switch_block_err_union` code path instead! + assert(.block == astgen.instructions.items(.tag)[@intFromEnum(peer_break_target.block_inst)]); + block_scope.break_target = peer_break_target.block_inst; + block_scope.setBreakResultInfo(peer_break_target.block_ri); + }, + } + + // We need a bunch of separate locations to store several capture values: + // `... |err| switch (err) { else => |e| { ... } }` // `err` and `e` + // `... => |payload, tag| { ... }` // `payload` and `tag` + // and result types: + // `foo => { ... }` // `foo` needs a result type + // `... => continue :sw val` // `val` needs a result type + // Some observations: + // - If we just use the switch inst itself we don't need a placeholder! + // - We can always tell for sure whether a capture exists. We also know + // that its existence implies that it has to be used. + // - We can't know whether there are any `continue`s before analyzing all + // prong bodies. At that point we already need a result location. We do + // know whether there even *could* be any though by looking for a label. + // - Sema wants a result location in `zirSwitchContinue`. If that's the + // switch inst itself, there's no need to look at the switch inst data. + // Some conclusions: + // - We should use the switch inst as the continue result location if needed. + // - If we need more insts for captures and our switch inst is already used + // for something else, we start creating placeholder insts. + + // Prong items use the switch block instruction as their result type. + // No other components of the switch statement are in scope while they are + // being resolved, so this is never a problem. + const item_ri: ResultInfo = .{ .rl = .{ .coerced_ty = switch_block.toRef() } }; + + var switch_block_inst_is_occupied: bool = false; if (switch_full.label_token) |label_token| { block_scope.label = .{ .token = label_token }; - block_scope.break_target = switch_block; block_scope.continue_target = .{ .switch_continue = switch_block }; block_scope.continue_result_info = .{ .rl = if (any_payload_is_ref) - .{ .ref_coerced_ty = raw_operand_ty_ref } + .{ .ref_coerced_ty = switch_block.toRef() } else - .{ .coerced_ty = raw_operand_ty_ref }, + .{ .coerced_ty = switch_block.toRef() }, }; + switch_block_inst_is_occupied = true; - // `break_result_info` already set by `setBreakResultInfo` above. + // `break_target` and `break_result_info` already set above. } + if (needs_non_err_handling) { + // `switch_block_err_union` uses the switch block inst as its err capture/ + // switch operand. This is always ok as its switch can never have a label. + assert(!switch_block_inst_is_occupied); + switch_block_inst_is_occupied = true; + } + // `... => |payload| { ... }` + const payload_capture_inst, const payload_capture_inst_is_placeholder = inst: { + if (!any_has_payload_capture) break :inst .{ undefined, false }; + if (!switch_block_inst_is_occupied) { + switch_block_inst_is_occupied = true; + break :inst .{ switch_block, false }; + } + break :inst .{ try astgen.appendPlaceholder(), true }; + }; + // `... => |_, tag| { ... }` + const tag_capture_inst, const tag_capture_inst_is_placeholder = inst: { + if (!any_has_tag_capture) break :inst .{ undefined, false }; + if (!switch_block_inst_is_occupied) { + switch_block_inst_is_occupied = true; + break :inst .{ switch_block, false }; + } + break :inst .{ try astgen.appendPlaceholder(), true }; + }; - // We re-use this same scope for all cases, including the special prong, if any. - var case_scope = parent_gz.makeSubBlock(&block_scope.base); - case_scope.instructions_top = GenZir.unstacked_top; + var prong_body_extra_insts_buf: [3]Zir.Inst.Index = undefined; + const prong_body_extra_insts: []const Zir.Inst.Index = extra_insts: { + var extra_insts: std.ArrayList(Zir.Inst.Index) = .initBuffer(&prong_body_extra_insts_buf); + if (switch_block_inst_is_occupied) extra_insts.appendAssumeCapacity(switch_block); + if (payload_capture_inst_is_placeholder) extra_insts.appendAssumeCapacity(payload_capture_inst); + if (tag_capture_inst_is_placeholder) extra_insts.appendAssumeCapacity(tag_capture_inst); + break :extra_insts extra_insts.items; + }; - // If any prong has an inline tag capture, allocate a shared dummy instruction for it - const tag_inst = if (any_has_tag_capture) tag_inst: { - const inst: Zir.Inst.Index = @enumFromInt(astgen.instructions.len); - try astgen.instructions.append(astgen.gpa, .{ - .tag = .extended, - .data = .{ .extended = .{ - .opcode = .value_placeholder, - .small = undefined, - .operand = undefined, - } }, - }); - break :tag_inst inst; - } else undefined; + const switch_operand, const catch_or_if_operand = if (needs_non_err_handling) + .{ switch_block.toRef(), raw_operand } + else + .{ raw_operand, undefined }; + + // We re-use this same scope for all case items and contents. + var scratch_scope = parent_gz.makeSubBlock(&block_scope.base); + scratch_scope.instructions_top = GenZir.unstacked_top; + + // We have to take care of the non-error body first if there is one. + non_err_body: { + if (!needs_non_err_handling) break :non_err_body; + + scratch_scope.instructions_top = parent_gz.instructions.items.len; + defer scratch_scope.unstack(); + + // It's always ok to use the switch block inst to refer to the error union + // payload as the actual switch statement isn't even in scope yet. + const non_err_payload_inst = switch_block; + var non_err_capture: Zir.Inst.SwitchBlock.ProngInfo.Capture = .none; + + switch (non_err) { + .none, .peer_break_target => unreachable, + .@"catch" => { + // We always effectively capture the error union payload; we use + // it to `break` from the entire `switch_block_err_union`. + non_err_capture = if (non_err_is_ref) .by_ref else .by_val; + + const then_result = switch (ri.rl) { + .ref, .ref_coerced_ty => non_err_payload_inst.toRef(), + else => try rvalue( + &scratch_scope, + block_scope.break_result_info, + non_err_payload_inst.toRef(), + catch_or_if_node, + ), + }; + _ = try scratch_scope.addBreakWithSrcNode( + .@"break", + switch_block, + then_result, + catch_or_if_node, + ); + }, + .@"if" => |if_full| { + var payload_val_scope: Scope.LocalVal = undefined; + + const then_node = if_full.ast.then_expr; + const then_sub_scope: *Scope = scope: { + if (if_full.payload_token) |payload_token| { + const ident_token = payload_token + @intFromBool(non_err_is_ref); + const ident_name = try astgen.identAsString(ident_token); + const ident_name_str = tree.tokenSlice(ident_token); + if (mem.eql(u8, "_", ident_name_str)) { + break :scope &scratch_scope.base; + } + non_err_capture = if (non_err_is_ref) .by_ref else .by_val; + try astgen.detectLocalShadowing(&scratch_scope.base, ident_name, ident_token, ident_name_str, .capture); + payload_val_scope = .{ + .parent = &scratch_scope.base, + .gen_zir = &scratch_scope, + .name = ident_name, + .inst = non_err_payload_inst.toRef(), + .token_src = ident_token, + .id_cat = .capture, + }; + try scratch_scope.addDbgVar(.dbg_var_val, ident_name, non_err_payload_inst.toRef()); + break :scope &payload_val_scope.base; + } else { + _ = try scratch_scope.addUnNode( + .ensure_err_union_payload_void, + catch_or_if_operand, + catch_or_if_node, + ); + break :scope &scratch_scope.base; + } + }; + const then_result = try fullBodyExpr(&scratch_scope, then_sub_scope, block_scope.break_result_info, then_node, .allow_branch_hint); + try checkUsed(parent_gz, &scratch_scope.base, then_sub_scope); + if (!scratch_scope.endsWithNoReturn()) { + _ = try scratch_scope.addBreakWithSrcNode(.@"break", switch_block, then_result, then_node); + } + }, + } + const body_slice = scratch_scope.instructionsSlice(); + const body_start: u32 = @intCast(payloads.items.len); + const body_len = astgen.countBodyLenAfterFixupsExtraRefs(body_slice, &.{non_err_payload_inst}); + try payloads.ensureUnusedCapacity(gpa, body_len); + astgen.appendBodyWithFixupsExtraRefsArrayList(payloads, body_slice, &.{non_err_payload_inst}); + + non_err_prong_body_start = body_start; + non_err_info = .{ + .body_len = @intCast(body_len), + .capture = non_err_capture, + .operand_is_ref = non_err_is_ref, + }; + } // In this pass we generate all the item and prong expressions. var multi_case_index: u32 = 0; var scalar_case_index: u32 = 0; + var multi_item_offset: usize = 0; for (case_nodes) |case_node| { const case = tree.fullSwitchCase(case_node).?; - const is_multi_case = case.ast.values.len > 1 or - (case.ast.values.len == 1 and tree.nodeTag(case.ast.values[0]) == .switch_range); + const case_has_under = case_node.toOptional() == under_case_node; + const ranges_len: u32 = if (any_ranges) blk: { + var ranges_len: u32 = 0; + for (case.ast.values) |value| { + ranges_len += @intFromBool(tree.nodeTag(value) == .switch_range); + } + break :blk ranges_len; + } else 0; + const items_len: u32 = @intCast(case.ast.values.len - ranges_len - @intFromBool(case_has_under)); + const is_multi_case = items_len > 1 or ranges_len > 0; - var dbg_var_name: Zir.NullTerminatedString = .empty; - var dbg_var_inst: Zir.Inst.Ref = undefined; + // item/range bodies in order of occurence + var item_i: usize = 0; + var range_i: usize = 0; + for (case.ast.values) |value| { + if (value.toOptional() == underscore_node) continue; + const is_range = tree.nodeTag(value) == .switch_range; + const range: [2]Ast.Node.Index = if (is_range) tree.nodeData(value).node_and_node else undefined; + const nodes: []const Ast.Node.Index = if (is_range) &range else &.{value}; + for (nodes) |item| { + // We lower enum literals, error values and number literals + // manually to save space since they are very commonly used as + // switch case items. + const body_start: u32 = @intCast(payloads.items.len); + const item_info: Zir.Inst.SwitchBlock.ItemInfo = blk: switch (tree.nodeTag(item)) { + .enum_literal => { + const str_index = try astgen.identAsString(tree.nodeMainToken(item)); + break :blk .wrap(.{ .enum_literal = str_index }); + }, + .error_value => { + const ident_token = tree.nodeMainToken(item) + 2; // skip 'error', '.' + const str_index = try astgen.identAsString(ident_token); + break :blk .wrap(.{ .error_value = str_index }); + }, + .number_literal => { + // We don't actually need a final result type for number + // literals, they can just be turned into `comptime_int` + // or `comptime_float` as usual and then be coerced to + // the correct type later during semantic analysis. + assert(scratch_scope.instructions_top == GenZir.unstacked_top); // important! we emit into `parent_gz` which `scratch_scope` is stacked on top of + const zir_ref = try comptimeExpr(parent_gz, scope, .{ .rl = .none }, item, .switch_item); + break :blk .wrap(.{ .number_literal = zir_ref }); + }, + else => { + scratch_scope.instructions_top = parent_gz.instructions.items.len; + defer scratch_scope.unstack(); + const item_result = try fullBodyExpr(&scratch_scope, scope, item_ri, item, .normal); + if (!scratch_scope.endsWithNoReturn()) { + _ = try scratch_scope.addBreakWithSrcNode(.break_inline, switch_block, item_result, item); + } + const item_slice = scratch_scope.instructionsSlice(); + const body_len = astgen.countBodyLenAfterFixupsExtraRefs(item_slice, &.{switch_block}); + try payloads.ensureUnusedCapacity(gpa, body_len); + astgen.appendBodyWithFixupsExtraRefsArrayList(payloads, item_slice, &.{switch_block}); + break :blk .wrap(.{ .body_len = body_len }); + }, + }; + if (is_multi_case) { + if (is_range) { + const offset = multi_item_offset + items_len + range_i; + payloads.items[multi_item_body_table + offset] = body_start; + payloads.items[multi_items_infos_start + offset] = @bitCast(item_info); + range_i += 1; + } else { + const offset = multi_item_offset + item_i; + payloads.items[multi_item_body_table + offset] = body_start; + payloads.items[multi_items_infos_start + offset] = @bitCast(item_info); + item_i += 1; + } + } else { + payloads.items[scalar_body_table + scalar_case_index] = body_start; + payloads.items[scalar_item_infos_start + scalar_case_index] = @bitCast(item_info); + } + } + } + if (is_multi_case) { + assert(item_i == items_len and range_i == 2 * ranges_len); + payloads.items[multi_case_items_lens_start + multi_case_index] = items_len; + if (any_ranges) { + payloads.items[multi_case_ranges_lens_start + multi_case_index] = ranges_len; + } + multi_item_offset += items_len + 2 * ranges_len; + } + + // Capture and prong body + + var dbg_var_payload_name: Zir.NullTerminatedString = .empty; + var dbg_var_payload_inst: Zir.Inst.Ref = undefined; var dbg_var_tag_name: Zir.NullTerminatedString = .empty; var dbg_var_tag_inst: Zir.Inst.Ref = undefined; var has_tag_capture = false; - var capture_val_scope: Scope.LocalVal = undefined; - var tag_scope: Scope.LocalVal = undefined; + var err_capture_scope: Scope.LocalVal = undefined; + var payload_capture_scope: Scope.LocalVal = undefined; + var tag_capture_scope: Scope.LocalVal = undefined; var capture: Zir.Inst.SwitchBlock.ProngInfo.Capture = .none; - const sub_scope = blk: { - const payload_token = case.payload_token orelse break :blk &case_scope.base; + // Check all captures and make them available to the prong body. + // Potential captures are: + // - for regular switch: payload and tag + // - for error switch: switch operand and payload + const prong_body_scope: *Scope = scope: { + const switch_scope: *Scope = if (needs_non_err_handling) blk: { + // We want to have the captured error we're switching on in scope! + err_capture_scope = .{ + .parent = &scratch_scope.base, + .gen_zir = &scratch_scope, + .name = err_capture_name, + .inst = switch_operand, + .token_src = err_token, + .id_cat = .capture, + }; + break :blk &err_capture_scope.base; + } else &scratch_scope.base; + + const payload_token = case.payload_token orelse break :scope switch_scope; const capture_is_ref = tree.tokenTag(payload_token) == .asterisk; const ident = payload_token + @intFromBool(capture_is_ref); @@ -7867,36 +7819,38 @@ fn switchExpr( return astgen.failTok(payload_token, "pointer modifier invalid on discard", .{}); } capture = .none; - payload_sub_scope = &case_scope.base; + payload_sub_scope = switch_scope; } else { const capture_name = try astgen.identAsString(ident); - try astgen.detectLocalShadowing(&case_scope.base, capture_name, ident, ident_slice, .capture); - capture_val_scope = .{ - .parent = &case_scope.base, - .gen_zir = &case_scope, + try astgen.detectLocalShadowing(&scratch_scope.base, capture_name, ident, ident_slice, .capture); + payload_capture_scope = .{ + .parent = switch_scope, + .gen_zir = &scratch_scope, .name = capture_name, - .inst = switch_block.toRef(), + .inst = payload_capture_inst.toRef(), .token_src = ident, .id_cat = .capture, }; - dbg_var_name = capture_name; - dbg_var_inst = switch_block.toRef(); - payload_sub_scope = &capture_val_scope.base; + dbg_var_payload_name = payload_capture_scope.name; + dbg_var_payload_inst = payload_capture_scope.inst; + payload_sub_scope = &payload_capture_scope.base; } - const tag_token = if (tree.tokenTag(ident + 1) == .comma) - ident + 2 - else if (capture == .none) { - // discarding the capture is only valid iff the tag is captured + if (is_err_switch and capture == .by_ref) { + return astgen.failTok(ident, "error set cannot be captured by reference", .{}); + } + + const tag_token = if (tree.tokenTag(ident + 1) == .comma) blk: { + break :blk ident + 2; + } else if (capture == .none) { + // discarding the capture is only valid if the tag is captured // whether the tag capture is discarded is handled below return astgen.failTok(payload_token, "discard of capture; omit it instead", .{}); - } else break :blk payload_sub_scope; + } else break :scope payload_sub_scope; const tag_slice = tree.tokenSlice(tag_token); if (mem.eql(u8, tag_slice, "_")) { return astgen.failTok(tag_token, "discard of tag capture; omit it instead", .{}); - } else if (case.inline_token == null) { - return astgen.failTok(tag_token, "tag capture on non-inline prong", .{}); } const tag_name = try astgen.identAsString(tag_token); try astgen.detectLocalShadowing(payload_sub_scope, tag_name, tag_token, tag_slice, .@"switch tag capture"); @@ -7904,123 +7858,155 @@ fn switchExpr( assert(any_has_tag_capture); has_tag_capture = true; - tag_scope = .{ + if (is_err_switch) { + return astgen.failTok(tag_token, "cannot capture tag of error union", .{}); + } + + tag_capture_scope = .{ .parent = payload_sub_scope, - .gen_zir = &case_scope, + .gen_zir = &scratch_scope, .name = tag_name, - .inst = tag_inst.toRef(), + .inst = tag_capture_inst.toRef(), .token_src = tag_token, .id_cat = .@"switch tag capture", }; - dbg_var_tag_name = tag_name; - dbg_var_tag_inst = tag_inst.toRef(); - break :blk &tag_scope.base; + dbg_var_tag_name = tag_capture_scope.name; + dbg_var_tag_inst = tag_capture_scope.inst; + break :scope &tag_capture_scope.base; }; - const header_index: u32 = @intCast(payloads.items.len); - const body_len_index = if (is_multi_case) blk: { - if (case_node.toOptional() == underscore_case_node) { - payloads.items[under_case_index] = header_index; - if (special_prongs.hasOneAdditionalItem()) { - try payloads.resize(gpa, header_index + 2); // item, body_len - const maybe_item_node = case.ast.values[0]; - const item_node = if (maybe_item_node.toOptional() == underscore_node) - case.ast.values[1] - else - maybe_item_node; - const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node, .switch_item); - payloads.items[header_index] = @intFromEnum(item_inst); - break :blk header_index + 1; - } - } else { - payloads.items[multi_case_table + multi_case_index] = header_index; - multi_case_index += 1; - } - try payloads.resize(gpa, header_index + 3); // items_len, ranges_len, body_len + if (capture != .none) assert(any_has_payload_capture); + if (is_err_switch) { + assert(!any_payload_is_ref); // should have failed by now + assert(!any_has_tag_capture); // should have failed by now + } - // items - var items_len: u32 = 0; - for (case.ast.values) |item_node| { - if (item_node.toOptional() == underscore_node or - tree.nodeTag(item_node) == .switch_range) - { - continue; - } - items_len += 1; + prong_body: { + scratch_scope.instructions_top = parent_gz.instructions.items.len; + defer scratch_scope.unstack(); - const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node, .switch_item); - try payloads.append(gpa, @intFromEnum(item_inst)); - } - - // ranges - var ranges_len: u32 = 0; - for (case.ast.values) |range| { - if (tree.nodeTag(range) != .switch_range) { - continue; - } - ranges_len += 1; - - const first_node, const last_node = tree.nodeData(range).node_and_node; - const first = try comptimeExpr(parent_gz, scope, item_ri, first_node, .switch_item); - const last = try comptimeExpr(parent_gz, scope, item_ri, last_node, .switch_item); - try payloads.appendSlice(gpa, &[_]u32{ - @intFromEnum(first), @intFromEnum(last), - }); - } - - payloads.items[header_index] = items_len; - payloads.items[header_index + 1] = ranges_len; - break :blk header_index + 2; - } else if (case_node.toOptional() == else_case_node) blk: { - payloads.items[else_case_index] = header_index; - try payloads.resize(gpa, header_index + 1); // body_len - break :blk header_index; - } else if (case_node.toOptional() == underscore_case_node) blk: { - assert(!special_prongs.hasAdditionalItems()); - payloads.items[under_case_index] = header_index; - try payloads.resize(gpa, header_index + 1); // body_len - break :blk header_index; - } else blk: { - payloads.items[scalar_case_table + scalar_case_index] = header_index; - scalar_case_index += 1; - try payloads.resize(gpa, header_index + 2); // item, body_len - const item_node = case.ast.values[0]; - const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node, .switch_item); - payloads.items[header_index] = @intFromEnum(item_inst); - break :blk header_index + 1; - }; - - { - // temporarily stack case_scope on parent_gz - case_scope.instructions_top = parent_gz.instructions.items.len; - defer case_scope.unstack(); - - if (dbg_var_name != .empty) { - try case_scope.addDbgVar(.dbg_var_val, dbg_var_name, dbg_var_inst); + if (dbg_var_payload_name != .empty) { + try scratch_scope.addDbgVar(.dbg_var_val, dbg_var_payload_name, dbg_var_payload_inst); } if (dbg_var_tag_name != .empty) { - try case_scope.addDbgVar(.dbg_var_val, dbg_var_tag_name, dbg_var_tag_inst); + try scratch_scope.addDbgVar(.dbg_var_val, dbg_var_tag_name, dbg_var_tag_inst); + } + if (do_err_trace and nodeMayAppendToErrorTrace(tree, operand_node)) { + _ = try scratch_scope.addSaveErrRetIndex(.always); } const target_expr_node = case.ast.target_expr; - const case_result = try fullBodyExpr(&case_scope, sub_scope, block_scope.break_result_info, target_expr_node, .allow_branch_hint); - try checkUsed(parent_gz, &case_scope.base, sub_scope); - if (!parent_gz.refIsNoReturn(case_result)) { - _ = try case_scope.addBreakWithSrcNode(.@"break", switch_block, case_result, target_expr_node); + const case_result = try fullBodyExpr(&scratch_scope, prong_body_scope, block_scope.break_result_info, target_expr_node, .allow_branch_hint); + if (needs_non_err_handling) { + // If we would check `scratch_scope` here, we would get a false + // positive, that being the switch operand itself! + try checkUsed(parent_gz, &err_capture_scope.base, prong_body_scope); + } else { + try checkUsed(parent_gz, &scratch_scope.base, prong_body_scope); + } + if (!scratch_scope.endsWithNoReturn()) { + // As our last action before the break, "pop" the error trace if needed + if (do_err_trace) { + try restoreErrRetIndex( + &scratch_scope, + .{ .block = switch_block }, + block_scope.break_result_info, + target_expr_node, + case_result, + ); + } + _ = try scratch_scope.addBreakWithSrcNode(.@"break", switch_block, case_result, target_expr_node); } - const case_slice = case_scope.instructionsSlice(); - const extra_insts: []const Zir.Inst.Index = if (has_tag_capture) &.{ switch_block, tag_inst } else &.{switch_block}; - const body_len = astgen.countBodyLenAfterFixupsExtraRefs(case_slice, extra_insts); + const body_slice = scratch_scope.instructionsSlice(); + const body_start: u32 = @intCast(payloads.items.len); + const body_len = astgen.countBodyLenAfterFixupsExtraRefs(body_slice, prong_body_extra_insts); try payloads.ensureUnusedCapacity(gpa, body_len); - payloads.items[body_len_index] = @bitCast(Zir.Inst.SwitchBlock.ProngInfo{ + astgen.appendBodyWithFixupsExtraRefsArrayList(payloads, body_slice, prong_body_extra_insts); + + if (case_node.toOptional() == else_case_node) { + assert(case.ast.values.len == 0); + + // Specific `else` bodies can cause Sema to omit the + // "unreachable else prong" error so that certain generic code + // patterns don't trigger it. We do that for these bodies: + // `else => unreachable,` + // `else => return,` + // `else => |e| return e,` (where `e` is any identifier) + const is_simple_noreturn = switch (tree.nodeTag(target_expr_node)) { + .unreachable_literal => true, // `=> unreachable,` + .@"return" => simple_noreturn: { + const retval_node = tree.nodeData(target_expr_node).opt_node.unwrap() orelse { + break :simple_noreturn true; // `=> return,` + }; + // Check for `=> |e| return e,` + if (capture != .by_val) break :simple_noreturn false; + if (tree.nodeTag(retval_node) != .identifier) break :simple_noreturn false; + const payload_name = try astgen.identAsString(case.payload_token.?); + const retval_name = try astgen.identAsString(tree.nodeMainToken(retval_node)); + break :simple_noreturn payload_name == retval_name; + }, + else => false, + }; + + else_info = .{ + .body_len = @intCast(body_len), + .capture = capture, + .is_inline = case.inline_token != null, + .has_tag_capture = has_tag_capture, + .is_simple_noreturn = is_simple_noreturn, + }; + else_prong_body_start = body_start; + break :prong_body; + } + + if (case_has_under) { + // We're either writing under_prong_info or under_index here. + if (under_is_bare) { + assert(case.ast.values.len == 1); // only `_` + const bare_under_info: Zir.Inst.SwitchBlock.ProngInfo.BareUnder = .{ + .body_len = @intCast(body_len), + .capture = capture, + .has_tag_capture = has_tag_capture, + }; + under_extra = @bitCast(bare_under_info); + bare_under_prong_body_start = body_start; + break :prong_body; + } else if (is_multi_case) { + under_extra = scalar_cases_len + multi_case_index; + } else { + under_extra = scalar_case_index; + } + } + + // We allow prongs with error items which are not inside the error set + // being switched on if their body is `=> comptime unreachable,`. + const is_comptime_unreach = comptime_unreach: { + if (tree.nodeTag(target_expr_node) != .@"comptime") break :comptime_unreach false; + const comptime_node = tree.nodeData(target_expr_node).node; + break :comptime_unreach tree.nodeTag(comptime_node) == .unreachable_literal; + }; + + const prong_info: Zir.Inst.SwitchBlock.ProngInfo = .{ .body_len = @intCast(body_len), .capture = capture, .is_inline = case.inline_token != null, .has_tag_capture = has_tag_capture, - }); - appendBodyWithFixupsExtraRefsArrayList(astgen, payloads, case_slice, extra_insts); + .is_comptime_unreach = is_comptime_unreach, + }; + + if (is_multi_case) { + payloads.items[multi_prong_body_table + multi_case_index] = body_start; + payloads.items[multi_prong_infos_start + multi_case_index] = @bitCast(prong_info); + multi_case_index += 1; + } else { + // prong body start is implicit, it's right behind our only item. + payloads.items[scalar_prong_infos_start + scalar_case_index] = @bitCast(prong_info); + scalar_case_index += 1; + } } } + assert(scalar_case_index + multi_case_index + @intFromBool(has_else) + @intFromBool(under_is_bare) == case_nodes.len); + assert(multi_items_infos_start + multi_item_offset == bodies_start); if (switch_full.label_token) |label_token| if (!block_scope.label.?.used) { try astgen.appendErrorTok(label_token, "unused switch label", .{}); @@ -8029,84 +8015,108 @@ fn switchExpr( // Now that the item expressions are generated we can add this. try parent_gz.instructions.append(gpa, switch_block); - try astgen.extra.ensureUnusedCapacity(gpa, @typeInfo(Zir.Inst.SwitchBlock).@"struct".fields.len + - @intFromBool(multi_cases_len != 0) + - @intFromBool(any_has_tag_capture) + - payloads.items.len - scratch_top); + // We've collected all of the data we need! Now we just have to finalize it + // by copying our bodies from `payloads` to `extra`, this time in the order + // expected by ZIR consumers. - const payload_index = astgen.addExtraAssumeCapacity(Zir.Inst.SwitchBlock{ - .operand = raw_operand, - .bits = Zir.Inst.SwitchBlock.Bits{ - .has_multi_cases = multi_cases_len != 0, - .special_prongs = special_prongs, - .any_has_tag_capture = any_has_tag_capture, - .any_non_inline_capture = any_non_inline_capture, + try astgen.extra.ensureUnusedCapacity(gpa, @typeInfo(Zir.Inst.SwitchBlock).@"struct".fields.len + + @intFromBool(multi_cases_len > 0) + // multi_cases_len + @intFromBool(payload_capture_inst_is_placeholder) + // payload_capture_placeholder + @intFromBool(tag_capture_inst_is_placeholder) + // tag_capture_placeholder + @intFromBool(needs_non_err_handling) + // catch_or_if_src_node_offset + @intFromBool(needs_non_err_handling) + // non_err_info + @intFromBool(has_else) + // else_info + @intFromBool(has_under) + // under_prong_info or under_index + payloads.items.len - body_table_end); // item infos and bodies + + // singular pieces of data + const zir_payload_index = astgen.addExtraAssumeCapacity(Zir.Inst.SwitchBlock{ + .raw_operand = raw_operand, + .bits = .{ + .has_multi_cases = multi_cases_len > 0, + .any_ranges = any_ranges, + .has_else = has_else, + .has_under = has_under, + .under_is_bare = under_is_bare, .has_continue = switch_full.label_token != null and block_scope.label.?.used_for_continue, + .any_maybe_runtime_capture = any_maybe_runtime_capture, + .payload_capture_inst_is_placeholder = payload_capture_inst_is_placeholder, + .tag_capture_inst_is_placeholder = tag_capture_inst_is_placeholder, .scalar_cases_len = @intCast(scalar_cases_len), }, }); + astgen.instructions.items(.data)[@intFromEnum(switch_block)].pl_node.payload_index = zir_payload_index; - if (multi_cases_len != 0) { - astgen.extra.appendAssumeCapacity(multi_cases_len); + if (multi_cases_len > 0) astgen.extra.appendAssumeCapacity(multi_cases_len); + if (payload_capture_inst_is_placeholder) astgen.extra.appendAssumeCapacity(@intFromEnum(payload_capture_inst)); + if (tag_capture_inst_is_placeholder) astgen.extra.appendAssumeCapacity(@intFromEnum(tag_capture_inst)); + if (needs_non_err_handling) { + const catch_or_if_src_node_offset = parent_gz.nodeIndexToRelative(catch_or_if_node); + astgen.extra.appendAssumeCapacity(@bitCast(@intFromEnum(catch_or_if_src_node_offset))); + astgen.extra.appendAssumeCapacity(@bitCast(non_err_info)); } + if (has_else) astgen.extra.appendAssumeCapacity(@bitCast(else_info)); + if (has_under) astgen.extra.appendAssumeCapacity(under_extra); - if (any_has_tag_capture) { - astgen.extra.appendAssumeCapacity(@intFromEnum(tag_inst)); + const extra_payloads_start = astgen.extra.items.len; + + // body lens + astgen.extra.appendSliceAssumeCapacity(payloads.items[body_table_end..bodies_start]); + + // bodies + if (needs_non_err_handling) { + const body = payloads.items[non_err_prong_body_start..][0..non_err_info.body_len]; + astgen.extra.appendSliceAssumeCapacity(body); } - - const zir_datas = astgen.instructions.items(.data); - zir_datas[@intFromEnum(switch_block)].pl_node.payload_index = payload_index; - if (has_else) { - const start_index = payloads.items[else_case_index]; - var end_index = start_index + 1; - const prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(payloads.items[start_index]); - end_index += prong_info.body_len; - astgen.extra.appendSliceAssumeCapacity(payloads.items[start_index..end_index]); + const body = payloads.items[else_prong_body_start..][0..else_info.body_len]; + astgen.extra.appendSliceAssumeCapacity(body); } - if (has_under) { - const start_index = payloads.items[under_case_index]; - var body_len_index = start_index; - var end_index = start_index; - switch (underscore_additional_items) { - .none => { - end_index += 1; - }, - .one => { - body_len_index += 1; - end_index += 2; - }, - .many => { - body_len_index += 2; - const items_len = payloads.items[start_index]; - const ranges_len = payloads.items[start_index + 1]; - end_index += 3 + items_len + 2 * ranges_len; - }, + if (under_is_bare) { + const under_prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(under_extra); + const body = payloads.items[bare_under_prong_body_start..][0..under_prong_info.body_len]; + astgen.extra.appendSliceAssumeCapacity(body); + } + for (0..scalar_cases_len) |scalar_i| { + const item_info: Zir.Inst.SwitchBlock.ItemInfo = @bitCast(payloads.items[scalar_item_infos_start + scalar_i]); + const item_body_start = payloads.items[scalar_body_table + scalar_i]; + const item_body = payloads.items[item_body_start..][0 .. item_info.bodyLen() orelse 0]; + const prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(payloads.items[scalar_prong_infos_start + scalar_i]); + const prong_body_start = item_body_start + item_body.len; + const prong_body = payloads.items[prong_body_start..][0..prong_info.body_len]; + astgen.extra.appendSliceAssumeCapacity(prong_body); + astgen.extra.appendSliceAssumeCapacity(item_body); + } + var multi_item_i: usize = 0; + for (0..multi_cases_len) |multi_i| { + const prong_body_start = payloads.items[multi_prong_body_table + multi_i]; + const prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(payloads.items[multi_prong_infos_start + multi_i]); + const prong_body = payloads.items[prong_body_start..][0..prong_info.body_len]; + astgen.extra.appendSliceAssumeCapacity(prong_body); + + const items_len = payloads.items[multi_case_items_lens_start + multi_i]; + const ranges_len = if (any_ranges) ranges_len: { + break :ranges_len payloads.items[multi_case_ranges_lens_start + multi_i]; + } else 0; + // The table entries and body lens are already in the correct order so we + // don't have to differentiate between items and ranges here. + for (0..items_len + 2 * ranges_len) |_| { + const item_info: Zir.Inst.SwitchBlock.ItemInfo = @bitCast(payloads.items[multi_items_infos_start + multi_item_i]); + if (item_info.bodyLen()) |body_len| { + const body_start = payloads.items[multi_item_body_table + multi_item_i]; + const body = payloads.items[body_start..][0..body_len]; + astgen.extra.appendSliceAssumeCapacity(body); + } + multi_item_i += 1; } - const prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(payloads.items[body_len_index]); - end_index += prong_info.body_len; - astgen.extra.appendSliceAssumeCapacity(payloads.items[start_index..end_index]); - } - for (payloads.items[scalar_case_table..case_table_end], 0..) |start_index, i| { - var body_len_index = start_index; - var end_index = start_index; - const table_index = scalar_case_table + i; - if (table_index < multi_case_table) { - body_len_index += 1; - end_index += 2; - } else { - body_len_index += 2; - const items_len = payloads.items[start_index]; - const ranges_len = payloads.items[start_index + 1]; - end_index += 3 + items_len + 2 * ranges_len; - } - const prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(payloads.items[body_len_index]); - end_index += prong_info.body_len; - astgen.extra.appendSliceAssumeCapacity(payloads.items[start_index..end_index]); } + // Make sure we didn't forget anything... + assert(multi_item_i == total_items_len + 2 * total_ranges_len - scalar_cases_len); + assert(astgen.extra.items.len - extra_payloads_start == payloads.items.len - body_table_end); + if (need_result_rvalue) { - return rvalue(parent_gz, ri, switch_block.toRef(), node); + return rvalue(parent_gz, ri, switch_block.toRef(), switch_node); } else { return switch_block.toRef(); } @@ -13786,6 +13796,19 @@ fn scanContainer( return error.AnalysisFail; } +fn appendPlaceholder(astgen: *AstGen) Allocator.Error!Zir.Inst.Index { + const inst: Zir.Inst.Index = @enumFromInt(astgen.instructions.len); + try astgen.instructions.append(astgen.gpa, .{ + .tag = .extended, + .data = .{ .extended = .{ + .opcode = .value_placeholder, + .small = undefined, + .operand = undefined, + } }, + }); + return inst; +} + /// Assumes capacity for body has already been added. Needed capacity taking into /// account fixups can be found with `countBodyLenAfterFixups`. fn appendBodyWithFixups(astgen: *AstGen, body: []const Zir.Inst.Index) void { diff --git a/lib/std/zig/Zir.zig b/lib/std/zig/Zir.zig index 37ce7b4cfa..092886ffd1 100644 --- a/lib/std/zig/Zir.zig +++ b/lib/std/zig/Zir.zig @@ -95,7 +95,6 @@ pub fn extraData(code: Zir, comptime T: type, index: usize) ExtraData(T) { Inst.Call.Flags, Inst.BuiltinCall.Flags, Inst.SwitchBlock.Bits, - Inst.SwitchBlockErrUnion.Bits, Inst.FuncFancy.Bits, Inst.Declaration.Flags, Inst.Param.Type, @@ -350,7 +349,8 @@ pub const Inst = struct { /// Uses the `break` union field. break_inline, /// Branch from within a switch case to the case specified by the operand. - /// Uses the `break` union field. `block_inst` refers to a `switch_block` or `switch_block_ref`. + /// Uses the `break` union field. `block_inst` refers to a `switch_block`/ + /// `switch_block_ref`/`switch_block_err_union`. switch_continue, /// Checks that comptime control flow does not happen inside a runtime block. /// Uses the `un_node` union field. @@ -722,8 +722,10 @@ pub const Inst = struct { /// A switch expression. Uses the `pl_node` union field. /// AST node is the switch, payload is `SwitchBlock`. Operand is a pointer. switch_block_ref, - /// A switch on an error union `a catch |err| switch (err) {...}`. - /// Uses the `pl_node` union field. AST node is the `catch`, payload is `SwitchBlockErrUnion`. + /// A switch on an error union: + /// - `eu catch |err| switch (err) {...}`, AST node is the `catch`. + /// - `if (eu) |payload| {...} else |err| {...}`, AST node is the `if`. + /// Uses the `pl_node` union field. Payload is `SwitchBlock`. switch_block_err_union, /// Check that operand type supports the dereference operand (.*). /// Uses the `un_node` field. @@ -3293,143 +3295,168 @@ pub const Inst = struct { }; /// Trailing: - /// 0. multi_cases_len: u32 // if `has_multi_cases` - /// 1. err_capture_inst: u32 // if `any_uses_err_capture` - /// 2. non_err_body { - /// info: ProngInfo, - /// inst: Index // for every `info.body_len` - /// } - /// 3. else_body { // if `has_else` - /// info: ProngInfo, - /// inst: Index // for every `info.body_len` - /// } - /// 4. scalar_cases: { // for every `scalar_cases_len` - /// item: Ref, - /// info: ProngInfo, - /// inst: Index // for every `info.body_len` - /// } - /// 5. multi_cases: { // for every `multi_cases_len` - /// items_len: u32, - /// ranges_len: u32, - /// info: ProngInfo, - /// item: Ref // for every `items_len` - /// ranges: { // for every `ranges_len` - /// item_first: Ref, - /// item_last: Ref, + /// 0. multi_cases_len: u32, // If has_multi_cases is set. + /// 1. payload_capture_placeholder: Inst.Index, // If payload_capture_inst_is_placeholder is set. + /// // Index of instruction prongs use to refer to their payload capture. + /// 2. tag_capture_placeholder: Inst.Index, // If tag_capture_inst_is_placeholder is set. + /// // Index of instruction prongs use to refer to their tag capture. + /// 3. catch_or_if_src_node_offset: Ast.Node.Offset, // If inst is switch_block_err_union. + /// 4. non_err_info: ProngInfo.NonErr, // If inst is switch_block_err_union. + /// 5. else_info: ProngInfo.Else, // If has_else is set. + /// 6. under_info: ProngInfo.Under, // If has_under is set and + /// // under_is_bare is set. + /// 7. under_index: u32, // If has_under is set and + /// // under_is_bare is *not* set. + /// // Index into switch cases. + /// 8. scalar_prong_info: ProngInfo, // for every scalar_cases_len + /// 9. multi_prong_info: ProngInfo, // for every multi_cases_len + /// 10. multi_case_items_len: u32, // for every multi_cases_len + /// 11. multi_case_ranges_len: u32, // If has_ranges is set: for every multi_cases_len + /// 12. scalar_item_info: ItemInfo, // for every scalar_cases_len + /// 13. multi_items_info: { // for every multi_cases_len + /// item_info: ItemInfo, // for each multi_case_items_len + /// range_items_info: { // for each multi_case_ranges_len + /// first_info: ItemInfo, + /// last_info: ItemInfo, /// } - /// inst: Index // for every `info.body_len` /// } - /// - /// When analyzing a case body, the switch instruction itself refers to the - /// captured error, or to the success value in `non_err_body`. Whether this - /// is captured by reference or by value depends on whether the `byref` bit - /// is set for the corresponding body. `err_capture_inst` refers to the error - /// capture outside of the `switch`, i.e. `err` in - /// `x catch |err| switch (err) { ... }`. - pub const SwitchBlockErrUnion = struct { - operand: Ref, + /// 14. non_err_body { + /// body_inst: Index // for every non_err_info.body_len + /// } + /// 15. else_body: { // If has_else is set. + /// body_inst: Inst.Index, // for every else_info.body_len + /// } + /// 16. under_body: { // If has_under is set and + /// // under_is_bare is set. + /// body_inst: Inst.Index, // for every under_info.body_len + /// } + /// 17. scalar_bodies: { // for every scalar_cases_len + /// prong_body: { // for each body_len in scalar_prong_info + /// body_inst: Inst.Index, // for every body_len + /// } + /// item_body: { // for each body_len in scalar_item_info + /// body_inst: Inst.Index, // for every body_len + /// } + /// } + /// 18. multi_bodies: { // for each multi_items_info + /// prong_body: { + /// body_inst: Inst.Index, // for each multi_prong_info.body_len + /// } + /// item_body: { // for each item_info + /// body_inst: Inst.Index, // for every item_info.body_len + /// } + /// range_bodies: { // for each .{first_info, last_info} in range_items_info + /// first_body_inst: Inst.Index, // for every first_info.body_len + /// last_body_inst: Inst.Index, // for every last_info.body_len + /// } + /// } + pub const SwitchBlock = struct { + /// Either `catch`/`if` or `switch` operand. + raw_operand: Ref, bits: Bits, - main_src_node_offset: Ast.Node.Offset, pub const Bits = packed struct(u32) { /// If true, one or more prongs have multiple items. has_multi_cases: bool, - /// If true, there is an else prong. This is mutually exclusive with `has_under`. + /// If true, one or more prongs have ranges. + /// Only valid if `has_multi_cases` is also set. + any_ranges: bool, has_else: bool, - any_uses_err_capture: bool, - payload_is_ref: bool, + has_under: bool, + /// Only valid if `has_under` is also set. + under_is_bare: bool, + /// If true, at least one prong contains a `continue`. + /// Only valid if `has_label` is set. + has_continue: bool, + // If true, at least one prong has a non-inline payload/tag capture. + any_maybe_runtime_capture: bool, + payload_capture_inst_is_placeholder: bool, + tag_capture_inst_is_placeholder: bool, scalar_cases_len: ScalarCasesLen, - pub const ScalarCasesLen = u28; + // NOTE maybe don't steal any more bits from poor `scalar_cases_len` + // and split `Bits` into two parts instead, `raw_operand` surely + // wouldn't mind donating a couple of bits for that purpose... + pub const ScalarCasesLen = u23; }; - pub const MultiProng = struct { - items: []const Ref, - body: []const Index, - }; - }; - - /// 0. multi_cases_len: u32 // If has_multi_cases is set. - /// 1. tag_capture_inst: u32 // If any_has_tag_capture is set. Index of instruction prongs use to refer to the inline tag capture. - /// 2. else_body { // If special_prong.hasElse() is set. - /// info: ProngInfo, - /// body member Index for every info.body_len - /// } - /// 3. under_body { // If special_prong.hasUnder() is set. - /// item: Ref, // If special_prong.hasOneAdditionalItem() is set. - /// items_len: u32, // If special_prong.hasManyAdditionalItems() is set. - /// ranges_len: u32, // If special_prong.hasManyAdditionalItems() is set. - /// info: ProngInfo, - /// item: Ref, // for every items_len - /// ranges: { // for every ranges_len - /// item_first: Ref, - /// item_last: Ref, - /// } - /// body member Index for every info.body_len - /// } - /// 4. scalar_cases: { // for every scalar_cases_len - /// item: Ref, - /// info: ProngInfo, - /// body member Index for every info.body_len - /// } - /// 5. multi_cases: { // for every multi_cases_len - /// items_len: u32, - /// ranges_len: u32, - /// info: ProngInfo, - /// item: Ref, // for every items_len - /// ranges: { // for every ranges_len - /// item_first: Ref, - /// item_last: Ref, - /// } - /// body member Index for every info.body_len - /// } - /// - /// When analyzing a case body, the switch instruction itself refers to the - /// captured payload. Whether this is captured by reference or by value - /// depends on whether the `byref` bit is set for the corresponding body. - pub const SwitchBlock = struct { - /// The operand passed to the `switch` expression. If this is a - /// `switch_block`, this is the operand value; if `switch_block_ref` it - /// is a pointer to the operand. `switch_block_ref` is always used if - /// any prong has a byref capture. - operand: Ref, - bits: Bits, - - /// These are stored in trailing data in `extra` for each prong. pub const ProngInfo = packed struct(u32) { - body_len: u28, + body_len: u27, capture: ProngInfo.Capture, is_inline: bool, has_tag_capture: bool, + is_comptime_unreach: bool, pub const Capture = enum(u2) { none, by_val, by_ref, }; + + pub const NonErr = packed struct(u32) { + body_len: u29, + capture: ProngInfo.Capture, + operand_is_ref: bool, + }; + + pub const Else = packed struct(u32) { + body_len: u27, + capture: ProngInfo.Capture, + is_inline: bool, + has_tag_capture: bool, + is_simple_noreturn: bool, + }; + + pub const BareUnder = packed struct(u32) { + body_len: u29, + capture: ProngInfo.Capture, + has_tag_capture: bool, + }; }; - pub const Bits = packed struct(u32) { - /// If true, one or more prongs have multiple items. - has_multi_cases: bool, - /// Information about the special prong. - special_prongs: SpecialProngs, - /// If true, at least one prong has an inline tag capture. - any_has_tag_capture: bool, - /// If true, at least one prong has a capture which may not - /// be comptime-known via `inline`. - any_non_inline_capture: bool, - /// If true, at least one prong contains a `continue`. - has_continue: bool, - scalar_cases_len: ScalarCasesLen, + pub const ItemInfo = packed struct(u32) { + kind: ItemInfo.Kind, + data: u30, - pub const ScalarCasesLen = u25; + pub const Kind = enum(u2) { + enum_literal, + error_value, + number_literal, + body_len, + }; + + pub const Unwrapped = union(ItemInfo.Kind) { + enum_literal: Zir.NullTerminatedString, + error_value: Zir.NullTerminatedString, + number_literal: Inst.Ref, + body_len: u32, + }; + + pub fn wrap(unwrapped: ItemInfo.Unwrapped) ItemInfo { + const data_uncasted: u32 = switch (unwrapped) { + .enum_literal => |str_index| @intFromEnum(str_index), + .error_value => |str_index| @intFromEnum(str_index), + .number_literal => |zir_ref| @intFromEnum(zir_ref), + .body_len => |body_len| body_len, + }; + return .{ .kind = unwrapped, .data = @intCast(data_uncasted) }; + } + + pub fn unwrap(item_info: ItemInfo) ItemInfo.Unwrapped { + return switch (item_info.kind) { + .enum_literal => .{ .enum_literal = @enumFromInt(item_info.data) }, + .error_value => .{ .error_value = @enumFromInt(item_info.data) }, + .number_literal => .{ .number_literal = @enumFromInt(item_info.data) }, + .body_len => .{ .body_len = item_info.data }, + }; + } + + pub fn bodyLen(item_info: ItemInfo) ?u32 { + return if (item_info.kind == .body_len) item_info.data else null; + } }; - pub const MultiProng = struct { - items: []const Ref, - body: []const Index, - }; + pub const Kind = enum { default, ref, err_union }; }; pub const ArrayInitRefTy = struct { @@ -4004,69 +4031,6 @@ pub const Inst = struct { }; }; -pub const SpecialProngs = enum(u3) { - none = 0b000, - /// Simple `else` prong. - /// `else => {},` - @"else" = 0b001, - /// Simple `_` prong. - /// `_ => {},` - under = 0b010, - /// Both an `else` and a `_` prong. - /// `else => {},` - /// `_ => {},` - under_and_else = 0b011, - /// `_` prong with 1 additional item. - /// `a, _ => {},` - under_one_item = 0b100, - /// Both an `else` and a `_` prong with 1 additional item. - /// `else => {},` - /// `a, _ => {},` - under_one_item_and_else = 0b101, - /// `_` prong with >1 additional items. - /// `a, _, b => {},` - under_many_items = 0b110, - /// Both an `else` and a `_` prong with >1 additional items. - /// `else => {},` - /// `a, _, b => {},` - under_many_items_and_else = 0b111, - - pub const AdditionalItems = enum(u3) { - none = @intFromEnum(SpecialProngs.under), - one = @intFromEnum(SpecialProngs.under_one_item), - many = @intFromEnum(SpecialProngs.under_many_items), - }; - - pub fn init(has_else: bool, has_under: bool, additional_items: AdditionalItems) SpecialProngs { - const else_bit: u3 = @intFromBool(has_else); - const under_bits: u3 = if (has_under) - @intFromEnum(additional_items) - else - @intFromEnum(SpecialProngs.none); - return @enumFromInt(else_bit | under_bits); - } - - pub fn hasElse(special_prongs: SpecialProngs) bool { - return (@intFromEnum(special_prongs) & 0b001) != 0; - } - - pub fn hasUnder(special_prongs: SpecialProngs) bool { - return (@intFromEnum(special_prongs) & 0b110) != 0; - } - - pub fn hasAdditionalItems(special_prongs: SpecialProngs) bool { - return (@intFromEnum(special_prongs) & 0b100) != 0; - } - - pub fn hasOneAdditionalItem(special_prongs: SpecialProngs) bool { - return (@intFromEnum(special_prongs) & 0b110) == @intFromEnum(SpecialProngs.under_one_item); - } - - pub fn hasManyAdditionalItems(special_prongs: SpecialProngs) bool { - return (@intFromEnum(special_prongs) & 0b110) == @intFromEnum(SpecialProngs.under_many_items); - } -}; - pub const DeclIterator = struct { extra_index: u32, decls_remaining: u32, @@ -4842,8 +4806,48 @@ fn findTrackableInner( const body = zir.bodySlice(extra.end, extra.data.body_len); try zir.findTrackableBody(gpa, contents, defers, body); }, - .switch_block, .switch_block_ref => return zir.findTrackableSwitch(gpa, contents, defers, inst, .normal), - .switch_block_err_union => return zir.findTrackableSwitch(gpa, contents, defers, inst, .err_union), + + .switch_block, + .switch_block_ref, + .switch_block_err_union, + => { + const zir_switch = zir.getSwitchBlock(inst); + if (zir_switch.non_err_case) |non_err_case| { + try zir.findTrackableBody(gpa, contents, defers, non_err_case.body); + } + if (zir_switch.else_case) |else_case| { + try zir.findTrackableBody(gpa, contents, defers, else_case.body); + } + if (zir_switch.under_case.resolve()) |under_case| { + try zir.findTrackableBody(gpa, contents, defers, under_case.body); + } + var extra_index = zir_switch.end; + var case_it = zir_switch.iterateCases(); + while (case_it.next()) |case| { + const prong_body = zir.bodySlice(extra_index, case.prong_info.body_len); + extra_index += prong_body.len; + try zir.findTrackableBody(gpa, contents, defers, prong_body); + for (case.item_infos) |item_info| { + if (item_info.bodyLen()) |body_len| { + const item_body = zir.bodySlice(extra_index, body_len); + extra_index += item_body.len; + try zir.findTrackableBody(gpa, contents, defers, item_body); + } + } + for (case.range_infos) |range_info| { + if (range_info[0].bodyLen()) |body_len| { + const first_body = zir.bodySlice(extra_index, body_len); + extra_index += first_body.len; + try zir.findTrackableBody(gpa, contents, defers, first_body); + } + if (range_info[1].bodyLen()) |body_len| { + const last_body = zir.bodySlice(extra_index, body_len); + extra_index += last_body.len; + try zir.findTrackableBody(gpa, contents, defers, last_body); + } + } + } + }, .suspend_block => @panic("TODO iterate suspend block"), @@ -4890,119 +4894,6 @@ fn findTrackableInner( } } -fn findTrackableSwitch( - zir: Zir, - gpa: Allocator, - contents: *DeclContents, - defers: *std.AutoHashMapUnmanaged(u32, void), - inst: Inst.Index, - /// Distinguishes between `switch_block[_ref]` and `switch_block_err_union`. - comptime kind: enum { normal, err_union }, -) Allocator.Error!void { - const inst_data = zir.instructions.items(.data)[@intFromEnum(inst)].pl_node; - const extra = zir.extraData(switch (kind) { - .normal => Inst.SwitchBlock, - .err_union => Inst.SwitchBlockErrUnion, - }, inst_data.payload_index); - - var extra_index: usize = extra.end; - - const multi_cases_len = if (extra.data.bits.has_multi_cases) blk: { - const multi_cases_len = zir.extra[extra_index]; - extra_index += 1; - break :blk multi_cases_len; - } else 0; - - if (switch (kind) { - .normal => extra.data.bits.any_has_tag_capture, - .err_union => extra.data.bits.any_uses_err_capture, - }) { - extra_index += 1; - } - - const has_special = switch (kind) { - .normal => extra.data.bits.special_prongs != .none, - .err_union => has_special: { - // Handle `non_err_body` first. - const prong_info: Inst.SwitchBlock.ProngInfo = @bitCast(zir.extra[extra_index]); - extra_index += 1; - const body = zir.bodySlice(extra_index, prong_info.body_len); - extra_index += body.len; - - try zir.findTrackableBody(gpa, contents, defers, body); - - break :has_special extra.data.bits.has_else; - }, - }; - - if (has_special) { - const has_else = if (kind == .normal) - extra.data.bits.special_prongs.hasElse() - else - true; - if (has_else) { - const prong_info: Inst.SwitchBlock.ProngInfo = @bitCast(zir.extra[extra_index]); - extra_index += 1; - const body = zir.bodySlice(extra_index, prong_info.body_len); - extra_index += body.len; - - try zir.findTrackableBody(gpa, contents, defers, body); - } - if (kind == .normal) { - const special_prongs = extra.data.bits.special_prongs; - - if (special_prongs.hasUnder()) { - var trailing_items_len: u32 = 0; - if (special_prongs.hasOneAdditionalItem()) { - extra_index += 1; - } else if (special_prongs.hasManyAdditionalItems()) { - const items_len = zir.extra[extra_index]; - extra_index += 1; - const ranges_len = zir.extra[extra_index]; - extra_index += 1; - trailing_items_len = items_len + ranges_len * 2; - } - const prong_info: Inst.SwitchBlock.ProngInfo = @bitCast(zir.extra[extra_index]); - extra_index += 1 + trailing_items_len; - const body = zir.bodySlice(extra_index, prong_info.body_len); - extra_index += body.len; - - try zir.findTrackableBody(gpa, contents, defers, body); - } - } - } - - { - const scalar_cases_len = extra.data.bits.scalar_cases_len; - for (0..scalar_cases_len) |_| { - extra_index += 1; - const prong_info: Inst.SwitchBlock.ProngInfo = @bitCast(zir.extra[extra_index]); - extra_index += 1; - const body = zir.bodySlice(extra_index, prong_info.body_len); - extra_index += body.len; - - try zir.findTrackableBody(gpa, contents, defers, body); - } - } - { - for (0..multi_cases_len) |_| { - const items_len = zir.extra[extra_index]; - extra_index += 1; - const ranges_len = zir.extra[extra_index]; - extra_index += 1; - const prong_info: Inst.SwitchBlock.ProngInfo = @bitCast(zir.extra[extra_index]); - extra_index += 1; - - extra_index += items_len + ranges_len * 2; - - const body = zir.bodySlice(extra_index, prong_info.body_len); - extra_index += body.len; - - try zir.findTrackableBody(gpa, contents, defers, body); - } - } -} - fn findTrackableBody( zir: Zir, gpa: Allocator, @@ -5337,6 +5228,306 @@ pub fn getAssociatedSrcHash(zir: Zir, inst: Zir.Inst.Index) ?std.zig.SrcHash { } } +pub fn getSwitchBlock(zir: *const Zir, switch_inst: Inst.Index) UnwrappedSwitchBlock { + const has_non_err = switch (zir.instructions.items(.tag)[@intFromEnum(switch_inst)]) { + .switch_block, .switch_block_ref => false, + .switch_block_err_union => true, + else => unreachable, + }; + const inst_data = zir.instructions.items(.data)[@intFromEnum(switch_inst)].pl_node; + const extra = zir.extraData(Inst.SwitchBlock, inst_data.payload_index); + const bits = extra.data.bits; + var extra_index = extra.end; + const multi_cases_len = if (bits.has_multi_cases) len: { + const multi_cases_len = zir.extra[extra_index]; + extra_index += 1; + break :len multi_cases_len; + } else 0; + const payload_capture_placeholder: Inst.OptionalIndex = if (bits.payload_capture_inst_is_placeholder) inst: { + const inst: Inst.Index = @enumFromInt(zir.extra[extra_index]); + extra_index += 1; + break :inst inst.toOptional(); + } else .none; + const tag_capture_placeholder: Inst.OptionalIndex = if (bits.tag_capture_inst_is_placeholder) inst: { + const inst: Inst.Index = @enumFromInt(zir.extra[extra_index]); + extra_index += 1; + break :inst inst.toOptional(); + } else .none; + const catch_or_if_src_node_offset: Ast.Node.OptionalOffset = if (has_non_err) node_offset: { + const node_offset: Ast.Node.Offset = @enumFromInt(@as(i32, @bitCast(zir.extra[extra_index]))); + extra_index += 1; + break :node_offset node_offset.toOptional(); + } else .none; + const non_err_info: Inst.SwitchBlock.ProngInfo.NonErr = if (has_non_err) non_err_info: { + const non_err_info: Inst.SwitchBlock.ProngInfo.NonErr = @bitCast(zir.extra[extra_index]); + extra_index += 1; + break :non_err_info non_err_info; + } else undefined; + const else_info: Inst.SwitchBlock.ProngInfo.Else = if (bits.has_else) else_info: { + const else_info: Inst.SwitchBlock.ProngInfo.Else = @bitCast(zir.extra[extra_index]); + extra_index += 1; + break :else_info else_info; + } else undefined; + const bare_under_info: Inst.SwitchBlock.ProngInfo.BareUnder = if (bits.has_under and bits.under_is_bare) bare_under_info: { + const bare_under_info: Inst.SwitchBlock.ProngInfo.BareUnder = @bitCast(zir.extra[extra_index]); + extra_index += 1; + break :bare_under_info bare_under_info; + } else undefined; + const under_index: u32 = if (bits.has_under and !bits.under_is_bare) under_index: { + const under_index = zir.extra[extra_index]; + extra_index += 1; + break :under_index under_index; + } else undefined; + const scalar_cases_len: u32 = bits.scalar_cases_len; + const prong_infos: []const Inst.SwitchBlock.ProngInfo = + @ptrCast(zir.extra[extra_index..][0 .. scalar_cases_len + multi_cases_len]); + extra_index += prong_infos.len; + const multi_case_items_lens = zir.extra[extra_index..][0..multi_cases_len]; + extra_index += multi_case_items_lens.len; + const multi_case_ranges_lens: ?[]const u32 = if (bits.any_ranges) lens: { + const multi_case_ranges_lens = zir.extra[extra_index..][0..multi_cases_len]; + extra_index += multi_case_ranges_lens.len; + break :lens multi_case_ranges_lens; + } else null; + var total_items_len: usize = scalar_cases_len; + for (multi_case_items_lens) |items_len| { + total_items_len += items_len; + } + if (multi_case_ranges_lens) |ranges_lens| for (ranges_lens) |ranges_len| { + total_items_len += 2 * ranges_len; + }; + const item_infos: []const Inst.SwitchBlock.ItemInfo = + @ptrCast(zir.extra[extra_index..][0..total_items_len]); + extra_index += item_infos.len; + const non_err_case: ?UnwrappedSwitchBlock.Case.NonErr = if (has_non_err) non_err_case: { + const body = zir.bodySlice(extra_index, non_err_info.body_len); + extra_index += body.len; + break :non_err_case .{ + .body = body, + .capture = non_err_info.capture, + .operand_is_ref = non_err_info.operand_is_ref, + }; + } else null; + const else_case: ?UnwrappedSwitchBlock.Case.Else = if (bits.has_else) else_case: { + const body = zir.bodySlice(extra_index, else_info.body_len); + extra_index += body.len; + break :else_case .{ + .index = .@"else", + .body = body, + .capture = else_info.capture, + .is_inline = else_info.is_inline, + .has_tag_capture = else_info.has_tag_capture, + .is_simple_noreturn = else_info.is_simple_noreturn, + }; + } else null; + const under_case: UnwrappedSwitchBlock.Case.Under = if (bits.has_under) under_case: { + if (bits.under_is_bare) { + const body = zir.bodySlice(extra_index, bare_under_info.body_len); + extra_index += body.len; + break :under_case .{ .bare = .{ + .index = .bare_under, + .body = body, + .capture = bare_under_info.capture, + .has_tag_capture = bare_under_info.has_tag_capture, + } }; + } else { + break :under_case .{ .index = under_index }; + } + } else .none; + return .{ + .main_operand = extra.data.raw_operand, + .switch_src_node_offset = inst_data.src_node, + .catch_or_if_src_node_offset = catch_or_if_src_node_offset, + .payload_capture_placeholder = payload_capture_placeholder, + .tag_capture_placeholder = tag_capture_placeholder, + .has_continue = bits.has_continue, + .any_maybe_runtime_capture = bits.any_maybe_runtime_capture, + .non_err_case = non_err_case, + .else_case = else_case, + .under_case = under_case, + .prong_infos = prong_infos, + .multi_case_items_lens = multi_case_items_lens, + .multi_case_ranges_lens = multi_case_ranges_lens, + .item_infos = item_infos, + .end = extra_index, + }; +} + +/// Trailing (starting at `end`): +/// 0. case_bodies: { // for each case in Case.Iterator.next() +/// prong_body: { +/// body_inst: Inst.Index, // for every case.prong_info.body_len, +/// } +/// item_body: { // for each body_len in case.item_infos +/// body_inst: Inst.Index, // for every body_len +/// } +/// range_bodies: { // for each .{first_info, last_info} in case.range_infos +/// first_body_inst: Inst.Index, // for every first_info.body_len +/// last_body_inst: Inst.Index, // for every last_info.body_len +/// } +/// } +pub const UnwrappedSwitchBlock = struct { + /// Either `catch`/`if` or `switch` operand. + main_operand: Inst.Ref, + switch_src_node_offset: Ast.Node.Offset, + catch_or_if_src_node_offset: Ast.Node.OptionalOffset, + payload_capture_placeholder: Inst.OptionalIndex, + tag_capture_placeholder: Inst.OptionalIndex, + has_continue: bool, + any_maybe_runtime_capture: bool, + non_err_case: ?Case.NonErr, + else_case: ?Case.Else, + under_case: Case.Under, + // Refer to doc comment and `iterateCases` to access everything below correctly. + prong_infos: []const Inst.SwitchBlock.ProngInfo, + multi_case_items_lens: []const u32, + multi_case_ranges_lens: ?[]const u32, + item_infos: []const Inst.SwitchBlock.ItemInfo, + end: usize, + + pub fn anyRanges(unwrapped: *const UnwrappedSwitchBlock) bool { + return unwrapped.multi_case_ranges_lens != null; + } + + pub fn scalarCasesLen(unwrapped: *const UnwrappedSwitchBlock) u32 { + return @intCast(unwrapped.prong_infos.len - unwrapped.multi_case_items_lens.len); + } + + pub fn multiCasesLen(unwrapped: *const UnwrappedSwitchBlock) u32 { + return @intCast(unwrapped.multi_case_items_lens.len); + } + + pub fn totalItemsLen(unwrapped: *const UnwrappedSwitchBlock) u32 { + var total_items_len: u32 = @intCast(unwrapped.item_infos.len); + if (unwrapped.multi_case_ranges_lens) |ranges_lens| { + for (ranges_lens) |len| total_items_len -= len; + } + return total_items_len; + } + + pub const Case = struct { + index: Case.Index, + prong_info: Inst.SwitchBlock.ProngInfo, + item_infos: []const Inst.SwitchBlock.ItemInfo, + range_infos: []const [2]Inst.SwitchBlock.ItemInfo, + + pub fn isUnder(case: *const Case) bool { + return case.index.is_under; + } + + pub const Index = packed struct(u32) { + kind: enum(u1) { scalar, multi }, + is_under: bool, + value: u30, + + pub const @"else": Case.Index = .{ + .kind = .scalar, + .is_under = false, + .value = std.math.maxInt(u30), + }; + + pub const bare_under: Case.Index = .{ + .kind = .scalar, + .is_under = true, + .value = std.math.maxInt(u30), + }; + }; + + pub const NonErr = struct { + body: []const Inst.Index, + capture: Inst.SwitchBlock.ProngInfo.Capture, + operand_is_ref: bool, + }; + + pub const Else = struct { + index: Case.Index, + body: []const Inst.Index, + capture: Inst.SwitchBlock.ProngInfo.Capture, + is_inline: bool, + has_tag_capture: bool, + is_simple_noreturn: bool, + }; + + pub const Under = union(enum) { + none, + bare: Under.Resolved, + index: u32, + + pub const Resolved = struct { + index: Case.Index, + body: []const Inst.Index, + capture: Inst.SwitchBlock.ProngInfo.Capture, + has_tag_capture: bool, + }; + + /// If this returns `null` and `under` is not `.none`, you'll have to + /// find the under case by iterating all cases and using `isUnder`! + pub fn resolve(under: Under) ?Under.Resolved { + return switch (under) { + .bare => |resolved| resolved, + .none, .index => null, + }; + } + }; + + pub const Iterator = struct { + next_idx: u32, + under_idx: ?u32, + prong_infos: []const Inst.SwitchBlock.ProngInfo, + multi_case_items_lens: []const u32, + multi_case_ranges_lens: ?[]const u32, + item_infos: []const Inst.SwitchBlock.ItemInfo, + + pub fn next(it: *Iterator) ?Case { + const idx = it.next_idx; + if (idx == it.prong_infos.len) return null; + it.next_idx += 1; + const scalar_cases_len = it.prong_infos.len - it.multi_case_items_lens.len; + return if (idx < scalar_cases_len) .{ + .index = .{ + .kind = .scalar, + .is_under = idx == it.under_idx, + .value = @intCast(idx), + }, + .prong_info = it.prong_infos[idx], + .item_infos = it.itemInfos(1), + .range_infos = &.{}, + } else .{ + .index = .{ + .kind = .multi, + .is_under = idx == it.under_idx, + .value = @intCast(idx - scalar_cases_len), + }, + .prong_info = it.prong_infos[idx], + .item_infos = it.itemInfos(it.multi_case_items_lens[idx - scalar_cases_len]), + .range_infos = if (it.multi_case_ranges_lens) |ranges_lens| b: { + break :b @ptrCast(it.itemInfos(2 * ranges_lens[idx - scalar_cases_len])); + } else &.{}, + }; + } + fn itemInfos(it: *Iterator, count: u32) []const Inst.SwitchBlock.ItemInfo { + const lens = it.item_infos[0..count]; + it.item_infos = it.item_infos[count..]; + return lens; + } + }; + }; + + pub fn iterateCases(unwrapped: UnwrappedSwitchBlock) Case.Iterator { + return .{ + .next_idx = 0, + .under_idx = switch (unwrapped.under_case) { + .none, .bare => null, + .index => |index| index, + }, + .prong_infos = unwrapped.prong_infos, + .multi_case_items_lens = unwrapped.multi_case_items_lens, + .multi_case_ranges_lens = unwrapped.multi_case_ranges_lens, + .item_infos = unwrapped.item_infos, + }; + } +}; + /// When the ZIR update tracking logic must be modified to consider new instructions, /// change this constant to trigger compile errors at all relevant locations. pub const inst_tracking_version = 0; diff --git a/src/print_zir.zig b/src/print_zir.zig index ced6c8e826..0006207d6d 100644 --- a/src/print_zir.zig +++ b/src/print_zir.zig @@ -447,10 +447,9 @@ const Writer = struct { .switch_block, .switch_block_ref, + .switch_block_err_union, => try self.writeSwitchBlock(stream, inst), - .switch_block_err_union => try self.writeSwitchBlockErrUnion(stream, inst), - .field_ptr_load, .field_ptr, .decl_literal, @@ -1987,322 +1986,150 @@ const Writer = struct { try self.writeSrcNode(stream, inst_data.src_node); } - fn writeSwitchBlockErrUnion(self: *Writer, stream: *std.Io.Writer, inst: Zir.Inst.Index) !void { - const inst_data = self.code.instructions.items(.data)[@intFromEnum(inst)].pl_node; - const extra = self.code.extraData(Zir.Inst.SwitchBlockErrUnion, inst_data.payload_index); + fn writeSwitchBlock( + self: *Writer, + stream: *std.Io.Writer, + inst: Zir.Inst.Index, + ) !void { + const zir_switch = self.code.getSwitchBlock(inst); + var extra_index = zir_switch.end; - var extra_index: usize = extra.end; - - const multi_cases_len = if (extra.data.bits.has_multi_cases) blk: { - const multi_cases_len = self.code.extra[extra_index]; - extra_index += 1; - break :blk multi_cases_len; - } else 0; - - const err_capture_inst: Zir.Inst.Index = if (extra.data.bits.any_uses_err_capture) blk: { - const tag_capture_inst = self.code.extra[extra_index]; - extra_index += 1; - break :blk @enumFromInt(tag_capture_inst); - } else undefined; - - try self.writeInstRef(stream, extra.data.operand); - - if (extra.data.bits.any_uses_err_capture) { - try stream.writeAll(", err_capture="); - try self.writeInstIndex(stream, err_capture_inst); - } + try self.writeInstRef(stream, zir_switch.main_operand); self.indent += 2; - { - const info = @as(Zir.Inst.SwitchBlock.ProngInfo, @bitCast(self.code.extra[extra_index])); - extra_index += 1; - - assert(!info.is_inline); - const body = self.code.bodySlice(extra_index, info.body_len); - extra_index += body.len; + if (zir_switch.non_err_case) |non_err_case| { + if (non_err_case.operand_is_ref) try stream.writeAll(" ref"); try stream.writeAll(",\n"); try stream.splatByteAll(' ', self.indent); + + try self.writeSwitchCaptures(stream, non_err_case.capture, false, inst, &zir_switch); + try stream.writeAll("non_err => "); - try self.writeBracedBody(stream, body); + try self.writeBracedBody(stream, non_err_case.body); + try stream.writeAll(" "); + try self.writeSrcNode(stream, zir_switch.catch_or_if_src_node_offset.unwrap().?); } - - if (extra.data.bits.has_else) { - const info = @as(Zir.Inst.SwitchBlock.ProngInfo, @bitCast(self.code.extra[extra_index])); - extra_index += 1; - const capture_text = switch (info.capture) { - .none => "", - .by_val => "by_val ", - .by_ref => "by_ref ", - }; - const inline_text = if (info.is_inline) "inline " else ""; - const body = self.code.bodySlice(extra_index, info.body_len); - extra_index += body.len; - + if (zir_switch.else_case) |else_case| { try stream.writeAll(",\n"); try stream.splatByteAll(' ', self.indent); - try stream.print("{s}{s}else => ", .{ capture_text, inline_text }); - try self.writeBracedBody(stream, body); + + try self.writeSwitchCaptures(stream, else_case.capture, else_case.has_tag_capture, inst, &zir_switch); + if (else_case.is_inline) try stream.writeAll("inline "); + + try stream.writeAll("else => "); + try self.writeBracedBody(stream, else_case.body); + } + if (zir_switch.under_case.resolve()) |under_case| { + try stream.writeAll(",\n"); + try stream.splatByteAll(' ', self.indent); + + try self.writeSwitchCaptures(stream, under_case.capture, under_case.has_tag_capture, inst, &zir_switch); + + try stream.writeAll("_ => "); + try self.writeBracedBody(stream, under_case.body); } - { - const scalar_cases_len = extra.data.bits.scalar_cases_len; - var scalar_i: usize = 0; - while (scalar_i < scalar_cases_len) : (scalar_i += 1) { - const item_ref = @as(Zir.Inst.Ref, @enumFromInt(self.code.extra[extra_index])); - extra_index += 1; - const info = @as(Zir.Inst.SwitchBlock.ProngInfo, @bitCast(self.code.extra[extra_index])); - extra_index += 1; - const body = self.code.bodySlice(extra_index, info.body_len); - extra_index += info.body_len; + var case_it = zir_switch.iterateCases(); + while (case_it.next()) |case| { + try stream.writeAll(",\n"); + try stream.splatByteAll(' ', self.indent); - try stream.writeAll(",\n"); - try stream.splatByteAll(' ', self.indent); - switch (info.capture) { - .none => {}, - .by_val => try stream.writeAll("by_val "), - .by_ref => try stream.writeAll("by_ref "), - } - if (info.is_inline) try stream.writeAll("inline "); - try self.writeInstRef(stream, item_ref); - try stream.writeAll(" => "); - try self.writeBracedBody(stream, body); + const prong_info = case.prong_info; + try self.writeSwitchCaptures(stream, prong_info.capture, prong_info.has_tag_capture, inst, &zir_switch); + if (prong_info.is_inline) try stream.writeAll("inline "); + + const prong_body = self.code.bodySlice(extra_index, prong_info.body_len); + extra_index += prong_body.len; + + var first_item: bool = true; + if (case.isUnder()) { + try stream.writeAll("_"); + first_item = false; } - } - { - var multi_i: usize = 0; - while (multi_i < multi_cases_len) : (multi_i += 1) { - const items_len = self.code.extra[extra_index]; - extra_index += 1; - const ranges_len = self.code.extra[extra_index]; - extra_index += 1; - const info = @as(Zir.Inst.SwitchBlock.ProngInfo, @bitCast(self.code.extra[extra_index])); - extra_index += 1; - const items = self.code.refSlice(extra_index, items_len); - extra_index += items_len; + for (case.item_infos) |item_info| { + if (!first_item) try stream.writeAll(", "); + first_item = false; - try stream.writeAll(",\n"); - try stream.splatByteAll(' ', self.indent); - switch (info.capture) { - .none => {}, - .by_val => try stream.writeAll("by_val "), - .by_ref => try stream.writeAll("by_ref "), + switch (item_info.unwrap()) { + .enum_literal => |str_index| { + const str = self.code.nullTerminatedString(str_index); + try stream.print("\".{f}\"", .{std.zig.fmtString(str)}); + }, + .error_value => |str_index| { + const str = self.code.nullTerminatedString(str_index); + try stream.print("\"error.{f}\"", .{std.zig.fmtString(str)}); + }, + .number_literal => |zir_ref| { + try self.writeInstRef(stream, zir_ref); + }, + .body_len => |body_len| { + const item_body = self.code.bodySlice(extra_index, body_len); + extra_index += item_body.len; + try self.writeBracedDecl(stream, item_body); + }, } - if (info.is_inline) try stream.writeAll("inline "); + } + for (case.range_infos) |range_info| { + if (!first_item) try stream.writeAll(", "); + first_item = false; - for (items, 0..) |item_ref, item_i| { - if (item_i != 0) try stream.writeAll(", "); - try self.writeInstRef(stream, item_ref); - } + var first_range_item = true; + for (&range_info) |item_info| { + if (!first_range_item) try stream.writeAll("..."); + first_range_item = false; - var range_i: usize = 0; - while (range_i < ranges_len) : (range_i += 1) { - const item_first = @as(Zir.Inst.Ref, @enumFromInt(self.code.extra[extra_index])); - extra_index += 1; - const item_last = @as(Zir.Inst.Ref, @enumFromInt(self.code.extra[extra_index])); - extra_index += 1; - - if (range_i != 0 or items.len != 0) { - try stream.writeAll(", "); + switch (item_info.unwrap()) { + .enum_literal => |str_index| { + const str = self.code.nullTerminatedString(str_index); + try stream.print("\".{f}\"", .{std.zig.fmtString(str)}); + }, + .error_value => |str_index| { + const str = self.code.nullTerminatedString(str_index); + try stream.print("\"error.{f}\"", .{std.zig.fmtString(str)}); + }, + .number_literal => |zir_ref| { + try self.writeInstRef(stream, zir_ref); + }, + .body_len => |body_len| { + const item_body = self.code.bodySlice(extra_index, body_len); + extra_index += item_body.len; + try self.writeBracedDecl(stream, item_body); + }, } - try self.writeInstRef(stream, item_first); - try stream.writeAll("..."); - try self.writeInstRef(stream, item_last); } - - const body = self.code.bodySlice(extra_index, info.body_len); - extra_index += info.body_len; - try stream.writeAll(" => "); - try self.writeBracedBody(stream, body); } + try stream.writeAll(" => "); + try self.writeBracedBody(stream, prong_body); } self.indent -= 2; try stream.writeAll(") "); - try self.writeSrcNode(stream, inst_data.src_node); + try self.writeSrcNode(stream, zir_switch.switch_src_node_offset); } - fn writeSwitchBlock(self: *Writer, stream: *std.Io.Writer, inst: Zir.Inst.Index) !void { - const inst_data = self.code.instructions.items(.data)[@intFromEnum(inst)].pl_node; - const extra = self.code.extraData(Zir.Inst.SwitchBlock, inst_data.payload_index); - - var extra_index: usize = extra.end; - - const multi_cases_len = if (extra.data.bits.has_multi_cases) blk: { - const multi_cases_len = self.code.extra[extra_index]; - extra_index += 1; - break :blk multi_cases_len; - } else 0; - - const tag_capture_inst: Zir.Inst.Index = if (extra.data.bits.any_has_tag_capture) blk: { - const tag_capture_inst = self.code.extra[extra_index]; - extra_index += 1; - break :blk @enumFromInt(tag_capture_inst); - } else undefined; - - try self.writeInstRef(stream, extra.data.operand); - - if (extra.data.bits.any_has_tag_capture) { - try stream.writeAll(", tag_capture="); - try self.writeInstIndex(stream, tag_capture_inst); + fn writeSwitchCaptures( + self: *Writer, + stream: *std.Io.Writer, + capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, + has_tag_capture: bool, + switch_inst: Zir.Inst.Index, + zir_switch: *const Zir.UnwrappedSwitchBlock, + ) !void { + if (capture != .none) { + try stream.print("{t}=", .{capture}); + const capture_inst = zir_switch.payload_capture_placeholder.unwrap() orelse switch_inst; + try self.writeInstIndex(stream, capture_inst); + try stream.writeAll(" "); } - - self.indent += 2; - - const special_prongs = extra.data.bits.special_prongs; - - if (special_prongs.hasElse()) { - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(self.code.extra[extra_index]); - const capture_text = switch (info.capture) { - .none => "", - .by_val => "by_val ", - .by_ref => "by_ref ", - }; - const inline_text = if (info.is_inline) "inline " else ""; - extra_index += 1; - const body = self.code.bodySlice(extra_index, info.body_len); - extra_index += body.len; - - try stream.writeAll(",\n"); - try stream.splatByteAll(' ', self.indent); - try stream.print("{s}{s}else => ", .{ capture_text, inline_text }); - try self.writeBracedBody(stream, body); + if (has_tag_capture) { + try stream.writeAll("tag="); + const capture_inst = zir_switch.tag_capture_placeholder.unwrap() orelse switch_inst; + try self.writeInstIndex(stream, capture_inst); + try stream.writeAll(" "); } - - if (special_prongs.hasUnder()) { - var single_item_ref: Zir.Inst.Ref = .none; - var items_len: u32 = 0; - var ranges_len: u32 = 0; - if (special_prongs.hasOneAdditionalItem()) { - single_item_ref = @enumFromInt(self.code.extra[extra_index]); - extra_index += 1; - } else if (special_prongs.hasManyAdditionalItems()) { - items_len = self.code.extra[extra_index]; - extra_index += 1; - ranges_len = self.code.extra[extra_index]; - extra_index += 1; - } - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(self.code.extra[extra_index]); - extra_index += 1; - const items = self.code.refSlice(extra_index, items_len); - extra_index += items_len; - - try stream.writeAll(",\n"); - try stream.splatByteAll(' ', self.indent); - switch (info.capture) { - .none => {}, - .by_val => try stream.writeAll("by_val "), - .by_ref => try stream.writeAll("by_ref "), - } - if (info.is_inline) try stream.writeAll("inline "); - - try stream.writeAll("_"); - if (single_item_ref != .none) { - try stream.writeAll(", "); - try self.writeInstRef(stream, single_item_ref); - } - for (items) |item_ref| { - try stream.writeAll(", "); - try self.writeInstRef(stream, item_ref); - } - - var range_i: usize = 0; - while (range_i < ranges_len) : (range_i += 1) { - const item_first: Zir.Inst.Ref = @enumFromInt(self.code.extra[extra_index]); - extra_index += 1; - const item_last: Zir.Inst.Ref = @enumFromInt(self.code.extra[extra_index]); - extra_index += 1; - - try stream.writeAll(", "); - try self.writeInstRef(stream, item_first); - try stream.writeAll("..."); - try self.writeInstRef(stream, item_last); - } - - const body = self.code.bodySlice(extra_index, info.body_len); - extra_index += info.body_len; - try stream.writeAll(" => "); - try self.writeBracedBody(stream, body); - } - - { - const scalar_cases_len = extra.data.bits.scalar_cases_len; - var scalar_i: usize = 0; - while (scalar_i < scalar_cases_len) : (scalar_i += 1) { - const item_ref: Zir.Inst.Ref = @enumFromInt(self.code.extra[extra_index]); - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(self.code.extra[extra_index]); - extra_index += 1; - const body = self.code.bodySlice(extra_index, info.body_len); - extra_index += info.body_len; - - try stream.writeAll(",\n"); - try stream.splatByteAll(' ', self.indent); - switch (info.capture) { - .none => {}, - .by_val => try stream.writeAll("by_val "), - .by_ref => try stream.writeAll("by_ref "), - } - if (info.is_inline) try stream.writeAll("inline "); - try self.writeInstRef(stream, item_ref); - try stream.writeAll(" => "); - try self.writeBracedBody(stream, body); - } - } - { - var multi_i: usize = 0; - while (multi_i < multi_cases_len) : (multi_i += 1) { - const items_len = self.code.extra[extra_index]; - extra_index += 1; - const ranges_len = self.code.extra[extra_index]; - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(self.code.extra[extra_index]); - extra_index += 1; - const items = self.code.refSlice(extra_index, items_len); - extra_index += items_len; - - try stream.writeAll(",\n"); - try stream.splatByteAll(' ', self.indent); - switch (info.capture) { - .none => {}, - .by_val => try stream.writeAll("by_val "), - .by_ref => try stream.writeAll("by_ref "), - } - if (info.is_inline) try stream.writeAll("inline "); - - for (items, 0..) |item_ref, item_i| { - if (item_i != 0) try stream.writeAll(", "); - try self.writeInstRef(stream, item_ref); - } - - var range_i: usize = 0; - while (range_i < ranges_len) : (range_i += 1) { - const item_first: Zir.Inst.Ref = @enumFromInt(self.code.extra[extra_index]); - extra_index += 1; - const item_last: Zir.Inst.Ref = @enumFromInt(self.code.extra[extra_index]); - extra_index += 1; - - if (range_i != 0 or items.len != 0) { - try stream.writeAll(", "); - } - try self.writeInstRef(stream, item_first); - try stream.writeAll("..."); - try self.writeInstRef(stream, item_last); - } - - const body = self.code.bodySlice(extra_index, info.body_len); - extra_index += info.body_len; - try stream.writeAll(" => "); - try self.writeBracedBody(stream, body); - } - } - - self.indent -= 2; - - try stream.writeAll(") "); - try self.writeSrcNode(stream, inst_data.src_node); } fn writePlNodeField(self: *Writer, stream: *std.Io.Writer, inst: Zir.Inst.Index) !void { From 9e949f95c15a7552f84209e01a78b5e0d12225db Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 7 Jan 2026 17:16:36 +0100 Subject: [PATCH 07/23] Sema: enhance comptime `is_non_err` resolution This makes `is_non_err` and `unwrap_errunion_err[_ptr]` friendlier towards being emitted into comptime blocks. Also, `analyzeIsNonErr` now actually attempts to do something at comptime. --- src/Sema.zig | 98 +++++++++++++++++++++++++--------------------------- 1 file changed, 47 insertions(+), 51 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index 88e70ff889..9b1c5f4349 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -1927,9 +1927,8 @@ fn analyzeBodyInner( break :msg msg; }); } - const is_non_err = try sema.analyzeIsNonErrComptimeOnly(block, operand_src, err_union); - assert(is_non_err != .none); - const is_non_err_val = try sema.resolveConstDefinedValue(block, operand_src, is_non_err, null); + const is_non_err_val = (try sema.resolveIsNonErrVal(block, operand_src, err_union)).?; + if (is_non_err_val.isUndef(zcu)) return sema.failWithUseOfUndef(block, operand_src, null); if (is_non_err_val.toBool()) { break :blk try sema.analyzeErrUnionPayload(block, src, err_union_ty, err_union, operand_src, false); } @@ -1945,9 +1944,8 @@ fn analyzeBodyInner( const inline_body = sema.code.bodySlice(extra.end, extra.data.body_len); const operand = try sema.resolveInst(extra.data.operand); const err_union = try sema.analyzeLoad(block, src, operand, operand_src); - const is_non_err = try sema.analyzeIsNonErrComptimeOnly(block, operand_src, err_union); - assert(is_non_err != .none); - const is_non_err_val = try sema.resolveConstDefinedValue(block, operand_src, is_non_err, null); + const is_non_err_val = (try sema.resolveIsNonErrVal(block, operand_src, err_union)).?; + if (is_non_err_val.isUndef(zcu)) return sema.failWithUseOfUndef(block, operand_src, null); if (is_non_err_val.toBool()) { break :blk try sema.analyzeErrUnionPayloadPtr(block, src, operand, false, false); } @@ -8960,6 +8958,7 @@ fn analyzeErrUnionCode(sema: *Sema, block: *Block, src: LazySrcLoc, operand: Air const result_ty = operand_ty.errorUnionSet(zcu); if (try sema.resolveDefinedValue(block, src, operand)) |val| { + if (val.getErrorName(zcu) == .none) return .unreachable_value; return Air.internedToRef((try pt.intern(.{ .err = .{ .ty = result_ty.toIntern(), .name = zcu.intern_pool.indexToKey(val.toIntern()).error_union.val.err_name, @@ -8997,7 +8996,7 @@ fn analyzeErrUnionCodePtr(sema: *Sema, block: *Block, src: LazySrcLoc, operand: if (try sema.resolveDefinedValue(block, src, operand)) |pointer_val| { if (try sema.pointerDeref(block, src, pointer_val, operand_ty)) |val| { - assert(val.getErrorName(zcu) != .none); + if (val.getErrorName(zcu) == .none) return .unreachable_value; return Air.internedToRef((try pt.intern(.{ .err = .{ .ty = result_ty.toIntern(), .name = zcu.intern_pool.indexToKey(val.toIntern()).error_union.val.err_name, @@ -18689,14 +18688,13 @@ fn zirTry(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileError! break :msg msg; }); } - const is_non_err = try sema.analyzeIsNonErrComptimeOnly(parent_block, operand_src, err_union); - if (is_non_err != .none) { + if (try sema.resolveIsNonErrVal(parent_block, operand_src, err_union)) |is_non_err_val| { // We can propagate `.cold` hints from this branch since it's comptime-known // to be taken from the parent branch. const parent_hint = sema.branch_hint; defer sema.branch_hint = parent_hint orelse if (sema.branch_hint == .cold) .cold else null; - const is_non_err_val = (try sema.resolveDefinedValue(parent_block, operand_src, is_non_err)).?; + if (is_non_err_val.isUndef(zcu)) return sema.failWithUseOfUndef(parent_block, operand_src, null); if (is_non_err_val.toBool()) { return sema.analyzeErrUnionPayload(parent_block, src, err_union_ty, err_union, operand_src, false); } @@ -18753,14 +18751,13 @@ fn zirTryPtr(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileErr break :msg msg; }); } - const is_non_err = try sema.analyzeIsNonErrComptimeOnly(parent_block, operand_src, err_union); - if (is_non_err != .none) { + if (try sema.resolveIsNonErrVal(parent_block, operand_src, err_union)) |is_non_err_val| { // We can propagate `.cold` hints from this branch since it's comptime-known // to be taken from the parent branch. const parent_hint = sema.branch_hint; defer sema.branch_hint = parent_hint orelse if (sema.branch_hint == .cold) .cold else null; - const is_non_err_val = (try sema.resolveDefinedValue(parent_block, operand_src, is_non_err)).?; + if (is_non_err_val.isUndef(zcu)) return sema.failWithUseOfUndef(parent_block, operand_src, null); if (is_non_err_val.toBool()) { return sema.analyzeErrUnionPayloadPtr(parent_block, src, operand, false, false); } @@ -31800,12 +31797,12 @@ fn analyzeIsNull( return block.addUnOp(air_tag, operand); } -fn analyzePtrIsNonErrComptimeOnly( +fn resolvePtrIsNonErrVal( sema: *Sema, block: *Block, src: LazySrcLoc, operand: Air.Inst.Ref, -) CompileError!Air.Inst.Ref { +) CompileError!?Value { const pt = sema.pt; const zcu = pt.zcu; const ptr_ty = sema.typeOf(operand); @@ -31813,41 +31810,44 @@ fn analyzePtrIsNonErrComptimeOnly( const child_ty = ptr_ty.childType(zcu); const child_tag = child_ty.zigTypeTag(zcu); - if (child_tag != .error_set and child_tag != .error_union) return .bool_true; - if (child_tag == .error_set) return .bool_false; + if (child_tag != .error_set and child_tag != .error_union) return .true; + if (child_tag == .error_set) return .false; assert(child_tag == .error_union); - _ = block; - _ = src; - - return .none; + if (try sema.resolveValue(operand)) |ptr_val| { + if (ptr_val.isUndef(zcu)) return .undef_bool; + if (try sema.pointerDeref(block, src, ptr_val, ptr_ty)) |val| { + return try sema.resolveIsNonErrVal(block, src, .fromValue(val)); + } + } + return null; } -fn analyzeIsNonErrComptimeOnly( +fn resolveIsNonErrVal( sema: *Sema, block: *Block, src: LazySrcLoc, operand: Air.Inst.Ref, -) CompileError!Air.Inst.Ref { +) CompileError!?Value { const pt = sema.pt; const zcu = pt.zcu; const ip = &zcu.intern_pool; const operand_ty = sema.typeOf(operand); const ot = operand_ty.zigTypeTag(zcu); - if (ot != .error_set and ot != .error_union) return .bool_true; - if (ot == .error_set) return .bool_false; + if (ot != .error_set and ot != .error_union) return .true; + if (ot == .error_set) return .false; assert(ot == .error_union); const payload_ty = operand_ty.errorUnionPayload(zcu); if (payload_ty.zigTypeTag(zcu) == .noreturn) { - return .bool_false; + return .false; } if (operand == .undef) { return .undef_bool; } else if (@intFromEnum(operand) < InternPool.static_len) { // None of the ref tags can be errors. - return .bool_true; + return .true; } const maybe_operand_val = try sema.resolveValue(operand); @@ -31870,23 +31870,23 @@ fn analyzeIsNonErrComptimeOnly( if (maybe_operand_val != null) break :blk; // Try to avoid resolving inferred error set if possible. - if (ies.errors.count() != 0) return .none; + if (ies.errors.count() != 0) return null; switch (ies.resolved) { - .anyerror_type => return .none, + .anyerror_type => return null, .none => {}, else => switch (ip.indexToKey(ies.resolved).error_set_type.names.len) { - 0 => return .bool_true, - else => return .none, + 0 => return .true, + else => return null, }, } // We do not have a comptime answer because this inferred error // set is not resolved, and an instruction later in this function // body may or may not cause an error to be added to this set. - return .none; + return null; }, else => switch (ip.indexToKey(set_ty)) { .error_set_type => |error_set_type| { - if (error_set_type.names.len == 0) return .bool_true; + if (error_set_type.names.len == 0) return .true; }, .inferred_error_set_type => |func_index| blk: { // If the error set is empty, we must return a comptime true or false. @@ -31902,35 +31902,35 @@ fn analyzeIsNonErrComptimeOnly( if (sema.fn_ret_ty_ies) |ies| { if (ies.func == func_index) { // Try to avoid resolving inferred error set if possible. - if (ies.errors.count() != 0) return .none; + if (ies.errors.count() != 0) return null; switch (ies.resolved) { - .anyerror_type => return .none, + .anyerror_type => return null, .none => {}, else => switch (ip.indexToKey(ies.resolved).error_set_type.names.len) { - 0 => return .bool_true, - else => return .none, + 0 => return .true, + else => return null, }, } // We do not have a comptime answer because this inferred error // set is not resolved, and an instruction later in this function // body may or may not cause an error to be added to this set. - return .none; + return null; } } const resolved_ty = try sema.resolveInferredErrorSet(block, src, set_ty); if (resolved_ty == .anyerror_type) break :blk; if (ip.indexToKey(resolved_ty).error_set_type.names.len == 0) - return .bool_true; + return .true; }, else => unreachable, }, } if (maybe_operand_val) |err_union| { - return if (err_union.isUndef(zcu)) .undef_bool else if (err_union.getErrorName(zcu) == .none) .bool_true else .bool_false; + return if (err_union.isUndef(zcu)) .undef_bool else if (err_union.getErrorName(zcu) == .none) .true else .false; } - return .none; + return null; } fn analyzeIsNonErr( @@ -31939,12 +31939,10 @@ fn analyzeIsNonErr( src: LazySrcLoc, operand: Air.Inst.Ref, ) CompileError!Air.Inst.Ref { - const result = try sema.analyzeIsNonErrComptimeOnly(block, src, operand); - if (result == .none) { - try sema.requireRuntimeBlock(block, src, null); - return block.addUnOp(.is_non_err, operand); + if (try sema.resolveIsNonErrVal(block, src, operand)) |val| { + return .fromValue(val); } else { - return result; + return block.addUnOp(.is_non_err, operand); } } @@ -31954,12 +31952,10 @@ fn analyzePtrIsNonErr( src: LazySrcLoc, operand: Air.Inst.Ref, ) CompileError!Air.Inst.Ref { - const result = try sema.analyzePtrIsNonErrComptimeOnly(block, src, operand); - if (result == .none) { - try sema.requireRuntimeBlock(block, src, null); - return block.addUnOp(.is_non_err_ptr, operand); + if (try sema.resolvePtrIsNonErrVal(block, src, operand)) |val| { + return .fromValue(val); } else { - return result; + return block.addUnOp(.is_non_err_ptr, operand); } } From 6e35138901d052f19d41ec7bafeaf31f1adf54f6 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Fri, 9 Jan 2026 04:43:31 +0100 Subject: [PATCH 08/23] all: prefer `else => |e| return e,` over `else => return err,` When switching on an error, using the captured value instead of the original one is always preferable since its error set has been narrowed to only contain errors which haven't already been handled by other switch prongs. The subsequent commits will disallow this form as an unreachable `else` prong. --- lib/std/tar/Writer.zig | 4 ++-- lib/std/testing.zig | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/std/tar/Writer.zig b/lib/std/tar/Writer.zig index 583bc8cfc1..aa4d9b1bbb 100644 --- a/lib/std/tar/Writer.zig +++ b/lib/std/tar/Writer.zig @@ -114,7 +114,7 @@ fn writeHeader( if (typeflag == .symbolic_link) header.setLinkname(link_name) catch |err| switch (err) { error.NameTooLong => try w.writeExtendedHeader(.gnu_long_link, &.{link_name}), - else => return err, + else => |e| return e, }; try header.write(w.underlying_writer); } @@ -131,7 +131,7 @@ fn setPath(w: *Writer, header: *Header, sub_path: []const u8) Error!void { &.{ w.prefix, "/", sub_path }; try w.writeExtendedHeader(.gnu_long_name, buffers); }, - else => return err, + else => |e| return e, }; } diff --git a/lib/std/testing.zig b/lib/std/testing.zig index 24287f65aa..6d2a045abb 100644 --- a/lib/std/testing.zig +++ b/lib/std/testing.zig @@ -1187,7 +1187,7 @@ pub fn checkAllAllocationFailures(backing_allocator: std.mem.Allocator, comptime return error.MemoryLeakDetected; } }, - else => return err, + else => |e| return e, } } } From d94137d23fff3958e003c7d6d70a5d1087a1e98c Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Fri, 9 Jan 2026 04:49:00 +0100 Subject: [PATCH 09/23] src/Type: make doc comments prettier :) --- src/Type.zig | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Type.zig b/src/Type.zig index e6a8965409..57d8a0a5ed 100644 --- a/src/Type.zig +++ b/src/Type.zig @@ -1933,12 +1933,12 @@ pub fn isPtrLikeOptional(ty: Type, zcu: *const Zcu) bool { }; } -/// For *[N]T, returns [N]T. -/// For *T, returns T. -/// For [*]T, returns T. -/// For @Vector(N, T), returns T. -/// For [N]T, returns T. -/// For ?T, returns T. +/// For `*[N]T`, returns `[N]T`. +/// For `*T`, returns `T`. +/// For `[*]T`, returns `T`. +/// For `@Vector(N, T)`, returns `T`. +/// For `[N]T`, returns `T`. +/// For `?T`, returns `T`. pub fn childType(ty: Type, zcu: *const Zcu) Type { return childTypeIp(ty, &zcu.intern_pool); } @@ -1947,15 +1947,15 @@ pub fn childTypeIp(ty: Type, ip: *const InternPool) Type { return Type.fromInterned(ip.childType(ty.toIntern())); } -/// For *[N]T, returns T. -/// For ?*T, returns T. -/// For ?*[N]T, returns T. -/// For ?[*]T, returns T. -/// For *T, returns T. -/// For [*]T, returns T. -/// For [N]T, returns T. -/// For []T, returns T. -/// For anyframe->T, returns T. +/// For `*[N]T`, returns `T`. +/// For `?*T`, returns `T`. +/// For `?*[N]T`, returns `T`. +/// For `?[*]T`, returns `T`. +/// For `*T`, returns `T`. +/// For `[*]T`, returns `T`. +/// For `[N]T`, returns `T`. +/// For `[]T`, returns `T`. +/// For `anyframe->T`, returns `T`. pub fn elemType2(ty: Type, zcu: *const Zcu) Type { return switch (zcu.intern_pool.indexToKey(ty.toIntern())) { .ptr_type => |ptr_type| switch (ptr_type.flags.size) { From b79bd313566347873e022adcd61891339735d9a6 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Fri, 9 Jan 2026 05:04:58 +0100 Subject: [PATCH 10/23] Sema: rework `switch_block[_ref/_err_union]` logic This commit aims to simplify and de-duplicate the logic required for semantically analyzing `switch` expressions. The core logic has been moved to `analyzeSwitchBlock`, `resolveSwitchBlock` and `finishSwitchBlock` and has been rewritten around the new iterator-based API exposed by `Zir.UnwrappedSwitchBlock`. All validation logic and switch prong item resolution have been moved to `validateSwitchBlock`, which produces a `ValidatedSwitchBlock` containing all the necessary information for further analysis. `Zir.UnwrappedSwitchBlock`, `ValidatedSwitchBlock` and `SwitchOperand` replace `SwitchProngAnalysis` while adding more flexibility, mainly for better integration with `switch_block_err_union`. `analyzeSwitchBlock` has an explicit code path for OPV types which lowers them to either a `block`-`br` or a `loop`-`repeat` construct instead of a switch. Backends expect `switch` to actually have an operand that exists at runtime, so this is a bug fix and avoids further special cases in the rest of the switch logic. `resolveSwitchBlock` and `finishSwitchBr` exclusively deal with operands which can have more than one value, at comptime and at runtime respectively. This commit also reworks `RangeSet` to be an unmanaged container and adds `Air.SwitchBr.BranchHints` to offload some complexity from Sema to there and save a few bytes of memory in the process. Additionally, some new features have been implemented: - decl literals and everything else requiring a result type (`@enumFromInt`!) may now be used as switch prong items - union tag captures are now allowed for all prongs, not just `inline` ones - switch prongs may contain errors which are not in the error set being switched on, if these prongs contain `=> comptime unreachable` and some bugs have been fixed: - lots of issues with switching on OPV types are now fixed - the rules around unreachable `else` prongs when switching on errors now apply to *any* switch on an error, not just to `switch_block_err_union`, and are applied properly based on the AST - switching on `void` no longer requires an `else` prong unconditionally - lazy values are properly resolved before any comparisons with prong items - evaluation order between all kinds of switch statements is now the same, with or without label --- src/Air.zig | 44 + src/RangeSet.zig | 128 +- src/Sema.zig | 5577 ++++++++--------- src/Zcu.zig | 78 +- test/behavior/switch.zig | 156 + test/behavior/switch_loop.zig | 166 +- test/behavior/switch_on_captured_error.zig | 225 +- .../duplicate_boolean_switch_value.zig | 2 + .../compile_errors/invalid_switch_item.zig | 8 +- ...h_on_error_with_1_field_with_no_prongs.zig | 30 +- .../tag_capture_on_non_inline_prong.zig | 12 - 11 files changed, 3260 insertions(+), 3166 deletions(-) delete mode 100644 test/cases/compile_errors/tag_capture_on_non_inline_prong.zig diff --git a/src/Air.zig b/src/Air.zig index b5cb950d49..d5a2fa3e10 100644 --- a/src/Air.zig +++ b/src/Air.zig @@ -1339,6 +1339,50 @@ pub const SwitchBr = struct { ranges_len: u32, body_len: u32, }; + + pub const BranchHints = struct { + bags: std.ArrayList(u32), + count: u32, + + const hints_per_bag = 10; + const hint_bits = @bitSizeOf(std.builtin.BranchHint); + + pub const empty: BranchHints = .{ + .bags = .empty, + .count = 0, + }; + + pub fn initCapacity(gpa: std.mem.Allocator, num: u32) std.mem.Allocator.Error!BranchHints { + const bags_required = std.math.divCeil(u32, num, hints_per_bag) catch unreachable; + const bags: std.ArrayList(u32) = try .initCapacity(gpa, bags_required); + return .{ .bags = bags, .count = 0 }; + } + + pub fn ensureUnusedCapacity(hints: *BranchHints, gpa: std.mem.Allocator, additional_count: u32) std.mem.Allocator.Error!void { + const unused_hints = hints.bags.capacity * hints_per_bag - hints.count; + if (unused_hints >= additional_count) return; + const bags_required = std.math.divCeil(u32, hints.count + additional_count, hints_per_bag) catch unreachable; + return hints.bags.ensureUnusedCapacity(gpa, bags_required); + } + + pub fn appendAssumeCapacity(hints: *BranchHints, hint: std.builtin.BranchHint) void { + const idx_in_bag = hints.count % hints_per_bag; + var bag: u32 = if (idx_in_bag > 0) hints.bags.pop().? else 0; + bag |= @as(u32, @intFromEnum(hint)) << @intCast(hint_bits * idx_in_bag); + hints.count += 1; + return hints.bags.appendAssumeCapacity(bag); + } + + pub fn append(hints: *BranchHints, gpa: std.mem.Allocator, hint: std.builtin.BranchHint) std.mem.Allocator.Error!void { + try hints.ensureUnusedCapacity(gpa, 1); + return hints.appendAssumeCapacity(hint); + } + + pub fn deinit(hints: *BranchHints, gpa: std.mem.Allocator) void { + hints.bags.deinit(gpa); + hints.* = undefined; + } + }; }; /// This data is stored inside extra. Trailing: diff --git a/src/RangeSet.zig b/src/RangeSet.zig index ec04078b0d..3459706202 100644 --- a/src/RangeSet.zig +++ b/src/RangeSet.zig @@ -1,102 +1,92 @@ -const std = @import("std"); -const assert = std.debug.assert; -const Order = std.math.Order; - -const InternPool = @import("InternPool.zig"); -const Type = @import("Type.zig"); -const Value = @import("Value.zig"); -const Zcu = @import("Zcu.zig"); const RangeSet = @This(); -const LazySrcLoc = Zcu.LazySrcLoc; -zcu: *Zcu, -ranges: std.array_list.Managed(Range), +ranges: std.ArrayList(Range), pub const Range = struct { - first: InternPool.Index, - last: InternPool.Index, + first: Value, + last: Value, src: LazySrcLoc, }; -pub fn init(allocator: std.mem.Allocator, zcu: *Zcu) RangeSet { - return .{ - .zcu = zcu, - .ranges = std.array_list.Managed(Range).init(allocator), - }; +pub const empty: RangeSet = .{ .ranges = .empty }; + +pub fn deinit(self: *RangeSet, allocator: Allocator) void { + self.ranges.deinit(allocator); + self.* = undefined; } -pub fn deinit(self: *RangeSet) void { - self.ranges.deinit(); +pub fn ensureUnusedCapacity(self: *RangeSet, allocator: Allocator, additional_count: usize) Allocator.Error!void { + return self.ranges.ensureUnusedCapacity(allocator, additional_count); } -pub fn add( - self: *RangeSet, - first: InternPool.Index, - last: InternPool.Index, - src: LazySrcLoc, -) !?LazySrcLoc { - const zcu = self.zcu; - const ip = &zcu.intern_pool; +pub fn addAssumeCapacity(set: *RangeSet, new: Range, ty: Type, zcu: *Zcu) ?LazySrcLoc { + assert(new.first.typeOf(zcu).eql(ty, zcu)); + assert(new.last.typeOf(zcu).eql(ty, zcu)); - const ty = ip.typeOf(first); - assert(ty == ip.typeOf(last)); - - for (self.ranges.items) |range| { - assert(ty == ip.typeOf(range.first)); - assert(ty == ip.typeOf(range.last)); - - if (Value.fromInterned(last).compareScalar(.gte, Value.fromInterned(range.first), Type.fromInterned(ty), zcu) and - Value.fromInterned(first).compareScalar(.lte, Value.fromInterned(range.last), Type.fromInterned(ty), zcu)) + for (set.ranges.items) |range| { + if (new.last.compareScalar(.gte, range.first, ty, zcu) and + new.first.compareScalar(.lte, range.last, ty, zcu)) { return range.src; // They overlap. } } - - try self.ranges.append(.{ - .first = first, - .last = last, - .src = src, - }); + set.ranges.appendAssumeCapacity(new); return null; } -/// Assumes a and b do not overlap -fn lessThan(zcu: *Zcu, a: Range, b: Range) bool { - const ty = Type.fromInterned(zcu.intern_pool.typeOf(a.first)); - return Value.fromInterned(a.first).compareScalar(.lt, Value.fromInterned(b.first), ty, zcu); +pub fn add(set: *RangeSet, allocator: Allocator, new: Range, ty: Type, zcu: *Zcu) Allocator.Error!?LazySrcLoc { + try set.ensureUnusedCapacity(allocator, 1); + return set.addAssumeCapacity(new, ty, zcu); } -pub fn spans(self: *RangeSet, first: InternPool.Index, last: InternPool.Index) !bool { - const zcu = self.zcu; - const ip = &zcu.intern_pool; - assert(ip.typeOf(first) == ip.typeOf(last)); +const SortCtx = struct { + ty: Type, + zcu: *Zcu, +}; +/// Assumes a and b do not overlap +fn lessThan(ctx: SortCtx, a: Range, b: Range) bool { + return a.first.compareScalar(.lt, b.first, ctx.ty, ctx.zcu); +} - if (self.ranges.items.len == 0) - return false; +pub fn spans( + set: *RangeSet, + allocator: Allocator, + first: Value, + last: Value, + ty: Type, + zcu: *Zcu, +) Allocator.Error!bool { + assert(first.typeOf(zcu).eql(ty, zcu)); + assert(last.typeOf(zcu).eql(ty, zcu)); + if (set.ranges.items.len == 0) return false; - std.mem.sort(Range, self.ranges.items, zcu, lessThan); + std.mem.sort(Range, set.ranges.items, SortCtx{ .ty = ty, .zcu = zcu }, lessThan); - if (self.ranges.items[0].first != first or - self.ranges.items[self.ranges.items.len - 1].last != last) + if (!set.ranges.items[0].first.eql(first, ty, zcu) or + !set.ranges.items[set.ranges.items.len - 1].last.eql(last, ty, zcu)) { return false; } + const limbs = try allocator.alloc( + std.math.big.Limb, + std.math.big.int.calcTwosCompLimbCount(ty.intInfo(zcu).bits), + ); + defer allocator.free(limbs); + var counter: std.math.big.int.Mutable = .init(limbs, 0); + var space: InternPool.Key.Int.Storage.BigIntSpace = undefined; - var counter = try std.math.big.int.Managed.init(self.ranges.allocator); - defer counter.deinit(); - // look for gaps - for (self.ranges.items[1..], 0..) |cur, i| { + for (set.ranges.items[1..], 0..) |cur, i| { // i starts counting from the second item. - const prev = self.ranges.items[i]; + const prev = set.ranges.items[i]; // prev.last + 1 == cur.first - try counter.copy(Value.fromInterned(prev.last).toBigInt(&space, zcu)); - try counter.addScalar(&counter, 1); + counter.copy(prev.last.toBigInt(&space, zcu)); + counter.addScalar(counter.toConst(), 1); - const cur_start_int = Value.fromInterned(cur.first).toBigInt(&space, zcu); + const cur_start_int = cur.first.toBigInt(&space, zcu); if (!cur_start_int.eql(counter.toConst())) { return false; } @@ -104,3 +94,13 @@ pub fn spans(self: *RangeSet, first: InternPool.Index, last: InternPool.Index) ! return true; } + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const InternPool = @import("InternPool.zig"); +const Type = @import("Type.zig"); +const Value = @import("Value.zig"); +const Zcu = @import("Zcu.zig"); +const LazySrcLoc = Zcu.LazySrcLoc; diff --git a/src/Sema.zig b/src/Sema.zig index 9b1c5f4349..1f28343547 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -509,7 +509,7 @@ pub const Block = struct { .parent = parent, .sema = parent.sema, .namespace = parent.namespace, - .instructions = .{}, + .instructions = .empty, .label = null, .inlining = parent.inlining, .comptime_reason = parent.comptime_reason, @@ -6496,26 +6496,23 @@ fn zirSwitchContinue(sema: *Sema, start_block: *Block, inst: Zir.Inst.Index) Com switch (sema.code.instructions.items(.tag)[@intFromEnum(switch_inst)]) { .switch_block, .switch_block_ref => {}, + .switch_block_err_union => unreachable, // wrong code path! else => unreachable, // assertion failure } - const switch_payload_index = sema.code.instructions.items(.data)[@intFromEnum(switch_inst)].pl_node.payload_index; - const switch_operand_ref = sema.code.extraData(Zir.Inst.SwitchBlock, switch_payload_index).data.operand; - const switch_operand_ty = sema.typeOf(try sema.resolveInst(switch_operand_ref)); - - const operand = try sema.coerce(start_block, switch_operand_ty, uncoerced_operand, operand_src); - + const operand_ty = (try sema.resolveInst(switch_inst.toRef())).toType(); + const operand = try sema.coerce(start_block, operand_ty, uncoerced_operand, operand_src); try sema.validateRuntimeValue(start_block, operand_src, operand); // We want to generate a `switch_dispatch` instruction with the switch condition, // possibly preceded by a store to the stack alloc containing the raw operand. // However, to avoid too much special-case state in Sema, this is handled by the - // `switch` lowering logic. As such, we will find the `Block` corresponding to the - // parent `switch_block[_ref]` instruction, create a dummy `br`, and add a merge - // to signal to the switch logic to rewrite this into an appropriate dispatch. + // `switch` lowering logic. As such, we will find the `Block` corresponding to + // the parent `switch_block[_ref]` instruction, create a dummy `br`, and add a + // merge to signal to the switch logic to rewrite this into an appropriate dispatch. var block = start_block; - while (true) { + while (true) : (block = block.parent.?) { if (block.label) |label| { if (label.zir_block == switch_inst) { const br_ref = try start_block.addBr(label.merges.block_inst, operand); @@ -6529,7 +6526,6 @@ fn zirSwitchContinue(sema: *Sema, start_block: *Block, inst: Zir.Inst.Index) Com return; } } - block = block.parent.?; } } @@ -8483,8 +8479,20 @@ fn zirDeclLiteral(sema: *Sema, block: *Block, inst: Zir.Inst.Index, do_coerce: b sema.code.nullTerminatedString(extra.field_name_start), .no_embedded_nulls, ); - const orig_ty: Type = try sema.resolveTypeOrPoison(block, src, extra.lhs) orelse .generic_poison; + return sema.analyzeDeclLiteral(block, src, name, orig_ty, do_coerce); +} + +fn analyzeDeclLiteral( + sema: *Sema, + block: *Block, + src: LazySrcLoc, + name: InternPool.NullTerminatedString, + orig_ty: Type, + do_coerce: bool, +) CompileError!Air.Inst.Ref { + const pt = sema.pt; + const zcu = pt.zcu; const uncoerced_result = res: { if (orig_ty.toIntern() == .generic_poison_type) { @@ -10518,668 +10526,6 @@ fn zirSliceSentinelTy(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileE return Air.internedToRef(sentinel_ty.toIntern()); } -/// Holds common data used when analyzing or resolving switch prong bodies, -/// including setting up captures. -const SwitchProngAnalysis = struct { - sema: *Sema, - /// The block containing the `switch_block` itself. - parent_block: *Block, - operand: Operand, - /// If this switch is on an error set, this is the type to assign to the - /// `else` prong. If `null`, the prong should be unreachable. - else_error_ty: ?Type, - /// The index of the `switch_block` instruction itself. - switch_block_inst: Zir.Inst.Index, - /// The dummy index into which inline tag captures should be placed. May be - /// undefined if no prong has a tag capture. - tag_capture_inst: Zir.Inst.Index, - - const Operand = union(enum) { - /// This switch will be dispatched only once, with the given operand. - simple: struct { - /// The raw switch operand value. Always defined. - by_val: Air.Inst.Ref, - /// The switch operand *pointer*. Defined only if there is a prong - /// with a by-ref capture. - by_ref: Air.Inst.Ref, - /// The switch condition value. For unions, `operand` is the union - /// and `cond` is its enum tag value. - cond: Air.Inst.Ref, - }, - /// This switch may be dispatched multiple times with `continue` syntax. - /// As such, the operand is stored in an alloc if needed. - loop: struct { - /// The `alloc` containing the `switch` operand for the active dispatch. - /// Each prong must load from this `alloc` to get captures. - /// If there are no captures, this may be undefined. - operand_alloc: Air.Inst.Ref, - /// Whether `operand_alloc` contains a by-val operand or a by-ref - /// operand. - operand_is_ref: bool, - /// The switch condition value for the *initial* dispatch. For - /// unions, this is the enum tag value. - init_cond: Air.Inst.Ref, - }, - }; - - /// Resolve a switch prong which is determined at comptime to have no peers. - /// Uses `resolveBlockBody`. Sets up captures as needed. - fn resolveProngComptime( - spa: SwitchProngAnalysis, - child_block: *Block, - prong_type: enum { normal, special }, - prong_body: []const Zir.Inst.Index, - capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, - /// Must use the `switch_capture` field in `offset`. - capture_src: LazySrcLoc, - /// The set of all values which can reach this prong. May be undefined - /// if the prong is special or contains ranges. - case_vals: []const Air.Inst.Ref, - /// The inline capture of this prong. If this is not an inline prong, - /// this is `.none`. - inline_case_capture: Air.Inst.Ref, - /// Whether this prong has an inline tag capture. If `true`, then - /// `inline_case_capture` cannot be `.none`. - has_tag_capture: bool, - merges: *Block.Merges, - ) CompileError!Air.Inst.Ref { - const sema = spa.sema; - const src = spa.parent_block.nodeOffset( - sema.code.instructions.items(.data)[@intFromEnum(spa.switch_block_inst)].pl_node.src_node, - ); - - // We can propagate `.cold` hints from this branch since it's comptime-known - // to be taken from the parent branch. - const parent_hint = sema.branch_hint; - defer sema.branch_hint = parent_hint orelse if (sema.branch_hint == .cold) .cold else null; - - if (has_tag_capture) { - const tag_ref = try spa.analyzeTagCapture(child_block, capture_src, inline_case_capture); - sema.inst_map.putAssumeCapacity(spa.tag_capture_inst, tag_ref); - } - defer if (has_tag_capture) assert(sema.inst_map.remove(spa.tag_capture_inst)); - - switch (capture) { - .none => { - return sema.resolveBlockBody(spa.parent_block, src, child_block, prong_body, spa.switch_block_inst, merges); - }, - - .by_val, .by_ref => { - const capture_ref = try spa.analyzeCapture( - child_block, - capture == .by_ref, - prong_type == .special, - capture_src, - case_vals, - inline_case_capture, - ); - - if (sema.typeOf(capture_ref).isNoReturn(sema.pt.zcu)) { - // This prong should be unreachable! - return .unreachable_value; - } - - sema.inst_map.putAssumeCapacity(spa.switch_block_inst, capture_ref); - defer assert(sema.inst_map.remove(spa.switch_block_inst)); - - return sema.resolveBlockBody(spa.parent_block, src, child_block, prong_body, spa.switch_block_inst, merges); - }, - } - } - - /// Analyze a switch prong which may have peers at runtime. - /// Uses `analyzeBodyRuntimeBreak`. Sets up captures as needed. - /// Returns the `BranchHint` for the prong. - fn analyzeProngRuntime( - spa: SwitchProngAnalysis, - case_block: *Block, - prong_type: enum { normal, special }, - prong_body: []const Zir.Inst.Index, - capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, - /// Must use the `switch_capture` field in `offset`. - capture_src: LazySrcLoc, - /// The set of all values which can reach this prong. May be undefined - /// if the prong is special or contains ranges. - case_vals: []const Air.Inst.Ref, - /// The inline capture of this prong. If this is not an inline prong, - /// this is `.none`. - inline_case_capture: Air.Inst.Ref, - /// Whether this prong has an inline tag capture. If `true`, then - /// `inline_case_capture` cannot be `.none`. - has_tag_capture: bool, - ) CompileError!std.builtin.BranchHint { - const sema = spa.sema; - - if (has_tag_capture) { - const tag_ref = try spa.analyzeTagCapture(case_block, capture_src, inline_case_capture); - sema.inst_map.putAssumeCapacity(spa.tag_capture_inst, tag_ref); - } - defer if (has_tag_capture) assert(sema.inst_map.remove(spa.tag_capture_inst)); - - switch (capture) { - .none => { - return sema.analyzeBodyRuntimeBreak(case_block, prong_body); - }, - - .by_val, .by_ref => { - const capture_ref = try spa.analyzeCapture( - case_block, - capture == .by_ref, - prong_type == .special, - capture_src, - case_vals, - inline_case_capture, - ); - - if (sema.typeOf(capture_ref).isNoReturn(sema.pt.zcu)) { - // No need to analyze any further, the prong is unreachable - return .none; - } - - sema.inst_map.putAssumeCapacity(spa.switch_block_inst, capture_ref); - defer assert(sema.inst_map.remove(spa.switch_block_inst)); - - return sema.analyzeBodyRuntimeBreak(case_block, prong_body); - }, - } - } - - fn analyzeTagCapture( - spa: SwitchProngAnalysis, - block: *Block, - capture_src: LazySrcLoc, - inline_case_capture: Air.Inst.Ref, - ) CompileError!Air.Inst.Ref { - const sema = spa.sema; - const pt = sema.pt; - const zcu = pt.zcu; - const operand_ty = switch (spa.operand) { - .simple => |s| sema.typeOf(s.by_val), - .loop => |l| ty: { - const alloc_ty = sema.typeOf(l.operand_alloc); - const alloc_child = alloc_ty.childType(zcu); - if (l.operand_is_ref) break :ty alloc_child.childType(zcu); - break :ty alloc_child; - }, - }; - if (operand_ty.zigTypeTag(zcu) != .@"union") { - const tag_capture_src: LazySrcLoc = .{ - .base_node_inst = capture_src.base_node_inst, - .offset = .{ .switch_tag_capture = capture_src.offset.switch_capture }, - }; - return sema.fail(block, tag_capture_src, "cannot capture tag of non-union type '{f}'", .{ - operand_ty.fmt(pt), - }); - } - assert(inline_case_capture != .none); - return inline_case_capture; - } - - fn analyzeCapture( - spa: SwitchProngAnalysis, - block: *Block, - capture_byref: bool, - is_special_prong: bool, - capture_src: LazySrcLoc, - case_vals: []const Air.Inst.Ref, - inline_case_capture: Air.Inst.Ref, - ) CompileError!Air.Inst.Ref { - const sema = spa.sema; - const pt = sema.pt; - const zcu = pt.zcu; - const ip = &zcu.intern_pool; - - const zir_datas = sema.code.instructions.items(.data); - const switch_node_offset = zir_datas[@intFromEnum(spa.switch_block_inst)].pl_node.src_node; - - const operand_src = block.src(.{ .node_offset_switch_operand = switch_node_offset }); - - const operand_val, const operand_ptr = switch (spa.operand) { - .simple => |s| .{ s.by_val, s.by_ref }, - .loop => |l| op: { - const loaded = try sema.analyzeLoad(block, operand_src, l.operand_alloc, operand_src); - if (l.operand_is_ref) { - const by_val = try sema.analyzeLoad(block, operand_src, loaded, operand_src); - break :op .{ by_val, loaded }; - } else { - break :op .{ loaded, undefined }; - } - }, - }; - - const operand_ty = sema.typeOf(operand_val); - const operand_ptr_ty = if (capture_byref) sema.typeOf(operand_ptr) else undefined; - - if (inline_case_capture != .none) { - const item_val = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, inline_case_capture, undefined) catch unreachable; - if (operand_ty.zigTypeTag(zcu) == .@"union") { - 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_byref) { - const ptr_field_ty = try pt.ptrTypeSema(.{ - .child = field_ty.toIntern(), - .flags = .{ - .is_const = !operand_ptr_ty.ptrIsMutable(zcu), - .is_volatile = operand_ptr_ty.isVolatilePtr(zcu), - .address_space = operand_ptr_ty.ptrAddressSpace(zcu), - }, - }); - if (try sema.resolveDefinedValue(block, operand_src, operand_ptr)) |union_ptr| { - return Air.internedToRef((try union_ptr.ptrField(field_index, pt)).toIntern()); - } - return block.addStructFieldPtr(operand_ptr, field_index, ptr_field_ty); - } else { - if (try sema.resolveDefinedValue(block, operand_src, operand_val)) |union_val| { - const tag_and_val = ip.indexToKey(union_val.toIntern()).un; - return Air.internedToRef(tag_and_val.val); - } - return block.addStructFieldVal(operand_val, field_index, field_ty); - } - } else if (capture_byref) { - return sema.uavRef(item_val.toIntern()); - } else { - return inline_case_capture; - } - } - - if (is_special_prong) { - if (capture_byref) { - return operand_ptr; - } - - switch (operand_ty.zigTypeTag(zcu)) { - .error_set => if (spa.else_error_ty) |ty| { - return sema.bitCast(block, ty, operand_val, operand_src, null); - } else { - try sema.analyzeUnreachable(block, operand_src, false); - return .unreachable_value; - }, - else => return operand_val, - } - } - - switch (operand_ty.zigTypeTag(zcu)) { - .@"union" => { - const union_obj = zcu.typeToUnion(operand_ty).?; - const first_item_val = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, case_vals[0], undefined) catch unreachable; - - 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.resolveConstDefinedValue(block, LazySrcLoc.unneeded, item, undefined) catch unreachable; - 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 = if (same_types) first_field_ty else capture_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); - } - - const case_srcs = try sema.arena.alloc(?LazySrcLoc, case_vals.len); - for (case_srcs, 0..) |*case_src, 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, .index = @intCast(i) }, - } }, - }; - } - - break :capture_ty sema.resolvePeerTypes(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_byref) { - const operand_ptr_info = operand_ptr_ty.ptrInfo(zcu); - const capture_ptr_ty = resolve: { - // By-ref captures of hetereogeneous types are only allowed if all field - // pointer types are peer resolvable to each other. - // 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_idx, *dummy| { - const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); - const field_ptr_ty = try pt.ptrTypeSema(.{ - .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, - .alignment = union_obj.fieldAlign(ip, field_idx), - }, - }); - dummy.* = try pt.undefRef(field_ptr_ty); - } - const case_srcs = try sema.arena.alloc(?LazySrcLoc, case_vals.len); - for (case_srcs, 0..) |*case_src, 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, .index = @intCast(i) }, - } }, - }; - } - - break :resolve sema.resolvePeerTypes(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(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 Air.internedToRef((try pt.getCoerced(field_ptr_val, capture_ptr_ty)).toIntern()); - } - - try sema.requireRuntimeBlock(block, operand_src, null); - return block.addStructFieldPtr(operand_ptr, first_field_index, capture_ptr_ty); - } - - if (try sema.resolveDefinedValue(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.internedToRef(union_val.val); - return sema.coerce(block, capture_ty, uncoerced, operand_src); - } - - try sema.requireRuntimeBlock(block, operand_src, null); - - if (same_types) { - return block.addStructFieldVal(operand_val, 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(block, capture_ty, field_ty, false, zcu.getTarget(), LazySrcLoc.unneeded, LazySrcLoc.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 block.addStructFieldVal(operand_val, first_field_index, first_field_ty); - return 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 = try std.DynamicBitSet.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(block, capture_ty, field_ty, false, zcu.getTarget(), LazySrcLoc.unneeded, LazySrcLoc.unneeded, null)) { - in_mem_coercible.unset(i); - } - } - } - - const capture_block_inst = try block.addInstAsIndex(.{ - .tag = .block, - .data = .{ - .ty_pl = .{ - .ty = Air.internedToRef(capture_ty.toIntern()), - .payload = undefined, // updated below - }, - }, - }); - - 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(); - - { - // 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 = 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, .index = @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 - var coerce_block = 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); - _ = 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".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); - - // Set up block body - switch (spa.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 = Air.internedToRef(union_obj.enum_tag_ty), - .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_byref) { - return sema.fail( - block, - capture_src, - "error set cannot be captured by reference", - .{}, - ); - } - - if (case_vals.len == 1) { - const item_val = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, case_vals[0], undefined) catch unreachable; - const item_ty = try pt.singleErrorSetType(item_val.getErrorName(zcu).unwrap().?); - return sema.bitCast(block, item_ty, operand_val, operand_src, null); - } - - var names: InferredErrorSet.NameMap = .{}; - try names.ensureUnusedCapacity(sema.arena, case_vals.len); - for (case_vals) |err| { - const err_val = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, err, undefined) catch unreachable; - names.putAssumeCapacityNoClobber(err_val.getErrorName(zcu).unwrap().?, {}); - } - const error_ty = try pt.errorSetFromUnsortedNames(names.keys()); - return sema.bitCast(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. - if (capture_byref) { - return operand_ptr; - } else { - return operand_val; - } - }, - } - } -}; - -fn switchCond( - sema: *Sema, - block: *Block, - src: LazySrcLoc, - operand: Air.Inst.Ref, -) CompileError!Air.Inst.Ref { - const pt = sema.pt; - const zcu = pt.zcu; - const operand_ty = sema.typeOf(operand); - switch (operand_ty.zigTypeTag(zcu)) { - .type, - .void, - .bool, - .int, - .float, - .comptime_float, - .comptime_int, - .enum_literal, - .pointer, - .@"fn", - .error_set, - .@"enum", - => { - if (operand_ty.isSlice(zcu)) { - return sema.fail(block, src, "switch on type '{f}'", .{operand_ty.fmt(pt)}); - } - if ((try sema.typeHasOnePossibleValue(operand_ty))) |opv| { - return Air.internedToRef(opv.toIntern()); - } - return operand; - }, - - .@"union" => { - try operand_ty.resolveFields(pt); - const enum_ty = operand_ty.unionTagType(zcu) orelse { - const msg = msg: { - const msg = try sema.errMsg(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; - }; - return sema.failWithOwnedErrorMsg(block, msg); - }; - return sema.unionToTag(block, enum_ty, operand, src); - }, - - .error_union, - .noreturn, - .array, - .@"struct", - .undefined, - .null, - .optional, - .@"opaque", - .vector, - .frame, - .@"anyframe", - => return sema.fail(block, src, "switch on type '{f}'", .{operand_ty.fmt(pt)}), - } -} - -const SwitchErrorSet = std.AutoHashMap(InternPool.NullTerminatedString, LazySrcLoc); - fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air.Inst.Ref { const tracy = trace(@src()); defer tracy.end(); @@ -11187,100 +10533,13 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp const pt = sema.pt; const zcu = pt.zcu; const gpa = sema.gpa; - const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].pl_node; - const switch_src = block.nodeOffset(inst_data.src_node); - const switch_src_node_offset = inst_data.src_node; - const switch_operand_src = block.src(.{ .node_offset_switch_operand = switch_src_node_offset }); - const else_prong_src = block.src(.{ .node_offset_switch_else_prong = switch_src_node_offset }); - const extra = sema.code.extraData(Zir.Inst.SwitchBlockErrUnion, inst_data.payload_index); - const main_operand_src = block.src(.{ .node_offset_if_cond = extra.data.main_src_node_offset }); - const main_src = block.src(.{ .node_offset_main_token = extra.data.main_src_node_offset }); - const raw_operand_val = try sema.resolveInst(extra.data.operand); + const zir_switch = sema.code.getSwitchBlock(inst); + const src_node_offset = zir_switch.catch_or_if_src_node_offset.unwrap().?; + const src = block.src(.{ .node_offset_main_token = src_node_offset }); + const operand_src = block.src(.{ .node_offset_if_cond = src_node_offset }); - // AstGen guarantees that the instruction immediately preceding - // switch_block_err_union is a dbg_stmt - const cond_dbg_node_index: Zir.Inst.Index = @enumFromInt(@intFromEnum(inst) - 1); - - var header_extra_index: usize = extra.end; - - const scalar_cases_len = extra.data.bits.scalar_cases_len; - const multi_cases_len = if (extra.data.bits.has_multi_cases) blk: { - const multi_cases_len = sema.code.extra[header_extra_index]; - header_extra_index += 1; - break :blk multi_cases_len; - } else 0; - - const err_capture_inst: Zir.Inst.Index = if (extra.data.bits.any_uses_err_capture) blk: { - const err_capture_inst: Zir.Inst.Index = @enumFromInt(sema.code.extra[header_extra_index]); - header_extra_index += 1; - // SwitchProngAnalysis wants inst_map to have space for the tag capture. - // Note that the normal capture is referred to via the switch block - // index, which there is already necessarily space for. - try sema.inst_map.ensureSpaceForInstructions(gpa, &.{err_capture_inst}); - break :blk err_capture_inst; - } else undefined; - - var case_vals = try std.ArrayList(Air.Inst.Ref).initCapacity(gpa, scalar_cases_len + 2 * multi_cases_len); - defer case_vals.deinit(gpa); - - const NonError = struct { - body: []const Zir.Inst.Index, - end: usize, - capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, - }; - - const non_error_case: NonError = non_error: { - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[header_extra_index]); - const extra_body_start = header_extra_index + 1; - break :non_error .{ - .body = sema.code.bodySlice(extra_body_start, info.body_len), - .end = extra_body_start + info.body_len, - .capture = info.capture, - }; - }; - - const Else = struct { - body: []const Zir.Inst.Index, - end: usize, - is_inline: bool, - has_capture: bool, - }; - - const else_case: Else = if (!extra.data.bits.has_else) .{ - .body = &.{}, - .end = non_error_case.end, - .is_inline = false, - .has_capture = false, - } else special: { - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[non_error_case.end]); - const extra_body_start = non_error_case.end + 1; - assert(info.capture != .by_ref); - assert(!info.has_tag_capture); - break :special .{ - .body = sema.code.bodySlice(extra_body_start, info.body_len), - .end = extra_body_start + info.body_len, - .is_inline = info.is_inline, - .has_capture = info.capture != .none, - }; - }; - - var seen_errors = SwitchErrorSet.init(gpa); - defer seen_errors.deinit(); - - const operand_ty = sema.typeOf(raw_operand_val); - const operand_err_set = if (extra.data.bits.payload_is_ref) - operand_ty.childType(zcu) - else - operand_ty; - - if (operand_err_set.zigTypeTag(zcu) != .error_union) { - return sema.fail(block, switch_src, "expected error union type, found '{f}'", .{ - operand_ty.fmt(pt), - }); - } - - const operand_err_set_ty = operand_err_set.errorUnionSet(zcu); + assert(!zir_switch.has_continue); // wrong codepath! const block_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len); try sema.air_instructions.append(gpa, .{ @@ -11296,396 +10555,1403 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp .block_inst = block_inst, }, }; - - var child_block: Block = .{ - .parent = block, - .sema = sema, - .namespace = block.namespace, - .instructions = .{}, - .label = &label, - .inlining = block.inlining, - .comptime_reason = block.comptime_reason, - .is_typeof = block.is_typeof, - .c_import_buf = block.c_import_buf, - .runtime_cond = block.runtime_cond, - .runtime_loop = block.runtime_loop, - .runtime_index = block.runtime_index, - .error_return_trace_index = block.error_return_trace_index, - .want_safety = block.want_safety, - .src_base_inst = block.src_base_inst, - .type_name_ctx = block.type_name_ctx, - }; + var child_block = block.makeSubBlock(); + child_block.label = &label; const merges = &child_block.label.?.merges; defer child_block.instructions.deinit(gpa); defer merges.deinit(gpa); - const resolved_err_set = try sema.resolveInferredErrorSetTy(block, main_src, operand_err_set_ty.toIntern()); - if (Type.fromInterned(resolved_err_set).errorSetIsEmpty(zcu)) { - return sema.resolveBlockBody(block, main_operand_src, &child_block, non_error_case.body, inst, merges); - } + const non_err_case = zir_switch.non_err_case.?; - const else_error_ty: ?Type = try validateErrSetSwitch( - sema, - block, - &seen_errors, - &case_vals, - operand_err_set_ty, - inst_data, - scalar_cases_len, - multi_cases_len, - .{ .body = else_case.body, .end = else_case.end, .src = else_prong_src }, - extra.data.bits.has_else, - ); + var non_err_block: Block = child_block.makeSubBlock(); + non_err_block.runtime_loop = null; + non_err_block.runtime_cond = operand_src; + non_err_block.runtime_index.increment(); + non_err_block.need_debug_scope = null; + defer non_err_block.instructions.deinit(gpa); - var spa: SwitchProngAnalysis = .{ - .sema = sema, - .parent_block = block, - .operand = .{ - .simple = .{ - .by_val = undefined, // must be set to the unwrapped error code before use - .by_ref = undefined, - .cond = raw_operand_val, - }, - }, - .else_error_ty = else_error_ty, - .switch_block_inst = inst, - .tag_capture_inst = undefined, + var switch_block: Block = child_block.makeSubBlock(); + switch_block.runtime_loop = null; + switch_block.runtime_cond = operand_src; + switch_block.runtime_index.increment(); + switch_block.need_debug_scope = null; + defer switch_block.instructions.deinit(gpa); + + // We begin with unwrapping the error union we're switching on as necessary. + // Then we analyze the non-error prong if it's not comptime-unreachable. + // Lastly, we analyze the error prong(s) as a regular switch. + + const raw_switch_operand, const non_err_reachable, const non_err_cond, const non_err_hint, const err_set_empty = non_err: { + const eu_maybe_ptr = try sema.resolveInst(zir_switch.main_operand); + const err_union_ty: Type = err_union_ty: { + const raw_operand_ty = sema.typeOf(eu_maybe_ptr); + if (!non_err_case.operand_is_ref) break :err_union_ty raw_operand_ty; + try sema.checkPtrOperand(block, operand_src, raw_operand_ty); + break :err_union_ty raw_operand_ty.childType(zcu); + }; + if (err_union_ty.zigTypeTag(zcu) != .error_union) { + return sema.fail(block, src, "expected error union type, found '{f}'", .{ + err_union_ty.fmt(pt), + }); + } + + const non_err_cond = if (non_err_case.operand_is_ref) + try sema.analyzePtrIsNonErr(block, operand_src, eu_maybe_ptr) + else + try sema.analyzeIsNonErr(block, operand_src, eu_maybe_ptr); + + const is_non_err = try sema.resolveDefinedValue(block, operand_src, non_err_cond); + const non_err_reachable = if (is_non_err) |val| val.toBool() else true; + + const err_set_empty = err_set_empty: { + const err_set_ty = err_union_ty.errorUnionSet(zcu); + break :err_set_empty err_set_ty.errorSetIsEmpty(zcu); + }; + + const non_err_hint: std.builtin.BranchHint = hint: { + // don't analyze the non-error body if it's unreachable + if (!non_err_reachable) { + break :hint undefined; + } + + const eu_payload: Air.Inst.Ref = switch (non_err_case.capture) { + .by_val => try sema.analyzeErrUnionPayload(&non_err_block, src, err_union_ty, eu_maybe_ptr, operand_src, false), + .by_ref => try sema.analyzeErrUnionPayloadPtr(&non_err_block, src, eu_maybe_ptr, false, false), + .none => undefined, + }; + if (non_err_case.capture != .none) sema.inst_map.putAssumeCapacity(inst, eu_payload); + defer if (non_err_case.capture != .none) assert(sema.inst_map.remove(inst)); + + const always_non_err = if (is_non_err) |val| val.toBool() else err_set_empty; + if (always_non_err) { + // Early return; we don't analyze the switch as it's unreachable. + return sema.resolveBlockBody(block, src, &non_err_block, non_err_case.body, inst, merges); + } + break :hint try sema.analyzeBodyRuntimeBreak(&non_err_block, non_err_case.body); + }; + + // Emit this into the switch block as it's our error case! + const eu_code = if (non_err_case.operand_is_ref) + try sema.analyzeErrUnionCodePtr(&switch_block, operand_src, eu_maybe_ptr) + else + try sema.analyzeErrUnionCode(&switch_block, operand_src, eu_maybe_ptr); + + break :non_err .{ eu_code, non_err_reachable, non_err_cond, non_err_hint, err_set_empty }; }; - if (try sema.resolveDefinedValue(&child_block, main_src, raw_operand_val)) |ov| { - const operand_val = if (extra.data.bits.payload_is_ref) - (try sema.pointerDeref(&child_block, main_src, ov, operand_ty)).? - else - ov; + const validated_switch = try sema.validateSwitchBlock(block, raw_switch_operand, false, inst, &zir_switch); - if (operand_val.errorUnionIsPayload(zcu)) { - return sema.resolveBlockBody(block, main_operand_src, &child_block, non_error_case.body, inst, merges); - } else { - const err_val = Value.fromInterned(try pt.intern(.{ - .err = .{ - .ty = operand_err_set_ty.toIntern(), - .name = operand_val.getErrorName(zcu).unwrap().?, - }, - })); - spa.operand.simple.by_val = if (extra.data.bits.payload_is_ref) - try sema.analyzeErrUnionCodePtr(block, switch_operand_src, raw_operand_val) - else - try sema.analyzeErrUnionCode(block, switch_operand_src, raw_operand_val); + const maybe_switch_ref: ?Air.Inst.Ref = ref: { + if (err_set_empty) break :ref .unreachable_value; + // make err capture (i.e. switch operand) available to switch prong bodies + sema.inst_map.putAssumeCapacityNoClobber(inst, raw_switch_operand); + defer assert(sema.inst_map.remove(inst)); + break :ref try sema.analyzeSwitchBlock(block, &switch_block, raw_switch_operand, false, merges, inst, &zir_switch, &validated_switch); + }; - if (extra.data.bits.any_uses_err_capture) { - sema.inst_map.putAssumeCapacity(err_capture_inst, spa.operand.simple.by_val); - } - defer if (extra.data.bits.any_uses_err_capture) assert(sema.inst_map.remove(err_capture_inst)); - - return resolveSwitchComptime( - sema, - spa, - &child_block, - try sema.switchCond(block, switch_operand_src, spa.operand.simple.by_val), - err_val, - operand_err_set_ty, - switch_src_node_offset, - null, - .{ - .body = else_case.body, - .end = else_case.end, - .capture = if (else_case.has_capture) .by_val else .none, - .is_inline = else_case.is_inline, - .has_tag_capture = false, - }, - false, - case_vals, - scalar_cases_len, - multi_cases_len, - true, - false, - ); - } - } - - if (scalar_cases_len + multi_cases_len == 0) { - if (else_error_ty) |ty| if (ty.errorSetIsEmpty(zcu)) { - return sema.resolveBlockBody(block, main_operand_src, &child_block, non_error_case.body, inst, merges); + if (!non_err_reachable) { + return maybe_switch_ref orelse { + const switch_src = block.nodeOffset(zir_switch.switch_src_node_offset); + return sema.resolveAnalyzedBlock(block, switch_src, &switch_block, merges, false); }; } + if (maybe_switch_ref) |switch_ref| { + if (sema.typeOf(switch_ref).isNoReturn(zcu)) { + _ = try switch_block.addNoOp(.unreach); + } else { + const br_ref = try switch_block.addBr(merges.block_inst, switch_ref); + try merges.results.append(gpa, switch_ref); + try merges.br_list.append(gpa, br_ref.toIndex().?); + try merges.src_locs.append(gpa, null); + } + } + + try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.CondBr).@"struct".fields.len + + non_err_block.instructions.items.len + switch_block.instructions.items.len); + const cond_br_payload = sema.addExtraAssumeCapacity(Air.CondBr{ + .then_body_len = @intCast(non_err_block.instructions.items.len), + .else_body_len = @intCast(switch_block.instructions.items.len), + .branch_hints = .{ + .true = non_err_hint, + .false = .unlikely, // errors are unlikely + // Code coverage is desired for error handling. + .then_cov = .poi, + .else_cov = .poi, + }, + }); + sema.air_extra.appendSliceAssumeCapacity(@ptrCast(non_err_block.instructions.items)); + sema.air_extra.appendSliceAssumeCapacity(@ptrCast(switch_block.instructions.items)); + + _ = try child_block.addInst(.{ .tag = .cond_br, .data = .{ .pl_op = .{ + .operand = non_err_cond, + .payload = cond_br_payload, + } } }); + + return sema.resolveAnalyzedBlock(block, src, &child_block, merges, false); +} + +fn zirSwitchBlock( + sema: *Sema, + block: *Block, + inst: Zir.Inst.Index, + operand_is_ref: bool, +) CompileError!Air.Inst.Ref { + const tracy = trace(@src()); + defer tracy.end(); + const zir_switch = sema.code.getSwitchBlock(inst); + + const block_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len); + try sema.air_instructions.append(sema.gpa, .{ + .tag = .block, + .data = undefined, + }); + var label: Block.Label = .{ + .zir_block = inst, + .merges = .{ + .src_locs = .{}, + .results = .{}, + .br_list = .{}, + .block_inst = block_inst, + }, + }; + var child_block = block.makeSubBlock(); + child_block.label = &label; + const merges = &child_block.label.?.merges; + defer child_block.instructions.deinit(sema.gpa); + defer merges.deinit(sema.gpa); + + const raw_operand = try sema.resolveInst(zir_switch.main_operand); + const validated_switch = try sema.validateSwitchBlock(block, raw_operand, operand_is_ref, inst, &zir_switch); + const maybe_ref = try sema.analyzeSwitchBlock(block, &child_block, raw_operand, operand_is_ref, merges, inst, &zir_switch, &validated_switch); + return maybe_ref orelse { + const src = block.nodeOffset(zir_switch.switch_src_node_offset); + return sema.resolveAnalyzedBlock(block, src, &child_block, merges, false); + }; +} + +/// If the switch can be resolved to a value at comptime, this will return a `Ref` +/// that's never `.none`. +/// If not, this will return `null` and emit its instructions into `child_block`. +fn analyzeSwitchBlock( + sema: *Sema, + block: *Block, + child_block: *Block, + raw_operand: Air.Inst.Ref, + operand_is_ref: bool, + merges: *Block.Merges, + switch_inst: Zir.Inst.Index, + zir_switch: *const Zir.UnwrappedSwitchBlock, + validated_switch: *const ValidatedSwitchBlock, +) CompileError!?Air.Inst.Ref { + const pt = sema.pt; + const zcu = pt.zcu; + const gpa = sema.gpa; + + const src_node_offset = zir_switch.switch_src_node_offset; + const src = block.nodeOffset(src_node_offset); + const operand_src = block.src(.{ .node_offset_switch_operand = src_node_offset }); + + const has_else = zir_switch.else_case != null; + const has_under = zir_switch.under_case != .none; + + const else_case = validated_switch.else_case; + const under_case = validated_switch.under_case; + + const operand: SwitchOperand, const operand_ty: Type, const maybe_operand_opv: ?Value, const item_ty: Type = operand: { + const val, const ref = if (operand_is_ref) + .{ try sema.analyzeLoad(block, src, raw_operand, operand_src), raw_operand } + else + .{ raw_operand, undefined }; + + const operand_ty = sema.typeOf(val); + const maybe_operand_opv = try sema.typeHasOnePossibleValue(operand_ty); + const init_cond: Air.Inst.Ref, const item_ty: Type = switch (operand_ty.zigTypeTag(zcu)) { + .@"union" => tag: { + const tag_ty = operand_ty.unionTagType(zcu).?; + const tag_val = try sema.unionToTag(block, tag_ty, val, operand_src); + break :tag .{ tag_val, tag_ty }; + }, + else => .{ + if (maybe_operand_opv) |operand_opv| .fromValue(operand_opv) else val, + operand_ty, + }, + }; + + if (zir_switch.has_continue and !block.isComptime()) { + const operand_alloc: Air.Inst.Ref = if (zir_switch.any_maybe_runtime_capture and + maybe_operand_opv == null) + alloc: { + const operand_ptr_ty = try pt.singleMutPtrType(sema.typeOf(raw_operand)); + const operand_alloc = try block.addTy(.alloc, operand_ptr_ty); + _ = try block.addBinOp(.store, operand_alloc, raw_operand); + break :alloc operand_alloc; + } else undefined; + break :operand .{ .{ .loop = .{ + .operand_alloc = operand_alloc, + .operand_is_ref = operand_is_ref, + .init_cond = init_cond, + } }, operand_ty, maybe_operand_opv, item_ty }; + } else { + // We always use `simple` in the comptime/OPV case, because as far as the + // dispatching logic is concerned, it really is dispatching a single prong. + break :operand .{ .{ .simple = .{ + .by_val = val, + .by_ref = ref, + .cond = init_cond, + } }, operand_ty, maybe_operand_opv, item_ty }; + } + }; + + const raw_operand_ty = sema.typeOf(raw_operand); + + const union_originally = operand_ty.zigTypeTag(zcu) == .@"union"; + const err_set = operand_ty.zigTypeTag(zcu) == .error_set; + + if (item_ty.zigTypeTag(zcu) == .@"enum" and + validated_switch.seen_enum_fields.len == 0 and + !operand_ty.isNonexhaustiveEnum(zcu)) + { + return .void_value; // switch on empty enum/union + } + + const cond_ref = switch (operand) { + .simple => |s| s.cond, + .loop => |l| l.init_cond, + }; + + // We treat `else` and `_` the same, except if both are present. + const else_is_named_only = has_else and has_under; + const catch_all_case: CatchAllSwitchCase = + if (has_under) .under else if (has_else) .@"else" else .none; + + resolve_at_comptime: { + // always runtime; evaluation in comptime scope uses `simple` + if (operand == .loop) break :resolve_at_comptime; + + var cur_cond_val = try sema.resolveDefinedValue(child_block, src, cond_ref) orelse { + break :resolve_at_comptime; + }; + var cur_operand = operand; + + while (true) { + if (sema.resolveSwitchBlock( + block, + child_block, + cur_operand, + raw_operand_ty, + cur_cond_val, + catch_all_case, + else_is_named_only, + merges, + switch_inst, + zir_switch, + validated_switch, + )) |result| { + return result; + } else |err| switch (err) { + error.ComptimeBreak => { + const break_inst = sema.code.instructions.get(@intFromEnum(sema.comptime_break_inst)); + if (break_inst.tag != .switch_continue) return error.ComptimeBreak; + const extra = sema.code.extraData(Zir.Inst.Break, break_inst.data.@"break".payload_index).data; + if (extra.block_inst != switch_inst) return error.ComptimeBreak; + // This is a `switch_continue` targeting this block. Change the operand and start over. + const new_operand_src = child_block.nodeOffset(extra.operand_src_node.unwrap().?); + const new_operand_uncoerced = try sema.resolveInst(break_inst.data.@"break".operand); + const new_operand = try sema.coerce(child_block, raw_operand_ty, new_operand_uncoerced, new_operand_src); + + try sema.emitBackwardBranch(child_block, src); + + const new_val, const new_ref = if (operand_is_ref) + .{ try sema.analyzeLoad(child_block, src, new_operand, new_operand_src), new_operand } + else + .{ new_operand, undefined }; + + const new_cond_ref = if (union_originally) + try sema.unionToTag(child_block, item_ty, new_val, src) + else + new_val; + + cur_cond_val = try sema.resolveConstDefinedValue(child_block, src, new_cond_ref, null); + cur_operand = .{ .simple = .{ + .by_val = new_val, + .by_ref = new_ref, + .cond = new_cond_ref, + } }; + }, + else => |e| return e, + } + } + } + if (child_block.isComptime()) { - _ = try sema.resolveConstDefinedValue(&child_block, main_operand_src, raw_operand_val, null); + _ = try sema.resolveConstDefinedValue(child_block, operand_src, operand.simple.cond, null); unreachable; } - const cond = if (extra.data.bits.payload_is_ref) blk: { - try sema.checkErrorType(block, main_src, sema.typeOf(raw_operand_val).elemType2(zcu)); - const loaded = try sema.analyzeLoad(block, main_src, raw_operand_val, main_src); - break :blk try sema.analyzeIsNonErr(block, main_src, loaded); - } else blk: { - try sema.checkErrorType(block, main_src, sema.typeOf(raw_operand_val)); - break :blk try sema.analyzeIsNonErr(block, main_src, raw_operand_val); - }; + if (try sema.typeHasOnePossibleValue(item_ty)) |item_opv| { + // 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 sub_block = child_block.makeSubBlock(); - sub_block.runtime_loop = null; - sub_block.runtime_cond = main_operand_src; - sub_block.runtime_index.increment(); - sub_block.need_debug_scope = null; // this body is emitted regardless - defer sub_block.instructions.deinit(gpa); + var case_block = child_block.makeSubBlock(); + case_block.runtime_loop = null; + case_block.runtime_cond = operand_src; + case_block.runtime_index.increment(); + case_block.need_debug_scope = null; // this body is emitted regardless + defer case_block.instructions.deinit(gpa); - const non_error_hint = try sema.analyzeBodyRuntimeBreak(&sub_block, non_error_case.body); - const true_instructions = try sub_block.instructions.toOwnedSlice(gpa); - defer gpa.free(true_instructions); + const case_vals = validated_switch.case_vals; - spa.operand.simple.by_val = if (extra.data.bits.payload_is_ref) - try sema.analyzeErrUnionCodePtr(&sub_block, switch_operand_src, raw_operand_val) - else - try sema.analyzeErrUnionCode(&sub_block, switch_operand_src, raw_operand_val); + const index, const body, const capture, const has_tag_capture, const is_inline, const is_special = find_prong: { + var case_val_idx: usize = 0; + var case_it = zir_switch.iterateCases(); + var extra_index = zir_switch.end; + while (case_it.next()) |case| { + const prong_info = case.prong_info; + const prong_body = sema.code.bodySlice(extra_index, prong_info.body_len); + extra_index += prong_body.len; + skip_case: { + if (!err_set) break :skip_case; + // This case might consist of errors which are not in the set + // we're switching on. If so we have to skip it! + const item_refs = case_vals[case_val_idx..][0..case.item_infos.len]; + case_val_idx += item_refs.len; + assert(case.range_infos.len == 0); + for (case.item_infos, item_refs) |item_info, item_ref| { + if (item_info.bodyLen()) |body_len| extra_index += body_len; + if (sema.wantSwitchProngBodyAnalysis(block, item_ref, operand_ty, false, true, prong_info.is_comptime_unreach)) { + break :skip_case; + } + } + continue; + } + break :find_prong .{ case.index, prong_body, prong_info.capture, prong_info.has_tag_capture, prong_info.is_inline, false }; + } + 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 }; + } + if (has_under) { + break :find_prong .{ under_case.index, under_case.body, under_case.capture, under_case.has_tag_capture, false, true }; + } + unreachable; // malformed validated switch + }; - if (extra.data.bits.any_uses_err_capture) { - sema.inst_map.putAssumeCapacity(err_capture_inst, spa.operand.simple.by_val); + const analyze_body = sema.wantSwitchProngBodyAnalysis(block, .fromValue(item_opv), operand_ty, union_originally, err_set, false); + if (!analyze_body) return .unreachable_value; + + 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 = zir_switch.payload_capture_placeholder.unwrap() orelse switch_inst; + const payload_ref: Air.Inst.Ref = payload_ref: { + const item_val: InternPool.Index = switch (operand_ty.zigTypeTag(zcu)) { + .@"union" => item_val: { + if (maybe_operand_opv) |operand_opv| { + break :item_val 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, undefined }; + } + }, + }; + 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, + is_special, + if (!is_special) case_vals else undefined, + if (is_inline) .fromValue(item_opv) else .none, + validated_switch.else_err_ty, + ); + }, + else => item_opv.toIntern(), + }; + break :payload_ref switch (capture) { + .by_val => .fromIntern(item_val), + .by_ref => try sema.uavRef(item_val), + .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; + 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; + sema.inst_map.putAssumeCapacity(tag_inst, .fromValue(item_opv)); + break :inst tag_inst; + } else undefined; + defer if (has_tag_capture) assert(sema.inst_map.remove(tag_inst)); + + if (zir_switch.has_continue) sema.inst_map.putAssumeCapacity(switch_inst, .fromType(raw_operand_ty)); + defer if (zir_switch.has_continue) assert(sema.inst_map.remove(switch_inst)); + + _ = try sema.analyzeBodyRuntimeBreak(&case_block, body); + } + + try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.Block).@"struct".fields.len + + case_block.instructions.items.len); + const payload_index = sema.addExtraAssumeCapacity(Air.Block{ + .body_len = @intCast(case_block.instructions.items.len), + }); + sema.air_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); + + const air_tag: Air.Inst.Tag = if (merges.extra_insts.items.len > 0) + .loop + else + .block; + const air_loop_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; } - defer if (extra.data.bits.any_uses_err_capture) assert(sema.inst_map.remove(err_capture_inst)); - _ = try sema.analyzeSwitchRuntimeBlock( - spa, - &sub_block, - switch_src, - try sema.switchCond(block, switch_operand_src, spa.operand.simple.by_val), - operand_err_set_ty, - switch_operand_src, - case_vals, - .{ - .body = else_case.body, - .end = else_case.end, - .capture = if (else_case.has_capture) .by_val else .none, - .is_inline = else_case.is_inline, - .has_tag_capture = false, - }, - scalar_cases_len, - multi_cases_len, - false, - undefined, - true, - switch_src_node_offset, - else_prong_src, - false, - undefined, - seen_errors, - undefined, - undefined, - undefined, - cond_dbg_node_index, - true, - null, - undefined, - &.{}, - &.{}, + + assert(maybe_operand_opv == null); // `operand_ty` can only be an OPV type if `item_ty` is one too! + + try sema.finishSwitchBr( + block, + child_block, + operand, + raw_operand_ty, + operand_is_ref, + merges, + switch_inst, + zir_switch, + validated_switch, ); - - try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.CondBr).@"struct".fields.len + - true_instructions.len + sub_block.instructions.items.len); - - _ = try child_block.addInst(.{ - .tag = .cond_br, - .data = .{ - .pl_op = .{ - .operand = cond, - .payload = sema.addExtraAssumeCapacity(Air.CondBr{ - .then_body_len = @intCast(true_instructions.len), - .else_body_len = @intCast(sub_block.instructions.items.len), - .branch_hints = .{ - .true = non_error_hint, - .false = .none, - // Code coverage is desired for error handling. - .then_cov = .poi, - .else_cov = .poi, - }, - }), - }, - }, - }); - sema.air_extra.appendSliceAssumeCapacity(@ptrCast(true_instructions)); - sema.air_extra.appendSliceAssumeCapacity(@ptrCast(sub_block.instructions.items)); - - return sema.resolveAnalyzedBlock(block, main_src, &child_block, merges, false); + return null; } -fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_ref: bool) CompileError!Air.Inst.Ref { - const tracy = trace(@src()); - defer tracy.end(); - +fn finishSwitchBr( + sema: *Sema, + block: *Block, + child_block: *Block, + operand: SwitchOperand, + raw_operand_ty: Type, + operand_is_ref: bool, + merges: *Block.Merges, + switch_inst: Zir.Inst.Index, + zir_switch: *const Zir.UnwrappedSwitchBlock, + validated_switch: *const ValidatedSwitchBlock, +) CompileError!void { const pt = sema.pt; const zcu = pt.zcu; const ip = &zcu.intern_pool; const gpa = sema.gpa; - const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].pl_node; - const src = block.nodeOffset(inst_data.src_node); - const src_node_offset = inst_data.src_node; + + const src_node_offset = zir_switch.switch_src_node_offset; + const src = block.nodeOffset(src_node_offset); + const operand_src = block.src(.{ .node_offset_switch_operand = src_node_offset }); + + const has_else = zir_switch.else_case != null; + const has_under = zir_switch.under_case != .none; + + const else_case = validated_switch.else_case; + const under_case = validated_switch.under_case; + + const scalar_cases_len = zir_switch.scalarCasesLen(); + const multi_cases_len = zir_switch.multiCasesLen(); + + const operand_ty = if (operand_is_ref) + raw_operand_ty.childType(zcu) + else + raw_operand_ty; + + const cond_ref = switch (operand) { + .simple => |s| s.cond, + .loop => |l| l.init_cond, + }; + + // AstGen guarantees that the instruction immediately preceding + // switch_block[_ref]/switch_block_err_union is a dbg_stmt. + const cond_dbg_node_index: Zir.Inst.Index = @enumFromInt(@intFromEnum(switch_inst) - 1); + + const else_is_named_only = has_else and has_under; + const catch_all_case: CatchAllSwitchCase = + if (has_under) .under else if (has_else) .@"else" else .none; + + 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 = operand_ty.zigTypeTag(zcu) == .error_set; + + const estimated_cases_len: u32 = scalar_cases_len + multi_cases_len + + @intFromBool(has_else or has_under); + + var cases_extra: std.ArrayList(u32) = try .initCapacity(gpa, estimated_cases_len * + @typeInfo(Air.SwitchBr.Case).@"struct".fields.len); + defer cases_extra.deinit(gpa); + var branch_hints: Air.SwitchBr.BranchHints = try .initCapacity(gpa, estimated_cases_len); + defer branch_hints.deinit(gpa); + + // We will reuse this block for each case. + var case_block = child_block.makeSubBlock(); + case_block.runtime_loop = null; + case_block.runtime_cond = operand_src; + case_block.runtime_index.increment(); + case_block.need_debug_scope = null; // this body is emitted regardless + defer case_block.instructions.deinit(gpa); + + const case_vals = validated_switch.case_vals; + var case_val_idx: usize = 0; + var case_it = zir_switch.iterateCases(); + var extra_index = zir_switch.end; + + var cases_len: u32 = 0; + while (case_it.next()) |case| { + if (case.isUnder()) { // we'll deal with this later + extra_index += case.prong_info.body_len; + for (case.item_infos) |item_info| { + if (item_info.bodyLen()) |body_len| extra_index += body_len; + } + for (case.range_infos) |range_info| { + if (range_info[0].bodyLen()) |body_len| extra_index += body_len; + if (range_info[1].bodyLen()) |body_len| extra_index += body_len; + } + continue; + } + + const item_refs = case_vals[case_val_idx..][0..case.item_infos.len]; + case_val_idx += item_refs.len; + const range_refs: []const [2]Air.Inst.Ref = + @ptrCast(case_vals[case_val_idx..][0 .. 2 * case.range_infos.len]); + case_val_idx += 2 * range_refs.len; + + const prong_info = case.prong_info; + const prong_body = sema.code.bodySlice(extra_index, prong_info.body_len); + extra_index += prong_body.len; + + // Enough capacity for inlining regular items, we can't really predict + // how many range items we will end up with (at least not in a safe and + // cheap manner) so we allocate on demand for those. + if (prong_info.is_inline) { + try branch_hints.ensureUnusedCapacity(gpa, @intCast(case.item_infos.len)); + } + + var emit_bb = false; + var any_analyze_body = false; + for (case.item_infos, item_refs, 0..) |item_info, item_ref, item_i| { + if (item_info.bodyLen()) |body_len| extra_index += body_len; + + const analyze_body = sema.wantSwitchProngBodyAnalysis(block, item_ref, operand_ty, union_originally, err_set, prong_info.is_comptime_unreach); + if (analyze_body) any_analyze_body = true; + + if (prong_info.is_inline) { + cases_len += 1; + case_block.instructions.clearRetainingCapacity(); + case_block.error_return_trace_index = child_block.error_return_trace_index; + + if (emit_bb) { + const bb_src = block.src(.{ .switch_case_item = .{ + .switch_node_offset = src_node_offset, + .case_idx = case.index, + .item_idx = .{ .kind = .single, .value = @intCast(item_i) }, + } }); + try sema.emitBackwardBranch(block, bb_src); + } + emit_bb = true; + + const prong_hint: std.builtin.BranchHint = hint: { + if (analyze_body) break :hint try sema.analyzeSwitchProng( + &case_block, + operand, + operand_ty, + raw_operand_ty, + prong_body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = src_node_offset, + .case_idx = case.index, + } }), + prong_info.capture, + prong_info.has_tag_capture, + item_ref, + .{ .item_refs = &.{item_ref} }, + validated_switch.else_err_ty, + switch_inst, + zir_switch, + ); + _ = try case_block.addNoOp(.unreach); + break :hint .cold; // unreachable branches are cold + }; + branch_hints.appendAssumeCapacity(prong_hint); + + try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + + 1 + // `item`, no ranges + case_block.instructions.items.len); + cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ + .items_len = 1, + .ranges_len = 0, + .body_len = @intCast(case_block.instructions.items.len), + })); + cases_extra.appendAssumeCapacity(@intFromEnum(item_ref)); + cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); + } + } + for (case.range_infos, range_refs, 0..) |range_info, range_ref, range_i| { + if (range_info[0].bodyLen()) |body_len| extra_index += body_len; + if (range_info[1].bodyLen()) |body_len| extra_index += body_len; + + any_analyze_body = true; // always an integer range, always needs analysis + + if (prong_info.is_inline) { + var item = sema.resolveConstDefinedValue(block, .unneeded, range_ref[0], undefined) catch unreachable; + const item_last = sema.resolveConstDefinedValue(block, .unneeded, range_ref[1], undefined) catch unreachable; + + if (try item.getUnsignedIntSema(pt)) |first_int| { + if (try item_last.getUnsignedIntSema(pt)) |last_int| { + if (std.math.cast(u32, last_int - first_int)) |range_len| { + try branch_hints.ensureUnusedCapacity(gpa, range_len); + } + } + } + + var prev_result_overflowed = false; + while (item.compareScalar(.lte, item_last, operand_ty, zcu)) : ({ + // Previous validation has resolved any possible lazy values. + const int_val: Value, const int_ty: Type = switch (operand_ty.zigTypeTag(zcu)) { + .int => .{ item, operand_ty }, + .@"enum" => b: { + const int_val: Value = .fromInterned(ip.indexToKey(item.toIntern()).enum_tag.int); + break :b .{ int_val, int_val.typeOf(zcu) }; + }, + else => unreachable, + }; + assert(!prev_result_overflowed); + const result = try arith.incrementDefinedInt(sema, int_ty, int_val); + prev_result_overflowed = result.overflow; + item = switch (operand_ty.zigTypeTag(zcu)) { + .int => result.val, + .@"enum" => .fromInterned(try pt.intern(.{ .enum_tag = .{ + .ty = operand_ty.toIntern(), + .int = result.val.toIntern(), + } })), + else => unreachable, + }; + }) { + cases_len += 1; + case_block.instructions.clearRetainingCapacity(); + case_block.error_return_trace_index = child_block.error_return_trace_index; + + const item_ref: Air.Inst.Ref = .fromValue(item); + + if (emit_bb) { + const bb_src = block.src(.{ .switch_case_item = .{ + .switch_node_offset = src_node_offset, + .case_idx = case.index, + .item_idx = .{ .kind = .range, .value = @intCast(range_i) }, + } }); + try sema.emitBackwardBranch(block, bb_src); + } + emit_bb = true; + + const prong_hint = try sema.analyzeSwitchProng( + &case_block, + operand, + operand_ty, + raw_operand_ty, + prong_body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = src_node_offset, + .case_idx = case.index, + } }), + prong_info.capture, + prong_info.has_tag_capture, + item_ref, + .has_ranges, + validated_switch.else_err_ty, + switch_inst, + zir_switch, + ); + try branch_hints.append(gpa, prong_hint); + + try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + + 1 + // `item`, no ranges + case_block.instructions.items.len); + cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ + .items_len = 1, + .ranges_len = 0, + .body_len = @intCast(case_block.instructions.items.len), + })); + cases_extra.appendAssumeCapacity(@intFromEnum(item_ref)); + cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); + } + } + } + + if (!prong_info.is_inline) { + cases_len += 1; + case_block.instructions.clearRetainingCapacity(); + case_block.error_return_trace_index = child_block.error_return_trace_index; + + const prong_hint: std.builtin.BranchHint = hint: { + if (any_analyze_body) break :hint try sema.analyzeSwitchProng( + &case_block, + operand, + operand_ty, + raw_operand_ty, + prong_body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = src_node_offset, + .case_idx = case.index, + } }), + prong_info.capture, + prong_info.has_tag_capture, + .none, + .{ .item_refs = item_refs }, + validated_switch.else_err_ty, + switch_inst, + zir_switch, + ); + _ = try case_block.addNoOp(.unreach); + break :hint .cold; // unreachable branches are cold + }; + try branch_hints.append(gpa, prong_hint); + + try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + + item_refs.len + + 2 * range_refs.len + + case_block.instructions.items.len); + cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ + .items_len = @intCast(item_refs.len), + .ranges_len = @intCast(range_refs.len), + .body_len = @intCast(case_block.instructions.items.len), + })); + cases_extra.appendSliceAssumeCapacity(@ptrCast(item_refs)); + cases_extra.appendSliceAssumeCapacity(@ptrCast(range_refs)); + cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); + } + } + + const catch_all_extra: []const u32 = catch_all_extra: { + if (catch_all_case == .none and !case_block.wantSafety()) { + try branch_hints.append(gpa, .none); + break :catch_all_extra &.{}; + } + var emit_bb = false; + if (has_else and else_case.is_inline) { + const else_prong_src = block.src(.{ .node_offset_switch_else_prong = src_node_offset }); + var error_names: InternPool.NullTerminatedString.Slice = undefined; + var min_int: Value = undefined; + check_enumerable: { + switch (item_ty.zigTypeTag(zcu)) { + .@"union" => unreachable, + .@"enum" => if (else_is_named_only or + !item_ty.isNonexhaustiveEnum(zcu) or union_originally) + { + try branch_hints.ensureUnusedCapacity(gpa, @intCast(validated_switch.seen_enum_fields.len)); + break :check_enumerable; + }, + .error_set => if (!operand_ty.isAnyError(zcu)) { + error_names = item_ty.errorSetNames(zcu); + try branch_hints.ensureUnusedCapacity(gpa, error_names.len); + break :check_enumerable; + }, + .int => { + min_int = try item_ty.minInt(pt, item_ty); + break :check_enumerable; + }, + .bool, .void => break :check_enumerable, + else => {}, + } + return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{ + item_ty.fmt(pt), + }); + } + var unhandled_it = validated_switch.iterateUnhandledItems(error_names, min_int); + while (try unhandled_it.next(sema, item_ty)) |item_val| { + cases_len += 1; + case_block.instructions.clearRetainingCapacity(); + case_block.error_return_trace_index = child_block.error_return_trace_index; + + const item_ref: Air.Inst.Ref = .fromValue(item_val); + + const analyze_body = sema.wantSwitchProngBodyAnalysis(block, item_ref, operand_ty, union_originally, err_set, false); + + if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src); + emit_bb = true; + + const prong_hint: std.builtin.BranchHint = hint: { + if (analyze_body) break :hint try sema.analyzeSwitchProng( + &case_block, + operand, + operand_ty, + raw_operand_ty, + else_case.body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = src_node_offset, + .case_idx = else_case.index, + } }), + else_case.capture, + else_case.has_tag_capture, + item_ref, + .special, + validated_switch.else_err_ty, + switch_inst, + zir_switch, + ); + _ = try case_block.addNoOp(.unreach); + break :hint .cold; // unreachable branches are cold + }; + try branch_hints.append(gpa, prong_hint); + + try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + + 1 + // `item`, no ranges + case_block.instructions.items.len); + cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ + .items_len = 1, + .ranges_len = 0, + .body_len = @intCast(case_block.instructions.items.len), + })); + cases_extra.appendAssumeCapacity(@intFromEnum(item_ref)); + cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); + } + } + + case_block.instructions.clearRetainingCapacity(); + case_block.error_return_trace_index = child_block.error_return_trace_index; + + if (zcu.backendSupportsFeature(.is_named_enum_value) and + catch_all_case != .none and block.wantSafety() and + item_ty.zigTypeTag(zcu) == .@"enum" and + (!operand_ty.isNonexhaustiveEnum(zcu) or 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); + } + } + + if (else_is_named_only and !else_case.is_inline) { + // If we have both an `else` and an `_` prong, all named values go + // into the `else` prong and all unnamed ones go into the `_` prong. + // We will manually enumerate all named values which haven't been + // encountered yet and create an extra prong for them, which will + // evaulate to the `else` body. + + assert(operand_ty.isNonexhaustiveEnum(zcu)); + + cases_len += 1; + + const prong_hint: std.builtin.BranchHint = hint: { + if (!else_case.is_inline) break :hint try sema.analyzeSwitchProng( + &case_block, + operand, + operand_ty, + raw_operand_ty, + else_case.body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = src_node_offset, + .case_idx = else_case.index, + } }), + else_case.capture, + else_case.has_tag_capture, + .none, + .special, + validated_switch.else_err_ty, + switch_inst, + zir_switch, + ); + _ = try case_block.addNoOp(.unreach); + break :hint .cold; // unreachable branches are cold + }; + try branch_hints.append(gpa, prong_hint); + + try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + + (validated_switch.seen_enum_fields.len - zir_switch.totalItemsLen()) + + case_block.instructions.items.len); + const extra_case = cases_extra.addManyAsArrayAssumeCapacity( + @typeInfo(Air.SwitchBr.Case).@"struct".fields.len, + ); + var items_len: u32 = 0; + for (validated_switch.seen_enum_fields, 0..) |seen_field, field_i| { + if (seen_field != null) continue; + const item_val = try pt.enumValueFieldIndex(item_ty, @intCast(field_i)); + const item_ref: Air.Inst.Ref = .fromValue(item_val); + cases_extra.appendAssumeCapacity(@intFromEnum(item_ref)); + items_len += 1; + } + assert(items_len > 0); // `else` must be reachable at this point + extra_case.* = payloadToExtraItems(Air.SwitchBr.Case{ + .items_len = items_len, + .ranges_len = 0, + .body_len = @intCast(case_block.instructions.items.len), + }); + cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); + + // We fall through to the regular catch-all prong generation. + + case_block.instructions.clearRetainingCapacity(); + case_block.error_return_trace_index = child_block.error_return_trace_index; + } + + const analyze_catch_all_body = analyze_body: { + switch (catch_all_case) { + .none => break :analyze_body false, // we still may want a safety check! + .under => break :analyze_body true, // can't be a union anyway + .@"else" => if (else_case.is_inline) break :analyze_body false, + } + if (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; + const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_i]); + if (!field_ty.isNoReturn(zcu)) break :analyze_body true; + } + break :analyze_body false; + } + if (err_set) { + const else_err_ty = validated_switch.else_err_ty orelse { + assert(else_case.is_simple_noreturn); + break :analyze_body false; + }; + if (else_err_ty.errorSetIsEmpty(zcu)) break :analyze_body false; + } + break :analyze_body true; + }; + + const catch_all_hint = hint: { + if (analyze_catch_all_body) { + const index, const body, const capture, const has_tag_capture = switch (catch_all_case) { + .@"else" => .{ else_case.index, else_case.body, else_case.capture, else_case.has_tag_capture }, + .under => .{ under_case.index, under_case.body, under_case.capture, under_case.has_tag_capture }, + .none => unreachable, + }; + break :hint try sema.analyzeSwitchProng( + &case_block, + operand, + operand_ty, + raw_operand_ty, + body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = src_node_offset, + .case_idx = index, + } }), + capture, + has_tag_capture, + .none, + .special, + validated_switch.else_err_ty, + switch_inst, + zir_switch, + ); + } + // We still need a terminator in this block, but we have proven + // that it is unreachable. + if (case_block.wantSafety()) { + try sema.zirDbgStmt(&case_block, cond_dbg_node_index); + try sema.safetyPanic(&case_block, src, .corrupt_switch); + } else { + _ = try case_block.addNoOp(.unreach); + } + break :hint .cold; // Safety check / unreachable branches are cold. + }; + try branch_hints.append(gpa, catch_all_hint); + break :catch_all_extra @ptrCast(case_block.instructions.items); + }; + + assert(branch_hints.count == cases_len + 1); // +1 for catch-all hint + + try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr).@"struct".fields.len + + branch_hints.bags.items.len + + cases_extra.items.len + + catch_all_extra.len); + const payload_index = sema.addExtraAssumeCapacity(Air.SwitchBr{ + .cases_len = @intCast(cases_len), + .else_body_len = @intCast(catch_all_extra.len), + }); + sema.air_extra.appendSliceAssumeCapacity(branch_hints.bags.items); + sema.air_extra.appendSliceAssumeCapacity(cases_extra.items); + sema.air_extra.appendSliceAssumeCapacity(catch_all_extra); + + const air_tag: Air.Inst.Tag = if (operand == .loop and merges.extra_insts.items.len > 0) + .loop_switch_br + else + .switch_br; + const air_switch_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_non_inline_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_non_inline_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)); + } +} + +const ValidatedSwitchBlock = struct { + seen_enum_fields: []const ?LazySrcLoc, + seen_errors: std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc), + seen_sparse_values: std.AutoHashMapUnmanaged(InternPool.Index, LazySrcLoc), + seen_ranges: []const RangeSet.Range, + true_src: ?LazySrcLoc, + false_src: ?LazySrcLoc, + void_src: ?LazySrcLoc, + + case_vals: []const Air.Inst.Ref, + else_case: Zir.UnwrappedSwitchBlock.Case.Else, + under_case: Zir.UnwrappedSwitchBlock.Case.Under.Resolved, + else_err_ty: ?Type, + + fn iterateUnhandledItems( + validated_switch: *const ValidatedSwitchBlock, + /// May be `undefined` if `item_ty` isn't an `error_set`. + error_names: InternPool.NullTerminatedString.Slice, + /// May be `undefined` if `item_ty` isn't an `int`. + min_int: Value, + ) UnhandledIterator { + return .{ + .next_idx = 0, + .next_val = min_int, + .error_names = error_names, + .seen_enum_fields = validated_switch.seen_enum_fields, + .seen_errors = &validated_switch.seen_errors, + .seen_ranges = validated_switch.seen_ranges, + .seen_true = validated_switch.true_src != null, + .seen_false = validated_switch.false_src != null, + .seen_void = validated_switch.void_src != null, + }; + } + + const UnhandledIterator = struct { + next_idx: u32, + next_val: ?Value, + error_names: InternPool.NullTerminatedString.Slice, + seen_enum_fields: []const ?LazySrcLoc, + seen_errors: *const std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc), + seen_ranges: []const RangeSet.Range, + seen_true: bool, + seen_false: bool, + seen_void: bool, + + fn next(it: *UnhandledIterator, sema: *Sema, item_ty: Type) CompileError!?Value { + const pt = sema.pt; + 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; + it.next_idx = @intCast(field_i + 1); + return try pt.enumValueFieldIndex(item_ty, @intCast(field_i)); + } + return null; + }, + .error_set => { + for (it.error_names.get(ip)[it.next_idx..], it.next_idx..) |err_name, name_i| { + if (it.seen_errors.contains(err_name)) continue; + it.next_idx = @intCast(name_i + 1); + return .fromInterned(try pt.intern(.{ .err = .{ + .ty = item_ty.toIntern(), + .name = err_name, + } })); + } + return null; + }, + .int => { + var cur = it.next_val orelse return null; + while (it.next_idx < it.seen_ranges.len and + cur.eql(it.seen_ranges[it.next_idx].first, item_ty, zcu)) + { + defer it.next_idx += 1; + const incr = try arith.incrementDefinedInt( + sema, + item_ty, + it.seen_ranges[it.next_idx].last, + ); + if (incr.overflow) { + it.next_val = null; + return null; + } + cur = incr.val; + } + const incr = try arith.incrementDefinedInt(sema, item_ty, cur); + it.next_val = if (incr.overflow) null else incr.val; + return cur; + }, + .bool => { + if (!it.seen_true) { + it.seen_true = true; + return .true; + } + if (!it.seen_false) { + it.seen_false = true; + return .false; + } + return null; + }, + .void => { + if (!it.seen_void) { + it.seen_void = true; + return .void; + } + return null; + }, + else => unreachable, // item type is not enumerable + } + } + }; +}; + +/// Validates operand type and `else`/`_` prong usage, resolves all prong items +/// and checks them for duplicates/invalid ranges. Does not emit into `block`. +/// Reserves inst map space for all placeholders associated with `zir_switch`. +/// Contents of returned `ValidatedSwitchBlock` belong to `sema.arena`. +fn validateSwitchBlock( + sema: *Sema, + block: *Block, + raw_operand: Air.Inst.Ref, + operand_is_ref: bool, + switch_inst: Zir.Inst.Index, + zir_switch: *const Zir.UnwrappedSwitchBlock, +) CompileError!ValidatedSwitchBlock { + const pt = sema.pt; + const zcu = pt.zcu; + const ip = &zcu.intern_pool; + const gpa = sema.gpa; + const arena = sema.arena; + + const src_node_offset = zir_switch.switch_src_node_offset; + const src = block.nodeOffset(src_node_offset); const operand_src = block.src(.{ .node_offset_switch_operand = src_node_offset }); const else_prong_src = block.src(.{ .node_offset_switch_else_prong = src_node_offset }); const under_prong_src = block.src(.{ .node_offset_switch_under_prong = src_node_offset }); - const extra = sema.code.extraData(Zir.Inst.SwitchBlock, inst_data.payload_index); + var extra_index = zir_switch.end; - const operand: SwitchProngAnalysis.Operand, const raw_operand_ty: Type = op: { - const maybe_ptr = try sema.resolveInst(extra.data.operand); - const val, const ref = if (operand_is_ref) - .{ try sema.analyzeLoad(block, src, maybe_ptr, operand_src), maybe_ptr } - else - .{ maybe_ptr, undefined }; + // We want to map values to our placeholders later on. + if (zir_switch.payload_capture_placeholder.unwrap()) |payload_capture_inst| { + assert(payload_capture_inst != switch_inst); // malformed zir + try sema.inst_map.ensureSpaceForInstructions(gpa, &.{payload_capture_inst}); + } + if (zir_switch.tag_capture_placeholder.unwrap()) |tag_capture_inst| { + assert(tag_capture_inst != switch_inst); // malformed zir + try sema.inst_map.ensureSpaceForInstructions(gpa, &.{tag_capture_inst}); + } - const init_cond = try sema.switchCond(block, operand_src, val); + const operand_ty: Type, const item_ty: Type = check_operand: { + const operand_ty = operand_ty: { + const raw_operand_ty = sema.typeOf(raw_operand); + if (operand_is_ref) { + try sema.checkPtrType(block, operand_src, raw_operand_ty, false); + break :operand_ty raw_operand_ty.childType(zcu); + } + break :operand_ty raw_operand_ty; + }; - const operand_ty = sema.typeOf(val); + const item_ty: Type = item_ty: { + switch (operand_ty.zigTypeTag(zcu)) { + .@"enum", + .error_set, + .int, + .comptime_int, + .type, + .enum_literal, + .@"fn", + .bool, + .void, + => break :item_ty operand_ty, - if (extra.data.bits.has_continue and !block.isComptime()) { - // Even if the operand is comptime-known, this `switch` is runtime. + .@"union" => { + try operand_ty.resolveFields(pt); + 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", .{}); + } + break :msg msg; + }); + }; + break :item_ty enum_ty; + }, + + .pointer => { + if (!operand_ty.isSlice(zcu)) { + break :item_ty operand_ty; + } + }, + + else => {}, + } + return sema.fail(block, operand_src, "switch on type '{f}'", .{operand_ty.fmt(pt)}); + }; + + if (zir_switch.has_continue and !block.isComptime()) { if (try operand_ty.comptimeOnlySema(pt)) { + // Even if the operand is comptime-known, this `switch` is runtime. return sema.failWithOwnedErrorMsg(block, msg: { const msg = try sema.errMsg(operand_src, "operand of switch loop has comptime-only type '{f}'", .{operand_ty.fmt(pt)}); errdefer msg.destroy(gpa); try sema.errNote(operand_src, msg, "switch loops are evaluated at runtime outside of comptime scopes", .{}); + try sema.explainWhyTypeIsComptime(msg, operand_src, operand_ty); break :msg msg; }); } - try sema.validateRuntimeValue(block, operand_src, maybe_ptr); - const operand_alloc = if (extra.data.bits.any_non_inline_capture or - extra.data.bits.any_has_tag_capture) - a: { - const operand_ptr_ty = try pt.singleMutPtrType(sema.typeOf(maybe_ptr)); - const operand_alloc = try block.addTy(.alloc, operand_ptr_ty); - _ = try block.addBinOp(.store, operand_alloc, maybe_ptr); - break :a operand_alloc; - } else undefined; - break :op .{ - .{ .loop = .{ - .operand_alloc = operand_alloc, - .operand_is_ref = operand_is_ref, - .init_cond = init_cond, - } }, - operand_ty, - }; + try sema.validateRuntimeValue(block, operand_src, raw_operand); } - // We always use `simple` in the comptime case, because as far as the dispatching logic - // is concerned, it really is dispatching a single prong. `resolveSwitchComptime` will - // be resposible for recursively resolving different prongs as needed. - break :op .{ - .{ .simple = .{ - .by_val = val, - .by_ref = ref, - .cond = init_cond, - } }, - operand_ty, - }; + break :check_operand .{ operand_ty, item_ty }; }; - const union_originally = raw_operand_ty.zigTypeTag(zcu) == .@"union"; - const err_set = raw_operand_ty.zigTypeTag(zcu) == .error_set; - const cond_ty = switch (raw_operand_ty.zigTypeTag(zcu)) { - .@"union" => raw_operand_ty.unionTagType(zcu).?, // validated by `switchCond` above - else => raw_operand_ty, - }; - - // AstGen guarantees that the instruction immediately preceding - // switch_block(_ref) is a dbg_stmt - const cond_dbg_node_index: Zir.Inst.Index = @enumFromInt(@intFromEnum(inst) - 1); - - var header_extra_index: usize = extra.end; - - const scalar_cases_len = extra.data.bits.scalar_cases_len; - const multi_cases_len = if (extra.data.bits.has_multi_cases) blk: { - const multi_cases_len = sema.code.extra[header_extra_index]; - header_extra_index += 1; - break :blk multi_cases_len; - } else 0; - - const tag_capture_inst: Zir.Inst.Index = if (extra.data.bits.any_has_tag_capture) blk: { - const tag_capture_inst: Zir.Inst.Index = @enumFromInt(sema.code.extra[header_extra_index]); - header_extra_index += 1; - // SwitchProngAnalysis wants inst_map to have space for the tag capture. - // Note that the normal capture is referred to via the switch block - // index, which there is already necessarily space for. - try sema.inst_map.ensureSpaceForInstructions(gpa, &.{tag_capture_inst}); - break :blk tag_capture_inst; - } else undefined; - - var case_vals = try std.ArrayList(Air.Inst.Ref).initCapacity(gpa, scalar_cases_len + 2 * multi_cases_len); - defer case_vals.deinit(gpa); - - var single_absorbed_item: Zir.Inst.Ref = .none; - var absorbed_items: []const Zir.Inst.Ref = &.{}; - var absorbed_ranges: []const Zir.Inst.Ref = &.{}; - - const special_prongs = extra.data.bits.special_prongs; - const has_else = special_prongs.hasElse(); - const has_under = special_prongs.hasUnder(); - const special_else: SpecialProng = if (has_else) blk: { - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[header_extra_index]); - const extra_body_start = header_extra_index + 1; - break :blk .{ - .body = sema.code.bodySlice(extra_body_start, info.body_len), - .end = extra_body_start + info.body_len, - .capture = info.capture, - .is_inline = info.is_inline, - .has_tag_capture = info.has_tag_capture, - }; - } else .{ - .body = &.{}, - .end = header_extra_index, - .capture = .none, - .is_inline = false, - .has_tag_capture = false, - }; - const special_under: SpecialProng = if (has_under) blk: { - var extra_index = special_else.end; - var trailing_items_len: usize = 0; - if (special_prongs.hasOneAdditionalItem()) { - single_absorbed_item = @enumFromInt(sema.code.extra[extra_index]); - extra_index += 1; - absorbed_items = @ptrCast(&single_absorbed_item); - } else if (special_prongs.hasManyAdditionalItems()) { - const items_len = sema.code.extra[extra_index]; - extra_index += 1; - const ranges_len = sema.code.extra[extra_index]; - extra_index += 1; - absorbed_items = sema.code.refSlice(extra_index + 1, items_len); - absorbed_ranges = sema.code.refSlice(extra_index + 1 + items_len, ranges_len * 2); - trailing_items_len = items_len + ranges_len * 2; - } - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1 + trailing_items_len; - break :blk .{ - .body = sema.code.bodySlice(extra_index, info.body_len), - .end = extra_index + info.body_len, - .capture = info.capture, - .is_inline = info.is_inline, - .has_tag_capture = info.has_tag_capture, - }; - } else .{ - .body = &.{}, - .end = special_else.end, - .capture = .none, - .is_inline = false, - .has_tag_capture = false, - }; - const special_end = special_under.end; - - // Duplicate checking variables later also used for `inline else`. - var seen_enum_fields: []?LazySrcLoc = &.{}; - var seen_errors = SwitchErrorSet.init(gpa); - var range_set = RangeSet.init(gpa, zcu); - var true_count: u8 = 0; - var false_count: u8 = 0; - - defer { - range_set.deinit(); - gpa.free(seen_enum_fields); - seen_errors.deinit(); - } - - var empty_enum = false; - - var else_error_ty: ?Type = null; + const has_else = zir_switch.else_case != null; + const has_under = zir_switch.under_case != .none; // Validate usage of '_' prongs. - if (has_under and !raw_operand_ty.isNonexhaustiveEnum(zcu)) { + if (has_under and !operand_ty.isNonexhaustiveEnum(zcu)) { const msg = msg: { const msg = try sema.errMsg( src, @@ -11710,92 +11976,122 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r return sema.failWithOwnedErrorMsg(block, msg); } - // Validate for duplicate items, missing else prong, and invalid range. - switch (cond_ty.zigTypeTag(zcu)) { - .@"union" => unreachable, // handled in `switchCond` + var case_vals: std.ArrayList(Air.Inst.Ref) = .empty; + try case_vals.ensureUnusedCapacity(arena, zir_switch.item_infos.len); + + // Duplicate checking variables later also used for `inline else`. + var seen_enum_fields: []?LazySrcLoc = &.{}; + var seen_errors: std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc) = .empty; + var seen_sparse_values: std.AutoHashMapUnmanaged(InternPool.Index, LazySrcLoc) = .empty; + var range_set: RangeSet = .empty; + var true_src: ?LazySrcLoc = null; + var false_src: ?LazySrcLoc = null; + var void_src: ?LazySrcLoc = null; + + var else_err_ty: ?Type = null; + + const else_case = zir_switch.else_case orelse undefined; + var under_case = zir_switch.under_case.resolve() orelse undefined; + + switch (item_ty.zigTypeTag(zcu)) { + .@"union" => unreachable, .@"enum" => { - seen_enum_fields = try gpa.alloc(?LazySrcLoc, cond_ty.enumFieldCount(zcu)); - empty_enum = seen_enum_fields.len == 0 and !cond_ty.isNonexhaustiveEnum(zcu); + seen_enum_fields = try arena.alloc(?LazySrcLoc, item_ty.enumFieldCount(zcu)); @memset(seen_enum_fields, null); - // `range_set` is used for non-exhaustive enum values that do not correspond to any tags. + // `range_set` is used for non-exhaustive enum values that do not + // correspond to any tags. Since this is rare, we only allocate on + // demand in `validateSwitchItem`. + }, + .error_set => { + try seen_errors.ensureUnusedCapacity(arena, zir_switch.totalItemsLen()); + }, + .int, .comptime_int => { + try range_set.ensureUnusedCapacity(arena, zir_switch.totalItemsLen()); + }, + .enum_literal, .@"fn", .pointer, .type => { + try seen_sparse_values.ensureUnusedCapacity(arena, zir_switch.totalItemsLen()); + }, + .bool, .void => {}, - for (absorbed_items, 0..) |item_ref, item_i| { - _ = try sema.validateSwitchItemEnum( - block, - seen_enum_fields, - &range_set, - item_ref, - cond_ty, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .special_under, - .item_idx = .{ .kind = .single, .index = @intCast(item_i) }, - } }), + else => unreachable, + } + + // Validate for duplicate items and invalid ranges. + var case_it = zir_switch.iterateCases(); + while (case_it.next()) |case| { + const prong_info = case.prong_info; + const is_under = case.isUnder(); + if (is_under) { + assert(!prong_info.is_inline); + under_case = .{ + .index = case.index, + .body = sema.code.bodySlice(extra_index, prong_info.body_len), + .capture = prong_info.capture, + .has_tag_capture = prong_info.has_tag_capture, + }; + } + extra_index += prong_info.body_len; + for (case.item_infos, 0..) |item_info, item_i| { + const item_src = block.src(.{ .switch_case_item = .{ + .switch_node_offset = src_node_offset, + .case_idx = case.index, + .item_idx = .{ .kind = .single, .value = @intCast(item_i) }, + } }); + const item, extra_index = try sema.resolveSwitchItem(block, item_src, item_ty, item_info, extra_index, switch_inst, prong_info.is_comptime_unreach, prong_info.is_inline); + try sema.validateSwitchItemOrRange(block, item_src, item.val, null, item_ty, seen_enum_fields, &seen_errors, &seen_sparse_values, &range_set, &true_src, &false_src, &void_src); + if (!is_under) case_vals.appendAssumeCapacity(item.ref); + } + for (case.range_infos, 0..) |range_info, range_i| { + const range_offset: LazySrcLoc.Offset.SwitchItem = .{ + .switch_node_offset = src_node_offset, + .case_idx = case.index, + .item_idx = .{ .kind = .range, .value = @intCast(range_i) }, + }; + const range_src = block.src(.{ .switch_case_item = range_offset }); + const first_src = block.src(.{ .switch_case_item_range_first = range_offset }); + const last_src = block.src(.{ .switch_case_item_range_last = range_offset }); + const first_item, extra_index = try sema.resolveSwitchItem(block, first_src, item_ty, range_info[0], extra_index, switch_inst, prong_info.is_comptime_unreach, prong_info.is_inline); + const last_item, extra_index = try sema.resolveSwitchItem(block, last_src, item_ty, range_info[1], extra_index, switch_inst, prong_info.is_comptime_unreach, prong_info.is_inline); + try sema.validateSwitchItemOrRange(block, range_src, first_item.val, last_item.val, item_ty, seen_enum_fields, &seen_errors, &seen_sparse_values, &range_set, &true_src, &false_src, &void_src); + if (!is_under) case_vals.appendSliceAssumeCapacity(&.{ first_item.ref, last_item.ref }); + } + } + + 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 }); + const msg = msg: { + const msg = try sema.errMsg( + operand_src, + "ranges not allowed when switching on type '{f}'", + .{operand_ty.fmt(sema.pt)}, ); - } - try sema.validateSwitchNoRange(block, @intCast(absorbed_ranges.len), cond_ty, src_node_offset); + errdefer msg.destroy(sema.gpa); + try sema.errNote( + range_src, + msg, + "range here", + .{}, + ); + break :msg msg; + }; + return sema.failWithOwnedErrorMsg(block, msg); + }, + } - var extra_index: usize = special_end; - { - var scalar_i: u32 = 0; - while (scalar_i < scalar_cases_len) : (scalar_i += 1) { - const item_ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]); - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1 + info.body_len; - - case_vals.appendAssumeCapacity(try sema.validateSwitchItemEnum( - block, - seen_enum_fields, - &range_set, - item_ref, - cond_ty, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .scalar, .index = @intCast(scalar_i) }, - .item_idx = .{ .kind = .single, .index = 0 }, - } }), - )); - } - } - { - var multi_i: u32 = 0; - while (multi_i < multi_cases_len) : (multi_i += 1) { - const items_len = sema.code.extra[extra_index]; - extra_index += 1; - const ranges_len = sema.code.extra[extra_index]; - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1; - const items = sema.code.refSlice(extra_index, items_len); - extra_index += items_len + info.body_len; - - try case_vals.ensureUnusedCapacity(gpa, items.len); - for (items, 0..) |item_ref, item_i| { - case_vals.appendAssumeCapacity(try sema.validateSwitchItemEnum( - block, - seen_enum_fields, - &range_set, - item_ref, - cond_ty, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - .item_idx = .{ .kind = .single, .index = @intCast(item_i) }, - } }), - )); - } - - try sema.validateSwitchNoRange(block, ranges_len, cond_ty, src_node_offset); - } - } + // 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; } else true; if (has_else) { if (all_tags_handled) { - if (cond_ty.isNonexhaustiveEnum(zcu)) { + if (item_ty.isNonexhaustiveEnum(zcu)) { if (has_under) return sema.fail( block, else_prong_src, @@ -11820,9 +12116,9 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r for (seen_enum_fields, 0..) |seen_src, i| { if (seen_src != null) continue; - const field_name = cond_ty.enumFieldName(i, zcu); + const field_name = item_ty.enumFieldName(i, zcu); try sema.addFieldErrNote( - cond_ty, + item_ty, i, msg, "unhandled enumeration value: '{f}'", @@ -11830,15 +12126,17 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r ); } try sema.errNote( - cond_ty.srcLoc(zcu), + item_ty.srcLoc(zcu), msg, "enum '{f}' declared here", - .{cond_ty.fmt(pt)}, + .{item_ty.fmt(pt)}, ); break :msg msg; }; return sema.failWithOwnedErrorMsg(block, msg); - } else if (special_prongs == .none and cond_ty.isNonexhaustiveEnum(zcu) and !union_originally) { + } else if (!has_else and !has_under and + item_ty.isNonexhaustiveEnum(zcu) and operand_ty.zigTypeTag(zcu) != .@"union") + { return sema.fail( block, src, @@ -11847,101 +12145,83 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r ); } }, - .error_set => else_error_ty = try validateErrSetSwitch( - sema, - block, - &seen_errors, - &case_vals, - cond_ty, - inst_data, - scalar_cases_len, - multi_cases_len, - .{ .body = special_else.body, .end = special_else.end, .src = else_prong_src }, - has_else, - ), - .int, .comptime_int => { - var extra_index: usize = special_end; - { - var scalar_i: u32 = 0; - while (scalar_i < scalar_cases_len) : (scalar_i += 1) { - const item_ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]); - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1 + info.body_len; - - case_vals.appendAssumeCapacity(try sema.validateSwitchItemInt( - block, - &range_set, - item_ref, - cond_ty, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .scalar, .index = @intCast(scalar_i) }, - .item_idx = .{ .kind = .single, .index = 0 }, - } }), - )); - } - } - { - var multi_i: u32 = 0; - while (multi_i < multi_cases_len) : (multi_i += 1) { - const items_len = sema.code.extra[extra_index]; - extra_index += 1; - const ranges_len = sema.code.extra[extra_index]; - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1; - const items = sema.code.refSlice(extra_index, items_len); - extra_index += items_len; - - try case_vals.ensureUnusedCapacity(gpa, items.len); - for (items, 0..) |item_ref, item_i| { - case_vals.appendAssumeCapacity(try sema.validateSwitchItemInt( + .error_set => { + else_err_ty = ty: switch (try sema.resolveInferredErrorSetTy(block, src, item_ty.toIntern())) { + .anyerror_type => { + if (!has_else) { + return sema.fail( block, - &range_set, - item_ref, - cond_ty, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - .item_idx = .{ .kind = .single, .index = @intCast(item_i) }, - } }), - )); - } - - try case_vals.ensureUnusedCapacity(gpa, 2 * ranges_len); - var range_i: u32 = 0; - while (range_i < ranges_len) : (range_i += 1) { - const item_first: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]); - extra_index += 1; - const item_last: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]); - extra_index += 1; - - const vals = try sema.validateSwitchRange( - block, - &range_set, - item_first, - item_last, - cond_ty, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - .item_idx = .{ .kind = .range, .index = @intCast(range_i) }, - } }), + src, + "else prong required when switching on type 'anyerror'", + .{}, ); - case_vals.appendAssumeCapacity(vals[0]); - case_vals.appendAssumeCapacity(vals[1]); + } + break :ty .anyerror; + }, + else => |err_set_ty_index| { + const error_names = ip.indexToKey(err_set_ty_index).error_set_type.names; + var maybe_msg: ?*Zcu.ErrorMsg = null; + errdefer if (maybe_msg) |msg| msg.destroy(sema.gpa); + + var seen_errors_from_set: u32 = 0; + for (error_names.get(ip)) |error_name| { + if (seen_errors.contains(error_name)) { + seen_errors_from_set += 1; + } else if (!has_else) { + const msg = maybe_msg orelse blk: { + maybe_msg = try sema.errMsg( + src, + "switch must handle all possibilities", + .{}, + ); + break :blk maybe_msg.?; + }; + + try sema.errNote( + src, + msg, + "unhandled error value: 'error.{f}'", + .{error_name.fmt(ip)}, + ); + } } - extra_index += info.body_len; - } - } + if (maybe_msg) |msg| { + maybe_msg = null; + try sema.addDeclaredHereNote(msg, operand_ty); + return sema.failWithOwnedErrorMsg(block, msg); + } + if (has_else and seen_errors_from_set == error_names.len) { + // This prong is unreachable anyway so we don't need its + // error set type, but we still allow it to exist. + if (else_case.is_simple_noreturn) break :ty null; + return sema.fail( + block, + else_prong_src, + "unreachable else prong; all cases already handled", + .{}, + ); + } + + var names: InferredErrorSet.NameMap = .{}; + try names.ensureUnusedCapacity(sema.arena, error_names.len); + for (error_names.get(ip)) |error_name| { + if (seen_errors.contains(error_name)) continue; + names.putAssumeCapacityNoClobber(error_name, {}); + } + // No need to keep the hash map metadata correct; here we + // extract the (sorted) keys only. + break :ty try pt.errorSetFromUnsortedNames(names.keys()); + }, + }; + }, + .int, .comptime_int => |type_tag| { check_range: { - if (cond_ty.zigTypeTag(zcu) == .int) { - const min_int = try cond_ty.minInt(pt, cond_ty); - const max_int = try cond_ty.maxInt(pt, cond_ty); - if (try range_set.spans(min_int.toIntern(), max_int.toIntern())) { + 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, @@ -11953,7 +12233,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r break :check_range; } } - if (special_prongs == .none) { + if (!has_else) { return sema.fail( block, src, @@ -11963,61 +12243,24 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r } } }, - .bool => { - var extra_index: usize = special_end; - { - var scalar_i: u32 = 0; - while (scalar_i < scalar_cases_len) : (scalar_i += 1) { - const item_ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]); - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1 + info.body_len; - - case_vals.appendAssumeCapacity(try sema.validateSwitchItemBool( - block, - &true_count, - &false_count, - item_ref, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .scalar, .index = @intCast(scalar_i) }, - .item_idx = .{ .kind = .single, .index = 0 }, - } }), - )); - } - } - { - var multi_i: u32 = 0; - while (multi_i < multi_cases_len) : (multi_i += 1) { - const items_len = sema.code.extra[extra_index]; - extra_index += 1; - const ranges_len = sema.code.extra[extra_index]; - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1; - const items = sema.code.refSlice(extra_index, items_len); - extra_index += items_len + info.body_len; - - try case_vals.ensureUnusedCapacity(gpa, items.len); - for (items, 0..) |item_ref, item_i| { - case_vals.appendAssumeCapacity(try sema.validateSwitchItemBool( - block, - &true_count, - &false_count, - item_ref, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - .item_idx = .{ .kind = .single, .index = @intCast(item_i) }, - } }), - )); - } - - try sema.validateSwitchNoRange(block, ranges_len, cond_ty, src_node_offset); - } + .enum_literal, .@"fn", .pointer, .type => { + if (!has_else) { + return sema.fail( + block, + src, + "else prong required when switching on type '{f}'", + .{item_ty.fmt(pt)}, + ); } + }, + .bool, .void => |type_tag| { + const all_values_handled = switch (type_tag) { + .bool => true_src != null and false_src != null, + .void => void_src != null, + else => unreachable, + }; if (has_else) { - if (true_count + false_count == 2) { + if (all_values_handled) { return sema.fail( block, else_prong_src, @@ -12026,7 +12269,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r ); } } else { - if (true_count + false_count < 2) { + if (!all_values_handled) { return sema.fail( block, src, @@ -12036,1775 +12279,1073 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r } } }, - .enum_literal, .void, .@"fn", .pointer, .type => { - if (!has_else) { - return sema.fail( - block, - src, - "else prong required when switching on type '{f}'", - .{cond_ty.fmt(pt)}, - ); - } - - var seen_values = ValueSrcMap{}; - defer seen_values.deinit(gpa); - - var extra_index: usize = special_end; - { - var scalar_i: u32 = 0; - while (scalar_i < scalar_cases_len) : (scalar_i += 1) { - const item_ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]); - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1; - extra_index += info.body_len; - - case_vals.appendAssumeCapacity(try sema.validateSwitchItemSparse( - block, - &seen_values, - item_ref, - cond_ty, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .scalar, .index = @intCast(scalar_i) }, - .item_idx = .{ .kind = .single, .index = 0 }, - } }), - )); - } - } - { - var multi_i: u32 = 0; - while (multi_i < multi_cases_len) : (multi_i += 1) { - const items_len = sema.code.extra[extra_index]; - extra_index += 1; - const ranges_len = sema.code.extra[extra_index]; - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1; - const items = sema.code.refSlice(extra_index, items_len); - extra_index += items_len + info.body_len; - - try case_vals.ensureUnusedCapacity(gpa, items.len); - for (items, 0..) |item_ref, item_i| { - case_vals.appendAssumeCapacity(try sema.validateSwitchItemSparse( - block, - &seen_values, - item_ref, - cond_ty, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - .item_idx = .{ .kind = .single, .index = @intCast(item_i) }, - } }), - )); - } - - try sema.validateSwitchNoRange(block, ranges_len, cond_ty, src_node_offset); - } - } - }, - - .error_union, - .noreturn, - .array, - .@"struct", - .undefined, - .null, - .optional, - .@"opaque", - .vector, - .frame, - .@"anyframe", - .comptime_float, - .float, - => return sema.fail(block, operand_src, "invalid switch operand type '{f}'", .{ - raw_operand_ty.fmt(pt), - }), + else => unreachable, } - var special_members_only: ?SpecialProng = null; - var special_members_only_src: LazySrcLoc = undefined; - const special_generic, const special_generic_src = if (has_under) b: { - if (has_else) { - special_members_only = special_else; - special_members_only_src = else_prong_src; - } - break :b .{ special_under, under_prong_src }; - } else .{ special_else, else_prong_src }; + return .{ + .seen_enum_fields = seen_enum_fields, + .seen_errors = seen_errors, + .seen_sparse_values = seen_sparse_values, + .seen_ranges = range_set.ranges.items, + .true_src = true_src, + .false_src = false_src, + .void_src = void_src, - const spa: SwitchProngAnalysis = .{ - .sema = sema, - .parent_block = block, - .operand = operand, - .else_error_ty = else_error_ty, - .switch_block_inst = inst, - .tag_capture_inst = tag_capture_inst, + .case_vals = case_vals.items, + .else_case = else_case, + .under_case = under_case, + .else_err_ty = else_err_ty, }; +} - const block_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len); - try sema.air_instructions.append(gpa, .{ - .tag = .block, - .data = undefined, - }); - var label: Block.Label = .{ - .zir_block = inst, - .merges = .{ - .src_locs = .{}, - .results = .{}, - .br_list = .{}, - .block_inst = block_inst, - }, +fn resolveSwitchBlock( + sema: *Sema, + block: *Block, + child_block: *Block, + operand: SwitchOperand, + raw_operand_ty: Type, + maybe_lazy_cond_val: Value, + catch_all_case: CatchAllSwitchCase, + else_is_named_only: bool, + merges: *Block.Merges, + switch_inst: Zir.Inst.Index, + zir_switch: *const Zir.UnwrappedSwitchBlock, + validated_switch: *const ValidatedSwitchBlock, +) CompileError!Air.Inst.Ref { + const pt = sema.pt; + const zcu = pt.zcu; + + 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; - var child_block: Block = .{ - .parent = block, - .sema = sema, - .namespace = block.namespace, - .instructions = .{}, - .label = &label, - .inlining = block.inlining, - .comptime_reason = block.comptime_reason, - .is_typeof = block.is_typeof, - .c_import_buf = block.c_import_buf, - .runtime_cond = block.runtime_cond, - .runtime_loop = block.runtime_loop, - .runtime_index = block.runtime_index, - .want_safety = block.want_safety, - .error_return_trace_index = block.error_return_trace_index, - .src_base_inst = block.src_base_inst, - .type_name_ctx = block.type_name_ctx, + const cond_ref = operand.simple.cond; + // We have to resolve lazy values to ensure that comparisons with switch + // prong items don't produce false negatives. + const cond_val = try sema.resolveLazyValue(maybe_lazy_cond_val); + + const case_vals = validated_switch.case_vals; + var case_val_idx: usize = 0; + var extra_index = zir_switch.end; + var case_it = zir_switch.iterateCases(); + while (case_it.next()) |case| { + if (case.isUnder()) { // we'll deal with this later + extra_index += case.prong_info.body_len; + for (case.item_infos) |item_info| { + if (item_info.bodyLen()) |body_len| extra_index += body_len; + } + for (case.range_infos) |range_info| { + if (range_info[0].bodyLen()) |body_len| extra_index += body_len; + if (range_info[1].bodyLen()) |body_len| extra_index += body_len; + } + continue; + } + + const prong_info = case.prong_info; + const prong_body = sema.code.bodySlice(extra_index, prong_info.body_len); + extra_index += prong_body.len; + for (case.item_infos) |item_info| { + if (item_info.bodyLen()) |body_len| extra_index += body_len; + } + for (case.range_infos) |range_info| { + if (range_info[0].bodyLen()) |body_len| extra_index += body_len; + if (range_info[1].bodyLen()) |body_len| extra_index += body_len; + } + + const item_refs = case_vals[case_val_idx..][0..case.item_infos.len]; + case_val_idx += item_refs.len; + const range_refs: []const [2]Air.Inst.Ref = @ptrCast(case_vals[case_val_idx..][0 .. 2 * case.range_infos.len]); + case_val_idx += 2 * range_refs.len; + for (item_refs) |item_ref| { + const item_val = sema.resolveConstDefinedValue(child_block, .unneeded, item_ref, undefined) catch unreachable; + 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)) { + // This prong should be unreachable! + return .unreachable_value; + } + return sema.resolveSwitchProng( + block, + child_block, + operand, + raw_operand_ty, + prong_body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = switch_node_offset, + .case_idx = case.index, + } }), + prong_info.capture, + prong_info.has_tag_capture, + if (prong_info.is_inline) cond_ref else .none, + .{ .item_refs = item_refs }, + validated_switch.else_err_ty, + merges, + switch_inst, + zir_switch, + ); + } + } + for (range_refs) |range_ref| { + const first_val = sema.resolveConstDefinedValue(child_block, .unneeded, range_ref[0], undefined) catch unreachable; + const last_val = sema.resolveConstDefinedValue(child_block, .unneeded, range_ref[1], undefined) catch unreachable; + if ((try sema.compareAll(cond_val, .gte, first_val, item_ty)) and + (try sema.compareAll(cond_val, .lte, last_val, item_ty))) + { + return sema.resolveSwitchProng( + block, + child_block, + operand, + raw_operand_ty, + prong_body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = switch_node_offset, + .case_idx = case.index, + } }), + prong_info.capture, + prong_info.has_tag_capture, + if (prong_info.is_inline) cond_ref else .none, + .has_ranges, + validated_switch.else_err_ty, + merges, + switch_inst, + zir_switch, + ); + } + } + } + + const else_case = validated_switch.else_case; + const under_case = validated_switch.under_case; + + // named-only prong + + if (else_is_named_only and item_ty.enumTagFieldIndex(cond_val, zcu) != null) { + assert(item_ty.isNonexhaustiveEnum(zcu)); + return sema.resolveSwitchProng( + block, + child_block, + operand, + raw_operand_ty, + else_case.body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = switch_node_offset, + .case_idx = else_case.index, + } }), + else_case.capture, + else_case.has_tag_capture, + if (else_case.is_inline) cond_ref else .none, + .special, + validated_switch.else_err_ty, + merges, + switch_inst, + zir_switch, + ); + } + + // catch-all prong + + const index, const body, const capture, const has_tag_capture, const is_inline = switch (catch_all_case) { + .@"else" => .{ else_case.index, else_case.body, else_case.capture, else_case.has_tag_capture, else_case.is_inline }, + .under => .{ under_case.index, under_case.body, under_case.capture, under_case.has_tag_capture, false }, + .none => unreachable, }; - const merges = &child_block.label.?.merges; - defer child_block.instructions.deinit(gpa); - defer merges.deinit(gpa); - - if (scalar_cases_len + multi_cases_len == 0 and - special_members_only == null and - !special_generic.is_inline) - { - if (empty_enum) { - return .void_value; - } - if (special_prongs == .none) { - return sema.fail(block, src, "switch must handle all possibilities", .{}); - } - const init_cond = switch (operand) { - .simple => |s| s.cond, - .loop => |l| l.init_cond, - }; - if (zcu.backendSupportsFeature(.is_named_enum_value) and block.wantSafety() and - raw_operand_ty.zigTypeTag(zcu) == .@"enum" and !raw_operand_ty.isNonexhaustiveEnum(zcu)) - { - try sema.zirDbgStmt(block, cond_dbg_node_index); - const ok = try block.addUnOp(.is_named_enum_value, init_cond); - try sema.addSafetyCheck(block, src, ok, .corrupt_switch); - } - if (err_set and try sema.maybeErrorUnwrap(block, special_generic.body, init_cond, operand_src, false)) { + if (err_set) try sema.maybeErrorUnwrapComptime(child_block, body, cond_ref); + if (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; + } else { + // This prong should be unreachable! return .unreachable_value; } } + return sema.resolveSwitchProng( + block, + child_block, + operand, + raw_operand_ty, + body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = switch_node_offset, + .case_idx = index, + } }), + capture, + has_tag_capture, + if (is_inline) cond_ref else .none, + .special, + validated_switch.else_err_ty, + merges, + switch_inst, + zir_switch, + ); +} - switch (operand) { - .loop => {}, // always runtime; evaluation in comptime scope uses `simple` - .simple => |s| { - if (try sema.resolveDefinedValue(&child_block, src, s.cond)) |cond_val| { - return resolveSwitchComptimeLoop( - sema, - spa, - &child_block, - if (operand_is_ref) - sema.typeOf(s.by_ref) - else - raw_operand_ty, - cond_ty, - cond_val, - src_node_offset, - special_members_only, - special_generic, - has_under, - case_vals, - scalar_cases_len, - multi_cases_len, - err_set, - empty_enum, - operand_is_ref, - ); - } +const SwitchOperand = union(enum) { + /// This switch will be dispatched only once, with the given operand. + simple: struct { + /// The raw switch operand value. Always defined. + by_val: Air.Inst.Ref, + /// The switch operand *pointer*. Defined only if there is a prong + /// with a by-ref capture. + by_ref: Air.Inst.Ref, + /// The switch condition value. For unions, `operand` is the union + /// and `cond` is its enum tag value. + cond: Air.Inst.Ref, + }, + /// This switch may be dispatched multiple times with `continue` syntax. + /// As such, the operand is stored in an alloc if needed. + loop: struct { + /// The `alloc` containing the `switch` operand for the active dispatch. + /// Each prong must load from this `alloc` to get captures. + /// If there are no captures, this may be undefined. + operand_alloc: Air.Inst.Ref, + /// Whether `operand_alloc` contains a by-val operand or a by-ref + /// operand. + operand_is_ref: bool, + /// The switch condition value for the *initial* dispatch. For + /// unions, this is the enum tag value. + init_cond: Air.Inst.Ref, + }, +}; - if (scalar_cases_len + multi_cases_len == 0 and - special_members_only == null and - !special_generic.is_inline and - !extra.data.bits.has_continue) - { - return spa.resolveProngComptime( - &child_block, - .special, - special_generic.body, - special_generic.capture, - block.src(.{ .switch_capture = .{ - .switch_node_offset = src_node_offset, - .case_idx = if (has_under) .special_under else .special_else, - } }), - undefined, // case_vals may be undefined for special prongs - .none, - false, - merges, - ); - } - }, +const CatchAllSwitchCase = enum { none, @"else", under }; + +const SwitchProngKind = union(enum) { + item_refs: []const Air.Inst.Ref, + has_ranges, + special, +}; + +/// Resolve a switch prong which is determined at comptime to have no peers. +/// Sets up captures as needed. Uses `analyzeBodyRuntimeBreak`. +fn resolveSwitchProng( + sema: *Sema, + block: *Block, + child_block: *Block, + operand: SwitchOperand, + raw_operand_ty: Type, + prong_body: []const Zir.Inst.Index, + /// Must use the `switch_capture` field in `offset`. + capture_src: LazySrcLoc, + capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, + has_tag_capture: bool, + inline_case_capture: Air.Inst.Ref, + kind: SwitchProngKind, + else_err_ty: ?Type, + merges: *Block.Merges, + switch_inst: Zir.Inst.Index, + zir_switch: *const Zir.UnwrappedSwitchBlock, +) CompileError!Air.Inst.Ref { + const src_node_offset = zir_switch.switch_src_node_offset; + const src = block.nodeOffset(src_node_offset); + const operand_src = block.src(.{ .node_offset_switch_operand = src_node_offset }); + + // We can propagate `.cold` hints from this branch since it's comptime-known + // to be taken from the parent branch. + 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 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 == .special, + switch (kind) { + .item_refs => |item_refs| item_refs, + .has_ranges, .special => undefined, + }, + inline_case_capture, + else_err_ty, + ); + assert(!sema.typeOf(payload_ref).isNoReturn(sema.pt.zcu)); + sema.inst_map.putAssumeCapacity(payload_inst, 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, + inline_case_capture, + ); + sema.inst_map.putAssumeCapacity(tag_inst, tag_ref); + break :inst tag_inst; + } else undefined; + defer if (has_tag_capture) assert(sema.inst_map.remove(tag_inst)); + + if (zir_switch.has_continue) sema.inst_map.putAssumeCapacity(switch_inst, .fromType(raw_operand_ty)); + defer if (zir_switch.has_continue) assert(sema.inst_map.remove(switch_inst)); + + return sema.resolveBlockBody(block, src, child_block, prong_body, switch_inst, merges); +} + +fn wantSwitchProngBodyAnalysis( + sema: *Sema, + block: *Block, + item_ref: Air.Inst.Ref, + operand_ty: Type, + union_originally: bool, + err_set: bool, + prong_is_comptime_unreach: bool, +) bool { + const zcu = sema.pt.zcu; + if (union_originally) { + const unresolved_item_val = sema.resolveConstDefinedValue(block, .unneeded, item_ref, undefined) catch unreachable; + const item_val = sema.resolveLazyValue(unresolved_item_val) catch unreachable; + const field_ty = operand_ty.unionFieldType(item_val, zcu).?; + if (field_ty.isNoReturn(zcu)) return false; } - - if (child_block.isComptime()) { - _ = try sema.resolveConstDefinedValue(&child_block, operand_src, operand.simple.cond, null); - unreachable; + if (err_set and prong_is_comptime_unreach) { + const unresolved_item_val = sema.resolveConstDefinedValue(block, .unneeded, item_ref, undefined) catch unreachable; + const item_val = sema.resolveLazyValue(unresolved_item_val) catch unreachable; + const err_name = item_val.getErrorName(zcu).unwrap().?; + if (!Type.errorSetHasFieldIp(&zcu.intern_pool, operand_ty.toIntern(), err_name)) return false; } + return true; +} - var extra_case_vals: struct { - items: std.ArrayList(Air.Inst.Ref), - ranges: std.ArrayList([2]Air.Inst.Ref), - } = .{ .items = .empty, .ranges = .empty }; - defer { - extra_case_vals.items.deinit(gpa); - extra_case_vals.ranges.deinit(gpa); - } +/// Assumes that `operand_ty` has more than one possible value. +/// Sets up captures as needed. Uses `analyzeBodyRuntimeBreak`. +fn analyzeSwitchProng( + sema: *Sema, + case_block: *Block, + operand: SwitchOperand, + operand_ty: Type, + raw_operand_ty: Type, + prong_body: []const Zir.Inst.Index, + /// Must use the `switch_capture` field in `offset`. + capture_src: LazySrcLoc, + capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, + has_tag_capture: bool, + inline_case_capture: Air.Inst.Ref, + kind: SwitchProngKind, + else_err_ty: ?Type, + switch_inst: Zir.Inst.Index, + zir_switch: *const Zir.UnwrappedSwitchBlock, +) CompileError!std.builtin.BranchHint { + const pt = sema.pt; + const zcu = pt.zcu; - // Runtime switch, if we have a special_members_only prong we need to unroll - // it to a prong with explicit items. - // Although this is potentially the same as `inline else` it does not count - // towards the backward branch quota because it's an implementation detail. - if (special_members_only != null) gen: { - assert(cond_ty.isNonexhaustiveEnum(zcu)); + const operand_src = case_block.src(.{ .node_offset_switch_operand = zir_switch.switch_src_node_offset }); - var min_i: usize = math.maxInt(usize); - var max_i: usize = 0; - var seen_field_count: usize = 0; - for (seen_enum_fields, 0..) |seen, enum_i| { - if (seen != null) { - seen_field_count += 1; - } else { - min_i = @min(min_i, enum_i); - max_i = @max(max_i, enum_i); - } - } - if (min_i == max_i) { - seen_enum_fields[min_i] = special_members_only_src; - const item_val = try pt.enumValueFieldIndex(cond_ty, @intCast(min_i)); - const item_ref = Air.internedToRef(item_val.toIntern()); - try extra_case_vals.items.append(gpa, item_ref); - break :gen; - } - const missing_field_count = seen_enum_fields.len - seen_field_count; - - extra_case_vals.items = try .initCapacity(gpa, missing_field_count / 2); - extra_case_vals.ranges = try .initCapacity(gpa, missing_field_count / 4); - const int_ty = cond_ty.intTagType(zcu); - - var last_val = try pt.enumValueFieldIndex(cond_ty, @intCast(min_i)); - var first_ref = Air.internedToRef(last_val.toIntern()); - seen_enum_fields[min_i] = special_members_only_src; - for (seen_enum_fields[(min_i + 1)..(max_i + 1)], (min_i + 1)..) |seen, enum_i| { - if (seen != null) continue; - seen_enum_fields[enum_i] = special_members_only_src; - - const item_val = try pt.enumValueFieldIndex(cond_ty, @intCast(enum_i)); - const item_ref = Air.internedToRef(item_val.toIntern()); - - const is_next = is_next: { - const prev_int = ip.indexToKey(last_val.toIntern()).enum_tag.int; - - const result = try arith.incrementDefinedInt(sema, int_ty, .fromInterned(prev_int)); - if (result.overflow) break :is_next false; - - const item_int = ip.indexToKey(item_val.toIntern()).enum_tag.int; - break :is_next try sema.valuesEqual(.fromInterned(item_int), result.val, int_ty); - }; - - if (is_next) { - last_val = item_val; - } else { - const last_ref = Air.internedToRef(last_val.toIntern()); - if (first_ref == last_ref) { - try extra_case_vals.items.append(gpa, first_ref); - } else { - try extra_case_vals.ranges.append(gpa, .{ first_ref, last_ref }); - } - first_ref = item_ref; - last_val = item_val; - } - } - const last_ref = Air.internedToRef(last_val.toIntern()); - if (first_ref == last_ref) { - try extra_case_vals.items.append(gpa, first_ref); - } else { - try extra_case_vals.ranges.append(gpa, .{ first_ref, last_ref }); - } - } - - const air_switch_ref = try sema.analyzeSwitchRuntimeBlock( - spa, - &child_block, - src, - switch (operand) { + if (operand_ty.zigTypeTag(zcu) == .error_set) { + const cond_ref = switch (operand) { .simple => |s| s.cond, .loop => |l| l.init_cond, - }, - cond_ty, - operand_src, - case_vals, - special_generic, - scalar_cases_len, - multi_cases_len, - union_originally, - raw_operand_ty, - err_set, - src_node_offset, - special_generic_src, - has_under, - seen_enum_fields, - seen_errors, - range_set, - true_count, - false_count, - cond_dbg_node_index, - false, - special_members_only, - special_members_only_src, - extra_case_vals.items.items, - extra_case_vals.ranges.items, - ); - - 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 (extra.data.bits.any_non_inline_capture) { - _ = try replacement_block.addBinOp(.store, operand.loop.operand_alloc, new_operand_maybe_ref); + }; + if (try sema.maybeErrorUnwrap(case_block, prong_body, cond_ref, operand_src, true)) { + // nothing to do here. weight against error branch + return .unlikely; } + } - 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.switchCond(&replacement_block, dispatch_src, new_operand_val); - - if (zcu.backendSupportsFeature(.is_named_enum_value) and block.wantSafety() and - cond_ty.zigTypeTag(zcu) == .@"enum" and !cond_ty.isNonexhaustiveEnum(zcu) and - !try sema.isComptimeKnown(new_cond)) + const operand_val, const operand_ptr = load_operand: { + if (capture == .none and !has_tag_capture) { + // No need to load the operand for this prong! + break :load_operand .{ undefined, undefined }; + } + if (inline_case_capture != .none and + !(capture != .none and operand_ty.zigTypeTag(zcu) == .@"union")) { - const ok = try replacement_block.addUnOp(.is_named_enum_value, new_cond); - try sema.addSafetyCheck(&replacement_block, src, ok, .corrupt_switch); + // We only need to load the operand if there's a union payload capture + // since it's always runtime-known; only the tag is comptime-known here. + break :load_operand .{ undefined, undefined }; } - - _ = try replacement_block.addInst(.{ - .tag = .switch_dispatch, - .data = .{ .br = .{ - .block_inst = air_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)); - } - - return sema.resolveAnalyzedBlock(block, src, &child_block, merges, false); -} - -const SpecialProng = struct { - body: []const Zir.Inst.Index, - end: usize, - capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, - is_inline: bool, - has_tag_capture: bool, -}; - -fn analyzeSwitchRuntimeBlock( - sema: *Sema, - spa: SwitchProngAnalysis, - child_block: *Block, - src: LazySrcLoc, - operand: Air.Inst.Ref, - operand_ty: Type, - operand_src: LazySrcLoc, - case_vals: std.ArrayList(Air.Inst.Ref), - else_prong: SpecialProng, - scalar_cases_len: usize, - multi_cases_len: usize, - union_originally: bool, - maybe_union_ty: Type, - err_set: bool, - switch_node_offset: std.zig.Ast.Node.Offset, - else_prong_src: LazySrcLoc, - else_prong_is_underscore: bool, - seen_enum_fields: []?LazySrcLoc, - seen_errors: SwitchErrorSet, - range_set: RangeSet, - true_count: u8, - false_count: u8, - cond_dbg_node_index: Zir.Inst.Index, - allow_err_code_unwrap: bool, - extra_prong: ?SpecialProng, - /// May be `undefined` if `extra_prong` is `null` - extra_prong_src: LazySrcLoc, - extra_prong_items: []const Air.Inst.Ref, - extra_prong_ranges: []const [2]Air.Inst.Ref, -) CompileError!Air.Inst.Ref { - const pt = sema.pt; - const zcu = pt.zcu; - const gpa = sema.gpa; - const ip = &zcu.intern_pool; - - const block = child_block.parent.?; - - const estimated_cases_extra = (scalar_cases_len + multi_cases_len) * - @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + 2; - var cases_extra = try std.ArrayList(u32).initCapacity(gpa, estimated_cases_extra); - defer cases_extra.deinit(gpa); - - var branch_hints = try std.ArrayList(std.builtin.BranchHint).initCapacity(gpa, scalar_cases_len); - defer branch_hints.deinit(gpa); - - var case_block = child_block.makeSubBlock(); - case_block.runtime_loop = null; - case_block.runtime_cond = operand_src; - case_block.runtime_index.increment(); - case_block.need_debug_scope = null; // this body is emitted regardless - defer case_block.instructions.deinit(gpa); - - var extra_index: usize = else_prong.end; - - var scalar_i: usize = 0; - while (scalar_i < scalar_cases_len) : (scalar_i += 1) { - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1; - const body = sema.code.bodySlice(extra_index, info.body_len); - extra_index += info.body_len; - - case_block.instructions.shrinkRetainingCapacity(0); - case_block.error_return_trace_index = child_block.error_return_trace_index; - - const item = case_vals.items[scalar_i]; - // `item` is already guaranteed to be constant known. - - const analyze_body = if (union_originally) blk: { - const unresolved_item_val = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, item, undefined) catch unreachable; - const item_val = sema.resolveLazyValue(unresolved_item_val) catch unreachable; - const field_ty = maybe_union_ty.unionFieldType(item_val, zcu).?; - break :blk field_ty.zigTypeTag(zcu) != .noreturn; - } else true; - - const prong_hint: std.builtin.BranchHint = if (err_set and - try sema.maybeErrorUnwrap(&case_block, body, operand, operand_src, allow_err_code_unwrap)) - h: { - // nothing to do here. weight against error branch - break :h .unlikely; - } else if (analyze_body) h: { - break :h try spa.analyzeProngRuntime( - &case_block, - .normal, - body, - info.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .{ .kind = .scalar, .index = @intCast(scalar_i) }, - } }), - &.{item}, - if (info.is_inline) item else .none, - info.has_tag_capture, - ); - } else h: { - _ = try case_block.addNoOp(.unreach); - break :h .none; - }; - - try branch_hints.append(gpa, prong_hint); - try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - 1 + // `item`, no ranges - case_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = 1, - .ranges_len = 0, - .body_len = @intCast(case_block.instructions.items.len), - })); - cases_extra.appendAssumeCapacity(@intFromEnum(item)); - cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); - } - - var cases_len = scalar_cases_len; - var case_val_idx: usize = scalar_cases_len; - const multi_cases_len_with_extra_prong = multi_cases_len + @intFromBool(extra_prong != null); - var multi_i: u32 = 0; - while (multi_i < multi_cases_len_with_extra_prong) : (multi_i += 1) { - const is_extra_prong = multi_i == multi_cases_len; - var items: []const Air.Inst.Ref = undefined; - var info: Zir.Inst.SwitchBlock.ProngInfo = undefined; - var ranges: []const [2]Air.Inst.Ref = undefined; - var body: []const Zir.Inst.Index = undefined; - if (is_extra_prong) { - const prong = extra_prong.?; - items = extra_prong_items; - ranges = extra_prong_ranges; - body = prong.body; - info = .{ - .body_len = undefined, - .capture = prong.capture, - .is_inline = prong.is_inline, - .has_tag_capture = prong.has_tag_capture, - }; - } else { - @branchHint(.likely); - const items_len = sema.code.extra[extra_index]; - extra_index += 1; - const ranges_len = sema.code.extra[extra_index]; - extra_index += 1; - info = @bitCast(sema.code.extra[extra_index]); - extra_index += 1 + items_len + ranges_len * 2; - - items = case_vals.items[case_val_idx..][0..items_len]; - case_val_idx += items_len; - ranges = @ptrCast(case_vals.items[case_val_idx..][0 .. ranges_len * 2]); - case_val_idx += ranges_len * 2; - - body = sema.code.bodySlice(extra_index, info.body_len); - extra_index += info.body_len; - } - - case_block.instructions.shrinkRetainingCapacity(0); - case_block.error_return_trace_index = child_block.error_return_trace_index; - - // Generate all possible cases as scalar prongs. - if (info.is_inline) { - var emit_bb = false; - - for (ranges, 0..) |range_items, range_i| { - var item = sema.resolveConstDefinedValue(block, .unneeded, range_items[0], undefined) catch unreachable; - const item_last = sema.resolveConstDefinedValue(block, .unneeded, range_items[1], undefined) catch unreachable; - - while (item.compareScalar(.lte, item_last, operand_ty, zcu)) : ({ - // Previous validation has resolved any possible lazy values. - const int_val: Value, const int_ty: Type = switch (operand_ty.zigTypeTag(zcu)) { - .int => .{ item, operand_ty }, - .@"enum" => b: { - const int_val = Value.fromInterned(ip.indexToKey(item.toIntern()).enum_tag.int); - break :b .{ int_val, int_val.typeOf(zcu) }; - }, - else => unreachable, - }; - const result = try arith.incrementDefinedInt(sema, int_ty, int_val); - assert(!result.overflow); - item = switch (operand_ty.zigTypeTag(zcu)) { - .int => result.val, - .@"enum" => .fromInterned(try pt.intern(.{ .enum_tag = .{ - .ty = operand_ty.toIntern(), - .int = result.val.toIntern(), - } })), - else => unreachable, - }; - }) { - cases_len += 1; - - const item_ref = Air.internedToRef(item.toIntern()); - - case_block.instructions.shrinkRetainingCapacity(0); - case_block.error_return_trace_index = child_block.error_return_trace_index; - - if (emit_bb) { - const bb_src = if (is_extra_prong) extra_prong_src else block.src(.{ .switch_case_item = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - .item_idx = .{ .kind = .range, .index = @intCast(range_i) }, - } }); - try sema.emitBackwardBranch(block, bb_src); - } - emit_bb = true; - - const prong_hint = try spa.analyzeProngRuntime( - &case_block, - .normal, - body, - info.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - } }), - undefined, // case_vals may be undefined for ranges - item_ref, - info.has_tag_capture, - ); - try branch_hints.append(gpa, prong_hint); - - try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - 1 + // `item`, no ranges - case_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = 1, - .ranges_len = 0, - .body_len = @intCast(case_block.instructions.items.len), - })); - cases_extra.appendAssumeCapacity(@intFromEnum(item_ref)); - cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); - - if (item.compareScalar(.eq, item_last, operand_ty, zcu)) break; - } - } - - for (items, 0..) |item, item_i| { - cases_len += 1; - - case_block.instructions.shrinkRetainingCapacity(0); - case_block.error_return_trace_index = child_block.error_return_trace_index; - - const analyze_body = if (union_originally) blk: { - const item_val = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, item, undefined) catch unreachable; - const field_ty = maybe_union_ty.unionFieldType(item_val, zcu).?; - break :blk field_ty.zigTypeTag(zcu) != .noreturn; - } else true; - - if (emit_bb) { - const bb_src = if (is_extra_prong) extra_prong_src else block.src(.{ .switch_case_item = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - .item_idx = .{ .kind = .single, .index = @intCast(item_i) }, - } }); - try sema.emitBackwardBranch(block, bb_src); - } - emit_bb = true; - - const prong_hint: std.builtin.BranchHint = if (analyze_body) h: { - break :h try spa.analyzeProngRuntime( - &case_block, - .normal, - body, - info.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - } }), - &.{item}, - item, - info.has_tag_capture, - ); - } else h: { - _ = try case_block.addNoOp(.unreach); - break :h .none; - }; - try branch_hints.append(gpa, prong_hint); - - try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - 1 + // `item`, no ranges - case_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = 1, - .ranges_len = 0, - .body_len = @intCast(case_block.instructions.items.len), - })); - cases_extra.appendAssumeCapacity(@intFromEnum(item)); - cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); - } - - continue; - } - - cases_len += 1; - - const analyze_body = if (union_originally) - for (items) |item| { - const item_val = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, item, undefined) catch unreachable; - const field_ty = maybe_union_ty.unionFieldType(item_val, zcu).?; - if (field_ty.zigTypeTag(zcu) != .noreturn) break true; - } else false - else - true; - - const prong_hint: std.builtin.BranchHint = if (err_set and - try sema.maybeErrorUnwrap(&case_block, body, operand, operand_src, allow_err_code_unwrap)) - h: { - // nothing to do here. weight against error branch - break :h .unlikely; - } else if (analyze_body) h: { - break :h try spa.analyzeProngRuntime( - &case_block, - .normal, - body, - info.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - } }), - items, - .none, - false, - ); - } else h: { - _ = try case_block.addNoOp(.unreach); - break :h .none; - }; - - try branch_hints.append(gpa, prong_hint); - - try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - items.len + ranges.len * 2 + - case_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = @intCast(items.len), - .ranges_len = @intCast(ranges.len), - .body_len = @intCast(case_block.instructions.items.len), - })); - - for (items) |item| { - cases_extra.appendAssumeCapacity(@intFromEnum(item)); - } - for (ranges) |range| { - cases_extra.appendSliceAssumeCapacity(&.{ - @intFromEnum(range[0]), - @intFromEnum(range[1]), - }); - } - - cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); - } - - const else_body: []const Air.Inst.Index = if (else_prong.body.len != 0 or case_block.wantSafety()) else_body: { - var emit_bb = false; - // If this is true we must have a 'true' else prong and not an underscore because - // underscore prongs can never be inlined. We've already checked for this. - if (else_prong.is_inline) switch (operand_ty.zigTypeTag(zcu)) { - .@"enum" => { - if (operand_ty.isNonexhaustiveEnum(zcu) and !union_originally) { - return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{ - operand_ty.fmt(pt), - }); - } - for (seen_enum_fields, 0..) |f, i| { - if (f != null) continue; - cases_len += 1; - - const item_val = try pt.enumValueFieldIndex(operand_ty, @intCast(i)); - const item_ref = Air.internedToRef(item_val.toIntern()); - - case_block.instructions.shrinkRetainingCapacity(0); - case_block.error_return_trace_index = child_block.error_return_trace_index; - - const analyze_body = if (union_originally) blk: { - const field_ty = maybe_union_ty.unionFieldType(item_val, zcu).?; - break :blk field_ty.zigTypeTag(zcu) != .noreturn; - } else true; - - if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src); - emit_bb = true; - - const prong_hint: std.builtin.BranchHint = if (analyze_body) h: { - break :h try spa.analyzeProngRuntime( - &case_block, - .special, - else_prong.body, - else_prong.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .special_else, - } }), - &.{item_ref}, - item_ref, - else_prong.has_tag_capture, - ); - } else h: { - _ = try case_block.addNoOp(.unreach); - break :h .none; - }; - try branch_hints.append(gpa, prong_hint); - - try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - 1 + // `item`, no ranges - case_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = 1, - .ranges_len = 0, - .body_len = @intCast(case_block.instructions.items.len), - })); - cases_extra.appendAssumeCapacity(@intFromEnum(item_ref)); - cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); + assert(zir_switch.any_maybe_runtime_capture); // should have caught everything else by now + 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, undefined }; } }, - .error_set => { - if (operand_ty.isAnyError(zcu)) { - return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{ - operand_ty.fmt(pt), - }); - } - const error_names = operand_ty.errorSetNames(zcu); - for (0..error_names.len) |name_index| { - const error_name = error_names.get(ip)[name_index]; - if (seen_errors.contains(error_name)) continue; - cases_len += 1; - - const item_val = try pt.intern(.{ .err = .{ - .ty = operand_ty.toIntern(), - .name = error_name, - } }); - const item_ref = Air.internedToRef(item_val); - - case_block.instructions.shrinkRetainingCapacity(0); - case_block.error_return_trace_index = child_block.error_return_trace_index; - - if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src); - emit_bb = true; - - const prong_hint = try spa.analyzeProngRuntime( - &case_block, - .special, - else_prong.body, - else_prong.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .special_else, - } }), - &.{item_ref}, - item_ref, - else_prong.has_tag_capture, - ); - try branch_hints.append(gpa, prong_hint); - - try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - 1 + // `item`, no ranges - case_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = 1, - .ranges_len = 0, - .body_len = @intCast(case_block.instructions.items.len), - })); - cases_extra.appendAssumeCapacity(@intFromEnum(item_ref)); - cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); - } - }, - .int => { - var it = try RangeSetUnhandledIterator.init(sema, operand_ty, range_set); - while (try it.next()) |cur| { - cases_len += 1; - - const item_ref = Air.internedToRef(cur); - - case_block.instructions.shrinkRetainingCapacity(0); - case_block.error_return_trace_index = child_block.error_return_trace_index; - - if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src); - emit_bb = true; - - const prong_hint = try spa.analyzeProngRuntime( - &case_block, - .special, - else_prong.body, - else_prong.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .special_else, - } }), - &.{item_ref}, - item_ref, - else_prong.has_tag_capture, - ); - try branch_hints.append(gpa, prong_hint); - - try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - 1 + // `item`, no ranges - case_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = 1, - .ranges_len = 0, - .body_len = @intCast(case_block.instructions.items.len), - })); - cases_extra.appendAssumeCapacity(@intFromEnum(item_ref)); - cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); - } - }, - .bool => { - if (true_count == 0) { - cases_len += 1; - - case_block.instructions.shrinkRetainingCapacity(0); - case_block.error_return_trace_index = child_block.error_return_trace_index; - - if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src); - emit_bb = true; - - const prong_hint = try spa.analyzeProngRuntime( - &case_block, - .special, - else_prong.body, - else_prong.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .special_else, - } }), - &.{.bool_true}, - .bool_true, - else_prong.has_tag_capture, - ); - try branch_hints.append(gpa, prong_hint); - - try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - 1 + // `item`, no ranges - case_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = 1, - .ranges_len = 0, - .body_len = @intCast(case_block.instructions.items.len), - })); - cases_extra.appendAssumeCapacity(@intFromEnum(Air.Inst.Ref.bool_true)); - cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); - } - if (false_count == 0) { - cases_len += 1; - - case_block.instructions.shrinkRetainingCapacity(0); - case_block.error_return_trace_index = child_block.error_return_trace_index; - - if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src); - emit_bb = true; - - const prong_hint = try spa.analyzeProngRuntime( - &case_block, - .special, - else_prong.body, - else_prong.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .special_else, - } }), - &.{.bool_false}, - .bool_false, - else_prong.has_tag_capture, - ); - try branch_hints.append(gpa, prong_hint); - - try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - 1 + // `item`, no ranges - case_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = 1, - .ranges_len = 0, - .body_len = @intCast(case_block.instructions.items.len), - })); - cases_extra.appendAssumeCapacity(@intFromEnum(Air.Inst.Ref.bool_false)); - cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); - } - }, - else => return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{ - operand_ty.fmt(pt), - }), - }; - - case_block.instructions.shrinkRetainingCapacity(0); - case_block.error_return_trace_index = child_block.error_return_trace_index; - - if (zcu.backendSupportsFeature(.is_named_enum_value) and - else_prong.body.len != 0 and block.wantSafety() and - operand_ty.zigTypeTag(zcu) == .@"enum" and - (!operand_ty.isNonexhaustiveEnum(zcu) or union_originally)) - { - try sema.zirDbgStmt(&case_block, cond_dbg_node_index); - const ok = try case_block.addUnOp(.is_named_enum_value, operand); - try sema.addSafetyCheck(&case_block, src, ok, .corrupt_switch); } - - const else_src_idx: LazySrcLoc.Offset.SwitchCaseIndex = if (else_prong_is_underscore) - .special_under - else - .special_else; - - const analyze_body = if (union_originally and !else_prong.is_inline) - for (seen_enum_fields, 0..) |seen_field, index| { - if (seen_field != null) continue; - const union_obj = zcu.typeToUnion(maybe_union_ty).?; - const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[index]); - if (field_ty.zigTypeTag(zcu) != .noreturn) break true; - } else false - else - true; - const else_hint: std.builtin.BranchHint = if (else_prong.body.len != 0 and err_set and - try sema.maybeErrorUnwrap(&case_block, else_prong.body, operand, operand_src, allow_err_code_unwrap)) - h: { - // nothing to do here. weight against error branch - break :h .unlikely; - } else if (else_prong.body.len != 0 and analyze_body and !else_prong.is_inline) h: { - break :h try spa.analyzeProngRuntime( - &case_block, - .special, - else_prong.body, - else_prong.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = else_src_idx, - } }), - undefined, // case_vals may be undefined for special prongs - .none, - false, - ); - } else h: { - // We still need a terminator in this block, but we have proven - // that it is unreachable. - if (case_block.wantSafety()) { - try sema.zirDbgStmt(&case_block, cond_dbg_node_index); - try sema.safetyPanic(&case_block, src, .corrupt_switch); - } else { - _ = try case_block.addNoOp(.unreach); - } - // Safety check / unreachable branches are cold. - break :h .cold; - }; - - try branch_hints.append(gpa, else_hint); - break :else_body case_block.instructions.items; - } else else_body: { - try branch_hints.append(gpa, .none); - break :else_body &.{}; }; - assert(branch_hints.items.len == cases_len + 1); - - try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr).@"struct".fields.len + - cases_extra.items.len + else_body.len + - (std.math.divCeil(usize, branch_hints.items.len, 10) catch unreachable)); // branch hints - - const payload_index = sema.addExtraAssumeCapacity(Air.SwitchBr{ - .cases_len = @intCast(cases_len), - .else_body_len = @intCast(else_body.len), - }); - - { - // Add branch hints. - var cur_bag: u32 = 0; - for (branch_hints.items, 0..) |hint, idx| { - const idx_in_bag = idx % 10; - cur_bag |= @as(u32, @intFromEnum(hint)) << @intCast(idx_in_bag * 3); - if (idx_in_bag == 9) { - sema.air_extra.appendAssumeCapacity(cur_bag); - cur_bag = 0; - } - } - if (branch_hints.items.len % 10 != 0) { - sema.air_extra.appendAssumeCapacity(cur_bag); - } - } - sema.air_extra.appendSliceAssumeCapacity(@ptrCast(cases_extra.items)); - sema.air_extra.appendSliceAssumeCapacity(@ptrCast(else_body)); - - const has_any_continues = spa.operand == .loop and child_block.label.?.merges.extra_insts.items.len > 0; - - return try child_block.addInst(.{ - .tag = if (has_any_continues) .loop_switch_br else .switch_br, - .data = .{ .pl_op = .{ - .operand = operand, - .payload = payload_index, - } }, - }); -} - -fn resolveSwitchComptimeLoop( - sema: *Sema, - init_spa: SwitchProngAnalysis, - child_block: *Block, - maybe_ptr_operand_ty: Type, - cond_ty: Type, - init_cond_val: Value, - switch_node_offset: std.zig.Ast.Node.Offset, - special_members_only: ?SpecialProng, - special_generic: SpecialProng, - special_generic_is_under: bool, - case_vals: std.ArrayList(Air.Inst.Ref), - scalar_cases_len: u32, - multi_cases_len: u32, - err_set: bool, - empty_enum: bool, - operand_is_ref: bool, -) CompileError!Air.Inst.Ref { - var spa = init_spa; - var cond_val = init_cond_val; - - while (true) { - if (resolveSwitchComptime( - sema, - spa, - child_block, - spa.operand.simple.cond, - cond_val, - cond_ty, - switch_node_offset, - special_members_only, - special_generic, - special_generic_is_under, - case_vals, - scalar_cases_len, - multi_cases_len, - err_set, - empty_enum, - )) |result| { - return result; - } else |err| switch (err) { - error.ComptimeBreak => { - const break_inst = sema.code.instructions.get(@intFromEnum(sema.comptime_break_inst)); - if (break_inst.tag != .switch_continue) return error.ComptimeBreak; - const extra = sema.code.extraData(Zir.Inst.Break, break_inst.data.@"break".payload_index).data; - if (extra.block_inst != spa.switch_block_inst) return error.ComptimeBreak; - // This is a `switch_continue` targeting this block. Change the operand and start over. - const src = child_block.nodeOffset(extra.operand_src_node.unwrap().?); - const new_operand_uncoerced = try sema.resolveInst(break_inst.data.@"break".operand); - const new_operand = try sema.coerce(child_block, maybe_ptr_operand_ty, new_operand_uncoerced, src); - - try sema.emitBackwardBranch(child_block, src); - - const val, const ref = if (operand_is_ref) - .{ try sema.analyzeLoad(child_block, src, new_operand, src), new_operand } - else - .{ new_operand, undefined }; - - const cond_ref = try sema.switchCond(child_block, src, val); - - cond_val = try sema.resolveConstDefinedValue(child_block, src, cond_ref, null); - spa.operand = .{ .simple = .{ - .by_val = val, - .by_ref = ref, - .cond = cond_ref, - } }; + 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 = try sema.analyzeSwitchPayloadCapture( + case_block, + operand, + operand_val, + operand_ptr, + operand_ty, + operand_src, + capture_src, + capture == .by_ref, + kind == .special, + switch (kind) { + .item_refs => |item_refs| item_refs, + .has_ranges, .special => undefined, }, - else => |e| return e, - } - } -} - -fn resolveSwitchComptime( - sema: *Sema, - spa: SwitchProngAnalysis, - child_block: *Block, - cond_operand: Air.Inst.Ref, - operand_val: Value, - operand_ty: Type, - switch_node_offset: std.zig.Ast.Node.Offset, - special_members_only: ?SpecialProng, - special_generic: SpecialProng, - special_generic_is_under: bool, - case_vals: std.ArrayList(Air.Inst.Ref), - scalar_cases_len: u32, - multi_cases_len: u32, - err_set: bool, - empty_enum: bool, -) CompileError!Air.Inst.Ref { - const zcu = sema.pt.zcu; - const merges = &child_block.label.?.merges; - const resolved_operand_val = try sema.resolveLazyValue(operand_val); - - var extra_index: usize = special_generic.end; - { - var scalar_i: usize = 0; - while (scalar_i < scalar_cases_len) : (scalar_i += 1) { - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1; - const body = sema.code.bodySlice(extra_index, info.body_len); - extra_index += info.body_len; - - const item = case_vals.items[scalar_i]; - const item_val = sema.resolveConstDefinedValue(child_block, LazySrcLoc.unneeded, item, undefined) catch unreachable; - if (operand_val.eql(item_val, operand_ty, sema.pt.zcu)) { - if (err_set) try sema.maybeErrorUnwrapComptime(child_block, body, cond_operand); - return spa.resolveProngComptime( - child_block, - .normal, - body, - info.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .{ .kind = .scalar, .index = @intCast(scalar_i) }, - } }), - &.{item}, - if (info.is_inline) cond_operand else .none, - info.has_tag_capture, - merges, - ); - } - } - } - { - var multi_i: usize = 0; - var case_val_idx: usize = scalar_cases_len; - while (multi_i < multi_cases_len) : (multi_i += 1) { - const items_len = sema.code.extra[extra_index]; - extra_index += 1; - const ranges_len = sema.code.extra[extra_index]; - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1 + items_len; - const body = sema.code.bodySlice(extra_index + 2 * ranges_len, info.body_len); - - const items = case_vals.items[case_val_idx..][0..items_len]; - case_val_idx += items_len; - - for (items) |item| { - // Validation above ensured these will succeed. - const item_val = sema.resolveConstDefinedValue(child_block, LazySrcLoc.unneeded, item, undefined) catch unreachable; - if (operand_val.eql(item_val, operand_ty, sema.pt.zcu)) { - if (err_set) try sema.maybeErrorUnwrapComptime(child_block, body, cond_operand); - return spa.resolveProngComptime( - child_block, - .normal, - body, - info.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - } }), - items, - if (info.is_inline) cond_operand else .none, - info.has_tag_capture, - merges, - ); - } - } - - var range_i: usize = 0; - while (range_i < ranges_len) : (range_i += 1) { - const range_items = case_vals.items[case_val_idx..][0..2]; - extra_index += 2; - case_val_idx += 2; - - // Validation above ensured these will succeed. - const first_val = sema.resolveConstDefinedValue(child_block, LazySrcLoc.unneeded, range_items[0], undefined) catch unreachable; - const last_val = sema.resolveConstDefinedValue(child_block, LazySrcLoc.unneeded, range_items[1], undefined) catch unreachable; - if ((try sema.compareAll(resolved_operand_val, .gte, first_val, operand_ty)) and - (try sema.compareAll(resolved_operand_val, .lte, last_val, operand_ty))) - { - if (err_set) try sema.maybeErrorUnwrapComptime(child_block, body, cond_operand); - return spa.resolveProngComptime( - child_block, - .normal, - body, - info.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - } }), - undefined, // case_vals may be undefined for ranges - if (info.is_inline) cond_operand else .none, - info.has_tag_capture, - merges, - ); - } - } - - extra_index += info.body_len; - } - } - if (err_set) try sema.maybeErrorUnwrapComptime(child_block, special_generic.body, cond_operand); - if (empty_enum) { - return .void_value; - } - if (special_members_only) |special| { - assert(operand_ty.isNonexhaustiveEnum(zcu)); - if (operand_ty.enumTagFieldIndex(operand_val, zcu)) |_| { - return spa.resolveProngComptime( - child_block, - .special, - special.body, - special.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = .special_else, - } }), - undefined, // case_vals may be undefined for special prongs - if (special.is_inline) cond_operand else .none, - special.has_tag_capture, - merges, - ); - } - } - - return spa.resolveProngComptime( - child_block, - .special, - special_generic.body, - special_generic.capture, - child_block.src(.{ .switch_capture = .{ - .switch_node_offset = switch_node_offset, - .case_idx = if (special_generic_is_under) - .special_under - else - .special_else, - } }), - undefined, // case_vals may be undefined for special prongs - if (special_generic.is_inline) cond_operand else .none, - special_generic.has_tag_capture, - merges, - ); -} - -const RangeSetUnhandledIterator = struct { - pt: Zcu.PerThread, - cur: ?InternPool.Index, - max: InternPool.Index, - range_i: usize, - ranges: []const RangeSet.Range, - limbs: []math.big.Limb, - - const preallocated_limbs = math.big.int.calcTwosCompLimbCount(128); - - fn init(sema: *Sema, ty: Type, range_set: RangeSet) !RangeSetUnhandledIterator { - const pt = sema.pt; - const int_type = pt.zcu.intern_pool.indexToKey(ty.toIntern()).int_type; - const needed_limbs = math.big.int.calcTwosCompLimbCount(int_type.bits); - return .{ - .pt = pt, - .cur = (try ty.minInt(pt, ty)).toIntern(), - .max = (try ty.maxInt(pt, ty)).toIntern(), - .range_i = 0, - .ranges = range_set.ranges.items, - .limbs = if (needed_limbs > preallocated_limbs) - try sema.arena.alloc(math.big.Limb, needed_limbs) - else - &.{}, - }; - } - - fn addOne(it: *const RangeSetUnhandledIterator, val: InternPool.Index) !?InternPool.Index { - if (val == it.max) return null; - const int = it.pt.zcu.intern_pool.indexToKey(val).int; - - switch (int.storage) { - inline .u64, .i64 => |val_int| { - const next_int = @addWithOverflow(val_int, 1); - if (next_int[1] == 0) - return (try it.pt.intValue(.fromInterned(int.ty), next_int[0])).toIntern(); - }, - .big_int => {}, - .lazy_align, .lazy_size => unreachable, - } - - var val_space: InternPool.Key.Int.Storage.BigIntSpace = undefined; - const val_bigint = int.storage.toBigInt(&val_space); - - var result_limbs: [preallocated_limbs]math.big.Limb = undefined; - var result_bigint = math.big.int.Mutable.init( - if (it.limbs.len > 0) it.limbs else &result_limbs, - 0, + inline_case_capture, + else_err_ty, ); + assert(!sema.typeOf(payload_ref).isNoReturn(sema.pt.zcu)); + sema.inst_map.putAssumeCapacity(payload_inst, payload_ref); + break :inst payload_inst; + } else undefined; + defer if (capture != .none) assert(sema.inst_map.remove(payload_inst)); - result_bigint.addScalar(val_bigint, 1); - return (try it.pt.intValue_big(.fromInterned(int.ty), result_bigint.toConst())).toIntern(); - } + 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, + inline_case_capture, + ); + sema.inst_map.putAssumeCapacity(tag_inst, tag_ref); + break :inst tag_inst; + } else undefined; + defer if (has_tag_capture) assert(sema.inst_map.remove(tag_inst)); - fn next(it: *RangeSetUnhandledIterator) !?InternPool.Index { - var cur = it.cur orelse return null; - while (it.range_i < it.ranges.len and cur == it.ranges[it.range_i].first) { - defer it.range_i += 1; - cur = (try it.addOne(it.ranges[it.range_i].last)) orelse { - it.cur = null; - return null; - }; - } - it.cur = try it.addOne(cur); - return cur; - } -}; + if (zir_switch.has_continue) sema.inst_map.putAssumeCapacity(switch_inst, .fromType(raw_operand_ty)); + defer if (zir_switch.has_continue) assert(sema.inst_map.remove(switch_inst)); -const ResolvedSwitchItem = struct { - ref: Air.Inst.Ref, - val: InternPool.Index, -}; -fn resolveSwitchItemVal( - sema: *Sema, - block: *Block, - item_ref: Zir.Inst.Ref, - /// Coerce `item_ref` to this type. - coerce_ty: Type, - item_src: LazySrcLoc, -) CompileError!ResolvedSwitchItem { - const uncoerced_item = try sema.resolveInst(item_ref); - - // Constructing a LazySrcLoc is costly because we only have the switch AST node. - // Only if we know for sure we need to report a compile error do we resolve the - // full source locations. - - const item = try sema.coerce(block, coerce_ty, uncoerced_item, item_src); - - const maybe_lazy = try sema.resolveConstDefinedValue(block, item_src, item, .{ .simple = .switch_item }); - - const val = try sema.resolveLazyValue(maybe_lazy); - const new_item = if (val.toIntern() != maybe_lazy.toIntern()) blk: { - break :blk Air.internedToRef(val.toIntern()); - } else item; - - return .{ .ref = new_item, .val = val.toIntern() }; + return sema.analyzeBodyRuntimeBreak(case_block, prong_body); } -fn validateErrSetSwitch( +fn analyzeSwitchTagCapture( sema: *Sema, - block: *Block, - seen_errors: *SwitchErrorSet, - case_vals: *std.ArrayList(Air.Inst.Ref), + case_block: *Block, + /// May be `undefined` if `inline_case_capture` is not `.none`. + operand_val: Air.Inst.Ref, operand_ty: Type, - inst_data: @FieldType(Zir.Inst.Data, "pl_node"), - scalar_cases_len: u32, - multi_cases_len: u32, - else_case: struct { body: []const Zir.Inst.Index, end: usize, src: LazySrcLoc }, - has_else: bool, -) CompileError!?Type { - const gpa = sema.gpa; + capture_src: LazySrcLoc, + inline_case_capture: Air.Inst.Ref, +) 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 (inline_case_capture != .none) { + return inline_case_capture; // this already is the tag, it's what we're switching on! + } + const tag_ty = operand_ty.unionTagType(zcu).?; + return sema.unionToTag(case_block, tag_ty, operand_val, tag_capture_src); +} + +fn analyzeSwitchPayloadCapture( + sema: *Sema, + case_block: *Block, + operand: SwitchOperand, + /// May be `undefined` if this is an inline capture and operand is not a union. + operand_val: Air.Inst.Ref, + /// May be `undefined` if `capture_by_ref` is `false` or if `operand_val` is also `undefined`. + operand_ptr: Air.Inst.Ref, + operand_ty: Type, + operand_src: LazySrcLoc, + capture_src: LazySrcLoc, + capture_by_ref: bool, + is_special_prong: bool, + /// May be `undefined` if `is_special_prong` is `true`. + case_vals: []const Air.Inst.Ref, + /// If this is not `.none`, this is an inline capture. + inline_case_capture: Air.Inst.Ref, + else_err_ty: ?Type, +) CompileError!Air.Inst.Ref { const pt = sema.pt; const zcu = pt.zcu; const ip = &zcu.intern_pool; - const src_node_offset = inst_data.src_node; - const src = block.nodeOffset(src_node_offset); + const switch_node_offset = operand_src.offset.node_offset_switch_operand; - var extra_index: usize = else_case.end; - { - var scalar_i: u32 = 0; - while (scalar_i < scalar_cases_len) : (scalar_i += 1) { - const item_ref: Zir.Inst.Ref = @enumFromInt(sema.code.extra[extra_index]); - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1 + info.body_len; - - case_vals.appendAssumeCapacity(try sema.validateSwitchItemError( - block, - seen_errors, - item_ref, - operand_ty, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .scalar, .index = @intCast(scalar_i) }, - .item_idx = .{ .kind = .single, .index = 0 }, - } }), - )); - } - } - { - var multi_i: u32 = 0; - while (multi_i < multi_cases_len) : (multi_i += 1) { - const items_len = sema.code.extra[extra_index]; - extra_index += 1; - const ranges_len = sema.code.extra[extra_index]; - extra_index += 1; - const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]); - extra_index += 1; - const items = sema.code.refSlice(extra_index, items_len); - extra_index += items_len + info.body_len; - - try case_vals.ensureUnusedCapacity(gpa, items.len); - for (items, 0..) |item_ref, item_i| { - case_vals.appendAssumeCapacity(try sema.validateSwitchItemError( - block, - seen_errors, - item_ref, - operand_ty, - block.src(.{ .switch_case_item = .{ - .switch_node_offset = src_node_offset, - .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) }, - .item_idx = .{ .kind = .single, .index = @intCast(item_i) }, - } }), - )); + if (inline_case_capture != .none) { + const item_val = sema.resolveConstDefinedValue(case_block, .unneeded, inline_case_capture, undefined) catch unreachable; + if (operand_ty.zigTypeTag(zcu) == .@"union") { + 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.ptrTypeSema(.{ + .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); + } + return case_block.addStructFieldVal(operand_val, field_index, field_ty); } - - try sema.validateSwitchNoRange(block, ranges_len, operand_ty, src_node_offset); + } else if (capture_by_ref) { + return sema.uavRef(item_val.toIntern()); + } else { + return inline_case_capture; } } - switch (try sema.resolveInferredErrorSetTy(block, src, operand_ty.toIntern())) { - .anyerror_type => { - if (!has_else) { - return sema.fail( - block, - src, - "else prong required when switching on type 'anyerror'", - .{}, - ); - } - return .anyerror; - }, - else => |err_set_ty_index| else_validation: { - const error_names = ip.indexToKey(err_set_ty_index).error_set_type.names; - var maybe_msg: ?*Zcu.ErrorMsg = null; - errdefer if (maybe_msg) |msg| msg.destroy(sema.gpa); + const operand_ptr_ty = if (capture_by_ref) sema.typeOf(operand_ptr) else undefined; - for (error_names.get(ip)) |error_name| { - if (!seen_errors.contains(error_name) and !has_else) { - const msg = maybe_msg orelse blk: { - maybe_msg = try sema.errMsg( - src, - "switch must handle all possibilities", - .{}, - ); - break :blk maybe_msg.?; + if (is_special_prong) { + 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, + }; + } + + switch (operand_ty.zigTypeTag(zcu)) { + .@"union" => { + const union_obj = zcu.typeToUnion(operand_ty).?; + const first_item_val = sema.resolveConstDefinedValue(case_block, .unneeded, case_vals[0], undefined) catch unreachable; + + 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.resolveConstDefinedValue(case_block, .unneeded, item, undefined) catch unreachable; + 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); + } + + 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) }, + } }, }; + } - try sema.errNote( - src, - msg, - "unhandled error value: 'error.{f}'", - .{error_name.fmt(ip)}, - ); + 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_info = operand_ptr_ty.ptrInfo(zcu); + const capture_ptr_ty = resolve: { + // By-ref captures of hetereogeneous types are only allowed if all field + // pointer types are peer resolvable to each other. + // 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_idx, *dummy| { + const field_ty: Type = .fromInterned(union_obj.field_types.get(ip)[field_idx]); + const field_ptr_ty = try pt.ptrTypeSema(.{ + .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, + .alignment = union_obj.fieldAlign(ip, field_idx), + }, + }); + 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 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); + } + + try sema.requireRuntimeBlock(case_block, operand_src, null); + + if (same_types) { + return case_block.addStructFieldVal(operand_val, 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; + } + } + // 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.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); + } } } - if (maybe_msg) |msg| { - maybe_msg = null; - try sema.addDeclaredHereNote(msg, operand_ty); - return sema.failWithOwnedErrorMsg(block, msg); + 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 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); } - if (has_else and seen_errors.count() == error_names.len) { - // In order to enable common patterns for generic code allow simple else bodies - // else => unreachable, - // else => return, - // else => |e| return e, - // even if all the possible errors were already handled. - const tags = sema.code.instructions.items(.tag); - const datas = sema.code.instructions.items(.data); - for (else_case.body) |else_inst| switch (tags[@intFromEnum(else_inst)]) { - .dbg_stmt, - .dbg_var_val, - .ret_type, - .as_node, - .ret_node, - .@"unreachable", - .@"defer", - .defer_err_code, - .err_union_code, - .ret_err_value_code, - .save_err_ret_index, - .restore_err_ret_index_unconditional, - .restore_err_ret_index_fn_entry, - .is_non_err, - .ret_is_non_err, - .condbr, - => {}, - .extended => switch (datas[@intFromEnum(else_inst)].extended.opcode) { - .restore_err_ret_index => {}, - else => break, - }, - else => break, - } else break :else_validation; + { + // 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 + 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); + _ = 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".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); + + // 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_ty), + .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( - block, - else_case.src, - "unreachable else prong; all cases already handled", + case_block, + capture_src, + "error set cannot be captured by reference", .{}, ); } + if (case_vals.len == 1) { + const item_val = sema.resolveConstDefinedValue(case_block, .unneeded, case_vals[0], undefined) catch unreachable; + const item_ty = try pt.singleErrorSetType(item_val.getErrorName(zcu).unwrap().?); + return sema.bitCast(case_block, item_ty, operand_val, operand_src, null); + } + var names: InferredErrorSet.NameMap = .{}; - try names.ensureUnusedCapacity(sema.arena, error_names.len); - for (error_names.get(ip)) |error_name| { - if (seen_errors.contains(error_name)) continue; - - names.putAssumeCapacityNoClobber(error_name, {}); + try names.ensureUnusedCapacity(sema.arena, case_vals.len); + for (case_vals) |err| { + const err_val = sema.resolveConstDefinedValue(case_block, .unneeded, err, undefined) catch unreachable; + 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. + if (capture_by_ref) { + return operand_ptr; + } else { + return operand_val; } - // No need to keep the hash map metadata correct; here we - // extract the (sorted) keys only. - return try pt.errorSetFromUnsortedNames(names.keys()); }, } - return null; } -fn validateSwitchRange( +const ResolvedSwitchItem = struct { + ref: Air.Inst.Ref, + val: Value, +}; +const ResolvedSwitchItemAndExtraIndex = struct { ResolvedSwitchItem, usize }; + +fn resolveSwitchItem( sema: *Sema, block: *Block, - range_set: *RangeSet, - first_ref: Zir.Inst.Ref, - last_ref: Zir.Inst.Ref, - operand_ty: Type, item_src: LazySrcLoc, -) CompileError![2]Air.Inst.Ref { - const first_src: LazySrcLoc = .{ - .base_node_inst = item_src.base_node_inst, - .offset = .{ .switch_case_item_range_first = item_src.offset.switch_case_item }, + item_ty: Type, + item_info: Zir.Inst.SwitchBlock.ItemInfo, + extra_index: usize, + switch_inst: Zir.Inst.Index, + prong_is_comptime_unreach: bool, + prong_is_inline: bool, +) CompileError!ResolvedSwitchItemAndExtraIndex { + const pt = sema.pt; + const zcu = pt.zcu; + const ip = &zcu.intern_pool; + const gpa = sema.gpa; + + var end = extra_index; + const uncoerced: Air.Inst.Ref, const uncoerced_ty: Type = uncoerced: switch (item_info.unwrap()) { + .enum_literal => |str_index| { + const zir_str = sema.code.nullTerminatedString(str_index); + const name = try ip.getOrPutString(gpa, pt.tid, zir_str, .no_embedded_nulls); + const uncoerced = try sema.analyzeDeclLiteral(block, item_src, name, item_ty, false); + break :uncoerced .{ uncoerced, .enum_literal }; + }, + .error_value => |str_index| { + const zir_str = sema.code.nullTerminatedString(str_index); + const name = try ip.getOrPutString(gpa, pt.tid, zir_str, .no_embedded_nulls); + // Make sure there's an error integer value associated with `name`. + _ = try pt.getErrorValue(name); + const err_set_ty = try pt.singleErrorSetType(name); + const uncoerced = Air.internedToRef(try pt.intern(.{ .err = .{ + .ty = err_set_ty.toIntern(), + .name = name, + } })); + break :uncoerced .{ uncoerced, err_set_ty }; + }, + .number_literal => |zir_ref| { + const uncoerced = try sema.resolveInst(zir_ref); + break :uncoerced .{ uncoerced, sema.typeOf(uncoerced) }; + }, + .body_len => |body_len| { + const body = sema.code.bodySlice(extra_index, body_len); + end += body.len; + + const uncoerced = ref: { + // The result location of item bodies is `.{ .coerce_ty = switch_inst }`. + sema.inst_map.putAssumeCapacity(switch_inst, .fromType(item_ty)); + defer assert(sema.inst_map.remove(switch_inst)); + break :ref try sema.resolveInlineBody(block, body, switch_inst); + }; + break :uncoerced .{ uncoerced, sema.typeOf(uncoerced) }; + }, }; - const last_src: LazySrcLoc = .{ - .base_node_inst = item_src.base_node_inst, - .offset = .{ .switch_case_item_range_last = item_src.offset.switch_case_item }, + const item_ref: Air.Inst.Ref = item_ref: { + if (item_ty.zigTypeTag(zcu) == .error_set and + uncoerced_ty.zigTypeTag(zcu) == .error_set) + { + // We allow prongs with errors which are not part of the error set + // being switched on if their prong body is `=> comptime unreachable,`. + switch (try sema.coerceInMemoryAllowedErrorSets(block, item_ty, uncoerced_ty, item_src, item_src)) { + .ok => if (try sema.resolveValue(uncoerced)) |uncoerced_val| { + break :item_ref try sema.coerceInMemory(uncoerced_val, item_ty); + }, + .missing_error => if (prong_is_comptime_unreach and !prong_is_inline) { + break :item_ref uncoerced; + }, + .from_anyerror => {}, + else => unreachable, + } + } + break :item_ref try sema.coerce(block, item_ty, uncoerced, item_src); }; - const first = try sema.resolveSwitchItemVal(block, first_ref, operand_ty, first_src); - const last = try sema.resolveSwitchItemVal(block, last_ref, operand_ty, last_src); - if (try Value.fromInterned(first.val).compareAll(.gt, Value.fromInterned(last.val), operand_ty, sema.pt)) { - return sema.fail(block, item_src, "range start value is greater than the end value", .{}); - } - const maybe_prev_src = try range_set.add(first.val, last.val, item_src); - try sema.validateSwitchDupe(block, maybe_prev_src, item_src); - return .{ first.ref, last.ref }; -} + const maybe_lazy = try sema.resolveConstDefinedValue(block, item_src, item_ref, .{ .simple = .switch_item }); -fn validateSwitchItemInt( - sema: *Sema, - block: *Block, - range_set: *RangeSet, - item_ref: Zir.Inst.Ref, - operand_ty: Type, - item_src: LazySrcLoc, -) CompileError!Air.Inst.Ref { - const item = try sema.resolveSwitchItemVal(block, item_ref, operand_ty, item_src); - const maybe_prev_src = try range_set.add(item.val, item.val, item_src); - try sema.validateSwitchDupe(block, maybe_prev_src, item_src); - return item.ref; -} + // We have to resolve lazy values here to avoid false negatives when detecting + // duplicate items and comparing items to a comptime-known switch operand. -fn validateSwitchItemEnum( - sema: *Sema, - block: *Block, - seen_fields: []?LazySrcLoc, - range_set: *RangeSet, - item_ref: Zir.Inst.Ref, - operand_ty: Type, - item_src: LazySrcLoc, -) CompileError!Air.Inst.Ref { - const ip = &sema.pt.zcu.intern_pool; - const item = try sema.resolveSwitchItemVal(block, item_ref, operand_ty, item_src); - const int = ip.indexToKey(item.val).enum_tag.int; - const field_index = ip.loadEnumType(ip.typeOf(item.val)).tagValueIndex(ip, int) orelse { - const maybe_prev_src = try range_set.add(int, int, item_src); - try sema.validateSwitchDupe(block, maybe_prev_src, item_src); - return item.ref; - }; - const maybe_prev_src = seen_fields[field_index]; - seen_fields[field_index] = item_src; - try sema.validateSwitchDupe(block, maybe_prev_src, item_src); - return item.ref; -} - -fn validateSwitchItemError( - sema: *Sema, - block: *Block, - seen_errors: *SwitchErrorSet, - item_ref: Zir.Inst.Ref, - operand_ty: Type, - item_src: LazySrcLoc, -) CompileError!Air.Inst.Ref { - const item = try sema.resolveSwitchItemVal(block, item_ref, operand_ty, item_src); - const error_name = sema.pt.zcu.intern_pool.indexToKey(item.val).err.name; - const maybe_prev_src = if (try seen_errors.fetchPut(error_name, item_src)) |prev| - prev.value + const val = try sema.resolveLazyValue(maybe_lazy); + const ref: Air.Inst.Ref = if (val.toIntern() == maybe_lazy.toIntern()) + item_ref else - null; - try sema.validateSwitchDupe(block, maybe_prev_src, item_src); - return item.ref; + .fromValue(val); + return .{ .{ .ref = ref, .val = val }, end }; } -fn validateSwitchDupe( +fn validateSwitchItemOrRange( sema: *Sema, block: *Block, - maybe_prev_src: ?LazySrcLoc, item_src: LazySrcLoc, + /// If `opt_last_val` is not `null`, this refers to the first val of a range. + item_val: Value, + opt_last_val: ?Value, + item_ty: Type, + seen_enum_fields: []?LazySrcLoc, + seen_errors: *std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc), + seen_sparse_values: *std.AutoHashMapUnmanaged(InternPool.Index, LazySrcLoc), + range_set: *RangeSet, + true_src: *?LazySrcLoc, + false_src: *?LazySrcLoc, + void_src: *?LazySrcLoc, ) CompileError!void { - const prev_item_src = maybe_prev_src orelse return; - return sema.failWithOwnedErrorMsg(block, msg: { - const msg = try sema.errMsg( - item_src, - "duplicate switch value", - .{}, - ); - errdefer msg.destroy(sema.gpa); - try sema.errNote( - prev_item_src, - msg, - "previous value here", - .{}, - ); - break :msg msg; - }); -} - -fn validateSwitchItemBool( - sema: *Sema, - block: *Block, - true_count: *u8, - false_count: *u8, - item_ref: Zir.Inst.Ref, - item_src: LazySrcLoc, -) CompileError!Air.Inst.Ref { - const item = try sema.resolveSwitchItemVal(block, item_ref, .bool, item_src); - if (Value.fromInterned(item.val).toBool()) { - true_count.* += 1; - } else { - false_count.* += 1; - } - if (true_count.* > 1 or false_count.* > 1) { - return sema.fail(block, item_src, "duplicate switch value", .{}); - } - return item.ref; -} - -const ValueSrcMap = std.AutoHashMapUnmanaged(InternPool.Index, LazySrcLoc); - -fn validateSwitchItemSparse( - sema: *Sema, - block: *Block, - seen_values: *ValueSrcMap, - item_ref: Zir.Inst.Ref, - operand_ty: Type, - item_src: LazySrcLoc, -) CompileError!Air.Inst.Ref { - const item = try sema.resolveSwitchItemVal(block, item_ref, operand_ty, item_src); - const kv = try seen_values.fetchPut(sema.gpa, item.val, item_src) orelse return item.ref; - try sema.validateSwitchDupe(block, kv.value, item_src); - unreachable; -} - -fn validateSwitchNoRange( - sema: *Sema, - block: *Block, - ranges_len: u32, - operand_ty: Type, - src_node_offset: std.zig.Ast.Node.Offset, -) CompileError!void { - if (ranges_len == 0) - return; - - const operand_src = block.src(.{ .node_offset_switch_operand = src_node_offset }); - const range_src = block.src(.{ .node_offset_switch_range = src_node_offset }); - - const msg = msg: { - const msg = try sema.errMsg( - operand_src, - "ranges not allowed when switching on type '{f}'", - .{operand_ty.fmt(sema.pt)}, - ); - errdefer msg.destroy(sema.gpa); - try sema.errNote( - range_src, - msg, - "range here", - .{}, - ); - break :msg msg; + const pt = sema.pt; + 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| { + const maybe_prev_src = seen_enum_fields[field_index]; + seen_enum_fields[field_index] = item_src; + break :maybe_prev_src maybe_prev_src; + } else { + break :maybe_prev_src try range_set.add(sema.arena, .{ + .first = .fromInterned(int), + .last = .fromInterned(int), + .src = item_src, + }, .fromInterned(ip.typeOf(int)), zcu); + } + }, + .error_set => { + const error_name = ip.indexToKey(item_val.toIntern()).err.name; + break :maybe_prev_src if (seen_errors.fetchPutAssumeCapacity(error_name, item_src)) |prev| + prev.value + else + null; + }, + .int, .comptime_int => { + if (opt_last_val) |last_val| { + const first_val = item_val; + if (try first_val.compareAll(.gt, last_val, item_ty, pt)) { + return sema.fail(block, item_src, "range start value is greater than the end value", .{}); + } + break :maybe_prev_src range_set.addAssumeCapacity(.{ + .first = first_val, + .last = last_val, + .src = item_src, + }, item_ty, zcu); + } else { + break :maybe_prev_src range_set.addAssumeCapacity(.{ + .first = item_val, + .last = item_val, + .src = item_src, + }, item_ty, zcu); + } + }, + .enum_literal, .@"fn", .pointer, .type => { + break :maybe_prev_src if (seen_sparse_values.fetchPutAssumeCapacity(item_val.toIntern(), item_src)) |prev| + prev.value + else + null; + }, + .bool => { + if (item_val.toBool()) { + if (true_src.*) |prev_src| break :maybe_prev_src prev_src; + true_src.* = item_src; + } else { + if (false_src.*) |prev_src| break :maybe_prev_src prev_src; + false_src.* = item_src; + } + break :maybe_prev_src null; + }, + .void => { + if (void_src.*) |prev_src| break :maybe_prev_src prev_src; + void_src.* = item_src; + break :maybe_prev_src null; + }, + else => unreachable, // should have already checked for invalid types }; - return sema.failWithOwnedErrorMsg(block, msg); + if (maybe_prev_src) |prev_src| { + return sema.failWithOwnedErrorMsg(block, msg: { + const msg = try sema.errMsg( + item_src, + "duplicate switch value", + .{}, + ); + errdefer msg.destroy(sema.gpa); + try sema.errNote( + prev_src, + msg, + "previous value here", + .{}, + ); + break :msg msg; + }); + } } fn maybeErrorUnwrap( diff --git a/src/Zcu.zig b/src/Zcu.zig index 531815fdf2..6de010c993 100644 --- a/src/Zcu.zig +++ b/src/Zcu.zig @@ -878,7 +878,7 @@ pub const Namespace = struct { ns: Namespace, zcu: *Zcu, name: InternPool.NullTerminatedString, - writer: anytype, + writer: *Writer, ) @TypeOf(writer).Error!void { const sep: u8 = if (ns.parent.unwrap()) |parent| sep: { try zcu.namespacePtr(parent).renderFullyQualifiedDebugName( @@ -1125,7 +1125,7 @@ pub const File = struct { return file.sub_file_path.len - ext.len; } - pub fn renderFullyQualifiedName(file: File, writer: anytype) !void { + pub fn renderFullyQualifiedName(file: File, writer: *Writer) !void { // Convert all the slashes into dots and truncate the extension. const ext = std.fs.path.extension(file.sub_file_path); const noext = file.sub_file_path[0 .. file.sub_file_path.len - ext.len]; @@ -1135,7 +1135,7 @@ pub const File = struct { }; } - pub fn renderFullyQualifiedDebugName(file: File, writer: anytype) !void { + pub fn renderFullyQualifiedDebugName(file: File, writer: *Writer) !void { for (file.sub_file_path) |byte| switch (byte) { '/', '\\' => try writer.writeByte('/'), else => try writer.writeByte(byte), @@ -2177,33 +2177,33 @@ pub const SrcLoc = struct { var multi_i: u32 = 0; var scalar_i: u32 = 0; var underscore_node: Ast.Node.OptionalIndex = .none; - const case = case: for (case_nodes) |case_node| { + const case: Ast.full.SwitchCase = case: for (case_nodes) |case_node| { const case = tree.fullSwitchCase(case_node).?; if (case.ast.values.len == 0) { - if (want_case_idx == LazySrcLoc.Offset.SwitchCaseIndex.special_else) { + if (want_case_idx == Zir.UnwrappedSwitchBlock.Case.Index.@"else") { break :case case; } continue :case; } - if (underscore_node == .none) for (case.ast.values) |val_node| { - if (tree.nodeTag(val_node) == .identifier and - mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(val_node)), "_")) - { - underscore_node = val_node.toOptional(); - if (want_case_idx == LazySrcLoc.Offset.SwitchCaseIndex.special_under) { - break :case case; + if (underscore_node == .none) { + for (case.ast.values) |value| { + if (tree.nodeTag(value) == .identifier and + mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(value)), "_")) + { + underscore_node = value.toOptional(); + if (want_case_idx.is_under) break :case case; + if (case.ast.values.len == 1) continue :case; } - continue :case; } - }; + } const is_multi = case.ast.values.len != 1 or tree.nodeTag(case.ast.values[0]) == .switch_range; switch (want_case_idx.kind) { - .scalar => if (!is_multi and want_case_idx.index == scalar_i) + .scalar => if (!is_multi and want_case_idx.value == scalar_i) break :case case, - .multi => if (is_multi and want_case_idx.index == multi_i) + .multi => if (is_multi and want_case_idx.value == multi_i) break :case case, } @@ -2214,12 +2214,13 @@ pub const SrcLoc = struct { } } else unreachable; - const want_item = switch (src_loc.lazy) { + const want_item_idx = switch (src_loc.lazy) { .switch_case_item, .switch_case_item_range_first, .switch_case_item_range_last, => |x| item_idx: { - assert(want_case_idx != LazySrcLoc.Offset.SwitchCaseIndex.special_else); + assert(want_case_idx != Zir.UnwrappedSwitchBlock.Case.Index.@"else"); + assert(want_case_idx != Zir.UnwrappedSwitchBlock.Case.Index.bare_under); break :item_idx x.item_idx; }, .switch_capture, .switch_tag_capture => { @@ -2242,7 +2243,7 @@ pub const SrcLoc = struct { else => unreachable, }; - switch (want_item.kind) { + switch (want_item_idx.kind) { .single => { var item_i: u32 = 0; for (case.ast.values) |item_node| { @@ -2251,12 +2252,21 @@ pub const SrcLoc = struct { { continue; } - if (item_i != want_item.index) { + if (item_i != want_item_idx.value) { item_i += 1; continue; } return tree.nodeToSpan(item_node); - } else unreachable; + } else { + for (case.ast.values) |item_node| { + const item_span = tree.nodeToSpan(item_node); + std.debug.print("{s}\n", .{tree.source[item_span.start..item_span.end]}); + } + std.debug.print("want_case_idx={any}\n", .{want_case_idx}); + std.debug.print("want_item_idx={any}\n", .{want_item_idx}); + unreachable; + } + // } else unreachable; }, .range => { var range_i: u32 = 0; @@ -2264,7 +2274,7 @@ pub const SrcLoc = struct { if (tree.nodeTag(item_node) != .switch_range) { continue; } - if (range_i != want_item.index) { + if (range_i != want_item_idx.value) { range_i += 1; continue; } @@ -2642,29 +2652,21 @@ pub const LazySrcLoc = struct { /// The offset of the switch AST node. switch_node_offset: Ast.Node.Offset, /// The index of the case to point to within this switch. - case_idx: SwitchCaseIndex, + case_idx: Zir.UnwrappedSwitchBlock.Case.Index, /// The index of the item to point to within this case. - item_idx: SwitchItemIndex, + item_idx: SwitchItem.Index, + + pub const Index = packed struct(u32) { + kind: enum(u1) { single, range }, + value: u31, + }; }; pub const SwitchCapture = struct { /// The offset of the switch AST node. switch_node_offset: Ast.Node.Offset, /// The index of the case whose capture to point to. - case_idx: SwitchCaseIndex, - }; - - pub const SwitchCaseIndex = packed struct(u32) { - kind: enum(u1) { scalar, multi }, - index: u31, - - pub const special_else: SwitchCaseIndex = @bitCast(@as(u32, std.math.maxInt(u32))); - pub const special_under: SwitchCaseIndex = @bitCast(@as(u32, std.math.maxInt(u32) - 1)); - }; - - pub const SwitchItemIndex = packed struct(u32) { - kind: enum(u1) { single, range }, - index: u31, + case_idx: Zir.UnwrappedSwitchBlock.Case.Index, }; pub const ArrayCat = struct { diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index d184bbc47d..60bda4a8a5 100644 --- a/test/behavior/switch.zig +++ b/test/behavior/switch.zig @@ -1120,3 +1120,159 @@ test "switch on non-exhaustive enum" { try E.doTheTest(.a); try comptime E.doTheTest(.a); } + +test "decl literals as switch cases" { + const E = enum(u8) { + bar = 3, + _, + + const foo: @This() = @enumFromInt(0xa); + + fn doTheTest() !void { + var e: @This() = .foo; + _ = &e; + const ok = switch (e) { + .bar => false, + .foo => true, + else => false, + }; + try expect(ok); + } + }; + + try E.doTheTest(); + try comptime E.doTheTest(); +} + +// TODO audit after #15909 and/or #19855 are decided/implemented +test "switch with uninstantiable union fields" { + const U = union(enum) { + ok: void, + a: noreturn, + b: noreturn, + c: error{}, + + fn doTheTest() !void { + var u: @This() = .ok; + _ = &u; + try expect(switch (u) { + .ok => true, + .a => comptime unreachable, + .b => comptime unreachable, + .c => comptime unreachable, + }); + try expect(switch (u) { + .ok => true, + .a, .b, .c => comptime unreachable, + }); + try expect(switch (u) { + .ok => true, + else => comptime unreachable, + }); + try expect(switch (u) { + .a => comptime unreachable, + .ok, .b, .c => true, + }); + } + }; + + try U.doTheTest(); + try comptime U.doTheTest(); +} + +test "switch with tag capture" { + const U = union(enum) { + a, + b: i32, + c: u8, + d: i32, + e: noreturn, + + fn doTheTest() !void { + try doTheSwitch(.a); + try doTheSwitch(.{ .b = 123 }); + try doTheSwitch(.{ .c = 0xFF }); + } + fn doTheSwitch(u: @This()) !void { + switch (u) { + .a => |nothing, tag| { + try expect(nothing == {}); + try expect(tag == .a); + try expect(@intFromEnum(tag) == @intFromEnum(@This().a)); + }, + .b, .d => |_, tag| { + try expect(tag == .b or tag == .d); + }, + .e => |payload, tag| { + _ = &payload; + _ = &tag; + comptime unreachable; + }, + else => |un, tag| { + try expect(tag == .c); + try expect(un == .c); + try expect(un.c == 0xFF); + }, + } + switch (u) { + inline .a, .b, .c => |payload, tag| { + if (@TypeOf(payload) == void) try expect(tag == .a); + if (@TypeOf(payload) == i32) try expect(tag == .b); + if (@TypeOf(payload) == u8) try expect(tag == .c); + }, + inline else => |payload, tag| { + if (@TypeOf(payload) == i32) try expect(tag == .d); + try expect(tag != .e); + }, + } + } + }; + + try U.doTheTest(); + try comptime U.doTheTest(); +} + +test "switch with advanced prong items" { + const S = struct { + fn doTheTest() !void { + try doTheSwitch(2000, 20); + try doTheSwitch(2000, 10); + try doTheSwitch(2000, 5); + + try doTheOtherSwitch(@enumFromInt(123)); + try doTheOtherSwitch(@enumFromInt(456)); + } + fn doTheSwitch(x: u32, comptime factor: u32) !void { + const ok = switch (x) { + num(factor) => true, + typedNum(u32, factor) => true, + blk: { + var val = 400; + val *= factor; + break :blk val; + } => true, + else => false, + }; + try expect(ok); + } + fn num(factor: u32) u32 { + return 100 * factor; + } + fn typedNum(comptime T: type, factor: T) T { + return 200 * factor; + } + + const E = enum(u32) { _ }; + fn doTheOtherSwitch(e: E) !void { + const ok = switch (e) { + @enumFromInt(123) => true, + @enumFromInt(456) => true, + else => false, + }; + try expect(ok); + } + }; + + try S.doTheTest(); + try comptime S.doTheTest(); +} diff --git a/test/behavior/switch_loop.zig b/test/behavior/switch_loop.zig index 5c973d9e1b..3da7838fbc 100644 --- a/test/behavior/switch_loop.zig +++ b/test/behavior/switch_loop.zig @@ -310,8 +310,8 @@ test "switch loop with single catch-all prong" { label: switch (E.a) { else => { x += 1; - if (x >= 5) continue :label .b; if (x == 10) break :label; + if (x >= 5) continue :label .b; continue :label .c; }, } @@ -320,8 +320,8 @@ test "switch loop with single catch-all prong" { label: switch (E.a) { .a, .b, .c => { x += 1; - if (x >= 15) continue :label .b; if (x == 20) break :label; + if (x >= 15) continue :label .b; continue :label .c; }, } @@ -346,3 +346,165 @@ test "switch loop with single catch-all prong" { try S.doTheTest(); try comptime S.doTheTest(); } + +test "switch loop on type with opv" { + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; + + const S = struct { + const E = enum { opv }; + const U = union(E) { opv: u0 }; + + fn doTheTest() !void { + var x: usize = 0; + label: switch (E.opv) { + .opv => { + x += 1; + if (x == 10) break :label; + if (x >= 5) continue :label .opv; + continue :label .opv; + }, + } + try expect(x == 10); + + label: switch (E.opv) { + else => { + x += 1; + if (x == 20) break :label; + if (x >= 15) continue :label .opv; + continue :label .opv; + }, + } + try expect(x == 20); + + label: switch (E.opv) { + .opv => if (false) continue :label true, + } + + label: switch (U{ .opv = 0 }) { + .opv => |val| { + x += 1; + if (x == 30) break :label; + if (x >= 25) continue :label .{ .opv = val }; + continue :label .{ .opv = 0 }; + }, + } + try expect(x == 30); + } + }; + try S.doTheTest(); + try comptime S.doTheTest(); +} + +test "switch loop with tag capture" { + const U = union(enum) { + a, + b: i32, + c: u8, + d: i32, + e: noreturn, + + fn doTheTest() !void { + try doTheSwitch(.a); + try doTheSwitch(.{ .b = 123 }); + try doTheSwitch(.{ .c = 0xFF }); + } + fn doTheSwitch(u: @This()) !void { + const ok1 = label: switch (u) { + .a => |nothing, tag| { + try expect(nothing == {}); + try expect(tag == .a); + try expect(@intFromEnum(tag) == @intFromEnum(@This().a)); + continue :label .{ .d = 456 }; + }, + .b, .d => |_, tag| { + try expect(tag == .b or tag == .d); + continue :label .{ .c = 0x0F }; + }, + .e => |payload, tag| { + _ = &payload; + _ = &tag; + return error.AnalyzedNoreturnProng; + }, + else => |un, tag| { + try expect(tag == .c); + try expect(un == .c); + if (un.c == 0xFF) continue :label .a; + if (un.c == 0x00) break :label false; + break :label true; + }, + }; + try expect(ok1); + + const ok2 = label: switch (u) { + inline .a, .b, .c => |payload, tag| { + if (@TypeOf(payload) == void) { + try expect(tag == .a); + continue :label .{ .b = 456 }; + } + if (@TypeOf(payload) == i32) { + try expect(tag == .b); + continue :label .{ .d = payload }; + } + if (@TypeOf(payload) == u8) { + try expect(tag == .c); + continue :label .{ .d = payload }; + } + }, + inline else => |payload, tag| { + if (@TypeOf(payload) == i32) try expect(tag == .d); + try expect(tag != .e); + if (payload == 0) break :label false; + break :label true; + }, + }; + try expect(ok2); + } + }; + + try U.doTheTest(); + try comptime U.doTheTest(); +} + +test "switch loop for error handling" { + const Error = error{ MyError, MyOtherError }; + const S = struct { + fn doTheTest() !void { + try doThePayloadSwitch(123); + try doTheErrSwitch(error.MyError); + try doTheErrSwitch(error.MyOtherError); + } + fn doThePayloadSwitch(eu: Error!u32) !void { + const x = eu catch |err| label: switch (err) { + error.MyError => continue :label error.MyOtherError, + error.MyOtherError => break :label 0, + }; + try expect(x == 123); + + const y = if (eu) |payload| label: { + break :label payload * 2; + } else |err| label: switch (err) { + error.MyError => continue :label error.MyOtherError, + error.MyOtherError => break :label 0, + }; + try expect(y == 246); + } + fn doTheErrSwitch(eu: Error!u32) !void { + const x = eu catch |err| label: switch (err) { + error.MyError => continue :label error.MyOtherError, + error.MyOtherError => break :label 123, + }; + try expect(x == 123); + + const y = if (eu) |payload| label: { + break :label payload * 2; + } else |err| label: switch (err) { + error.MyError => continue :label error.MyOtherError, + error.MyOtherError => break :label 123, + }; + try expect(y == 123); + } + }; + + try S.doTheTest(); + try comptime S.doTheTest(); +} diff --git a/test/behavior/switch_on_captured_error.zig b/test/behavior/switch_on_captured_error.zig index fcf51f6c9a..cc5172a32b 100644 --- a/test/behavior/switch_on_captured_error.zig +++ b/test/behavior/switch_on_captured_error.zig @@ -17,7 +17,7 @@ test "switch on error union catch capture" { try testElse(); try testCapture(); try testInline(); - try testEmptyErrSet(); + try testUnreachableElseProng(); try testAddressOf(); } @@ -240,20 +240,88 @@ test "switch on error union catch capture" { { var a: error{}!u64 = 0; _ = &a; - const b: u64 = a catch |err| switch (err) { - else => |e| return e, + const b = a catch |err| switch (err) { + undefined => @compileError("unreachable"), + }; + try expectEqual(@as(u64, 0), b); + } + } + + fn testUnreachableElseProng() !void { + { + var a: error{}!u64 = 0; + _ = &a; + const b = a catch |err| switch (err) { + else => unreachable, }; try expectEqual(@as(u64, 0), b); } { var a: error{}!u64 = 0; _ = &a; - const b: u64 = a catch |err| switch (err) { - error.UnknownError => return error.Fail, + const b = a catch |err| switch (err) { + else => return, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{}!u64 = 0; + _ = &a; + const b = a catch |err| switch (err) { else => |e| return e, }; try expectEqual(@as(u64, 0), b); } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = a catch |err| switch (err) { + error.MyError => 0, + else => unreachable, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = a catch |err| switch (err) { + error.MyError => 0, + else => return, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = a catch |err| switch (err) { + error.MyError => 0, + else => |e| return e, + }; + try expectEqual(@as(u64, 0), b); + } + } + + fn testErrNotInSet() !void { + { + var a: error{MyError}!u64 = 0; + _ = &a; + const b = a catch |err| switch (err) { + error.MyError => 1, + error.MyOtherError => comptime unreachable, + error.YetAnotherError, error.ThereIsAnother => comptime unreachable, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = a catch |err| switch (err) { + error.MyError => 0, + error.MyOtherError => comptime unreachable, + error.YetAnotherError, error.ThereIsAnother => comptime unreachable, + }; + try expectEqual(@as(u64, 0), b); + } } fn testAddressOf() !void { @@ -316,8 +384,8 @@ test "switch on error union if else capture" { try testCapturePtr(); try testInline(); try testInlinePtr(); - try testEmptyErrSet(); - try testEmptyErrSetPtr(); + try testUnreachableElseProng(); + try testUnreachableElseProngPtr(); try testAddressOf(); } @@ -755,17 +823,8 @@ test "switch on error union if else capture" { { var a: error{}!u64 = 0; _ = &a; - const b: u64 = if (a) |x| x else |err| switch (err) { - else => |e| return e, - }; - try expectEqual(@as(u64, 0), b); - } - { - var a: error{}!u64 = 0; - _ = &a; - const b: u64 = if (a) |x| x else |err| switch (err) { - error.UnknownError => return error.Fail, - else => |e| return e, + const b = if (a) |x| x else |err| switch (err) { + undefined => @compileError("unreachable"), }; try expectEqual(@as(u64, 0), b); } @@ -775,20 +834,144 @@ test "switch on error union if else capture" { { var a: error{}!u64 = 0; _ = &a; - const b: u64 = if (a) |*x| x.* else |err| switch (err) { - else => |e| return e, + const b = if (a) |*x| x.* else |err| switch (err) { + error.undefined => @compileError("unreachable"), + }; + try expectEqual(@as(u64, 0), b); + } + } + + fn testUnreachableElseProng() !void { + { + var a: error{}!u64 = 0; + _ = &a; + const b = if (a) |x| x else |err| switch (err) { + else => unreachable, }; try expectEqual(@as(u64, 0), b); } { var a: error{}!u64 = 0; _ = &a; - const b: u64 = if (a) |*x| x.* else |err| switch (err) { + const b = if (a) |x| x else |err| switch (err) { + error.UnknownError => return error.Fail, + else => return, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{}!u64 = 0; + _ = &a; + const b = if (a) |x| x else |err| switch (err) { error.UnknownError => return error.Fail, else => |e| return e, }; try expectEqual(@as(u64, 0), b); } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = if (a) |x| x else |err| switch (err) { + error.MyError => 0, + else => unreachable, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = if (a) |x| x else |err| switch (err) { + error.MyError => 0, + else => return, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = if (a) |x| x else |err| switch (err) { + error.MyError => 0, + else => |e| return e, + }; + try expectEqual(@as(u64, 0), b); + } + } + + fn testUnreachableElseProngPtr() !void { + { + var a: error{}!u64 = 0; + _ = &a; + const b = if (a) |*x| x.* else |err| switch (err) { + else => unreachable, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{}!u64 = 0; + _ = &a; + const b = if (a) |*x| x.* else |err| switch (err) { + else => return, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{}!u64 = 0; + _ = &a; + const b = if (a) |*x| x.* else |err| switch (err) { + else => |e| return e, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = if (a) |*x| x.* else |err| switch (err) { + error.MyError => 0, + else => unreachable, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = if (a) |*x| x.* else |err| switch (err) { + error.MyError => 0, + else => return, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = if (a) |*x| x.* else |err| switch (err) { + error.MyError => 0, + else => |e| return e, + }; + try expectEqual(@as(u64, 0), b); + } + } + + fn testErrNotInSet() !void { + { + var a: error{MyError}!u64 = 0; + _ = &a; + const b = if (a) |x| x else |err| switch (err) { + error.MyError => 1, + error.MyOtherError => comptime unreachable, + error.YetAnotherError, error.ThereIsAnother => comptime unreachable, + }; + try expectEqual(@as(u64, 0), b); + } + { + var a: error{MyError}!u64 = error.MyError; + _ = &a; + const b = if (a) |x| x else |err| switch (err) { + error.MyError => 0, + error.MyOtherError => comptime unreachable, + error.YetAnotherError, error.ThereIsAnother => comptime unreachable, + }; + try expectEqual(@as(u64, 0), b); + } } fn testAddressOf() !void { diff --git a/test/cases/compile_errors/duplicate_boolean_switch_value.zig b/test/cases/compile_errors/duplicate_boolean_switch_value.zig index a4ae379651..d3b7dba6e8 100644 --- a/test/cases/compile_errors/duplicate_boolean_switch_value.zig +++ b/test/cases/compile_errors/duplicate_boolean_switch_value.zig @@ -18,4 +18,6 @@ comptime { // error // // :5:9: error: duplicate switch value +// :3:9: note: previous value here // :13:9: error: duplicate switch value +// :11:9: note: previous value here diff --git a/test/cases/compile_errors/invalid_switch_item.zig b/test/cases/compile_errors/invalid_switch_item.zig index ee8c2a8b36..066e8a9fde 100644 --- a/test/cases/compile_errors/invalid_switch_item.zig +++ b/test/cases/compile_errors/invalid_switch_item.zig @@ -36,11 +36,11 @@ export fn f3() void { // error // -// :8:10: error: no field named 'x' in enum 'tmp.E' +// :8:10: error: enum 'tmp.E' has no member named 'x' // :1:11: note: enum declared here -// :16:10: error: no field named 'x' in enum 'tmp.E' +// :16:10: error: enum 'tmp.E' has no member named 'x' // :1:11: note: enum declared here -// :24:10: error: no field named 'x' in enum 'tmp.E' +// :24:10: error: enum 'tmp.E' has no member named 'x' // :1:11: note: enum declared here -// :32:10: error: no field named 'x' in enum 'tmp.E' +// :32:10: error: enum 'tmp.E' has no member named 'x' // :1:11: note: enum declared here diff --git a/test/cases/compile_errors/switch_on_error_with_1_field_with_no_prongs.zig b/test/cases/compile_errors/switch_on_error_with_1_field_with_no_prongs.zig index f6d38b94a6..a9bd662b50 100644 --- a/test/cases/compile_errors/switch_on_error_with_1_field_with_no_prongs.zig +++ b/test/cases/compile_errors/switch_on_error_with_1_field_with_no_prongs.zig @@ -1,18 +1,34 @@ const Error = error{M}; -export fn entry() void { - const f: Error!void = void{}; +export fn entry1() void { + var f: Error!void = {}; + _ = &f; if (f) {} else |e| switch (e) {} } export fn entry2() void { - const f: Error!void = void{}; + var f: Error!void = {}; + _ = &f; + f catch |e| switch (e) {}; +} + +export fn entry3() void { + const f: Error!void = error.M; + if (f) {} else |e| switch (e) {} +} + +export fn entry4() void { + const f: Error!void = error.M; f catch |e| switch (e) {}; } // error // -// :5:24: error: switch must handle all possibilities -// :5:24: note: unhandled error value: 'error.M' -// :10:17: error: switch must handle all possibilities -// :10:17: note: unhandled error value: 'error.M' +// :6:24: error: switch must handle all possibilities +// :6:24: note: unhandled error value: 'error.M' +// :12:17: error: switch must handle all possibilities +// :12:17: note: unhandled error value: 'error.M' +// :17:24: error: switch must handle all possibilities +// :17:24: note: unhandled error value: 'error.M' +// :22:17: error: switch must handle all possibilities +// :22:17: note: unhandled error value: 'error.M' diff --git a/test/cases/compile_errors/tag_capture_on_non_inline_prong.zig b/test/cases/compile_errors/tag_capture_on_non_inline_prong.zig deleted file mode 100644 index 2d93e5518a..0000000000 --- a/test/cases/compile_errors/tag_capture_on_non_inline_prong.zig +++ /dev/null @@ -1,12 +0,0 @@ -const E = enum { a, b, c, d }; -pub export fn entry() void { - var x: E = .a; - switch (x) { - .a, .b => |aorb, d| @compileLog(aorb, d), - inline .c, .d => |*cord| @compileLog(cord), - } -} - -// error -// -// :5:26: error: tag capture on non-inline prong From 39ca03e5156219c23b3b64d9fa682947ea3b37fa Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Fri, 9 Jan 2026 07:12:40 +0100 Subject: [PATCH 11/23] test: disable packed struct field type behavior test This test was previously masked by a bug which prevented its evaluation. Skipping it for now. --- test/behavior/packed-struct.zig | 58 +++++++++++++++++---------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/test/behavior/packed-struct.zig b/test/behavior/packed-struct.zig index 96995b0dc0..4a5317cd47 100644 --- a/test/behavior/packed-struct.zig +++ b/test/behavior/packed-struct.zig @@ -513,34 +513,36 @@ test "@intFromPtr on a packed struct field unaligned and nested" { }; }; - switch (comptime @alignOf(S2)) { - 4 => { - comptime assert(@TypeOf(&S2.s.base) == *align(4) u8); - comptime assert(@TypeOf(&S2.s.p0.a) == *align(1:0:2) u4); - comptime assert(@TypeOf(&S2.s.p0.b) == *align(1:4:2) u4); - comptime assert(@TypeOf(&S2.s.p0.c) == *u8); - comptime assert(@TypeOf(&S2.s.bit0) == *align(4:24:8) u1); - comptime assert(@TypeOf(&S2.s.p1.a) == *align(4:25:8) u8); - comptime assert(@TypeOf(&S2.s.p2.a) == *align(4:33:8) u7); - comptime assert(@TypeOf(&S2.s.p2.b) == *u8); - comptime assert(@TypeOf(&S2.s.p3.a) == *align(2:0:2) u4); - comptime assert(@TypeOf(&S2.s.p3.b) == *align(2:4:2) u4); - comptime assert(@TypeOf(&S2.s.p3.c) == *u8); - }, - 8 => { - comptime assert(@TypeOf(&S2.s.base) == *align(8) u8); - comptime assert(@TypeOf(&S2.s.p0.a) == *align(1:0:2) u4); - comptime assert(@TypeOf(&S2.s.p0.b) == *align(1:4:2) u4); - comptime assert(@TypeOf(&S2.s.p0.c) == *u8); - comptime assert(@TypeOf(&S2.s.bit0) == *align(8:24:8) u1); - comptime assert(@TypeOf(&S2.s.p1.a) == *align(8:25:8) u8); - comptime assert(@TypeOf(&S2.s.p2.a) == *align(8:33:8) u7); - comptime assert(@TypeOf(&S2.s.p2.b) == *u8); - comptime assert(@TypeOf(&S2.s.p3.a) == *align(2:0:2) u4); - comptime assert(@TypeOf(&S2.s.p3.b) == *align(2:4:2) u4); - comptime assert(@TypeOf(&S2.s.p3.c) == *u8); - }, - else => {}, + if (false) { + switch (comptime @alignOf(S2)) { + 4 => { + comptime assert(@TypeOf(&S2.s.base) == *align(4) u8); + comptime assert(@TypeOf(&S2.s.p0.a) == *align(1:0:2) u4); + comptime assert(@TypeOf(&S2.s.p0.b) == *align(1:4:2) u4); + comptime assert(@TypeOf(&S2.s.p0.c) == *u8); + comptime assert(@TypeOf(&S2.s.bit0) == *align(4:24:8) u1); + comptime assert(@TypeOf(&S2.s.p1.a) == *align(4:25:8) u8); + comptime assert(@TypeOf(&S2.s.p2.a) == *align(4:33:8) u7); + comptime assert(@TypeOf(&S2.s.p2.b) == *u8); + comptime assert(@TypeOf(&S2.s.p3.a) == *align(2:0:2) u4); + comptime assert(@TypeOf(&S2.s.p3.b) == *align(2:4:2) u4); + comptime assert(@TypeOf(&S2.s.p3.c) == *u8); + }, + 8 => { + comptime assert(@TypeOf(&S2.s.base) == *align(8) u8); + comptime assert(@TypeOf(&S2.s.p0.a) == *align(1:0:2) u4); + comptime assert(@TypeOf(&S2.s.p0.b) == *align(1:4:2) u4); + comptime assert(@TypeOf(&S2.s.p0.c) == *u8); + comptime assert(@TypeOf(&S2.s.bit0) == *align(8:24:8) u1); + comptime assert(@TypeOf(&S2.s.p1.a) == *align(8:25:8) u8); + comptime assert(@TypeOf(&S2.s.p2.a) == *align(8:33:8) u7); + comptime assert(@TypeOf(&S2.s.p2.b) == *u8); + comptime assert(@TypeOf(&S2.s.p3.a) == *align(2:0:2) u4); + comptime assert(@TypeOf(&S2.s.p3.b) == *align(2:4:2) u4); + comptime assert(@TypeOf(&S2.s.p3.c) == *u8); + }, + else => {}, + } } try expect(@intFromPtr(&S2.s.base) - @intFromPtr(&S2.s) == 0); try expect(@intFromPtr(&S2.s.p0.a) - @intFromPtr(&S2.s) == 0); From bce7e7a52ba4dd9b46184925547b40ecedee63b6 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Fri, 9 Jan 2026 08:04:26 +0100 Subject: [PATCH 12/23] AstGen: Re-allow labeled `break` from loop `else` block targeting its label This fixes a regression from a couple of commits ago; breaking from the `else` block of a loop to the loop's tag should be allowed when explicitly targeting the label by name. --- lib/std/zig/AstGen.zig | 20 +++++++++---------- test/behavior/for.zig | 17 ++++++++++++++++ test/behavior/while.zig | 17 ++++++++++++++++ .../continue_loop_from_else_block.zig | 17 ++++++++++++++++ .../compile_errors/labeled_block_continue.zig | 2 +- 5 files changed, 61 insertions(+), 12 deletions(-) create mode 100644 test/cases/compile_errors/continue_loop_from_else_block.zig diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index 124543b883..01a8373e3f 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -2276,7 +2276,7 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) if (try astgen.tokenIdentEql(label.token, break_label)) { switch (gen_zir.continue_target) { .none => { - return astgen.failNode(node, "continue cannot target labeled block", .{}); + return astgen.failNode(node, "continue outside of loop or labeled switch expression", .{}); }, .@"break" => if (opt_rhs != .none) { return astgen.failNode(node, "cannot continue loop with operand", .{}); @@ -6803,12 +6803,11 @@ fn whileExpr( break :s &else_scope.base; } }; - // Remove label and forbid unlabeled control flow to this scope so that - // `continue` and `break` control flow apply to outer loops; not this one. - loop_scope.label = null; + // Disallow unlabeled control flow to this scope so that bare `continue` + // and `break` control flow apply to outer loops; not this one. + // Also disallow `continue` targeting the loop label. loop_scope.allow_unlabeled_control_flow = false; - loop_scope.continue_target = undefined; - loop_scope.break_target = undefined; + loop_scope.continue_target = .none; const else_result = try fullBodyExpr(&else_scope, sub_scope, loop_scope.break_result_info, else_node, .allow_branch_hint); if (is_statement) { _ = try addEnsureResult(&else_scope, else_result, else_node); @@ -7093,12 +7092,11 @@ fn forExpr( if (for_full.ast.else_expr.unwrap()) |else_node| { const sub_scope = &else_scope.base; - // Remove label and forbid unlabeled control flow to this scope so that - // `continue` and `break` control flow apply to outer loops; not this one. - loop_scope.label = null; + // Disallow unlabeled control flow to this scope so that bare `continue` + // and `break` control flow apply to outer loops; not this one. + // Also disallow `continue` targeting the loop label. loop_scope.allow_unlabeled_control_flow = false; - loop_scope.continue_target = undefined; - loop_scope.break_target = undefined; + loop_scope.continue_target = .none; const else_result = try fullBodyExpr(&else_scope, sub_scope, loop_scope.break_result_info, else_node, .allow_branch_hint); if (is_statement) { _ = try addEnsureResult(&else_scope, else_result, else_node); diff --git a/test/behavior/for.zig b/test/behavior/for.zig index a8ed6ec79e..0361c2d19d 100644 --- a/test/behavior/for.zig +++ b/test/behavior/for.zig @@ -524,3 +524,20 @@ test "for loop 0 length range" { comptime unreachable; } } + +test "labeled break from else prong" { + const S = struct { + fn doTheTest(x: u32) !void { + var y: u32 = 0; + const ok = label: while (y < x) : (y += 1) { + if (y == 10) break :label false; + } else { + break :label true; + }; + try expect(ok); + } + }; + + try S.doTheTest(5); + try comptime S.doTheTest(5); +} diff --git a/test/behavior/while.zig b/test/behavior/while.zig index 7a177d5690..9b5fe1fb84 100644 --- a/test/behavior/while.zig +++ b/test/behavior/while.zig @@ -399,3 +399,20 @@ test "breaking from a loop in an if statement" { } else 2; _ = opt; } + +test "labeled break from else prong" { + const S = struct { + fn doTheTest(x: u32) !void { + var y: u32 = 0; + const ok = label: while (y < x) : (y += 1) { + if (y == 10) break :label false; + } else { + break :label true; + }; + try expect(ok); + } + }; + + try S.doTheTest(5); + try comptime S.doTheTest(5); +} diff --git a/test/cases/compile_errors/continue_loop_from_else_block.zig b/test/cases/compile_errors/continue_loop_from_else_block.zig new file mode 100644 index 0000000000..84bd6568a8 --- /dev/null +++ b/test/cases/compile_errors/continue_loop_from_else_block.zig @@ -0,0 +1,17 @@ +export fn entry1() void { + var x: u32 = 0; + result: while (x < 5) : (x += 1) {} else { + continue :result; + } +} + +export fn entry2() void { + result: for (0..5) |_| {} else { + continue :result; + } +} + +// error +// +// :4:9: error: continue outside of loop or labeled switch expression +// :10:9: error: continue outside of loop or labeled switch expression diff --git a/test/cases/compile_errors/labeled_block_continue.zig b/test/cases/compile_errors/labeled_block_continue.zig index 80bf2ba979..e81e9486b3 100644 --- a/test/cases/compile_errors/labeled_block_continue.zig +++ b/test/cases/compile_errors/labeled_block_continue.zig @@ -7,4 +7,4 @@ export fn foo() void { // error // -// :3:9: error: continue cannot target labeled block +// :3:9: error: continue outside of loop or labeled switch expression From 0b3b536f18957979374a00b2411943b4a37beb0f Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Sat, 10 Jan 2026 14:24:28 +0100 Subject: [PATCH 13/23] test: re-enable packed struct field type behavior test With new code courtesy of mlugg --- test/behavior/packed-struct.zig | 44 +++++++++++---------------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/test/behavior/packed-struct.zig b/test/behavior/packed-struct.zig index 4a5317cd47..72e97bef95 100644 --- a/test/behavior/packed-struct.zig +++ b/test/behavior/packed-struct.zig @@ -513,37 +513,21 @@ test "@intFromPtr on a packed struct field unaligned and nested" { }; }; - if (false) { - switch (comptime @alignOf(S2)) { - 4 => { - comptime assert(@TypeOf(&S2.s.base) == *align(4) u8); - comptime assert(@TypeOf(&S2.s.p0.a) == *align(1:0:2) u4); - comptime assert(@TypeOf(&S2.s.p0.b) == *align(1:4:2) u4); - comptime assert(@TypeOf(&S2.s.p0.c) == *u8); - comptime assert(@TypeOf(&S2.s.bit0) == *align(4:24:8) u1); - comptime assert(@TypeOf(&S2.s.p1.a) == *align(4:25:8) u8); - comptime assert(@TypeOf(&S2.s.p2.a) == *align(4:33:8) u7); - comptime assert(@TypeOf(&S2.s.p2.b) == *u8); - comptime assert(@TypeOf(&S2.s.p3.a) == *align(2:0:2) u4); - comptime assert(@TypeOf(&S2.s.p3.b) == *align(2:4:2) u4); - comptime assert(@TypeOf(&S2.s.p3.c) == *u8); - }, - 8 => { - comptime assert(@TypeOf(&S2.s.base) == *align(8) u8); - comptime assert(@TypeOf(&S2.s.p0.a) == *align(1:0:2) u4); - comptime assert(@TypeOf(&S2.s.p0.b) == *align(1:4:2) u4); - comptime assert(@TypeOf(&S2.s.p0.c) == *u8); - comptime assert(@TypeOf(&S2.s.bit0) == *align(8:24:8) u1); - comptime assert(@TypeOf(&S2.s.p1.a) == *align(8:25:8) u8); - comptime assert(@TypeOf(&S2.s.p2.a) == *align(8:33:8) u7); - comptime assert(@TypeOf(&S2.s.p2.b) == *u8); - comptime assert(@TypeOf(&S2.s.p3.a) == *align(2:0:2) u4); - comptime assert(@TypeOf(&S2.s.p3.b) == *align(2:4:2) u4); - comptime assert(@TypeOf(&S2.s.p3.c) == *u8); - }, - else => {}, - } + { + const a = @alignOf(S2); + comptime assert(@TypeOf(&S2.s.base) == *align(a:0:8) u8); + comptime assert(@TypeOf(&S2.s.p0.a) == *align(a:8:8) u4); + comptime assert(@TypeOf(&S2.s.p0.b) == *align(a:12:8) u4); + comptime assert(@TypeOf(&S2.s.p0.c) == *align(a:16:8) u8); + comptime assert(@TypeOf(&S2.s.bit0) == *align(a:24:8) u1); + comptime assert(@TypeOf(&S2.s.p1.a) == *align(a:25:8) u8); + comptime assert(@TypeOf(&S2.s.p2.a) == *align(a:33:8) u7); + comptime assert(@TypeOf(&S2.s.p2.b) == *align(a:40:8) u8); + comptime assert(@TypeOf(&S2.s.p3.a) == *align(a:48:8) u4); + comptime assert(@TypeOf(&S2.s.p3.b) == *align(a:52:8) u4); + comptime assert(@TypeOf(&S2.s.p3.c) == *align(a:56:8) u8); } + try expect(@intFromPtr(&S2.s.base) - @intFromPtr(&S2.s) == 0); try expect(@intFromPtr(&S2.s.p0.a) - @intFromPtr(&S2.s) == 0); try expect(@intFromPtr(&S2.s.p0.b) - @intFromPtr(&S2.s) == 0); From 5a376d97d4595f12dffaf54c4cfc303b0e902cc6 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Sat, 10 Jan 2026 14:57:23 +0100 Subject: [PATCH 14/23] langref: document new switch features - switch on tagged union with runtime-captured tag - switch on errors special cases --- doc/langref.html.in | 17 ++++++--- doc/langref/test_switch_on_errors.zig | 52 +++++++++++++++++++++++++++ doc/langref/test_tagged_union.zig | 5 +++ 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 doc/langref/test_switch_on_errors.zig diff --git a/doc/langref.html.in b/doc/langref.html.in index f2508e175b..07a5b39f2f 100644 --- a/doc/langref.html.in +++ b/doc/langref.html.in @@ -2461,7 +2461,8 @@ or {#header_open|Tagged union#}

Unions can be declared with an enum tag type. This turns the union into a tagged union, which makes it eligible - to use with {#link|switch#} expressions. + to use with {#link|switch#} expressions. When switching on tagged unions, + the tag value can be obtained using an additional capture. Tagged unions coerce to their tag type: {#link|Type Coercion: Unions and Enums#}.

{#code|test_tagged_union.zig#} @@ -2594,6 +2595,13 @@ or {#header_close#} + {#header_open|Switching on errors#} +

+ When switching on errors, some special cases are allowed to simplify generic programming patterns: +

+ {#code|test_switch_on_errors.zig#} + {#header_close#} + {#header_open|Labeled switch#}

When a switch statement is labeled, it can be referenced from a @@ -2659,12 +2667,13 @@ or {#code|test_inline_else.zig#}

- When using an inline prong switching on an union an additional - capture can be used to obtain the union's enum tag value. + When using an inline prong switching on an union an additional capture + can be used to obtain the union's enum tag value at comptime, even though + its payload might only be known at runtime.

{#code|test_inline_switch_union_tag.zig#} - {#see_also|inline while|inline for#} + {#see_also|inline while|inline for|Tagged union#} {#header_close#} {#header_close#} diff --git a/doc/langref/test_switch_on_errors.zig b/doc/langref/test_switch_on_errors.zig new file mode 100644 index 0000000000..7fe534e7a3 --- /dev/null +++ b/doc/langref/test_switch_on_errors.zig @@ -0,0 +1,52 @@ +const FileOpenError0 = error{ + AccessDenied, + OutOfMemory, + FileNotFound, +}; + +fn openFile0() FileOpenError0 { + return error.OutOfMemory; +} + +test "unreachable else prong" { + switch (openFile0()) { + error.AccessDenied, error.FileNotFound => |e| return e, + error.OutOfMemory => {}, + else => unreachable, // technically unreachable, but will still compile! + } + + // Allowed unreachable else prongs are: + // `else => unreachable,` + // `else => return,` + // `else => |e| return e,` (where `e` is any identifier) +} + +const FileOpenError1 = error{ + AccessDenied, + SystemResources, + FileNotFound, +}; + +fn openFile1() FileOpenError1 { + return error.SystemResources; +} + +fn openFileGeneric(comptime kind: u1) switch (kind) { + 0 => FileOpenError0, + 1 => FileOpenError1, +} { + return switch (kind) { + 0 => openFile0(), + 1 => openFile1(), + }; +} + +test "comptime unreachable errors not in error set" { + switch (openFileGeneric(1)) { + error.AccessDenied, error.FileNotFound => |e| return e, + error.OutOfMemory => comptime unreachable, // not in `FileOpenError1`! + error.SystemResources => {}, + } +} + +// test diff --git a/doc/langref/test_tagged_union.zig b/doc/langref/test_tagged_union.zig index dbe765f5b8..58df9dc38d 100644 --- a/doc/langref/test_tagged_union.zig +++ b/doc/langref/test_tagged_union.zig @@ -18,6 +18,11 @@ test "switch on tagged union" { .ok => |value| try expect(value == 42), .not_ok => unreachable, } + + switch (c) { + .ok => |_, tag| try expect(tag == .ok), + .not_ok => unreachable, + } } test "get tag type" { From 2479966df2ab1c375ec48be025cb265b6887ded5 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Sat, 10 Jan 2026 15:31:55 +0100 Subject: [PATCH 15/23] Sema: fix integration with `Io.Threaded` --- src/Sema.zig | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index 1f28343547..fa066bcd33 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -13174,19 +13174,21 @@ fn resolveSwitchItem( const pt = sema.pt; const zcu = pt.zcu; const ip = &zcu.intern_pool; - const gpa = sema.gpa; + const comp = zcu.comp; + const gpa = comp.gpa; + const io = comp.io; var end = extra_index; const uncoerced: Air.Inst.Ref, const uncoerced_ty: Type = uncoerced: switch (item_info.unwrap()) { .enum_literal => |str_index| { const zir_str = sema.code.nullTerminatedString(str_index); - const name = try ip.getOrPutString(gpa, pt.tid, zir_str, .no_embedded_nulls); + const name = try ip.getOrPutString(gpa, io, pt.tid, zir_str, .no_embedded_nulls); const uncoerced = try sema.analyzeDeclLiteral(block, item_src, name, item_ty, false); break :uncoerced .{ uncoerced, .enum_literal }; }, .error_value => |str_index| { const zir_str = sema.code.nullTerminatedString(str_index); - const name = try ip.getOrPutString(gpa, pt.tid, zir_str, .no_embedded_nulls); + const name = try ip.getOrPutString(gpa, io, pt.tid, zir_str, .no_embedded_nulls); // Make sure there's an error integer value associated with `name`. _ = try pt.getErrorValue(name); const err_set_ty = try pt.singleErrorSetType(name); From 2e99c3042e2641dda21768a898905784aa939031 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Sat, 10 Jan 2026 15:38:31 +0100 Subject: [PATCH 16/23] test: add some more switch regression tests switch evaluation order, switch lazy value resolution --- test/behavior/switch.zig | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index 60bda4a8a5..1e1cfdd3a9 100644 --- a/test/behavior/switch.zig +++ b/test/behavior/switch.zig @@ -1276,3 +1276,35 @@ test "switch with advanced prong items" { try S.doTheTest(); try comptime S.doTheTest(); } + +test "switch evaluation order" { + const eval = comptime eval: { + var eval = false; + const eu: anyerror!u32 = 0; + _ = eu catch |err| switch (err) { + blk: { + eval = true; + break :blk error.MyError; + } => {}, + else => unreachable, + }; + break :eval eval; + }; + try comptime expect(!eval); +} + +test "switch resolves lazy values correctly" { + const S = extern struct { + a: u16, + b: i16, + }; + const ok1 = switch (@sizeOf(S)) { + 4 => true, + else => false, + }; + const ok2 = switch (@sizeOf(S)) { + 4 => true, + else => false, + }; + try comptime expect(ok1 == ok2); +} From dbfeade22114d03c5f6dbf1f3f1d5946a10877a7 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Sat, 10 Jan 2026 18:17:58 +0100 Subject: [PATCH 17/23] Sema: better `switch_block_err_union` result location if operand has wrong type also fixes related test case and makes it run everywhere, not just on x86_64-linux! --- src/Sema.zig | 2 +- test/cases/compile_errors/switch_on_non_err_union.zig | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index fa066bcd33..29c5c14702 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -10590,7 +10590,7 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp break :err_union_ty raw_operand_ty.childType(zcu); }; if (err_union_ty.zigTypeTag(zcu) != .error_union) { - return sema.fail(block, src, "expected error union type, found '{f}'", .{ + return sema.fail(block, operand_src, "expected error union type, found '{f}'", .{ err_union_ty.fmt(pt), }); } diff --git a/test/cases/compile_errors/switch_on_non_err_union.zig b/test/cases/compile_errors/switch_on_non_err_union.zig index d505c6d2f1..67eb2035ba 100644 --- a/test/cases/compile_errors/switch_on_non_err_union.zig +++ b/test/cases/compile_errors/switch_on_non_err_union.zig @@ -5,6 +5,5 @@ pub fn main() void { } // error -// target=x86_64-linux // -// :2:23: error: expected error union type, found 'bool' +// :2:11: error: expected error union type, found 'bool' From 078e100573443488837e317b64bf00c704ee2a46 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Sat, 10 Jan 2026 18:18:48 +0100 Subject: [PATCH 18/23] wasm: fix getting pointer type instead of error union type for `is_err_ptr` --- src/codegen/wasm/CodeGen.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/codegen/wasm/CodeGen.zig b/src/codegen/wasm/CodeGen.zig index 5799ee614d..b6f8145875 100644 --- a/src/codegen/wasm/CodeGen.zig +++ b/src/codegen/wasm/CodeGen.zig @@ -4114,7 +4114,10 @@ fn airIsErr(cg: *CodeGen, inst: Air.Inst.Index, opcode: std.wasm.Opcode, op_kind const zcu = cg.pt.zcu; const un_op = cg.air.instructions.items(.data)[@intFromEnum(inst)].un_op; const operand = try cg.resolveInst(un_op); - const err_union_ty = cg.typeOf(un_op); + const err_union_ty = switch (op_kind) { + .value => cg.typeOf(un_op), + .ptr => cg.typeOf(un_op).childType(zcu), + }; const pl_ty = err_union_ty.errorUnionPayload(zcu); const result: WValue = result: { From 8ec4c5cb137d9a93c35df3488408c44056c586b4 Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 11 Jan 2026 11:36:51 +0000 Subject: [PATCH 19/23] Sema: evaluate switch items at comptime --- src/Sema.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Sema.zig b/src/Sema.zig index 29c5c14702..204d4a736c 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -13210,6 +13210,12 @@ fn resolveSwitchItem( // The result location of item bodies is `.{ .coerce_ty = switch_inst }`. sema.inst_map.putAssumeCapacity(switch_inst, .fromType(item_ty)); defer assert(sema.inst_map.remove(switch_inst)); + const old_comptime_reason = block.comptime_reason; + defer block.comptime_reason = old_comptime_reason; + block.comptime_reason = .{ .reason = .{ + .src = item_src, + .r = .{ .simple = .switch_item }, + } }; break :ref try sema.resolveInlineBody(block, body, switch_inst); }; break :uncoerced .{ uncoerced, sema.typeOf(uncoerced) }; From ed1268d0e63d3af144a262a1fa41b8e69ab3073b Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 11 Jan 2026 13:16:25 +0000 Subject: [PATCH 20/23] Zir: simplify '_' prong of 'switch' statements --- lib/std/zig/AstGen.zig | 88 ++++----------- lib/std/zig/Zir.zig | 123 ++++----------------- src/Sema.zig | 241 +++++++++++++++++++---------------------- src/Zcu.zig | 42 +------ src/print_zir.zig | 84 +++++++------- 5 files changed, 188 insertions(+), 390 deletions(-) diff --git a/lib/std/zig/AstGen.zig b/lib/std/zig/AstGen.zig index 01a8373e3f..667f5ef045 100644 --- a/lib/std/zig/AstGen.zig +++ b/lib/std/zig/AstGen.zig @@ -7267,11 +7267,7 @@ fn switchExpr( var total_items_len: usize = 0; var total_ranges_len: usize = 0; var else_case_node: Ast.Node.OptionalIndex = .none; - var else_src: ?Ast.TokenIndex = null; - var under_case_node: Ast.Node.OptionalIndex = .none; var underscore_node: Ast.Node.OptionalIndex = .none; - var underscore_src: ?Ast.TokenIndex = null; - var under_is_bare = false; for (case_nodes) |case_node| { const case = tree.fullSwitchCase(case_node).?; if (case.payload_token) |payload_token| { @@ -7304,22 +7300,21 @@ fn switchExpr( // Check for else prong. if (case.ast.values.len == 0) { - const case_src = case.ast.arrow_token - 1; - if (else_src) |src| { + if (else_case_node.unwrap()) |prev_case_node| { + const prev_else_tok = tree.fullSwitchCase(prev_case_node).?.ast.arrow_token - 1; + const else_tok = case.ast.arrow_token - 1; return astgen.failTokNotes( - case_src, + else_tok, "multiple else prongs in switch expression", .{}, - &.{try astgen.errNoteTok(src, "previous else prong here", .{})}, + &.{try astgen.errNoteTok(prev_else_tok, "previous else prong here", .{})}, ); } else_case_node = case_node.toOptional(); - else_src = case_src; continue; } // Check for '_' prong and ranges. - var case_has_underscore = false; var case_has_ranges = false; for (case.ast.values) |val| { switch (tree.nodeTag(val)) { @@ -7329,10 +7324,10 @@ fn switchExpr( }, .string_literal => return astgen.failNode(val, "cannot switch on strings", .{}), else => |tag| { + total_items_len += 1; if (tag == .identifier and mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(val)), "_")) { - const val_src = tree.nodeMainToken(val); if (is_err_switch) { const case_src = case.ast.arrow_token - 1; return astgen.failTokNotes( @@ -7348,30 +7343,24 @@ fn switchExpr( }, ); } - if (underscore_src) |src| { - return astgen.failTokNotes( - val_src, + if (underscore_node.unwrap()) |prev_src| { + return astgen.failNodeNotes( + val, "multiple '_' prongs in switch expression", .{}, - &.{try astgen.errNoteTok(src, "previous '_' prong here", .{})}, + &.{try astgen.errNoteNode(prev_src, "previous '_' prong here", .{})}, ); } if (case.inline_token != null) { - return astgen.failTok(val_src, "cannot inline '_' prong", .{}); + return astgen.failNode(val, "cannot inline '_' prong", .{}); } - under_case_node = case_node.toOptional(); - underscore_src = val_src; underscore_node = val.toOptional(); - under_is_bare = case.ast.values.len == 1; - case_has_underscore = true; - } else { - total_items_len += 1; } }, } } - const case_len = case.ast.values.len - @intFromBool(case_has_underscore); + const case_len = case.ast.values.len; if (case_len == 1 and !case_has_ranges) { scalar_cases_len += 1; } else if (case_len >= 1) { @@ -7379,9 +7368,8 @@ fn switchExpr( } } - const has_else = else_src != null; - const has_under = underscore_src != null; - if (under_is_bare) assert(has_under); // make sure that the former implies the latter + const has_else = else_case_node != .none; + const has_under = underscore_node != .none; if (is_err_switch) assert(!has_under); // should have failed by now const any_ranges = total_ranges_len > 0; @@ -7430,10 +7418,8 @@ fn switchExpr( var non_err_prong_body_start: u32 = undefined; var else_prong_body_start: u32 = undefined; - var bare_under_prong_body_start: u32 = undefined; var non_err_info: Zir.Inst.SwitchBlock.ProngInfo.NonErr = undefined; var else_info: Zir.Inst.SwitchBlock.ProngInfo.Else = undefined; - var under_extra: u32 = undefined; var block_scope = parent_gz.makeSubBlock(scope); // block_scope not used for collecting instructions @@ -7688,7 +7674,6 @@ fn switchExpr( for (case_nodes) |case_node| { const case = tree.fullSwitchCase(case_node).?; - const case_has_under = case_node.toOptional() == under_case_node; const ranges_len: u32 = if (any_ranges) blk: { var ranges_len: u32 = 0; for (case.ast.values) |value| { @@ -7696,14 +7681,13 @@ fn switchExpr( } break :blk ranges_len; } else 0; - const items_len: u32 = @intCast(case.ast.values.len - ranges_len - @intFromBool(case_has_under)); + const items_len: u32 = @intCast(case.ast.values.len - ranges_len); const is_multi_case = items_len > 1 or ranges_len > 0; // item/range bodies in order of occurence var item_i: usize = 0; var range_i: usize = 0; for (case.ast.values) |value| { - if (value.toOptional() == underscore_node) continue; const is_range = tree.nodeTag(value) == .switch_range; const range: [2]Ast.Node.Index = if (is_range) tree.nodeData(value).node_and_node else undefined; const nodes: []const Ast.Node.Index = if (is_range) &range else &.{value}; @@ -7722,16 +7706,9 @@ fn switchExpr( const str_index = try astgen.identAsString(ident_token); break :blk .wrap(.{ .error_value = str_index }); }, - .number_literal => { - // We don't actually need a final result type for number - // literals, they can just be turned into `comptime_int` - // or `comptime_float` as usual and then be coerced to - // the correct type later during semantic analysis. - assert(scratch_scope.instructions_top == GenZir.unstacked_top); // important! we emit into `parent_gz` which `scratch_scope` is stacked on top of - const zir_ref = try comptimeExpr(parent_gz, scope, .{ .rl = .none }, item, .switch_item); - break :blk .wrap(.{ .number_literal = zir_ref }); - }, - else => { + else => if (value.toOptional() == underscore_node) { + break :blk .wrap(.under); + } else { scratch_scope.instructions_top = parent_gz.instructions.items.len; defer scratch_scope.unstack(); const item_result = try fullBodyExpr(&scratch_scope, scope, item_ri, item, .normal); @@ -7957,25 +7934,6 @@ fn switchExpr( break :prong_body; } - if (case_has_under) { - // We're either writing under_prong_info or under_index here. - if (under_is_bare) { - assert(case.ast.values.len == 1); // only `_` - const bare_under_info: Zir.Inst.SwitchBlock.ProngInfo.BareUnder = .{ - .body_len = @intCast(body_len), - .capture = capture, - .has_tag_capture = has_tag_capture, - }; - under_extra = @bitCast(bare_under_info); - bare_under_prong_body_start = body_start; - break :prong_body; - } else if (is_multi_case) { - under_extra = scalar_cases_len + multi_case_index; - } else { - under_extra = scalar_case_index; - } - } - // We allow prongs with error items which are not inside the error set // being switched on if their body is `=> comptime unreachable,`. const is_comptime_unreach = comptime_unreach: { @@ -8003,7 +7961,7 @@ fn switchExpr( } } } - assert(scalar_case_index + multi_case_index + @intFromBool(has_else) + @intFromBool(under_is_bare) == case_nodes.len); + assert(scalar_case_index + multi_case_index + @intFromBool(has_else) == case_nodes.len); assert(multi_items_infos_start + multi_item_offset == bodies_start); if (switch_full.label_token) |label_token| if (!block_scope.label.?.used) { @@ -8024,7 +7982,6 @@ fn switchExpr( @intFromBool(needs_non_err_handling) + // catch_or_if_src_node_offset @intFromBool(needs_non_err_handling) + // non_err_info @intFromBool(has_else) + // else_info - @intFromBool(has_under) + // under_prong_info or under_index payloads.items.len - body_table_end); // item infos and bodies // singular pieces of data @@ -8035,7 +7992,6 @@ fn switchExpr( .any_ranges = any_ranges, .has_else = has_else, .has_under = has_under, - .under_is_bare = under_is_bare, .has_continue = switch_full.label_token != null and block_scope.label.?.used_for_continue, .any_maybe_runtime_capture = any_maybe_runtime_capture, .payload_capture_inst_is_placeholder = payload_capture_inst_is_placeholder, @@ -8054,7 +8010,6 @@ fn switchExpr( astgen.extra.appendAssumeCapacity(@bitCast(non_err_info)); } if (has_else) astgen.extra.appendAssumeCapacity(@bitCast(else_info)); - if (has_under) astgen.extra.appendAssumeCapacity(under_extra); const extra_payloads_start = astgen.extra.items.len; @@ -8070,11 +8025,6 @@ fn switchExpr( const body = payloads.items[else_prong_body_start..][0..else_info.body_len]; astgen.extra.appendSliceAssumeCapacity(body); } - if (under_is_bare) { - const under_prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(under_extra); - const body = payloads.items[bare_under_prong_body_start..][0..under_prong_info.body_len]; - astgen.extra.appendSliceAssumeCapacity(body); - } for (0..scalar_cases_len) |scalar_i| { const item_info: Zir.Inst.SwitchBlock.ItemInfo = @bitCast(payloads.items[scalar_item_infos_start + scalar_i]); const item_body_start = payloads.items[scalar_body_table + scalar_i]; diff --git a/lib/std/zig/Zir.zig b/lib/std/zig/Zir.zig index 092886ffd1..386217d378 100644 --- a/lib/std/zig/Zir.zig +++ b/lib/std/zig/Zir.zig @@ -3303,34 +3303,25 @@ pub const Inst = struct { /// 3. catch_or_if_src_node_offset: Ast.Node.Offset, // If inst is switch_block_err_union. /// 4. non_err_info: ProngInfo.NonErr, // If inst is switch_block_err_union. /// 5. else_info: ProngInfo.Else, // If has_else is set. - /// 6. under_info: ProngInfo.Under, // If has_under is set and - /// // under_is_bare is set. - /// 7. under_index: u32, // If has_under is set and - /// // under_is_bare is *not* set. - /// // Index into switch cases. - /// 8. scalar_prong_info: ProngInfo, // for every scalar_cases_len - /// 9. multi_prong_info: ProngInfo, // for every multi_cases_len - /// 10. multi_case_items_len: u32, // for every multi_cases_len - /// 11. multi_case_ranges_len: u32, // If has_ranges is set: for every multi_cases_len - /// 12. scalar_item_info: ItemInfo, // for every scalar_cases_len - /// 13. multi_items_info: { // for every multi_cases_len + /// 6. scalar_prong_info: ProngInfo, // for every scalar_cases_len + /// 7. multi_prong_info: ProngInfo, // for every multi_cases_len + /// 8. multi_case_items_len: u32, // for every multi_cases_len + /// 9. multi_case_ranges_len: u32, // If has_ranges is set: for every multi_cases_len + /// 10. scalar_item_info: ItemInfo, // for every scalar_cases_len + /// 11. multi_items_info: { // for every multi_cases_len /// item_info: ItemInfo, // for each multi_case_items_len /// range_items_info: { // for each multi_case_ranges_len /// first_info: ItemInfo, /// last_info: ItemInfo, /// } /// } - /// 14. non_err_body { + /// 12. non_err_body { /// body_inst: Index // for every non_err_info.body_len /// } - /// 15. else_body: { // If has_else is set. + /// 13. else_body: { // If has_else is set. /// body_inst: Inst.Index, // for every else_info.body_len /// } - /// 16. under_body: { // If has_under is set and - /// // under_is_bare is set. - /// body_inst: Inst.Index, // for every under_info.body_len - /// } - /// 17. scalar_bodies: { // for every scalar_cases_len + /// 14. scalar_bodies: { // for every scalar_cases_len /// prong_body: { // for each body_len in scalar_prong_info /// body_inst: Inst.Index, // for every body_len /// } @@ -3338,7 +3329,7 @@ pub const Inst = struct { /// body_inst: Inst.Index, // for every body_len /// } /// } - /// 18. multi_bodies: { // for each multi_items_info + /// 15. multi_bodies: { // for each multi_items_info /// prong_body: { /// body_inst: Inst.Index, // for each multi_prong_info.body_len /// } @@ -3363,8 +3354,6 @@ pub const Inst = struct { any_ranges: bool, has_else: bool, has_under: bool, - /// Only valid if `has_under` is also set. - under_is_bare: bool, /// If true, at least one prong contains a `continue`. /// Only valid if `has_label` is set. has_continue: bool, @@ -3377,7 +3366,7 @@ pub const Inst = struct { // NOTE maybe don't steal any more bits from poor `scalar_cases_len` // and split `Bits` into two parts instead, `raw_operand` surely // wouldn't mind donating a couple of bits for that purpose... - pub const ScalarCasesLen = u23; + pub const ScalarCasesLen = u24; }; pub const ProngInfo = packed struct(u32) { @@ -3406,12 +3395,6 @@ pub const Inst = struct { has_tag_capture: bool, is_simple_noreturn: bool, }; - - pub const BareUnder = packed struct(u32) { - body_len: u29, - capture: ProngInfo.Capture, - has_tag_capture: bool, - }; }; pub const ItemInfo = packed struct(u32) { @@ -3421,23 +3404,23 @@ pub const Inst = struct { pub const Kind = enum(u2) { enum_literal, error_value, - number_literal, body_len, + under, }; pub const Unwrapped = union(ItemInfo.Kind) { enum_literal: Zir.NullTerminatedString, error_value: Zir.NullTerminatedString, - number_literal: Inst.Ref, body_len: u32, + under, }; pub fn wrap(unwrapped: ItemInfo.Unwrapped) ItemInfo { const data_uncasted: u32 = switch (unwrapped) { .enum_literal => |str_index| @intFromEnum(str_index), .error_value => |str_index| @intFromEnum(str_index), - .number_literal => |zir_ref| @intFromEnum(zir_ref), .body_len => |body_len| body_len, + .under => 0, }; return .{ .kind = unwrapped, .data = @intCast(data_uncasted) }; } @@ -3446,8 +3429,8 @@ pub const Inst = struct { return switch (item_info.kind) { .enum_literal => .{ .enum_literal = @enumFromInt(item_info.data) }, .error_value => .{ .error_value = @enumFromInt(item_info.data) }, - .number_literal => .{ .number_literal = @enumFromInt(item_info.data) }, .body_len => .{ .body_len = item_info.data }, + .under => .under, }; } @@ -4818,9 +4801,6 @@ fn findTrackableInner( if (zir_switch.else_case) |else_case| { try zir.findTrackableBody(gpa, contents, defers, else_case.body); } - if (zir_switch.under_case.resolve()) |under_case| { - try zir.findTrackableBody(gpa, contents, defers, under_case.body); - } var extra_index = zir_switch.end; var case_it = zir_switch.iterateCases(); while (case_it.next()) |case| { @@ -5268,16 +5248,6 @@ pub fn getSwitchBlock(zir: *const Zir, switch_inst: Inst.Index) UnwrappedSwitchB extra_index += 1; break :else_info else_info; } else undefined; - const bare_under_info: Inst.SwitchBlock.ProngInfo.BareUnder = if (bits.has_under and bits.under_is_bare) bare_under_info: { - const bare_under_info: Inst.SwitchBlock.ProngInfo.BareUnder = @bitCast(zir.extra[extra_index]); - extra_index += 1; - break :bare_under_info bare_under_info; - } else undefined; - const under_index: u32 = if (bits.has_under and !bits.under_is_bare) under_index: { - const under_index = zir.extra[extra_index]; - extra_index += 1; - break :under_index under_index; - } else undefined; const scalar_cases_len: u32 = bits.scalar_cases_len; const prong_infos: []const Inst.SwitchBlock.ProngInfo = @ptrCast(zir.extra[extra_index..][0 .. scalar_cases_len + multi_cases_len]); @@ -5320,20 +5290,6 @@ pub fn getSwitchBlock(zir: *const Zir, switch_inst: Inst.Index) UnwrappedSwitchB .is_simple_noreturn = else_info.is_simple_noreturn, }; } else null; - const under_case: UnwrappedSwitchBlock.Case.Under = if (bits.has_under) under_case: { - if (bits.under_is_bare) { - const body = zir.bodySlice(extra_index, bare_under_info.body_len); - extra_index += body.len; - break :under_case .{ .bare = .{ - .index = .bare_under, - .body = body, - .capture = bare_under_info.capture, - .has_tag_capture = bare_under_info.has_tag_capture, - } }; - } else { - break :under_case .{ .index = under_index }; - } - } else .none; return .{ .main_operand = extra.data.raw_operand, .switch_src_node_offset = inst_data.src_node, @@ -5344,7 +5300,7 @@ pub fn getSwitchBlock(zir: *const Zir, switch_inst: Inst.Index) UnwrappedSwitchB .any_maybe_runtime_capture = bits.any_maybe_runtime_capture, .non_err_case = non_err_case, .else_case = else_case, - .under_case = under_case, + .has_under = bits.has_under, .prong_infos = prong_infos, .multi_case_items_lens = multi_case_items_lens, .multi_case_ranges_lens = multi_case_ranges_lens, @@ -5377,7 +5333,7 @@ pub const UnwrappedSwitchBlock = struct { any_maybe_runtime_capture: bool, non_err_case: ?Case.NonErr, else_case: ?Case.Else, - under_case: Case.Under, + has_under: bool, // Refer to doc comment and `iterateCases` to access everything below correctly. prong_infos: []const Inst.SwitchBlock.ProngInfo, multi_case_items_lens: []const u32, @@ -5411,25 +5367,13 @@ pub const UnwrappedSwitchBlock = struct { item_infos: []const Inst.SwitchBlock.ItemInfo, range_infos: []const [2]Inst.SwitchBlock.ItemInfo, - pub fn isUnder(case: *const Case) bool { - return case.index.is_under; - } - pub const Index = packed struct(u32) { kind: enum(u1) { scalar, multi }, - is_under: bool, - value: u30, + value: u31, pub const @"else": Case.Index = .{ .kind = .scalar, - .is_under = false, - .value = std.math.maxInt(u30), - }; - - pub const bare_under: Case.Index = .{ - .kind = .scalar, - .is_under = true, - .value = std.math.maxInt(u30), + .value = std.math.maxInt(u31), }; }; @@ -5448,31 +5392,8 @@ pub const UnwrappedSwitchBlock = struct { is_simple_noreturn: bool, }; - pub const Under = union(enum) { - none, - bare: Under.Resolved, - index: u32, - - pub const Resolved = struct { - index: Case.Index, - body: []const Inst.Index, - capture: Inst.SwitchBlock.ProngInfo.Capture, - has_tag_capture: bool, - }; - - /// If this returns `null` and `under` is not `.none`, you'll have to - /// find the under case by iterating all cases and using `isUnder`! - pub fn resolve(under: Under) ?Under.Resolved { - return switch (under) { - .bare => |resolved| resolved, - .none, .index => null, - }; - } - }; - pub const Iterator = struct { next_idx: u32, - under_idx: ?u32, prong_infos: []const Inst.SwitchBlock.ProngInfo, multi_case_items_lens: []const u32, multi_case_ranges_lens: ?[]const u32, @@ -5486,7 +5407,6 @@ pub const UnwrappedSwitchBlock = struct { return if (idx < scalar_cases_len) .{ .index = .{ .kind = .scalar, - .is_under = idx == it.under_idx, .value = @intCast(idx), }, .prong_info = it.prong_infos[idx], @@ -5495,7 +5415,6 @@ pub const UnwrappedSwitchBlock = struct { } else .{ .index = .{ .kind = .multi, - .is_under = idx == it.under_idx, .value = @intCast(idx - scalar_cases_len), }, .prong_info = it.prong_infos[idx], @@ -5516,10 +5435,6 @@ pub const UnwrappedSwitchBlock = struct { pub fn iterateCases(unwrapped: UnwrappedSwitchBlock) Case.Iterator { return .{ .next_idx = 0, - .under_idx = switch (unwrapped.under_case) { - .none, .bare => null, - .index => |index| index, - }, .prong_infos = unwrapped.prong_infos, .multi_case_items_lens = unwrapped.multi_case_items_lens, .multi_case_ranges_lens = unwrapped.multi_case_ranges_lens, diff --git a/src/Sema.zig b/src/Sema.zig index 204d4a736c..659f1d4987 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -10753,10 +10753,9 @@ fn analyzeSwitchBlock( const operand_src = block.src(.{ .node_offset_switch_operand = src_node_offset }); const has_else = zir_switch.else_case != null; - const has_under = zir_switch.under_case != .none; + const has_under = zir_switch.has_under; const else_case = validated_switch.else_case; - const under_case = validated_switch.under_case; const operand: SwitchOperand, const operand_ty: Type, const maybe_operand_opv: ?Value, const item_ty: Type = operand: { const val, const ref = if (operand_is_ref) @@ -10933,9 +10932,6 @@ fn analyzeSwitchBlock( // 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 }; } - if (has_under) { - break :find_prong .{ under_case.index, under_case.body, under_case.capture, under_case.has_tag_capture, false, true }; - } unreachable; // malformed validated switch }; @@ -11083,10 +11079,9 @@ fn finishSwitchBr( const operand_src = block.src(.{ .node_offset_switch_operand = src_node_offset }); const has_else = zir_switch.else_case != null; - const has_under = zir_switch.under_case != .none; + const has_under = zir_switch.has_under; const else_case = validated_switch.else_case; - const under_case = validated_switch.under_case; const scalar_cases_len = zir_switch.scalarCasesLen(); const multi_cases_len = zir_switch.multiCasesLen(); @@ -11138,20 +11133,15 @@ fn finishSwitchBr( var case_it = zir_switch.iterateCases(); var extra_index = zir_switch.end; + var under_prong: ?struct { + index: Zir.UnwrappedSwitchBlock.Case.Index, + body: []const Zir.Inst.Index, + capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, + has_tag_capture: bool, + } = null; + var cases_len: u32 = 0; while (case_it.next()) |case| { - if (case.isUnder()) { // we'll deal with this later - extra_index += case.prong_info.body_len; - for (case.item_infos) |item_info| { - if (item_info.bodyLen()) |body_len| extra_index += body_len; - } - for (case.range_infos) |range_info| { - if (range_info[0].bodyLen()) |body_len| extra_index += body_len; - if (range_info[1].bodyLen()) |body_len| extra_index += body_len; - } - continue; - } - const item_refs = case_vals[case_val_idx..][0..case.item_infos.len]; case_val_idx += item_refs.len; const range_refs: []const [2]Air.Inst.Ref = @@ -11171,7 +11161,9 @@ fn finishSwitchBr( var emit_bb = false; var any_analyze_body = false; + var is_under_prong = false; for (case.item_infos, item_refs, 0..) |item_info, item_ref, item_i| { + if (item_ref == .none) is_under_prong = true; if (item_info.bodyLen()) |body_len| extra_index += body_len; const analyze_body = sema.wantSwitchProngBodyAnalysis(block, item_ref, operand_ty, union_originally, err_set, prong_info.is_comptime_unreach); @@ -11319,48 +11311,58 @@ fn finishSwitchBr( } } - if (!prong_info.is_inline) { - cases_len += 1; - case_block.instructions.clearRetainingCapacity(); - case_block.error_return_trace_index = child_block.error_return_trace_index; + if (prong_info.is_inline) continue; // handled above - const prong_hint: std.builtin.BranchHint = hint: { - if (any_analyze_body) break :hint try sema.analyzeSwitchProng( - &case_block, - operand, - operand_ty, - raw_operand_ty, - prong_body, - block.src(.{ .switch_capture = .{ - .switch_node_offset = src_node_offset, - .case_idx = case.index, - } }), - prong_info.capture, - prong_info.has_tag_capture, - .none, - .{ .item_refs = item_refs }, - validated_switch.else_err_ty, - switch_inst, - zir_switch, - ); - _ = try case_block.addNoOp(.unreach); - break :hint .cold; // unreachable branches are cold + if (is_under_prong) { + under_prong = .{ + .index = case.index, + .body = prong_body, + .capture = case.prong_info.capture, + .has_tag_capture = case.prong_info.has_tag_capture, }; - try branch_hints.append(gpa, prong_hint); - - try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - item_refs.len + - 2 * range_refs.len + - case_block.instructions.items.len); - cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ - .items_len = @intCast(item_refs.len), - .ranges_len = @intCast(range_refs.len), - .body_len = @intCast(case_block.instructions.items.len), - })); - cases_extra.appendSliceAssumeCapacity(@ptrCast(item_refs)); - cases_extra.appendSliceAssumeCapacity(@ptrCast(range_refs)); - cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); + continue; } + + cases_len += 1; + case_block.instructions.clearRetainingCapacity(); + case_block.error_return_trace_index = child_block.error_return_trace_index; + + const prong_hint: std.builtin.BranchHint = hint: { + if (any_analyze_body) break :hint try sema.analyzeSwitchProng( + &case_block, + operand, + operand_ty, + raw_operand_ty, + prong_body, + block.src(.{ .switch_capture = .{ + .switch_node_offset = src_node_offset, + .case_idx = case.index, + } }), + prong_info.capture, + prong_info.has_tag_capture, + .none, + .{ .item_refs = item_refs }, + validated_switch.else_err_ty, + switch_inst, + zir_switch, + ); + _ = try case_block.addNoOp(.unreach); + break :hint .cold; // unreachable branches are cold + }; + try branch_hints.append(gpa, prong_hint); + + try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + + item_refs.len + + 2 * range_refs.len + + case_block.instructions.items.len); + cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{ + .items_len = @intCast(item_refs.len), + .ranges_len = @intCast(range_refs.len), + .body_len = @intCast(case_block.instructions.items.len), + })); + cases_extra.appendSliceAssumeCapacity(@ptrCast(item_refs)); + cases_extra.appendSliceAssumeCapacity(@ptrCast(range_refs)); + cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items)); } const catch_all_extra: []const u32 = catch_all_extra: { @@ -11499,7 +11501,7 @@ fn finishSwitchBr( try branch_hints.append(gpa, prong_hint); try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len + - (validated_switch.seen_enum_fields.len - zir_switch.totalItemsLen()) + + (validated_switch.seen_enum_fields.len + 1 - zir_switch.totalItemsLen()) + // +1 because totalItemsLen includes the _ case_block.instructions.items.len); const extra_case = cases_extra.addManyAsArrayAssumeCapacity( @typeInfo(Air.SwitchBr.Case).@"struct".fields.len, @@ -11555,7 +11557,7 @@ fn finishSwitchBr( if (analyze_catch_all_body) { const index, const body, const capture, const has_tag_capture = switch (catch_all_case) { .@"else" => .{ else_case.index, else_case.body, else_case.capture, else_case.has_tag_capture }, - .under => .{ under_case.index, under_case.body, under_case.capture, under_case.has_tag_capture }, + .under => .{ under_prong.?.index, under_prong.?.body, under_prong.?.capture, under_prong.?.has_tag_capture }, .none => unreachable, }; break :hint try sema.analyzeSwitchProng( @@ -11734,7 +11736,6 @@ fn fixupSwitchContinues( const ValidatedSwitchBlock = struct { seen_enum_fields: []const ?LazySrcLoc, seen_errors: std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc), - seen_sparse_values: std.AutoHashMapUnmanaged(InternPool.Index, LazySrcLoc), seen_ranges: []const RangeSet.Range, true_src: ?LazySrcLoc, false_src: ?LazySrcLoc, @@ -11742,7 +11743,6 @@ const ValidatedSwitchBlock = struct { case_vals: []const Air.Inst.Ref, else_case: Zir.UnwrappedSwitchBlock.Case.Else, - under_case: Zir.UnwrappedSwitchBlock.Case.Under.Resolved, else_err_ty: ?Type, fn iterateUnhandledItems( @@ -11868,7 +11868,6 @@ fn validateSwitchBlock( const src = block.nodeOffset(src_node_offset); const operand_src = block.src(.{ .node_offset_switch_operand = src_node_offset }); const else_prong_src = block.src(.{ .node_offset_switch_else_prong = src_node_offset }); - const under_prong_src = block.src(.{ .node_offset_switch_under_prong = src_node_offset }); var extra_index = zir_switch.end; // We want to map values to our placeholders later on. @@ -11948,33 +11947,7 @@ fn validateSwitchBlock( }; const has_else = zir_switch.else_case != null; - const has_under = zir_switch.under_case != .none; - - // Validate usage of '_' prongs. - if (has_under and !operand_ty.isNonexhaustiveEnum(zcu)) { - const msg = msg: { - const msg = try sema.errMsg( - src, - "'_' prong only allowed when switching on non-exhaustive enums", - .{}, - ); - errdefer msg.destroy(gpa); - try sema.errNote( - under_prong_src, - msg, - "'_' prong here", - .{}, - ); - try sema.errNote( - src, - msg, - "consider using 'else'", - .{}, - ); - break :msg msg; - }; - return sema.failWithOwnedErrorMsg(block, msg); - } + 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); @@ -11991,7 +11964,6 @@ fn validateSwitchBlock( var else_err_ty: ?Type = null; const else_case = zir_switch.else_case orelse undefined; - var under_case = zir_switch.under_case.resolve() orelse undefined; switch (item_ty.zigTypeTag(zcu)) { .@"union" => unreachable, @@ -12020,16 +11992,6 @@ fn validateSwitchBlock( var case_it = zir_switch.iterateCases(); while (case_it.next()) |case| { const prong_info = case.prong_info; - const is_under = case.isUnder(); - if (is_under) { - assert(!prong_info.is_inline); - under_case = .{ - .index = case.index, - .body = sema.code.bodySlice(extra_index, prong_info.body_len), - .capture = prong_info.capture, - .has_tag_capture = prong_info.has_tag_capture, - }; - } extra_index += prong_info.body_len; for (case.item_infos, 0..) |item_info, item_i| { const item_src = block.src(.{ .switch_case_item = .{ @@ -12037,9 +11999,34 @@ fn validateSwitchBlock( .case_idx = case.index, .item_idx = .{ .kind = .single, .value = @intCast(item_i) }, } }); - const item, extra_index = try sema.resolveSwitchItem(block, item_src, item_ty, item_info, extra_index, switch_inst, prong_info.is_comptime_unreach, prong_info.is_inline); - try sema.validateSwitchItemOrRange(block, item_src, item.val, null, item_ty, seen_enum_fields, &seen_errors, &seen_sparse_values, &range_set, &true_src, &false_src, &void_src); - if (!is_under) case_vals.appendAssumeCapacity(item.ref); + if (item_info.unwrap() == .under) { + if (!operand_ty.isNonexhaustiveEnum(zcu)) return sema.failWithOwnedErrorMsg(block, msg: { + const msg = try sema.errMsg( + src, + "'_' prong only allowed when switching on non-exhaustive enums", + .{}, + ); + errdefer msg.destroy(gpa); + try sema.errNote( + item_src, + msg, + "'_' prong here", + .{}, + ); + try sema.errNote( + src, + msg, + "consider using 'else'", + .{}, + ); + break :msg msg; + }); + case_vals.appendAssumeCapacity(.none); + } else { + const item, extra_index = try sema.resolveSwitchItem(block, item_src, item_ty, item_info, extra_index, switch_inst, prong_info.is_comptime_unreach); + try sema.validateSwitchItemOrRange(block, item_src, item.val, null, item_ty, seen_enum_fields, &seen_errors, &seen_sparse_values, &range_set, &true_src, &false_src, &void_src); + case_vals.appendAssumeCapacity(item.ref); + } } for (case.range_infos, 0..) |range_info, range_i| { const range_offset: LazySrcLoc.Offset.SwitchItem = .{ @@ -12050,10 +12037,10 @@ fn validateSwitchBlock( const range_src = block.src(.{ .switch_case_item = range_offset }); const first_src = block.src(.{ .switch_case_item_range_first = range_offset }); const last_src = block.src(.{ .switch_case_item_range_last = range_offset }); - const first_item, extra_index = try sema.resolveSwitchItem(block, first_src, item_ty, range_info[0], extra_index, switch_inst, prong_info.is_comptime_unreach, prong_info.is_inline); - const last_item, extra_index = try sema.resolveSwitchItem(block, last_src, item_ty, range_info[1], extra_index, switch_inst, prong_info.is_comptime_unreach, prong_info.is_inline); + const first_item, extra_index = try sema.resolveSwitchItem(block, first_src, item_ty, range_info[0], extra_index, switch_inst, prong_info.is_comptime_unreach); + const last_item, extra_index = try sema.resolveSwitchItem(block, last_src, item_ty, range_info[1], extra_index, switch_inst, prong_info.is_comptime_unreach); try sema.validateSwitchItemOrRange(block, range_src, first_item.val, last_item.val, item_ty, seen_enum_fields, &seen_errors, &seen_sparse_values, &range_set, &true_src, &false_src, &void_src); - if (!is_under) case_vals.appendSliceAssumeCapacity(&.{ first_item.ref, last_item.ref }); + case_vals.appendSliceAssumeCapacity(&.{ first_item.ref, last_item.ref }); } } @@ -12285,7 +12272,6 @@ fn validateSwitchBlock( return .{ .seen_enum_fields = seen_enum_fields, .seen_errors = seen_errors, - .seen_sparse_values = seen_sparse_values, .seen_ranges = range_set.ranges.items, .true_src = true_src, .false_src = false_src, @@ -12293,7 +12279,6 @@ fn validateSwitchBlock( .case_vals = case_vals.items, .else_case = else_case, - .under_case = under_case, .else_err_ty = else_err_ty, }; } @@ -12334,19 +12319,13 @@ fn resolveSwitchBlock( var case_val_idx: usize = 0; var extra_index = zir_switch.end; var case_it = zir_switch.iterateCases(); + var under_prong: ?struct { + index: Zir.UnwrappedSwitchBlock.Case.Index, + body: []const Zir.Inst.Index, + capture: Zir.Inst.SwitchBlock.ProngInfo.Capture, + has_tag_capture: bool, + } = null; while (case_it.next()) |case| { - if (case.isUnder()) { // we'll deal with this later - extra_index += case.prong_info.body_len; - for (case.item_infos) |item_info| { - if (item_info.bodyLen()) |body_len| extra_index += body_len; - } - for (case.range_infos) |range_info| { - if (range_info[0].bodyLen()) |body_len| extra_index += body_len; - if (range_info[1].bodyLen()) |body_len| extra_index += body_len; - } - continue; - } - const prong_info = case.prong_info; const prong_body = sema.code.bodySlice(extra_index, prong_info.body_len); extra_index += prong_body.len; @@ -12363,6 +12342,15 @@ fn resolveSwitchBlock( const range_refs: []const [2]Air.Inst.Ref = @ptrCast(case_vals[case_val_idx..][0 .. 2 * case.range_infos.len]); case_val_idx += 2 * range_refs.len; for (item_refs) |item_ref| { + if (item_ref == .none) { + under_prong = .{ + .index = case.index, + .body = prong_body, + .capture = case.prong_info.capture, + .has_tag_capture = case.prong_info.has_tag_capture, + }; + continue; + } const item_val = sema.resolveConstDefinedValue(child_block, .unneeded, item_ref, undefined) catch unreachable; if (cond_val.eql(item_val, item_ty, zcu)) { if (err_set) try sema.maybeErrorUnwrapComptime(child_block, prong_body, cond_ref); @@ -12421,7 +12409,6 @@ fn resolveSwitchBlock( } const else_case = validated_switch.else_case; - const under_case = validated_switch.under_case; // named-only prong @@ -12452,7 +12439,7 @@ fn resolveSwitchBlock( const index, const body, const capture, const has_tag_capture, const is_inline = switch (catch_all_case) { .@"else" => .{ else_case.index, else_case.body, else_case.capture, else_case.has_tag_capture, else_case.is_inline }, - .under => .{ under_case.index, under_case.body, under_case.capture, under_case.has_tag_capture, false }, + .under => .{ under_prong.?.index, under_prong.?.body, under_prong.?.capture, under_prong.?.has_tag_capture, false }, .none => unreachable, }; if (err_set) try sema.maybeErrorUnwrapComptime(child_block, body, cond_ref); @@ -13169,7 +13156,6 @@ fn resolveSwitchItem( extra_index: usize, switch_inst: Zir.Inst.Index, prong_is_comptime_unreach: bool, - prong_is_inline: bool, ) CompileError!ResolvedSwitchItemAndExtraIndex { const pt = sema.pt; const zcu = pt.zcu; @@ -13180,6 +13166,7 @@ fn resolveSwitchItem( var end = extra_index; const uncoerced: Air.Inst.Ref, const uncoerced_ty: Type = uncoerced: switch (item_info.unwrap()) { + .under => unreachable, // caller must check this before calling us .enum_literal => |str_index| { const zir_str = sema.code.nullTerminatedString(str_index); const name = try ip.getOrPutString(gpa, io, pt.tid, zir_str, .no_embedded_nulls); @@ -13198,10 +13185,6 @@ fn resolveSwitchItem( } })); break :uncoerced .{ uncoerced, err_set_ty }; }, - .number_literal => |zir_ref| { - const uncoerced = try sema.resolveInst(zir_ref); - break :uncoerced .{ uncoerced, sema.typeOf(uncoerced) }; - }, .body_len => |body_len| { const body = sema.code.bodySlice(extra_index, body_len); end += body.len; @@ -13231,7 +13214,7 @@ fn resolveSwitchItem( .ok => if (try sema.resolveValue(uncoerced)) |uncoerced_val| { break :item_ref try sema.coerceInMemory(uncoerced_val, item_ty); }, - .missing_error => if (prong_is_comptime_unreach and !prong_is_inline) { + .missing_error => if (prong_is_comptime_unreach) { break :item_ref uncoerced; }, .from_anyerror => {}, diff --git a/src/Zcu.zig b/src/Zcu.zig index 6de010c993..1de99157ab 100644 --- a/src/Zcu.zig +++ b/src/Zcu.zig @@ -1742,27 +1742,6 @@ pub const SrcLoc = struct { } else unreachable; }, - .node_offset_switch_under_prong => |node_off| { - const tree = try src_loc.file_scope.getTree(zcu); - const switch_node = node_off.toAbsolute(src_loc.base_node); - _, const extra_index = tree.nodeData(switch_node).node_and_extra; - const case_nodes = tree.extraDataSlice(tree.extraData(extra_index, Ast.Node.SubRange), Ast.Node.Index); - for (case_nodes) |case_node| { - const case = tree.fullSwitchCase(case_node).?; - for (case.ast.values) |val| { - if (tree.nodeTag(val) == .identifier and - mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(val)), "_")) - { - return tree.tokensToSpan( - tree.firstToken(case_node), - tree.lastToken(case_node), - tree.nodeMainToken(val), - ); - } - } - } else unreachable; - }, - .node_offset_switch_range => |node_off| { const tree = try src_loc.file_scope.getTree(zcu); const switch_node = node_off.toAbsolute(src_loc.base_node); @@ -2176,7 +2155,6 @@ pub const SrcLoc = struct { var multi_i: u32 = 0; var scalar_i: u32 = 0; - var underscore_node: Ast.Node.OptionalIndex = .none; const case: Ast.full.SwitchCase = case: for (case_nodes) |case_node| { const case = tree.fullSwitchCase(case_node).?; if (case.ast.values.len == 0) { @@ -2185,17 +2163,6 @@ pub const SrcLoc = struct { } continue :case; } - if (underscore_node == .none) { - for (case.ast.values) |value| { - if (tree.nodeTag(value) == .identifier and - mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(value)), "_")) - { - underscore_node = value.toOptional(); - if (want_case_idx.is_under) break :case case; - if (case.ast.values.len == 1) continue :case; - } - } - } const is_multi = case.ast.values.len != 1 or tree.nodeTag(case.ast.values[0]) == .switch_range; @@ -2220,7 +2187,6 @@ pub const SrcLoc = struct { .switch_case_item_range_last, => |x| item_idx: { assert(want_case_idx != Zir.UnwrappedSwitchBlock.Case.Index.@"else"); - assert(want_case_idx != Zir.UnwrappedSwitchBlock.Case.Index.bare_under); break :item_idx x.item_idx; }, .switch_capture, .switch_tag_capture => { @@ -2247,9 +2213,7 @@ pub const SrcLoc = struct { .single => { var item_i: u32 = 0; for (case.ast.values) |item_node| { - if (item_node.toOptional() == underscore_node or - tree.nodeTag(item_node) == .switch_range) - { + if (tree.nodeTag(item_node) == .switch_range) { continue; } if (item_i != want_item_idx.value) { @@ -2456,10 +2420,6 @@ pub const LazySrcLoc = struct { /// by taking this AST node index offset from the containing base node, /// which points to a switch expression AST node. Next, navigate to the else prong. node_offset_switch_else_prong: Ast.Node.Offset, - /// The source location points to the `_` prong of a switch expression, found - /// by taking this AST node index offset from the containing base node, - /// which points to a switch expression AST node. Next, navigate to the `_` prong. - node_offset_switch_under_prong: Ast.Node.Offset, /// The source location points to all the ranges of a switch expression, found /// by taking this AST node index offset from the containing base node, /// which points to a switch expression AST node. Next, navigate to any of the diff --git a/src/print_zir.zig b/src/print_zir.zig index 0006207d6d..c3a494de84 100644 --- a/src/print_zir.zig +++ b/src/print_zir.zig @@ -2021,15 +2021,6 @@ const Writer = struct { try stream.writeAll("else => "); try self.writeBracedBody(stream, else_case.body); } - if (zir_switch.under_case.resolve()) |under_case| { - try stream.writeAll(",\n"); - try stream.splatByteAll(' ', self.indent); - - try self.writeSwitchCaptures(stream, under_case.capture, under_case.has_tag_capture, inst, &zir_switch); - - try stream.writeAll("_ => "); - try self.writeBracedBody(stream, under_case.body); - } var case_it = zir_switch.iterateCases(); while (case_it.next()) |case| { @@ -2043,14 +2034,8 @@ const Writer = struct { const prong_body = self.code.bodySlice(extra_index, prong_info.body_len); extra_index += prong_body.len; - var first_item: bool = true; - if (case.isUnder()) { - try stream.writeAll("_"); - first_item = false; - } - for (case.item_infos) |item_info| { - if (!first_item) try stream.writeAll(", "); - first_item = false; + for (case.item_infos, 0..) |item_info, i| { + if (i > 0) try stream.writeAll(", "); switch (item_info.unwrap()) { .enum_literal => |str_index| { @@ -2061,9 +2046,7 @@ const Writer = struct { const str = self.code.nullTerminatedString(str_index); try stream.print("\"error.{f}\"", .{std.zig.fmtString(str)}); }, - .number_literal => |zir_ref| { - try self.writeInstRef(stream, zir_ref); - }, + .under => try stream.writeByte('_'), .body_len => |body_len| { const item_body = self.code.bodySlice(extra_index, body_len); extra_index += item_body.len; @@ -2071,33 +2054,40 @@ const Writer = struct { }, } } - for (case.range_infos) |range_info| { - if (!first_item) try stream.writeAll(", "); - first_item = false; - - var first_range_item = true; - for (&range_info) |item_info| { - if (!first_range_item) try stream.writeAll("..."); - first_range_item = false; - - switch (item_info.unwrap()) { - .enum_literal => |str_index| { - const str = self.code.nullTerminatedString(str_index); - try stream.print("\".{f}\"", .{std.zig.fmtString(str)}); - }, - .error_value => |str_index| { - const str = self.code.nullTerminatedString(str_index); - try stream.print("\"error.{f}\"", .{std.zig.fmtString(str)}); - }, - .number_literal => |zir_ref| { - try self.writeInstRef(stream, zir_ref); - }, - .body_len => |body_len| { - const item_body = self.code.bodySlice(extra_index, body_len); - extra_index += item_body.len; - try self.writeBracedDecl(stream, item_body); - }, - } + for (case.range_infos, 0..) |range_info, i| { + if (i > 0 and case.item_infos.len == 0) try stream.writeAll(", "); + switch (range_info[0].unwrap()) { + .enum_literal => |str_index| { + const str = self.code.nullTerminatedString(str_index); + try stream.print("\".{f}\"", .{std.zig.fmtString(str)}); + }, + .error_value => |str_index| { + const str = self.code.nullTerminatedString(str_index); + try stream.print("\"error.{f}\"", .{std.zig.fmtString(str)}); + }, + .under => unreachable, // '_..._' is not allowed + .body_len => |body_len| { + const item_body = self.code.bodySlice(extra_index, body_len); + extra_index += item_body.len; + try self.writeBracedDecl(stream, item_body); + }, + } + try stream.writeAll("..."); + switch (range_info[1].unwrap()) { + .enum_literal => |str_index| { + const str = self.code.nullTerminatedString(str_index); + try stream.print("\".{f}\"", .{std.zig.fmtString(str)}); + }, + .error_value => |str_index| { + const str = self.code.nullTerminatedString(str_index); + try stream.print("\"error.{f}\"", .{std.zig.fmtString(str)}); + }, + .under => unreachable, // '_..._' is not allowed + .body_len => |body_len| { + const item_body = self.code.bodySlice(extra_index, body_len); + extra_index += item_body.len; + try self.writeBracedDecl(stream, item_body); + }, } } try stream.writeAll(" => "); From 9a225456cb6e9079a6e435b264d25bd5f750e7bb Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 11 Jan 2026 13:24:22 +0000 Subject: [PATCH 21/23] Sema: minor cleanup --- src/Sema.zig | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Sema.zig b/src/Sema.zig index 659f1d4987..642d7768a6 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -10581,7 +10581,7 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp // Then we analyze the non-error prong if it's not comptime-unreachable. // Lastly, we analyze the error prong(s) as a regular switch. - const raw_switch_operand, const non_err_reachable, const non_err_cond, const non_err_hint, const err_set_empty = non_err: { + const raw_switch_operand, const non_err_cond, const non_err_hint = non_err: { const eu_maybe_ptr = try sema.resolveInst(zir_switch.main_operand); const err_union_ty: Type = err_union_ty: { const raw_operand_ty = sema.typeOf(eu_maybe_ptr); @@ -10600,17 +10600,9 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp else try sema.analyzeIsNonErr(block, operand_src, eu_maybe_ptr); - const is_non_err = try sema.resolveDefinedValue(block, operand_src, non_err_cond); - const non_err_reachable = if (is_non_err) |val| val.toBool() else true; - - const err_set_empty = err_set_empty: { - const err_set_ty = err_union_ty.errorUnionSet(zcu); - break :err_set_empty err_set_ty.errorSetIsEmpty(zcu); - }; - const non_err_hint: std.builtin.BranchHint = hint: { // don't analyze the non-error body if it's unreachable - if (!non_err_reachable) { + if (non_err_cond == .bool_false) { break :hint undefined; } @@ -10622,8 +10614,7 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp if (non_err_case.capture != .none) sema.inst_map.putAssumeCapacity(inst, eu_payload); defer if (non_err_case.capture != .none) assert(sema.inst_map.remove(inst)); - const always_non_err = if (is_non_err) |val| val.toBool() else err_set_empty; - if (always_non_err) { + if (non_err_cond == .bool_true) { // Early return; we don't analyze the switch as it's unreachable. return sema.resolveBlockBody(block, src, &non_err_block, non_err_case.body, inst, merges); } @@ -10636,20 +10627,23 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp else try sema.analyzeErrUnionCode(&switch_block, operand_src, eu_maybe_ptr); - break :non_err .{ eu_code, non_err_reachable, non_err_cond, non_err_hint, err_set_empty }; + break :non_err .{ + eu_code, + non_err_cond, + non_err_hint, + }; }; const validated_switch = try sema.validateSwitchBlock(block, raw_switch_operand, false, inst, &zir_switch); const maybe_switch_ref: ?Air.Inst.Ref = ref: { - if (err_set_empty) break :ref .unreachable_value; // make err capture (i.e. switch operand) available to switch prong bodies sema.inst_map.putAssumeCapacityNoClobber(inst, raw_switch_operand); defer assert(sema.inst_map.remove(inst)); break :ref try sema.analyzeSwitchBlock(block, &switch_block, raw_switch_operand, false, merges, inst, &zir_switch, &validated_switch); }; - if (!non_err_reachable) { + if (non_err_cond == .bool_false) { return maybe_switch_ref orelse { const switch_src = block.nodeOffset(zir_switch.switch_src_node_offset); return sema.resolveAnalyzedBlock(block, switch_src, &switch_block, merges, false); From 3a4a7d2ca378862dd6b31678a143315a9e306f8c Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 11 Jan 2026 13:31:22 +0000 Subject: [PATCH 22/23] Sema: minor cleanup (the second) --- src/Air.zig | 44 -------------------------------------------- src/Sema.zig | 30 ++++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 46 deletions(-) diff --git a/src/Air.zig b/src/Air.zig index d5a2fa3e10..b5cb950d49 100644 --- a/src/Air.zig +++ b/src/Air.zig @@ -1339,50 +1339,6 @@ pub const SwitchBr = struct { ranges_len: u32, body_len: u32, }; - - pub const BranchHints = struct { - bags: std.ArrayList(u32), - count: u32, - - const hints_per_bag = 10; - const hint_bits = @bitSizeOf(std.builtin.BranchHint); - - pub const empty: BranchHints = .{ - .bags = .empty, - .count = 0, - }; - - pub fn initCapacity(gpa: std.mem.Allocator, num: u32) std.mem.Allocator.Error!BranchHints { - const bags_required = std.math.divCeil(u32, num, hints_per_bag) catch unreachable; - const bags: std.ArrayList(u32) = try .initCapacity(gpa, bags_required); - return .{ .bags = bags, .count = 0 }; - } - - pub fn ensureUnusedCapacity(hints: *BranchHints, gpa: std.mem.Allocator, additional_count: u32) std.mem.Allocator.Error!void { - const unused_hints = hints.bags.capacity * hints_per_bag - hints.count; - if (unused_hints >= additional_count) return; - const bags_required = std.math.divCeil(u32, hints.count + additional_count, hints_per_bag) catch unreachable; - return hints.bags.ensureUnusedCapacity(gpa, bags_required); - } - - pub fn appendAssumeCapacity(hints: *BranchHints, hint: std.builtin.BranchHint) void { - const idx_in_bag = hints.count % hints_per_bag; - var bag: u32 = if (idx_in_bag > 0) hints.bags.pop().? else 0; - bag |= @as(u32, @intFromEnum(hint)) << @intCast(hint_bits * idx_in_bag); - hints.count += 1; - return hints.bags.appendAssumeCapacity(bag); - } - - pub fn append(hints: *BranchHints, gpa: std.mem.Allocator, hint: std.builtin.BranchHint) std.mem.Allocator.Error!void { - try hints.ensureUnusedCapacity(gpa, 1); - return hints.appendAssumeCapacity(hint); - } - - pub fn deinit(hints: *BranchHints, gpa: std.mem.Allocator) void { - hints.bags.deinit(gpa); - hints.* = undefined; - } - }; }; /// This data is stored inside extra. Trailing: diff --git a/src/Sema.zig b/src/Sema.zig index 642d7768a6..ceb4077325 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -11108,11 +11108,37 @@ fn finishSwitchBr( const estimated_cases_len: u32 = scalar_cases_len + multi_cases_len + @intFromBool(has_else or has_under); + const BranchHints = struct { + bags: std.ArrayList(u32), + count: u32, + const hints_per_bag = 10; + fn ensureUnusedCapacity(hints: *@This(), gpa_inner: Allocator, additional_count: u32) Allocator.Error!void { + const unused_hints = hints.bags.capacity * hints_per_bag - hints.count; + if (unused_hints >= additional_count) return; + const bags_required = std.math.divCeil(u32, hints.count + additional_count, hints_per_bag) catch unreachable; + return hints.bags.ensureUnusedCapacity(gpa_inner, bags_required); + } + fn appendAssumeCapacity(hints: *@This(), hint: std.builtin.BranchHint) void { + const idx_in_bag = hints.count % hints_per_bag; + var bag: u32 = if (idx_in_bag > 0) hints.bags.pop().? else 0; + bag |= @as(u32, @intFromEnum(hint)) << @intCast(@bitSizeOf(std.builtin.BranchHint) * idx_in_bag); + hints.count += 1; + return hints.bags.appendAssumeCapacity(bag); + } + fn append(hints: *@This(), gpa_inner: Allocator, hint: std.builtin.BranchHint) Allocator.Error!void { + try hints.ensureUnusedCapacity(gpa_inner, 1); + return hints.appendAssumeCapacity(hint); + } + }; + var branch_hints: BranchHints = hints: { + const num_bags = std.math.divCeil(u32, estimated_cases_len, BranchHints.hints_per_bag) catch unreachable; + break :hints .{ .bags = try .initCapacity(gpa, num_bags), .count = 0 }; + }; + defer branch_hints.bags.deinit(gpa); + var cases_extra: std.ArrayList(u32) = try .initCapacity(gpa, estimated_cases_len * @typeInfo(Air.SwitchBr.Case).@"struct".fields.len); defer cases_extra.deinit(gpa); - var branch_hints: Air.SwitchBr.BranchHints = try .initCapacity(gpa, estimated_cases_len); - defer branch_hints.deinit(gpa); // We will reuse this block for each case. var case_block = child_block.makeSubBlock(); From 01546e68cd0d82ef78498a10649e6bc2937680da Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Sun, 11 Jan 2026 14:27:00 +0000 Subject: [PATCH 23/23] compiler: handle switch rewrite review feedback --- doc/langref.html.in | 2 +- doc/langref/test_switch_on_errors.zig | 5 +- doc/langref/test_tagged_union.zig | 5 +- src/Air/Liveness.zig | 5 +- src/Sema.zig | 68 ++++++++----- src/Zcu.zig | 11 +- test/behavior/for.zig | 2 +- test/behavior/switch.zig | 113 ++++++++++----------- test/behavior/switch_loop.zig | 17 ++-- test/behavior/switch_on_captured_error.zig | 7 +- test/behavior/while.zig | 8 +- 11 files changed, 128 insertions(+), 115 deletions(-) diff --git a/doc/langref.html.in b/doc/langref.html.in index 07a5b39f2f..49cc385200 100644 --- a/doc/langref.html.in +++ b/doc/langref.html.in @@ -2595,7 +2595,7 @@ or {#header_close#} - {#header_open|Switching on errors#} + {#header_open|Switching on Errors#}

When switching on errors, some special cases are allowed to simplify generic programming patterns:

diff --git a/doc/langref/test_switch_on_errors.zig b/doc/langref/test_switch_on_errors.zig index 7fe534e7a3..cb1a8249e3 100644 --- a/doc/langref/test_switch_on_errors.zig +++ b/doc/langref/test_switch_on_errors.zig @@ -12,7 +12,10 @@ test "unreachable else prong" { switch (openFile0()) { error.AccessDenied, error.FileNotFound => |e| return e, error.OutOfMemory => {}, - else => unreachable, // technically unreachable, but will still compile! + // 'openFile0' cannot return any more errors, so an 'else' prong would be + // statically known to be unreachable. Nonetheless, in this case, adding + // one does not raise an "unreachable else prong" compile error: + else => unreachable, } // Allowed unreachable else prongs are: diff --git a/doc/langref/test_tagged_union.zig b/doc/langref/test_tagged_union.zig index 58df9dc38d..d118d7e6bd 100644 --- a/doc/langref/test_tagged_union.zig +++ b/doc/langref/test_tagged_union.zig @@ -20,7 +20,10 @@ test "switch on tagged union" { } switch (c) { - .ok => |_, tag| try expect(tag == .ok), + .ok => |_, tag| { + // Because we're in the '.ok' prong, 'tag' is compile-time known to be '.ok': + comptime std.debug.assert(tag == .ok); + }, .not_ok => unreachable, } } diff --git a/src/Air/Liveness.zig b/src/Air/Liveness.zig index 051472a97a..9b722973a6 100644 --- a/src/Air/Liveness.zig +++ b/src/Air/Liveness.zig @@ -176,10 +176,7 @@ pub fn analyze(zcu: *Zcu, air: Air, intern_pool: *InternPool) Allocator.Error!Li data.old_extra = a.extra; a.extra = .{}; try analyzeBody(&a, .main_analysis, &data, main_body); - if (std.debug.runtime_safety and data.live_set.count() != 0) { - log.debug("instructions still in live set after analysis: {f}", .{fmtInstSet(&data.live_set)}); - @panic("liveness analysis failed"); - } + assert(data.live_set.count() == 0); } return .{ diff --git a/src/Sema.zig b/src/Sema.zig index ceb4077325..8a33a8136c 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -12591,6 +12591,7 @@ fn resolveSwitchProng( sema.typeOf(operand.simple.by_val), capture_src, inline_case_capture, + kind, ); sema.inst_map.putAssumeCapacity(tag_inst, tag_ref); break :inst tag_inst; @@ -12723,6 +12724,7 @@ fn analyzeSwitchProng( operand_ty, capture_src, inline_case_capture, + kind, ); sema.inst_map.putAssumeCapacity(tag_inst, tag_ref); break :inst tag_inst; @@ -12743,6 +12745,7 @@ fn analyzeSwitchTagCapture( operand_ty: Type, capture_src: LazySrcLoc, inline_case_capture: Air.Inst.Ref, + kind: SwitchProngKind, ) CompileError!Air.Inst.Ref { const pt = sema.pt; const zcu = pt.zcu; @@ -12760,6 +12763,10 @@ fn analyzeSwitchTagCapture( if (inline_case_capture != .none) { return inline_case_capture; // this already is the tag, it's what we're switching on! } + switch (kind) { + .has_ranges, .special => {}, + .item_refs => |refs| if (refs.len == 1) return refs[0], + } const tag_ty = operand_ty.unionTagType(zcu).?; return sema.unionToTag(case_block, tag_ty, operand_val, tag_capture_src); } @@ -13150,10 +13157,12 @@ fn analyzeSwitchPayloadCapture( 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. + // 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; + } else if (case_vals.len == 1) { + return case_vals[0]; } else { return operand_val; } @@ -31361,17 +31370,19 @@ fn resolvePtrIsNonErrVal( assert(ptr_ty.zigTypeTag(zcu) == .pointer); const child_ty = ptr_ty.childType(zcu); - const child_tag = child_ty.zigTypeTag(zcu); - if (child_tag != .error_set and child_tag != .error_union) return .true; - if (child_tag == .error_set) return .false; - assert(child_tag == .error_union); + if (try sema.resolveIsNonErrFromType(block, src, child_ty)) |res| { + return res; + } + assert(child_ty.zigTypeTag(zcu) == .error_union); - if (try sema.resolveValue(operand)) |ptr_val| { - if (ptr_val.isUndef(zcu)) return .undef_bool; - if (try sema.pointerDeref(block, src, ptr_val, ptr_ty)) |val| { - return try sema.resolveIsNonErrVal(block, src, .fromValue(val)); + if (try sema.resolveValue(operand)) |eu_ptr_val| { + if (eu_ptr_val.isUndef(zcu)) return .undef_bool; + if (try sema.pointerDeref(block, src, eu_ptr_val, ptr_ty)) |err_union| { + if (err_union.isUndef(zcu)) return .undef_bool; + return .makeBool(err_union.getErrorName(zcu) == .none); } } + return null; } @@ -31380,11 +31391,30 @@ fn resolveIsNonErrVal( block: *Block, src: LazySrcLoc, operand: Air.Inst.Ref, +) CompileError!?Value { + const zcu = sema.pt.zcu; + if (try sema.resolveIsNonErrFromType(block, src, sema.typeOf(operand))) |res| { + return res; + } + assert(sema.typeOf(operand).zigTypeTag(zcu) == .error_union); + + if (try sema.resolveValue(operand)) |err_union| { + if (err_union.isUndef(zcu)) return .undef_bool; + return .makeBool(err_union.getErrorName(zcu) == .none); + } + + return null; +} + +fn resolveIsNonErrFromType( + sema: *Sema, + block: *Block, + src: LazySrcLoc, + operand_ty: Type, ) CompileError!?Value { const pt = sema.pt; const zcu = pt.zcu; const ip = &zcu.intern_pool; - const operand_ty = sema.typeOf(operand); const ot = operand_ty.zigTypeTag(zcu); if (ot != .error_set and ot != .error_union) return .true; if (ot == .error_set) return .false; @@ -31395,15 +31425,6 @@ fn resolveIsNonErrVal( return .false; } - if (operand == .undef) { - return .undef_bool; - } else if (@intFromEnum(operand) < InternPool.static_len) { - // None of the ref tags can be errors. - return .true; - } - - const maybe_operand_val = try sema.resolveValue(operand); - // exception if the error union error set is known to be empty, // we allow the comparison but always make it comptime-known. const set_ty = ip.errorUnionSet(operand_ty.toIntern()); @@ -31419,9 +31440,6 @@ fn resolveIsNonErrVal( else => |i| if (ip.indexToKey(i).error_set_type.names.len != 0) break :blk, } - if (maybe_operand_val != null) break :blk; - - // Try to avoid resolving inferred error set if possible. if (ies.errors.count() != 0) return null; switch (ies.resolved) { .anyerror_type => return null, @@ -31450,7 +31468,6 @@ fn resolveIsNonErrVal( .none => {}, else => |i| if (ip.indexToKey(i).error_set_type.names.len != 0) break :blk, } - if (maybe_operand_val != null) break :blk; if (sema.fn_ret_ty_ies) |ies| { if (ies.func == func_index) { // Try to avoid resolving inferred error set if possible. @@ -31479,9 +31496,6 @@ fn resolveIsNonErrVal( }, } - if (maybe_operand_val) |err_union| { - return if (err_union.isUndef(zcu)) .undef_bool else if (err_union.getErrorName(zcu) == .none) .true else .false; - } return null; } diff --git a/src/Zcu.zig b/src/Zcu.zig index 1de99157ab..d1e20c8551 100644 --- a/src/Zcu.zig +++ b/src/Zcu.zig @@ -2221,16 +2221,7 @@ pub const SrcLoc = struct { continue; } return tree.nodeToSpan(item_node); - } else { - for (case.ast.values) |item_node| { - const item_span = tree.nodeToSpan(item_node); - std.debug.print("{s}\n", .{tree.source[item_span.start..item_span.end]}); - } - std.debug.print("want_case_idx={any}\n", .{want_case_idx}); - std.debug.print("want_item_idx={any}\n", .{want_item_idx}); - unreachable; - } - // } else unreachable; + } else unreachable; }, .range => { var range_i: u32 = 0; diff --git a/test/behavior/for.zig b/test/behavior/for.zig index 0361c2d19d..3f12767c9f 100644 --- a/test/behavior/for.zig +++ b/test/behavior/for.zig @@ -525,7 +525,7 @@ test "for loop 0 length range" { } } -test "labeled break from else prong" { +test "labeled break from else" { const S = struct { fn doTheTest(x: u32) !void { var y: u32 = 0; diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index 1e1cfdd3a9..483194e00b 100644 --- a/test/behavior/switch.zig +++ b/test/behavior/switch.zig @@ -1128,20 +1128,17 @@ test "decl literals as switch cases" { const foo: @This() = @enumFromInt(0xa); - fn doTheTest() !void { - var e: @This() = .foo; - _ = &e; - const ok = switch (e) { - .bar => false, - .foo => true, - else => false, - }; - try expect(ok); + fn doTheTest(e: @This()) !void { + switch (e) { + .bar => return error.TestFailed, + .foo => {}, + else => return error.TestFailed, + } } }; - try E.doTheTest(); - try comptime E.doTheTest(); + try E.doTheTest(.foo); + try comptime E.doTheTest(.foo); } // TODO audit after #15909 and/or #19855 are decided/implemented @@ -1152,32 +1149,30 @@ test "switch with uninstantiable union fields" { b: noreturn, c: error{}, - fn doTheTest() !void { - var u: @This() = .ok; - _ = &u; - try expect(switch (u) { - .ok => true, + fn doTheTest(u: @This()) void { + switch (u) { + .ok => {}, .a => comptime unreachable, .b => comptime unreachable, .c => comptime unreachable, - }); - try expect(switch (u) { - .ok => true, + } + switch (u) { + .ok => {}, .a, .b, .c => comptime unreachable, - }); - try expect(switch (u) { - .ok => true, + } + switch (u) { + .ok => {}, else => comptime unreachable, - }); - try expect(switch (u) { + } + switch (u) { .a => comptime unreachable, - .ok, .b, .c => true, - }); + .ok, .b, .c => {}, + } } }; - try U.doTheTest(); - try comptime U.doTheTest(); + U.doTheTest(.ok); + comptime U.doTheTest(.ok); } test "switch with tag capture" { @@ -1196,8 +1191,8 @@ test "switch with tag capture" { fn doTheSwitch(u: @This()) !void { switch (u) { .a => |nothing, tag| { - try expect(nothing == {}); - try expect(tag == .a); + comptime assert(nothing == {}); + comptime assert(tag == .a); try expect(@intFromEnum(tag) == @intFromEnum(@This().a)); }, .b, .d => |_, tag| { @@ -1216,13 +1211,13 @@ test "switch with tag capture" { } switch (u) { inline .a, .b, .c => |payload, tag| { - if (@TypeOf(payload) == void) try expect(tag == .a); - if (@TypeOf(payload) == i32) try expect(tag == .b); - if (@TypeOf(payload) == u8) try expect(tag == .c); + if (@TypeOf(payload) == void) comptime assert(tag == .a); + if (@TypeOf(payload) == i32) comptime assert(tag == .b); + if (@TypeOf(payload) == u8) comptime assert(tag == .c); }, inline else => |payload, tag| { - if (@TypeOf(payload) == i32) try expect(tag == .d); - try expect(tag != .e); + if (@TypeOf(payload) == i32) comptime assert(tag == .d); + comptime assert(tag != .e); }, } } @@ -1232,7 +1227,7 @@ test "switch with tag capture" { try comptime U.doTheTest(); } -test "switch with advanced prong items" { +test "switch with complex item expressions" { const S = struct { fn doTheTest() !void { try doTheSwitch(2000, 20); @@ -1278,19 +1273,11 @@ test "switch with advanced prong items" { } test "switch evaluation order" { - const eval = comptime eval: { - var eval = false; - const eu: anyerror!u32 = 0; - _ = eu catch |err| switch (err) { - blk: { - eval = true; - break :blk error.MyError; - } => {}, - else => unreachable, - }; - break :eval eval; + const eu: anyerror!u32 = 0; + _ = eu catch |err| switch (err) { + if (true) @compileError("unreachable") => unreachable, + else => unreachable, }; - try comptime expect(!eval); } test "switch resolves lazy values correctly" { @@ -1298,13 +1285,25 @@ test "switch resolves lazy values correctly" { a: u16, b: i16, }; - const ok1 = switch (@sizeOf(S)) { - 4 => true, - else => false, - }; - const ok2 = switch (@sizeOf(S)) { - 4 => true, - else => false, - }; - try comptime expect(ok1 == ok2); + switch (@sizeOf(S)) { + 4 => {}, + else => comptime unreachable, + } +} + +test "single-item prong in switch on enum has comptime-known capture" { + const E = enum { + a, + b, + c, + fn doTheTest(e: @This()) !void { + switch (e) { + .a => |tag| comptime assert(tag == .a), + .b => return error.TestFailed, + .c => return error.TestFailed, + } + } + }; + try E.doTheTest(.a); + try comptime E.doTheTest(.a); } diff --git a/test/behavior/switch_loop.zig b/test/behavior/switch_loop.zig index 3da7838fbc..bbf9c86d50 100644 --- a/test/behavior/switch_loop.zig +++ b/test/behavior/switch_loop.zig @@ -1,5 +1,6 @@ const builtin = @import("builtin"); const std = @import("std"); +const assert = std.debug.assert; const expect = std.testing.expect; test "simple switch loop" { @@ -340,7 +341,7 @@ test "switch loop with single catch-all prong" { continue :label .{ .b = 456 }; }, }; - try expect(ok); + comptime assert(ok); } }; try S.doTheTest(); @@ -411,8 +412,8 @@ test "switch loop with tag capture" { fn doTheSwitch(u: @This()) !void { const ok1 = label: switch (u) { .a => |nothing, tag| { - try expect(nothing == {}); - try expect(tag == .a); + comptime assert(nothing == {}); + comptime assert(tag == .a); try expect(@intFromEnum(tag) == @intFromEnum(@This().a)); continue :label .{ .d = 456 }; }, @@ -438,21 +439,21 @@ test "switch loop with tag capture" { const ok2 = label: switch (u) { inline .a, .b, .c => |payload, tag| { if (@TypeOf(payload) == void) { - try expect(tag == .a); + comptime assert(tag == .a); continue :label .{ .b = 456 }; } if (@TypeOf(payload) == i32) { - try expect(tag == .b); + comptime assert(tag == .b); continue :label .{ .d = payload }; } if (@TypeOf(payload) == u8) { - try expect(tag == .c); + comptime assert(tag == .c); continue :label .{ .d = payload }; } }, inline else => |payload, tag| { - if (@TypeOf(payload) == i32) try expect(tag == .d); - try expect(tag != .e); + if (@TypeOf(payload) == i32) comptime assert(tag == .d); + comptime assert(tag != .e); if (payload == 0) break :label false; break :label true; }, diff --git a/test/behavior/switch_on_captured_error.zig b/test/behavior/switch_on_captured_error.zig index cc5172a32b..c4353cd5df 100644 --- a/test/behavior/switch_on_captured_error.zig +++ b/test/behavior/switch_on_captured_error.zig @@ -17,7 +17,9 @@ test "switch on error union catch capture" { try testElse(); try testCapture(); try testInline(); + try testEmptyErrSet(); try testUnreachableElseProng(); + try testErrNotInSet(); try testAddressOf(); } @@ -384,8 +386,11 @@ test "switch on error union if else capture" { try testCapturePtr(); try testInline(); try testInlinePtr(); + try testEmptyErrSet(); + try testEmptyErrSetPtr(); try testUnreachableElseProng(); try testUnreachableElseProngPtr(); + try testErrNotInSet(); try testAddressOf(); } @@ -835,7 +840,7 @@ test "switch on error union if else capture" { var a: error{}!u64 = 0; _ = &a; const b = if (a) |*x| x.* else |err| switch (err) { - error.undefined => @compileError("unreachable"), + undefined => @compileError("unreachable"), }; try expectEqual(@as(u64, 0), b); } diff --git a/test/behavior/while.zig b/test/behavior/while.zig index 9b5fe1fb84..2854f8a8d4 100644 --- a/test/behavior/while.zig +++ b/test/behavior/while.zig @@ -400,12 +400,12 @@ test "breaking from a loop in an if statement" { _ = opt; } -test "labeled break from else prong" { +test "labeled break from else" { const S = struct { fn doTheTest(x: u32) !void { - var y: u32 = 0; - const ok = label: while (y < x) : (y += 1) { - if (y == 10) break :label false; + const arr: []const u32 = &.{ 1, 3, 10 }; + const ok = label: for (arr) |y| { + if (y == x) break :label false; } else { break :label true; };