Zig 0.17.0-dev (Split by item)

This is an example of documentation generated by ZigDoc, an alternative to Zig's built-in Auto Doc feature. See also examples in other modes/formats. The project being documented here (as the example) is the Zig library itself.

switchExpr

AstGen.switchExpr
fn switchExpr(
    parent_gz: *GenZir,
    scope: *Scope,
    ri: ResultInfo,
    node: Ast.Node.Index,
    switch_full: Ast.full.Switch,
    non_err: SwitchNonErr,
) InnerError!Zir.Inst.Ref

File

lib/std/zig/AstGen.zig:7003

Code

fn switchExpr(
    parent_gz: *GenZir,
    scope: *Scope,
    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 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) {
            .ptr => .{ .ty = (try ri.rl.resultType(parent_gz, node)).? },
            .inferred_ptr => .none,
            else => ri.rl,
        },
        .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: enum { no, yes, yes_const } = switch (non_err) {
        .none, .peer_break_target => undefined,
        .@"catch" => switch (ri.rl) {
            .ref, .ref_coerced_ty => .yes,
            .ref_const => .yes_const,
            else => .no,
        },
        .@"if" => |if_full| if (if_full.payload_token != null and
            tree.tokenTag(if_full.payload_token.?) == .asterisk) .yes else .no,
    };

    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 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_maybe_runtime_capture = false;
    var scalar_cases_len: u32 = 0;
    var multi_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 underscore_node: Ast.Node.OptionalIndex = .none;
    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 (case.inline_token == null) {
                    any_maybe_runtime_capture = true;
                }
            }
        }

        // Check for else prong.
        if (case.ast.values.len == 0) {
            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(
                    else_tok,
                    "multiple else prongs in switch expression",
                    .{},
                    &.{try astgen.errNoteTok(prev_else_tok, "previous else prong here", .{})},
                );
            }
            else_case_node = case_node.toOptional();
            continue;
        }

        // Check for '_' prong and ranges.
        var case_has_ranges = false;
        for (case.ast.values) |val| {
            switch (tree.nodeTag(val)) {
                .switch_range => {
                    total_ranges_len += 1;
                    case_has_ranges = true;
                },
                .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)), "_"))
                    {
                        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_node.unwrap()) |prev_src| {
                            return astgen.failNodeNotes(
                                val,
                                "multiple '_' prongs in switch expression",
                                .{},
                                &.{try astgen.errNoteNode(prev_src, "previous '_' prong here", .{})},
                            );
                        }
                        if (case.inline_token != null) {
                            return astgen.failNode(val, "cannot inline '_' prong", .{});
                        }
                        underscore_node = val.toOptional();
                    }
                },
            }
        }

        const case_len = case.ast.values.len;
        if (case_len == 1 and !case_has_ranges) {
            scalar_cases_len += 1;
        } else if (case_len >= 1) {
            multi_cases_len += 1;
        }
    }

    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;

    // 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;
    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 non_err_info: Zir.Inst.SwitchBlock.ProngInfo.NonErr = undefined;
    var else_info: Zir.Inst.SwitchBlock.ProngInfo.Else = undefined;

    var block_scope = parent_gz.makeSubBlock(scope);
    // block_scope not used for collecting instructions
    block_scope.instructions_top = GenZir.unstacked_top;

    const operand_ri: ResultInfo = .{
        .rl = loc: {
            if (any_payload_is_ref) break :loc .ref;
            if (needs_non_err_handling and non_err_is_ref == .yes) break :loc .ref;
            if (needs_non_err_handling and non_err_is_ref == .yes_const) break :loc .ref_const;
            break :loc .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 = 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)[@backingInt(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.continue_target = .{ .switch_continue = switch_block };
        block_scope.continue_result_info = .{
            .rl = if (any_payload_is_ref)
                .{ .ref_coerced_ty = switch_block.toRef() }
            else
                .{ .coerced_ty = switch_block.toRef() },
        };
        switch_block_inst_is_occupied = true;

        // `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 };
    };

    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;
    };

    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 != .no) .by_ref else .by_val;

                const then_result = switch (ri.rl) {
                    .ref, .ref_const, .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 != .no);
                        const ident_name = try astgen.identAsString(ident_token);
                        const ident_name_str = tree.tokenSlice(ident_token);
                        if (mem.eql(u8, "_", ident_name_str)) {
                            if (non_err_is_ref != .no) return astgen.failTok(payload_token, "pointer modifier invalid on discard", .{});
                            break :scope &scratch_scope.base;
                        }
                        non_err_capture = if (non_err_is_ref != .no) .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 != .no,
        };
    }

    // 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 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);
        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| {
            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 });
                    },
                    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);
                        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 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;

        // 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);

            capture = if (capture_is_ref) .by_ref else .by_val;

            const ident_slice = tree.tokenSlice(ident);
            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 = switch_scope;
            } else {
                const capture_name = try astgen.identAsString(ident);
                try astgen.detectLocalShadowing(switch_scope, capture_name, ident, ident_slice, .capture);
                payload_capture_scope = .{
                    .parent = switch_scope,
                    .gen_zir = &scratch_scope,
                    .name = capture_name,
                    .inst = payload_capture_inst.toRef(),
                    .token_src = ident,
                    .id_cat = .capture,
                };
                dbg_var_payload_name = payload_capture_scope.name;
                dbg_var_payload_inst = payload_capture_scope.inst;
                payload_sub_scope = &payload_capture_scope.base;
            }

            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 :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", .{});
            }
            const tag_name = try astgen.identAsString(tag_token);
            try astgen.detectLocalShadowing(payload_sub_scope, tag_name, tag_token, tag_slice, .@"switch tag capture");

            assert(any_has_tag_capture);
            has_tag_capture = true;

            if (is_err_switch) {
                return astgen.failTok(tag_token, "cannot capture tag of error union", .{});
            }

            tag_capture_scope = .{
                .parent = payload_sub_scope,
                .gen_zir = &scratch_scope,
                .name = tag_name,
                .inst = tag_capture_inst.toRef(),
                .token_src = tag_token,
                .id_cat = .@"switch tag capture",
            };
            dbg_var_tag_name = tag_capture_scope.name;
            dbg_var_tag_inst = tag_capture_scope.inst;
            break :scope &tag_capture_scope.base;
        };

        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
        }

        prong_body: {
            scratch_scope.instructions_top = parent_gz.instructions.items.len;
            defer scratch_scope.unstack();

            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 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(&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 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);
            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;
            }

            // 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,
                .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) == 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", .{});
    };

    // Now that the item expressions are generated we can add this.
    try parent_gz.instructions.append(gpa, switch_block);

    // 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.

    try astgen.extra.ensureUnusedCapacity(gpa, @typeInfo(Zir.Inst.SwitchBlock).@"struct".field_names.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
        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,
            .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)[@backingInt(switch_block)].pl_node.payload_index = zir_payload_index;

    if (multi_cases_len > 0) astgen.extra.appendAssumeCapacity(multi_cases_len);
    if (payload_capture_inst_is_placeholder) astgen.extra.appendAssumeCapacity(@backingInt(payload_capture_inst));
    if (tag_capture_inst_is_placeholder) astgen.extra.appendAssumeCapacity(@backingInt(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(@backingInt(catch_or_if_src_node_offset)));
        astgen.extra.appendAssumeCapacity(@bitCast(non_err_info));
    }
    if (has_else) astgen.extra.appendAssumeCapacity(@bitCast(else_info));

    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);
    }
    if (has_else) {
        const body = payloads.items[else_prong_body_start..][0..else_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;
        }
    }

    // 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(), switch_node);
    } else {
        return switch_block.toRef();
    }
}