Zig 0.17.0-dev (Split by item)

Shortcut keys
?
Show these tips
S
Focus search field
Esc
Clear focus & close tips
U
Jump to source code
↑
Move up in search results
↓
Move down in search results
↵
Go to active search result
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 standard library. Private Function

switchExpr

Private Function
std.zig.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

Params

Errors

OutOfMemory
AnalysisFail

File

zig/AstGen.zig:7023
7023
7024
7025
7026
7027
7028
7029
7030
7031
7032
7033
7034
7035
7036
7037
7038
7039
7040
7041
7042
7043
7044
7045
7046
7047
7048
7049
7050
7051
7052
7053
7054
7055
7056
7057
7058
7059
7060
7061
7062
7063
7064
7065
7066
7067
7068
7069
7070
7071
7072
7073
7074
7075
7076
7077
7078
7079
7080
7081
7082
7083
7084
7085
7086
7087
7088
7089
7090
7091
7092
7093
7094
7095
7096
7097
7098
7099
7100
7101
7102
7103
7104
7105
7106
7107
7108
7109
7110
7111
7112
7113
7114
7115
7116
7117
7118
7119
7120
7121
7122
7123
7124
7125
7126
7127
7128
7129
7130
7131
7132
7133
7134
7135
7136
7137
7138
7139
7140
7141
7142
7143
7144
7145
7146
7147
7148
7149
7150
7151
7152
7153
7154
7155
7156
7157
7158
7159
7160
7161
7162
7163
7164
7165
7166
7167
7168
7169
7170
7171
7172
7173
7174
7175
7176
7177
7178
7179
7180
7181
7182
7183
7184
7185
7186
7187
7188
7189
7190
7191
7192
7193
7194
7195
7196
7197
7198
7199
7200
7201
7202
7203
7204
7205
7206
7207
7208
7209
7210
7211
7212
7213
7214
7215
7216
7217
7218
7219
7220
7221
7222
7223
7224
7225
7226
7227
7228
7229
7230
7231
7232
7233
7234
7235
7236
7237
7238
7239
7240
7241
7242
7243
7244
7245
7246
7247
7248
7249
7250
7251
7252
7253
7254
7255
7256
7257
7258
7259
7260
7261
7262
7263
7264
7265
7266
7267
7268
7269
7270
7271
7272
7273
7274
7275
7276
7277
7278
7279
7280
7281
7282
7283
7284
7285
7286
7287
7288
7289
7290
7291
7292
7293
7294
7295
7296
7297
7298
7299
7300
7301
7302
7303
7304
7305
7306
7307
7308
7309
7310
7311
7312
7313
7314
7315
7316
7317
7318
7319
7320
7321
7322
7323
7324
7325
7326
7327
7328
7329
7330
7331
7332
7333
7334
7335
7336
7337
7338
7339
7340
7341
7342
7343
7344
7345
7346
7347
7348
7349
7350
7351
7352
7353
7354
7355
7356
7357
7358
7359
7360
7361
7362
7363
7364
7365
7366
7367
7368
7369
7370
7371
7372
7373
7374
7375
7376
7377
7378
7379
7380
7381
7382
7383
7384
7385
7386
7387
7388
7389
7390
7391
7392
7393
7394
7395
7396
7397
7398
7399
7400
7401
7402
7403
7404
7405
7406
7407
7408
7409
7410
7411
7412
7413
7414
7415
7416
7417
7418
7419
7420
7421
7422
7423
7424
7425
7426
7427
7428
7429
7430
7431
7432
7433
7434
7435
7436
7437
7438
7439
7440
7441
7442
7443
7444
7445
7446
7447
7448
7449
7450
7451
7452
7453
7454
7455
7456
7457
7458
7459
7460
7461
7462
7463
7464
7465
7466
7467
7468
7469
7470
7471
7472
7473
7474
7475
7476
7477
7478
7479
7480
7481
7482
7483
7484
7485
7486
7487
7488
7489
7490
7491
7492
7493
7494
7495
7496
7497
7498
7499
7500
7501
7502
7503
7504
7505
7506
7507
7508
7509
7510
7511
7512
7513
7514
7515
7516
7517
7518
7519
7520
7521
7522
7523
7524
7525
7526
7527
7528
7529
7530
7531
7532
7533
7534
7535
7536
7537
7538
7539
7540
7541
7542
7543
7544
7545
7546
7547
7548
7549
7550
7551
7552
7553
7554
7555
7556
7557
7558
7559
7560
7561
7562
7563
7564
7565
7566
7567
7568
7569
7570
7571
7572
7573
7574
7575
7576
7577
7578
7579
7580
7581
7582
7583
7584
7585
7586
7587
7588
7589
7590
7591
7592
7593
7594
7595
7596
7597
7598
7599
7600
7601
7602
7603
7604
7605
7606
7607
7608
7609
7610
7611
7612
7613
7614
7615
7616
7617
7618
7619
7620
7621
7622
7623
7624
7625
7626
7627
7628
7629
7630
7631
7632
7633
7634
7635
7636
7637
7638
7639
7640
7641
7642
7643
7644
7645
7646
7647
7648
7649
7650
7651
7652
7653
7654
7655
7656
7657
7658
7659
7660
7661
7662
7663
7664
7665
7666
7667
7668
7669
7670
7671
7672
7673
7674
7675
7676
7677
7678
7679
7680
7681
7682
7683
7684
7685
7686
7687
7688
7689
7690
7691
7692
7693
7694
7695
7696
7697
7698
7699
7700
7701
7702
7703
7704
7705
7706
7707
7708
7709
7710
7711
7712
7713
7714
7715
7716
7717
7718
7719
7720
7721
7722
7723
7724
7725
7726
7727
7728
7729
7730
7731
7732
7733
7734
7735
7736
7737
7738
7739
7740
7741
7742
7743
7744
7745
7746
7747
7748
7749
7750
7751
7752
7753
7754
7755
7756
7757
7758
7759
7760
7761
7762
7763
7764
7765
7766
7767
7768
7769
7770
7771
7772
7773
7774
7775
7776
7777
7778
7779
7780
7781
7782
7783
7784
7785
7786
7787
7788
7789
7790
7791
7792
7793
7794
7795
7796
7797
7798
7799
7800
7801
7802
7803
7804
7805
7806
7807
7808
7809
7810
7811
7812
7813
7814
7815
7816
7817
7818
7819
7820
7821
7822
7823
7824
7825
7826
7827
7828
7829
7830
7831
7832
7833
7834
7835
7836
7837
7838
7839
7840
7841
7842
7843
7844
7845
7846
7847
7848
7849
7850
7851
7852
7853
7854
7855
7856
7857
7858
7859
7860
7861
7862
7863
7864
7865
7866
7867
7868
7869
7870
7871
7872
7873
7874
7875
7876
7877
7878
7879
7880
7881
7882
7883
7884
7885
7886
7887
7888
7889
7890
7891
7892
7893
7894
7895
7896
7897
7898
7899
7900
7901
7902
7903
7904
7905
7906
7907
7908
7909
7910
7911
7912
7913
7914
7915
7916
7917
7918
7919
7920
7921
7922
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();
    }
}