Commit 5e12ca9fe3

mlugg <mlugg@mlugg.co.uk>
2024-04-28 22:44:57
compiler: implement labeled switch/continue
1 parent 5fb4a7d
lib/std/zig/Ast.zig
@@ -1184,14 +1184,7 @@ pub fn lastToken(tree: Ast, node: Node.Index) TokenIndex {
             n = extra.sentinel;
         },
 
-        .@"continue" => {
-            if (datas[n].lhs != 0) {
-                return datas[n].lhs + end_offset;
-            } else {
-                return main_tokens[n] + end_offset;
-            }
-        },
-        .@"break" => {
+        .@"continue", .@"break" => {
             if (datas[n].rhs != 0) {
                 n = datas[n].rhs;
             } else if (datas[n].lhs != 0) {
@@ -1895,6 +1888,15 @@ pub fn taggedUnionEnumTag(tree: Ast, node: Node.Index) full.ContainerDecl {
     });
 }
 
+pub fn switchFull(tree: Ast, node: Node.Index) full.Switch {
+    const data = &tree.nodes.items(.data)[node];
+    return tree.fullSwitchComponents(.{
+        .switch_token = tree.nodes.items(.main_token)[node],
+        .condition = data.lhs,
+        .sub_range = data.rhs,
+    });
+}
+
 pub fn switchCaseOne(tree: Ast, node: Node.Index) full.SwitchCase {
     const data = &tree.nodes.items(.data)[node];
     const values: *[1]Node.Index = &data.lhs;
@@ -2206,6 +2208,21 @@ fn fullContainerDeclComponents(tree: Ast, info: full.ContainerDecl.Components) f
     return result;
 }
 
+fn fullSwitchComponents(tree: Ast, info: full.Switch.Components) full.Switch {
+    const token_tags = tree.tokens.items(.tag);
+    const tok_i = info.switch_token -| 1;
+    var result: full.Switch = .{
+        .ast = info,
+        .label_token = null,
+    };
+    if (token_tags[tok_i] == .colon and
+        token_tags[tok_i -| 1] == .identifier)
+    {
+        result.label_token = tok_i - 1;
+    }
+    return result;
+}
+
 fn fullSwitchCaseComponents(tree: Ast, info: full.SwitchCase.Components, node: Node.Index) full.SwitchCase {
     const token_tags = tree.tokens.items(.tag);
     const node_tags = tree.nodes.items(.tag);
@@ -2477,6 +2494,13 @@ pub fn fullContainerDecl(tree: Ast, buffer: *[2]Ast.Node.Index, node: Node.Index
     };
 }
 
+pub fn fullSwitch(tree: Ast, node: Node.Index) ?full.Switch {
+    return switch (tree.nodes.items(.tag)[node]) {
+        .@"switch", .switch_comma => tree.switchFull(node),
+        else => null,
+    };
+}
+
 pub fn fullSwitchCase(tree: Ast, node: Node.Index) ?full.SwitchCase {
     return switch (tree.nodes.items(.tag)[node]) {
         .switch_case_one, .switch_case_inline_one => tree.switchCaseOne(node),
@@ -2829,6 +2853,17 @@ pub const full = struct {
         };
     };
 
+    pub const Switch = struct {
+        ast: Components,
+        label_token: ?TokenIndex,
+
+        pub const Components = struct {
+            switch_token: TokenIndex,
+            condition: Node.Index,
+            sub_range: Node.Index,
+        };
+    };
+
     pub const SwitchCase = struct {
         inline_token: ?TokenIndex,
         /// Points to the first token after the `|`. Will either be an identifier or
@@ -3287,7 +3322,8 @@ pub const Node = struct {
         @"suspend",
         /// `resume lhs`. rhs is unused.
         @"resume",
-        /// `continue`. lhs is token index of label if any. rhs is unused.
+        /// `continue :lhs rhs`
+        /// both lhs and rhs may be omitted.
         @"continue",
         /// `break :lhs rhs`
         /// both lhs and rhs may be omitted.
lib/std/zig/AstGen.zig
@@ -1144,7 +1144,7 @@ fn expr(gz: *GenZir, scope: *Scope, ri: ResultInfo, node: Ast.Node.Index) InnerE
         .error_set_decl => return errorSetDecl(gz, ri, node),
         .array_access => return arrayAccess(gz, scope, ri, node),
         .@"comptime" => return comptimeExprAst(gz, scope, ri, node),
-        .@"switch", .switch_comma => return switchExpr(gz, scope, ri.br(), node),
+        .@"switch", .switch_comma => return switchExpr(gz, scope, ri.br(), node, tree.fullSwitch(node).?),
 
         .@"nosuspend" => return nosuspendExpr(gz, scope, ri, node),
         .@"suspend" => return suspendExpr(gz, scope, node),
@@ -2160,6 +2160,11 @@ fn breakExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index) Inn
                     if (break_label != 0) {
                         if (block_gz.label) |*label| {
                             if (try astgen.tokenIdentEql(label.token, break_label)) {
+                                const maybe_switch_tag = astgen.instructions.items(.tag)[@intFromEnum(label.block_inst)];
+                                switch (maybe_switch_tag) {
+                                    .switch_block, .switch_block_ref => return astgen.failNode(node, "cannot break from switch", .{}),
+                                    else => {},
+                                }
                                 label.used = true;
                                 break :blk label.block_inst;
                             }
@@ -2234,6 +2239,11 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index)
     const tree = astgen.tree;
     const node_datas = tree.nodes.items(.data);
     const break_label = node_datas[node].lhs;
+    const rhs = node_datas[node].rhs;
+
+    if (break_label == 0 and rhs != 0) {
+        return astgen.failNode(node, "cannot continue with operand without label", .{});
+    }
 
     // Look for the label in the scope.
     var scope = parent_scope;
@@ -2258,6 +2268,15 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index)
                 if (break_label != 0) blk: {
                     if (gen_zir.label) |*label| {
                         if (try astgen.tokenIdentEql(label.token, break_label)) {
+                            const maybe_switch_tag = astgen.instructions.items(.tag)[@intFromEnum(label.block_inst)];
+                            if (rhs != 0) switch (maybe_switch_tag) {
+                                .switch_block, .switch_block_ref => {},
+                                else => return astgen.failNode(node, "cannot continue loop with operand", .{}),
+                            } else switch (maybe_switch_tag) {
+                                .switch_block, .switch_block_ref => return astgen.failNode(node, "cannot continue switch without operand", .{}),
+                                else => {},
+                            }
+
                             label.used = true;
                             break :blk;
                         }
@@ -2265,8 +2284,35 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index)
                     // found continue but either it has a different label, or no label
                     scope = gen_zir.parent;
                     continue;
+                } else if (gen_zir.label) |label| {
+                    // This `continue` is unlabeled. If the gz we've found corresponds to a labeled
+                    // `switch`, ignore it and continue to parent scopes.
+                    switch (astgen.instructions.items(.tag)[@intFromEnum(label.block_inst)]) {
+                        .switch_block, .switch_block_ref => {
+                            scope = gen_zir.parent;
+                            continue;
+                        },
+                        else => {},
+                    }
+                }
+
+                if (rhs != 0) {
+                    // We need to figure out the result info to use.
+                    // The type should match
+                    const operand = try reachableExpr(parent_gz, parent_scope, gen_zir.continue_result_info, rhs, node);
+
+                    try genDefers(parent_gz, scope, parent_scope, .normal_only);
+
+                    // As our last action before the continue, "pop" the error trace if needed
+                    if (!gen_zir.is_comptime)
+                        _ = try parent_gz.addRestoreErrRetIndex(.{ .block = continue_block }, .always, node);
+
+                    _ = try parent_gz.addBreakWithSrcNode(.switch_continue, continue_block, operand, rhs);
+                    return Zir.Inst.Ref.unreachable_value;
                 }
 
+                try genDefers(parent_gz, scope, parent_scope, .normal_only);
+
                 const break_tag: Zir.Inst.Tag = if (gen_zir.is_inline)
                     .break_inline
                 else
@@ -2284,12 +2330,7 @@ fn continueExpr(parent_gz: *GenZir, parent_scope: *Scope, node: Ast.Node.Index)
             },
             .local_val => scope = scope.cast(Scope.LocalVal).?.parent,
             .local_ptr => scope = scope.cast(Scope.LocalPtr).?.parent,
-            .defer_normal => {
-                const defer_scope = scope.cast(Scope.Defer).?;
-                scope = defer_scope.parent;
-                try parent_gz.addDefer(defer_scope.index, defer_scope.len);
-            },
-            .defer_error => scope = scope.cast(Scope.Defer).?.parent,
+            .defer_normal, .defer_error => scope = scope.cast(Scope.Defer).?.parent,
             .namespace => break,
             .top => unreachable,
         }
@@ -2881,6 +2922,7 @@ fn addEnsureResult(gz: *GenZir, maybe_unused_result: Zir.Inst.Ref, statement: As
             .panic,
             .trap,
             .check_comptime_control_flow,
+            .switch_continue,
             => {
                 noreturn_src_node = statement;
                 break :b true;
@@ -7546,7 +7588,8 @@ fn switchExpr(
     parent_gz: *GenZir,
     scope: *Scope,
     ri: ResultInfo,
-    switch_node: Ast.Node.Index,
+    node: Ast.Node.Index,
+    switch_full: Ast.full.Switch,
 ) InnerError!Zir.Inst.Ref {
     const astgen = parent_gz.astgen;
     const gpa = astgen.gpa;
@@ -7555,14 +7598,14 @@ fn switchExpr(
     const node_tags = tree.nodes.items(.tag);
     const main_tokens = tree.nodes.items(.main_token);
     const token_tags = tree.tokens.items(.tag);
-    const operand_node = node_datas[switch_node].lhs;
-    const extra = tree.extraData(node_datas[switch_node].rhs, Ast.Node.SubRange);
+    const operand_node = node_datas[node].lhs;
+    const extra = tree.extraData(node_datas[node].rhs, Ast.Node.SubRange);
     const case_nodes = tree.extra_data[extra.start..extra.end];
 
-    const need_rl = astgen.nodes_need_rl.contains(switch_node);
+    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, switch_node)).? },
+            .ptr => .{ .ty = (try ri.rl.resultType(parent_gz, node)).? },
             .inferred_ptr => .none,
             else => ri.rl,
         },
@@ -7573,11 +7616,16 @@ fn switchExpr(
     const LocTag = @typeInfo(ResultInfo.Loc).@"union".tag_type.?;
     const need_result_rvalue = @as(LocTag, block_ri.rl) != @as(LocTag, ri.rl);
 
+    if (switch_full.label_token) |label_token| {
+        try astgen.checkLabelRedefinition(scope, label_token);
+    }
+
     // We perform two passes over the AST. This first pass is to collect information
     // for the following variables, make note of the special prong AST node index,
     // and bail out with a compile error if there are multiple special prongs present.
     var any_payload_is_ref = false;
     var any_has_tag_capture = false;
+    var any_non_inline_capture = false;
     var scalar_cases_len: u32 = 0;
     var multi_cases_len: u32 = 0;
     var inline_cases_len: u32 = 0;
@@ -7595,6 +7643,15 @@ fn switchExpr(
             if (token_tags[ident + 1] == .comma) {
                 any_has_tag_capture = true;
             }
+
+            // If the first capture is ignored, then there is no runtime-known
+            // capture, as the tag capture must be for an inline prong.
+            // This check isn't perfect, because for things like enums, the
+            // first prong *is* comptime-known for inline prongs! But such
+            // knowledge requires semantic analysis.
+            if (!mem.eql(u8, tree.tokenSlice(ident), "_")) {
+                any_non_inline_capture = true;
+            }
         }
         // Check for else/`_` prong.
         if (case.ast.values.len == 0) {
@@ -7614,7 +7671,7 @@ fn switchExpr(
                 );
             } else if (underscore_src) |some_underscore| {
                 return astgen.failNodeNotes(
-                    switch_node,
+                    node,
                     "else and '_' prong in switch expression",
                     .{},
                     &[_]u32{
@@ -7655,7 +7712,7 @@ fn switchExpr(
                 );
             } else if (else_src) |some_else| {
                 return astgen.failNodeNotes(
-                    switch_node,
+                    node,
                     "else and '_' prong in switch expression",
                     .{},
                     &[_]u32{
@@ -7704,6 +7761,12 @@ fn switchExpr(
     const raw_operand = try expr(parent_gz, scope, operand_ri, operand_node);
     const item_ri: ResultInfo = .{ .rl = .none };
 
+    // If this switch is labeled, it will have `continue`s targeting it, and thus we need the operand type
+    // to provide a result type.
+    const raw_operand_ty_ref = if (switch_full.label_token != null) t: {
+        break :t try parent_gz.addUnNode(.typeof, raw_operand, operand_node);
+    } else undefined;
+
     // This contains the data that goes into the `extra` array for the SwitchBlock/SwitchBlockMulti,
     // except the first cases_nodes.len slots are a table that indexes payloads later in the array, with
     // the special case index coming first, then scalar_case_len indexes, then multi_cases_len indexes
@@ -7725,7 +7788,22 @@ fn switchExpr(
     try emitDbgStmtForceCurrentIndex(parent_gz, operand_lc);
     // This gets added to the parent block later, after the item expressions.
     const switch_tag: Zir.Inst.Tag = if (any_payload_is_ref) .switch_block_ref else .switch_block;
-    const switch_block = try parent_gz.makeBlockInst(switch_tag, switch_node);
+    const switch_block = try parent_gz.makeBlockInst(switch_tag, node);
+
+    if (switch_full.label_token) |label_token| {
+        block_scope.continue_block = switch_block.toOptional();
+        block_scope.continue_result_info = .{
+            .rl = if (any_payload_is_ref)
+                .{ .ref_coerced_ty = raw_operand_ty_ref }
+            else
+                .{ .coerced_ty = raw_operand_ty_ref },
+        };
+
+        block_scope.label = .{
+            .token = label_token,
+            .block_inst = switch_block,
+        };
+    }
 
     // We re-use this same scope for all cases, including the special prong, if any.
     var case_scope = parent_gz.makeSubBlock(&block_scope.base);
@@ -7946,6 +8024,8 @@ fn switchExpr(
             .has_else = special_prong == .@"else",
             .has_under = special_prong == .under,
             .any_has_tag_capture = any_has_tag_capture,
+            .any_non_inline_capture = any_non_inline_capture,
+            .has_continue = switch_full.label_token != null,
             .scalar_cases_len = @intCast(scalar_cases_len),
         },
     });
@@ -7982,7 +8062,7 @@ fn switchExpr(
     }
 
     if (need_result_rvalue) {
-        return rvalue(parent_gz, ri, switch_block.toRef(), switch_node);
+        return rvalue(parent_gz, ri, switch_block.toRef(), node);
     } else {
         return switch_block.toRef();
     }
@@ -11824,6 +11904,7 @@ const GenZir = struct {
     continue_block: Zir.Inst.OptionalIndex = .none,
     /// Only valid when setBreakResultInfo is called.
     break_result_info: AstGen.ResultInfo = undefined,
+    continue_result_info: AstGen.ResultInfo = undefined,
 
     suspend_node: Ast.Node.Index = 0,
     nosuspend_node: Ast.Node.Index = 0,
lib/std/zig/Parse.zig
@@ -924,7 +924,6 @@ fn expectContainerField(p: *Parse) !Node.Index {
 ///      / KEYWORD_errdefer Payload? BlockExprStatement
 ///      / IfStatement
 ///      / LabeledStatement
-///      / SwitchExpr
 ///      / VarDeclExprStatement
 fn expectStatement(p: *Parse, allow_defer_var: bool) Error!Node.Index {
     if (p.eatToken(.keyword_comptime)) |comptime_token| {
@@ -995,7 +994,6 @@ fn expectStatement(p: *Parse, allow_defer_var: bool) Error!Node.Index {
                 .rhs = try p.expectBlockExprStatement(),
             },
         }),
-        .keyword_switch => return p.expectSwitchExpr(),
         .keyword_if => return p.expectIfStatement(),
         .keyword_enum, .keyword_struct, .keyword_union => {
             const identifier = p.tok_i + 1;
@@ -1238,7 +1236,7 @@ fn expectIfStatement(p: *Parse) !Node.Index {
     });
 }
 
-/// LabeledStatement <- BlockLabel? (Block / LoopStatement)
+/// LabeledStatement <- BlockLabel? (Block / LoopStatement / SwitchExpr)
 fn parseLabeledStatement(p: *Parse) !Node.Index {
     const label_token = p.parseBlockLabel();
     const block = try p.parseBlock();
@@ -1247,6 +1245,9 @@ fn parseLabeledStatement(p: *Parse) !Node.Index {
     const loop_stmt = try p.parseLoopStatement();
     if (loop_stmt != 0) return loop_stmt;
 
+    const switch_expr = try p.parseSwitchExpr();
+    if (switch_expr != 0) return switch_expr;
+
     if (label_token != 0) {
         const after_colon = p.tok_i;
         const node = try p.parseTypeExpr();
@@ -2072,7 +2073,7 @@ fn expectTypeExpr(p: *Parse) Error!Node.Index {
 ///      / KEYWORD_break BreakLabel? Expr?
 ///      / KEYWORD_comptime Expr
 ///      / KEYWORD_nosuspend Expr
-///      / KEYWORD_continue BreakLabel?
+///      / KEYWORD_continue BreakLabel? Expr?
 ///      / KEYWORD_resume Expr
 ///      / KEYWORD_return Expr?
 ///      / BlockLabel? LoopExpr
@@ -2098,7 +2099,7 @@ fn parsePrimaryExpr(p: *Parse) !Node.Index {
                 .main_token = p.nextToken(),
                 .data = .{
                     .lhs = try p.parseBreakLabel(),
-                    .rhs = undefined,
+                    .rhs = try p.parseExpr(),
                 },
             });
         },
@@ -2627,7 +2628,6 @@ fn parseSuffixExpr(p: *Parse) !Node.Index {
 ///      / KEYWORD_anyframe
 ///      / KEYWORD_unreachable
 ///      / STRINGLITERAL
-///      / SwitchExpr
 ///
 /// ContainerDecl <- (KEYWORD_extern / KEYWORD_packed)? ContainerDeclAuto
 ///
@@ -2647,6 +2647,7 @@ fn parseSuffixExpr(p: *Parse) !Node.Index {
 /// LabeledTypeExpr
 ///     <- BlockLabel Block
 ///      / BlockLabel? LoopTypeExpr
+///      / BlockLabel? SwitchExpr
 ///
 /// LoopTypeExpr <- KEYWORD_inline? (ForTypeExpr / WhileTypeExpr)
 fn parsePrimaryTypeExpr(p: *Parse) !Node.Index {
@@ -2753,6 +2754,10 @@ fn parsePrimaryTypeExpr(p: *Parse) !Node.Index {
                     p.tok_i += 2;
                     return p.parseWhileTypeExpr();
                 },
+                .keyword_switch => {
+                    p.tok_i += 2;
+                    return p.expectSwitchExpr();
+                },
                 .l_brace => {
                     p.tok_i += 2;
                     return p.parseBlock();
@@ -3029,8 +3034,17 @@ fn parseWhileTypeExpr(p: *Parse) !Node.Index {
 }
 
 /// SwitchExpr <- KEYWORD_switch LPAREN Expr RPAREN LBRACE SwitchProngList RBRACE
+fn parseSwitchExpr(p: *Parse) !Node.Index {
+    const switch_token = p.eatToken(.keyword_switch) orelse return null_node;
+    return p.expectSwitchSuffix(switch_token);
+}
+
 fn expectSwitchExpr(p: *Parse) !Node.Index {
     const switch_token = p.assertToken(.keyword_switch);
+    return p.expectSwitchSuffix(switch_token);
+}
+
+fn expectSwitchSuffix(p: *Parse, switch_token: TokenIndex) !Node.Index {
     _ = try p.expectToken(.l_paren);
     const expr_node = try p.expectExpr();
     _ = try p.expectToken(.r_paren);
lib/std/zig/Zir.zig
@@ -314,6 +314,9 @@ pub const Inst = struct {
         /// break instruction in a block, and the target block is the parent.
         /// Uses the `break` union field.
         break_inline,
+        /// Branch from within a switch case to the case specified by the operand.
+        /// Uses the `break` union field. `block_inst` refers to a `switch_block` or `switch_block_ref`.
+        switch_continue,
         /// Checks that comptime control flow does not happen inside a runtime block.
         /// Uses the `un_node` union field.
         check_comptime_control_flow,
@@ -1273,6 +1276,7 @@ pub const Inst = struct {
                 .panic,
                 .trap,
                 .check_comptime_control_flow,
+                .switch_continue,
                 => true,
             };
         }
@@ -1512,6 +1516,7 @@ pub const Inst = struct {
                 .break_inline,
                 .condbr,
                 .condbr_inline,
+                .switch_continue,
                 .compile_error,
                 .ret_node,
                 .ret_load,
@@ -1597,6 +1602,7 @@ pub const Inst = struct {
                 .bool_br_or = .pl_node,
                 .@"break" = .@"break",
                 .break_inline = .@"break",
+                .switch_continue = .@"break",
                 .check_comptime_control_flow = .un_node,
                 .for_len = .pl_node,
                 .call = .pl_node,
@@ -2288,6 +2294,7 @@ pub const Inst = struct {
         },
         @"break": struct {
             operand: Ref,
+            /// Index of a `Break` payload.
             payload_index: u32,
         },
         dbg_stmt: LineColumn,
@@ -2945,9 +2952,13 @@ pub const Inst = struct {
             has_under: bool,
             /// If true, at least one prong has an inline tag capture.
             any_has_tag_capture: bool,
+            /// If true, at least one prong has a capture which may not
+            /// be comptime-known via `inline`.
+            any_non_inline_capture: bool,
+            has_continue: bool,
             scalar_cases_len: ScalarCasesLen,
 
-            pub const ScalarCasesLen = u28;
+            pub const ScalarCasesLen = u26;
 
             pub fn specialProng(bits: Bits) SpecialProng {
                 const has_else: u2 = @intFromBool(bits.has_else);
@@ -3750,6 +3761,7 @@ fn findDeclsInner(
         .bool_br_or,
         .@"break",
         .break_inline,
+        .switch_continue,
         .check_comptime_control_flow,
         .builtin_call,
         .cmp_lt,
src/Air/types_resolved.zig
@@ -222,7 +222,7 @@ fn checkBody(air: Air, body: []const Air.Inst.Index, zcu: *Zcu) bool {
                 if (!checkRef(data.un_op, zcu)) return false;
             },
 
-            .br => {
+            .br, .switch_dispatch => {
                 if (!checkRef(data.br.operand, zcu)) return false;
             },
 
@@ -380,7 +380,7 @@ fn checkBody(air: Air, body: []const Air.Inst.Index, zcu: *Zcu) bool {
                 )) return false;
             },
 
-            .switch_br => {
+            .switch_br, .loop_switch_br => {
                 const switch_br = air.unwrapSwitch(inst);
                 if (!checkRef(switch_br.operand, zcu)) return false;
                 var it = switch_br.iterateCases();
src/arch/aarch64/CodeGen.zig
@@ -735,6 +735,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .block           => try self.airBlock(inst),
             .br              => try self.airBr(inst),
             .repeat          => return self.fail("TODO implement `repeat`", .{}),
+            .switch_dispatch => return self.fail("TODO implement `switch_dispatch`", .{}),
             .trap            => try self.airTrap(),
             .breakpoint      => try self.airBreakpoint(),
             .ret_addr        => try self.airRetAddr(inst),
@@ -825,6 +826,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .field_parent_ptr => try self.airFieldParentPtr(inst),
 
             .switch_br       => try self.airSwitch(inst),
+            .loop_switch_br  => return self.fail("TODO implement `loop_switch_br`", .{}),
             .slice_ptr       => try self.airSlicePtr(inst),
             .slice_len       => try self.airSliceLen(inst),
 
src/arch/arm/CodeGen.zig
@@ -722,6 +722,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .block           => try self.airBlock(inst),
             .br              => try self.airBr(inst),
             .repeat          => return self.fail("TODO implement `repeat`", .{}),
+            .switch_dispatch => return self.fail("TODO implement `switch_dispatch`", .{}),
             .trap            => try self.airTrap(),
             .breakpoint      => try self.airBreakpoint(),
             .ret_addr        => try self.airRetAddr(inst),
@@ -812,6 +813,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .field_parent_ptr => try self.airFieldParentPtr(inst),
 
             .switch_br       => try self.airSwitch(inst),
+            .loop_switch_br  => return self.fail("TODO implement `loop_switch_br`", .{}),
             .slice_ptr       => try self.airSlicePtr(inst),
             .slice_len       => try self.airSliceLen(inst),
 
src/arch/riscv64/CodeGen.zig
@@ -1580,6 +1580,7 @@ fn genBody(func: *Func, body: []const Air.Inst.Index) InnerError!void {
             .block           => try func.airBlock(inst),
             .br              => try func.airBr(inst),
             .repeat          => return func.fail("TODO implement `repeat`", .{}),
+            .switch_dispatch => return func.fail("TODO implement `switch_dispatch`", .{}),
             .trap            => try func.airTrap(),
             .breakpoint      => try func.airBreakpoint(),
             .ret_addr        => try func.airRetAddr(inst),
@@ -1669,6 +1670,7 @@ fn genBody(func: *Func, body: []const Air.Inst.Index) InnerError!void {
             .field_parent_ptr => try func.airFieldParentPtr(inst),
 
             .switch_br       => try func.airSwitchBr(inst),
+            .loop_switch_br  => return func.fail("TODO implement `loop_switch_br`", .{}),
 
             .ptr_slice_len_ptr => try func.airPtrSliceLenPtr(inst),
             .ptr_slice_ptr_ptr => try func.airPtrSlicePtrPtr(inst),
src/arch/sparc64/CodeGen.zig
@@ -577,6 +577,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .block           => try self.airBlock(inst),
             .br              => try self.airBr(inst),
             .repeat          => return self.fail("TODO implement `repeat`", .{}),
+            .switch_dispatch => return self.fail("TODO implement `switch_dispatch`", .{}),
             .trap            => try self.airTrap(),
             .breakpoint      => try self.airBreakpoint(),
             .ret_addr        => @panic("TODO try self.airRetAddr(inst)"),
@@ -667,6 +668,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .field_parent_ptr => @panic("TODO try self.airFieldParentPtr(inst)"),
 
             .switch_br       => try self.airSwitch(inst),
+            .loop_switch_br  => return self.fail("TODO implement `loop_switch_br`", .{}),
             .slice_ptr       => try self.airSlicePtr(inst),
             .slice_len       => try self.airSliceLen(inst),
 
src/arch/wasm/CodeGen.zig
@@ -1904,6 +1904,7 @@ fn genInst(func: *CodeGen, inst: Air.Inst.Index) InnerError!void {
         .breakpoint => func.airBreakpoint(inst),
         .br => func.airBr(inst),
         .repeat => return func.fail("TODO implement `repeat`", .{}),
+        .switch_dispatch => return func.fail("TODO implement `switch_dispatch`", .{}),
         .int_from_bool => func.airIntFromBool(inst),
         .cond_br => func.airCondBr(inst),
         .intcast => func.airIntcast(inst),
@@ -1985,6 +1986,7 @@ fn genInst(func: *CodeGen, inst: Air.Inst.Index) InnerError!void {
         .field_parent_ptr => func.airFieldParentPtr(inst),
 
         .switch_br => func.airSwitchBr(inst),
+        .loop_switch_br => return func.fail("TODO implement `loop_switch_br`", .{}),
         .trunc => func.airTrunc(inst),
         .unreach => func.airUnreachable(inst),
 
src/arch/x86_64/CodeGen.zig
@@ -2248,6 +2248,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .block           => try self.airBlock(inst),
             .br              => try self.airBr(inst),
             .repeat          => return self.fail("TODO implement `repeat`", .{}),
+            .switch_dispatch => return self.fail("TODO implement `switch_dispatch`", .{}),
             .trap            => try self.airTrap(),
             .breakpoint      => try self.airBreakpoint(),
             .ret_addr        => try self.airRetAddr(inst),
@@ -2336,6 +2337,7 @@ fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void {
             .field_parent_ptr => try self.airFieldParentPtr(inst),
 
             .switch_br       => try self.airSwitchBr(inst),
+            .loop_switch_br  => return self.fail("TODO implement `loop_switch_br`", .{}),
             .slice_ptr       => try self.airSlicePtr(inst),
             .slice_len       => try self.airSliceLen(inst),
 
src/codegen/c.zig
@@ -321,6 +321,9 @@ pub const Function = struct {
     /// by type alignment.
     /// The value is whether the alloc needs to be emitted in the header.
     allocs: std.AutoArrayHashMapUnmanaged(LocalIndex, bool) = .{},
+    /// Maps from `loop_switch_br` instructions to the allocated local used
+    /// for the switch cond. Dispatches should set this local to the new cond.
+    loop_switch_conds: std.AutoHashMapUnmanaged(Air.Inst.Index, LocalIndex) = .{},
 
     fn resolveInst(f: *Function, ref: Air.Inst.Ref) !CValue {
         const gop = try f.value_map.getOrPut(ref);
@@ -531,6 +534,7 @@ pub const Function = struct {
         f.blocks.deinit(gpa);
         f.value_map.deinit();
         f.lazy_fns.deinit(gpa);
+        f.loop_switch_conds.deinit(gpa);
     }
 
     fn typeOf(f: *Function, inst: Air.Inst.Ref) Type {
@@ -3376,16 +3380,18 @@ fn genBodyInner(f: *Function, body: []const Air.Inst.Index) error{ AnalysisFail,
             => unreachable,
 
             // Instructions that are known to always be `noreturn` based on their tag.
-            .br         => return airBr(f, inst),
-            .repeat     => return airRepeat(f, inst),
-            .cond_br    => return airCondBr(f, inst),
-            .switch_br  => return airSwitchBr(f, inst),
-            .loop       => return airLoop(f, inst),
-            .ret        => return airRet(f, inst, false),
-            .ret_safe   => return airRet(f, inst, false), // TODO
-            .ret_load   => return airRet(f, inst, true),
-            .trap       => return airTrap(f, f.object.writer()),
-            .unreach    => return airUnreach(f),
+            .br              => return airBr(f, inst),
+            .repeat          => return airRepeat(f, inst),
+            .switch_dispatch => return airSwitchDispatch(f, inst),
+            .cond_br         => return airCondBr(f, inst),
+            .switch_br       => return airSwitchBr(f, inst, false),
+            .loop_switch_br  => return airSwitchBr(f, inst, true),
+            .loop            => return airLoop(f, inst),
+            .ret             => return airRet(f, inst, false),
+            .ret_safe        => return airRet(f, inst, false), // TODO
+            .ret_load        => return airRet(f, inst, true),
+            .trap            => return airTrap(f, f.object.writer()),
+            .unreach         => return airUnreach(f),
 
             // Instructions which may be `noreturn`.
             .block => res: {
@@ -4786,6 +4792,46 @@ fn airRepeat(f: *Function, inst: Air.Inst.Index) !void {
     try writer.print("goto zig_loop_{d};\n", .{@intFromEnum(repeat.loop_inst)});
 }
 
+fn airSwitchDispatch(f: *Function, inst: Air.Inst.Index) !void {
+    const pt = f.object.dg.pt;
+    const zcu = pt.zcu;
+    const br = f.air.instructions.items(.data)[@intFromEnum(inst)].br;
+    const writer = f.object.writer();
+
+    if (try f.air.value(br.operand, pt)) |cond_val| {
+        // Comptime-known dispatch. Iterate the cases to find the correct
+        // one, and branch directly to the corresponding case.
+        const switch_br = f.air.unwrapSwitch(br.block_inst);
+        var it = switch_br.iterateCases();
+        const target_case_idx: u32 = target: while (it.next()) |case| {
+            for (case.items) |item| {
+                const val = Value.fromInterned(item.toInterned().?);
+                if (cond_val.compareHetero(.eq, val, zcu)) break :target case.idx;
+            }
+            for (case.ranges) |range| {
+                const low = Value.fromInterned(range[0].toInterned().?);
+                const high = Value.fromInterned(range[1].toInterned().?);
+                if (cond_val.compareHetero(.gte, low, zcu) and
+                    cond_val.compareHetero(.lte, high, zcu))
+                {
+                    break :target case.idx;
+                }
+            }
+        } else switch_br.cases_len;
+        try writer.print("goto zig_switch_{d}_dispatch_{d};\n", .{ @intFromEnum(br.block_inst), target_case_idx });
+        return;
+    }
+
+    // Runtime-known dispatch. Set the switch condition, and branch back.
+    const cond = try f.resolveInst(br.operand);
+    const cond_local = f.loop_switch_conds.get(br.block_inst).?;
+    try f.writeCValue(writer, .{ .local = cond_local }, .Other);
+    try writer.writeAll(" = ");
+    try f.writeCValue(writer, cond, .Initializer);
+    try writer.writeAll(";\n");
+    try writer.print("goto zig_switch_{d}_loop;", .{@intFromEnum(br.block_inst)});
+}
+
 fn airBitcast(f: *Function, inst: Air.Inst.Index) !CValue {
     const ty_op = f.air.instructions.items(.data)[@intFromEnum(inst)].ty_op;
     const inst_ty = f.typeOfIndex(inst);
@@ -5004,15 +5050,34 @@ fn airCondBr(f: *Function, inst: Air.Inst.Index) !void {
     try genBodyInner(f, else_body);
 }
 
-fn airSwitchBr(f: *Function, inst: Air.Inst.Index) !void {
+fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void {
     const pt = f.object.dg.pt;
     const zcu = pt.zcu;
+    const gpa = f.object.dg.gpa;
     const switch_br = f.air.unwrapSwitch(inst);
-    const condition = try f.resolveInst(switch_br.operand);
+    const init_condition = try f.resolveInst(switch_br.operand);
     try reap(f, inst, &.{switch_br.operand});
     const condition_ty = f.typeOf(switch_br.operand);
     const writer = f.object.writer();
 
+    // For dispatches, we will create a local alloc to contain the condition value.
+    // This may not result in optimal codegen for switch loops, but it minimizes the
+    // amount of C code we generate, which is probably more desirable here (and is simpler).
+    const condition = if (is_dispatch_loop) cond: {
+        const new_local = try f.allocLocal(inst, condition_ty);
+        try f.writeCValue(writer, new_local, .Other);
+        try writer.writeAll(" = ");
+        try f.writeCValue(writer, init_condition, .Initializer);
+        try writer.writeAll(";\n");
+        try writer.print("zig_switch_{d}_loop:", .{@intFromEnum(inst)});
+        try f.loop_switch_conds.put(gpa, inst, new_local.new_local);
+        break :cond new_local;
+    } else init_condition;
+
+    defer if (is_dispatch_loop) {
+        assert(f.loop_switch_conds.remove(inst));
+    };
+
     try writer.writeAll("switch (");
 
     const lowered_condition_ty = if (condition_ty.toIntern() == .bool_type)
@@ -5030,7 +5095,6 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index) !void {
     try writer.writeAll(") {");
     f.object.indent_writer.pushIndent();
 
-    const gpa = f.object.dg.gpa;
     const liveness = try f.liveness.getSwitchBr(gpa, inst, switch_br.cases_len + 1);
     defer gpa.free(liveness.deaths);
 
@@ -5045,9 +5109,15 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index) !void {
             try f.object.indent_writer.insertNewline();
             try writer.writeAll("case ");
             const item_value = try f.air.value(item, pt);
-            if (item_value.?.getUnsignedInt(zcu)) |item_int| try writer.print("{}\n", .{
-                try f.fmtIntLiteral(try pt.intValue(lowered_condition_ty, item_int)),
-            }) else {
+            // If `item_value` is a pointer with a known integer address, print the address
+            // with no cast to avoid a warning.
+            write_val: {
+                if (condition_ty.isPtrAtRuntime(zcu)) {
+                    if (item_value.?.getUnsignedInt(zcu)) |item_int| {
+                        try writer.print("{}", .{try f.fmtIntLiteral(try pt.intValue(lowered_condition_ty, item_int))});
+                        break :write_val;
+                    }
+                }
                 if (condition_ty.isPtrAtRuntime(zcu)) {
                     try writer.writeByte('(');
                     try f.renderType(writer, Type.usize);
@@ -5057,9 +5127,14 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index) !void {
             }
             try writer.writeByte(':');
         }
-        try writer.writeByte(' ');
-
-        try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, false);
+        try writer.writeAll(" {\n");
+        f.object.indent_writer.pushIndent();
+        if (is_dispatch_loop) {
+            try writer.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), case.idx });
+        }
+        try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, true);
+        f.object.indent_writer.popIndent();
+        try writer.writeByte('}');
 
         // The case body must be noreturn so we don't need to insert a break.
     }
@@ -5095,11 +5170,19 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index) !void {
                 try f.object.dg.renderValue(writer, (try f.air.value(range[1], pt)).?, .Other);
                 try writer.writeByte(')');
             }
-            try writer.writeAll(") ");
-            try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, false);
+            try writer.writeAll(") {\n");
+            f.object.indent_writer.pushIndent();
+            if (is_dispatch_loop) {
+                try writer.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), case.idx });
+            }
+            try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, true);
+            f.object.indent_writer.popIndent();
+            try writer.writeByte('}');
         }
     }
-
+    if (is_dispatch_loop) {
+        try writer.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), switch_br.cases_len });
+    }
     if (else_body.len > 0) {
         // Note that this must be the last case, so we do not need to use `genBodyResolveState` since
         // the parent block will do it (because the case body is noreturn).
src/codegen/llvm.zig
@@ -1721,6 +1721,7 @@ pub const Object = struct {
             .func_inst_table = .{},
             .blocks = .{},
             .loops = .{},
+            .switch_dispatch_info = .{},
             .sync_scope = if (owner_mod.single_threaded) .singlethread else .system,
             .file = file,
             .scope = subprogram,
@@ -4845,6 +4846,10 @@ pub const FuncGen = struct {
     /// Maps `loop` instructions to the bb to branch to to repeat the loop.
     loops: std.AutoHashMapUnmanaged(Air.Inst.Index, Builder.Function.Block.Index),
 
+    /// Maps `loop_switch_br` instructions to the information required to lower
+    /// dispatches (`switch_dispatch` instructions).
+    switch_dispatch_info: std.AutoHashMapUnmanaged(Air.Inst.Index, SwitchDispatchInfo),
+
     sync_scope: Builder.SyncScope,
 
     const Fuzz = struct {
@@ -4857,6 +4862,33 @@ pub const FuncGen = struct {
         }
     };
 
+    const SwitchDispatchInfo = struct {
+        /// These are the blocks corresponding to each switch case.
+        /// The final element corresponds to the `else` case.
+        /// Slices allocated into `gpa`.
+        case_blocks: []Builder.Function.Block.Index,
+        /// This is `.none` if `jmp_table` is set, since we won't use a `switch` instruction to dispatch.
+        switch_weights: Builder.Function.Instruction.BrCond.Weights,
+        /// If not `null`, we have manually constructed a jump table to reach the desired block.
+        /// `table` can be used if the value is between `min` and `max` inclusive.
+        /// We perform this lowering manually to avoid some questionable behavior from LLVM.
+        /// See `airSwitchBr` for details.
+        jmp_table: ?JmpTable,
+
+        const JmpTable = struct {
+            min: Builder.Constant,
+            max: Builder.Constant,
+            in_bounds_hint: enum { none, unpredictable, likely, unlikely },
+            /// Pointer to the jump table itself, to be used with `indirectbr`.
+            /// The index into the jump table is the dispatch condition minus `min`.
+            /// The table values are `blockaddress` constants corresponding to blocks in `case_blocks`.
+            table: Builder.Constant,
+            /// `true` if `table` conatins a reference to the `else` block.
+            /// In this case, the `indirectbr` must include the `else` block in its target list.
+            table_includes_else: bool,
+        };
+    };
+
     const BreakList = union {
         list: std.MultiArrayList(struct {
             bb: Builder.Function.Block.Index,
@@ -4872,6 +4904,11 @@ pub const FuncGen = struct {
         self.func_inst_table.deinit(gpa);
         self.blocks.deinit(gpa);
         self.loops.deinit(gpa);
+        var it = self.switch_dispatch_info.valueIterator();
+        while (it.next()) |info| {
+            self.gpa.free(info.case_blocks);
+        }
+        self.switch_dispatch_info.deinit(gpa);
     }
 
     fn todo(self: *FuncGen, comptime format: []const u8, args: anytype) Error {
@@ -5182,16 +5219,18 @@ pub const FuncGen = struct {
                 .work_group_id => try self.airWorkGroupId(inst),
 
                 // Instructions that are known to always be `noreturn` based on their tag.
-                .br        => return self.airBr(inst),
-                .repeat    => return self.airRepeat(inst),
-                .cond_br   => return self.airCondBr(inst),
-                .switch_br => return self.airSwitchBr(inst),
-                .loop      => return self.airLoop(inst),
-                .ret       => return self.airRet(inst, false),
-                .ret_safe  => return self.airRet(inst, true),
-                .ret_load  => return self.airRetLoad(inst),
-                .trap      => return self.airTrap(inst),
-                .unreach   => return self.airUnreach(inst),
+                .br              => return self.airBr(inst),
+                .repeat          => return self.airRepeat(inst),
+                .switch_dispatch => return self.airSwitchDispatch(inst),
+                .cond_br         => return self.airCondBr(inst),
+                .switch_br       => return self.airSwitchBr(inst, false),
+                .loop_switch_br  => return self.airSwitchBr(inst, true),
+                .loop            => return self.airLoop(inst),
+                .ret             => return self.airRet(inst, false),
+                .ret_safe        => return self.airRet(inst, true),
+                .ret_load        => return self.airRetLoad(inst),
+                .trap            => return self.airTrap(inst),
+                .unreach         => return self.airUnreach(inst),
 
                 // Instructions which may be `noreturn`.
                 .block => res: {
@@ -6093,6 +6132,202 @@ pub const FuncGen = struct {
         _ = try self.wip.br(loop_bb);
     }
 
+    fn lowerSwitchDispatch(
+        self: *FuncGen,
+        switch_inst: Air.Inst.Index,
+        cond_ref: Air.Inst.Ref,
+        dispatch_info: SwitchDispatchInfo,
+    ) !void {
+        const o = self.ng.object;
+        const pt = o.pt;
+        const zcu = pt.zcu;
+        const cond_ty = self.typeOf(cond_ref);
+        const switch_br = self.air.unwrapSwitch(switch_inst);
+
+        if (try self.air.value(cond_ref, pt)) |cond_val| {
+            // Comptime-known dispatch. Iterate the cases to find the correct
+            // one, and branch to the corresponding element of `case_blocks`.
+            var it = switch_br.iterateCases();
+            const target_case_idx = target: while (it.next()) |case| {
+                for (case.items) |item| {
+                    const val = Value.fromInterned(item.toInterned().?);
+                    if (cond_val.compareHetero(.eq, val, zcu)) break :target case.idx;
+                }
+                for (case.ranges) |range| {
+                    const low = Value.fromInterned(range[0].toInterned().?);
+                    const high = Value.fromInterned(range[1].toInterned().?);
+                    if (cond_val.compareHetero(.gte, low, zcu) and
+                        cond_val.compareHetero(.lte, high, zcu))
+                    {
+                        break :target case.idx;
+                    }
+                }
+            } else dispatch_info.case_blocks.len - 1;
+            const target_block = dispatch_info.case_blocks[target_case_idx];
+            target_block.ptr(&self.wip).incoming += 1;
+            _ = try self.wip.br(target_block);
+            return;
+        }
+
+        // Runtime-known dispatch.
+        const cond = try self.resolveInst(cond_ref);
+
+        if (dispatch_info.jmp_table) |jmp_table| {
+            // We should use the constructed jump table.
+            // First, check the bounds to branch to the `else` case if needed.
+            const inbounds = try self.wip.bin(
+                .@"and",
+                try self.cmp(.normal, .gte, cond_ty, cond, jmp_table.min.toValue()),
+                try self.cmp(.normal, .lte, cond_ty, cond, jmp_table.max.toValue()),
+                "",
+            );
+            const jmp_table_block = try self.wip.block(1, "Then");
+            const else_block = dispatch_info.case_blocks[dispatch_info.case_blocks.len - 1];
+            else_block.ptr(&self.wip).incoming += 1;
+            _ = try self.wip.brCond(inbounds, jmp_table_block, else_block, switch (jmp_table.in_bounds_hint) {
+                .none => .none,
+                .unpredictable => .unpredictable,
+                .likely => .then_likely,
+                .unlikely => .else_likely,
+            });
+
+            self.wip.cursor = .{ .block = jmp_table_block };
+
+            // Figure out the list of blocks we might branch to.
+            // This includes all case blocks, but it might not include the `else` block if
+            // the table is dense.
+            const target_blocks_len = dispatch_info.case_blocks.len - @intFromBool(!jmp_table.table_includes_else);
+            const target_blocks = dispatch_info.case_blocks[0..target_blocks_len];
+
+            // Make sure to cast the index to a usize so it's not treated as negative!
+            const table_index = try self.wip.cast(
+                .zext,
+                try self.wip.bin(.@"sub nuw", cond, jmp_table.min.toValue(), ""),
+                try o.lowerType(Type.usize),
+                "",
+            );
+            const target_ptr_ptr = try self.wip.gep(
+                .inbounds,
+                .ptr,
+                jmp_table.table.toValue(),
+                &.{table_index},
+                "",
+            );
+            const target_ptr = try self.wip.load(.normal, .ptr, target_ptr_ptr, .default, "");
+
+            // Do the branch!
+            _ = try self.wip.indirectbr(target_ptr, target_blocks);
+
+            // Mark all target blocks as having one more incoming branch.
+            for (target_blocks) |case_block| {
+                case_block.ptr(&self.wip).incoming += 1;
+            }
+
+            return;
+        }
+
+        // We must lower to an actual LLVM `switch` instruction.
+        // The switch prongs will correspond to our scalar cases. Ranges will
+        // be handled by conditional branches in the `else` prong.
+
+        const llvm_usize = try o.lowerType(Type.usize);
+        const cond_int = if (cond.typeOfWip(&self.wip).isPointer(&o.builder))
+            try self.wip.cast(.ptrtoint, cond, llvm_usize, "")
+        else
+            cond;
+
+        const llvm_cases_len, const last_range_case = info: {
+            var llvm_cases_len: u32 = 0;
+            var last_range_case: ?u32 = null;
+            var it = switch_br.iterateCases();
+            while (it.next()) |case| {
+                if (case.ranges.len > 0) last_range_case = case.idx;
+                llvm_cases_len += @intCast(case.items.len);
+            }
+            break :info .{ llvm_cases_len, last_range_case };
+        };
+
+        // The `else` of the LLVM `switch` is the actual `else` prong only
+        // if there are no ranges. Otherwise, the `else` will have a
+        // conditional chain before the "true" `else` prong.
+        const llvm_else_block = if (last_range_case == null)
+            dispatch_info.case_blocks[dispatch_info.case_blocks.len - 1]
+        else
+            try self.wip.block(0, "RangeTest");
+
+        llvm_else_block.ptr(&self.wip).incoming += 1;
+
+        var wip_switch = try self.wip.@"switch"(cond_int, llvm_else_block, llvm_cases_len, dispatch_info.switch_weights);
+        defer wip_switch.finish(&self.wip);
+
+        // Construct the actual cases. Set the cursor to the `else` block so
+        // we can construct ranges at the same time as scalar cases.
+        self.wip.cursor = .{ .block = llvm_else_block };
+
+        var it = switch_br.iterateCases();
+        while (it.next()) |case| {
+            const case_block = dispatch_info.case_blocks[case.idx];
+
+            for (case.items) |item| {
+                const llvm_item = (try self.resolveInst(item)).toConst().?;
+                const llvm_int_item = if (llvm_item.typeOf(&o.builder).isPointer(&o.builder))
+                    try o.builder.castConst(.ptrtoint, llvm_item, llvm_usize)
+                else
+                    llvm_item;
+                try wip_switch.addCase(llvm_int_item, case_block, &self.wip);
+            }
+            case_block.ptr(&self.wip).incoming += @intCast(case.items.len);
+
+            if (case.ranges.len == 0) continue;
+
+            // Add a conditional for the ranges, directing to the relevant bb.
+            // We don't need to consider `cold` branch hints since that information is stored
+            // in the target bb body, but we do care about likely/unlikely/unpredictable.
+
+            const hint = switch_br.getHint(case.idx);
+
+            var range_cond: ?Builder.Value = null;
+            for (case.ranges) |range| {
+                const llvm_min = try self.resolveInst(range[0]);
+                const llvm_max = try self.resolveInst(range[1]);
+                const cond_part = try self.wip.bin(
+                    .@"and",
+                    try self.cmp(.normal, .gte, cond_ty, cond, llvm_min),
+                    try self.cmp(.normal, .lte, cond_ty, cond, llvm_max),
+                    "",
+                );
+                if (range_cond) |prev| {
+                    range_cond = try self.wip.bin(.@"or", prev, cond_part, "");
+                } else range_cond = cond_part;
+            }
+
+            // If the check fails, we either branch to the "true" `else` case,
+            // or to the next range condition.
+            const range_else_block = if (case.idx == last_range_case.?)
+                dispatch_info.case_blocks[dispatch_info.case_blocks.len - 1]
+            else
+                try self.wip.block(0, "RangeTest");
+
+            _ = try self.wip.brCond(range_cond.?, case_block, range_else_block, switch (hint) {
+                .none, .cold => .none,
+                .unpredictable => .unpredictable,
+                .likely => .then_likely,
+                .unlikely => .else_likely,
+            });
+            case_block.ptr(&self.wip).incoming += 1;
+            range_else_block.ptr(&self.wip).incoming += 1;
+
+            // Construct the next range conditional (if any) in the false branch.
+            self.wip.cursor = .{ .block = range_else_block };
+        }
+    }
+
+    fn airSwitchDispatch(self: *FuncGen, inst: Air.Inst.Index) !void {
+        const br = self.air.instructions.items(.data)[@intFromEnum(inst)].br;
+        const dispatch_info = self.switch_dispatch_info.get(br.block_inst).?;
+        return self.lowerSwitchDispatch(br.block_inst, br.operand, dispatch_info);
+    }
+
     fn airCondBr(self: *FuncGen, inst: Air.Inst.Index) !void {
         const pl_op = self.air.instructions.items(.data)[@intFromEnum(inst)].pl_op;
         const cond = try self.resolveInst(pl_op.operand);
@@ -6257,36 +6492,123 @@ pub const FuncGen = struct {
         return fg.wip.extractValue(err_union, &.{offset}, "");
     }
 
-    fn airSwitchBr(self: *FuncGen, inst: Air.Inst.Index) !void {
+    fn airSwitchBr(self: *FuncGen, inst: Air.Inst.Index, is_dispatch_loop: bool) !void {
         const o = self.ng.object;
+        const zcu = o.pt.zcu;
 
         const switch_br = self.air.unwrapSwitch(inst);
 
-        const cond = try self.resolveInst(switch_br.operand);
+        // For `loop_switch_br`, we need these BBs prepared ahead of time to generate dispatches.
+        // For `switch_br`, they allow us to sometimes generate better IR by sharing a BB between
+        // scalar and range cases in the same prong.
+        // +1 for `else` case. This is not the same as the LLVM `else` prong, as that may first contain
+        // conditionals to handle ranges.
+        const case_blocks = try self.gpa.alloc(Builder.Function.Block.Index, switch_br.cases_len + 1);
+        defer self.gpa.free(case_blocks);
+        // We set incoming as 0 for now, and increment it as we construct dispatches.
+        for (case_blocks[0 .. case_blocks.len - 1]) |*b| b.* = try self.wip.block(0, "Case");
+        case_blocks[case_blocks.len - 1] = try self.wip.block(0, "Default");
+
+        // There's a special case here to manually generate a jump table in some cases.
+        //
+        // Labeled switch in Zig is intended to follow the "direct threading" pattern. We would ideally use a jump
+        // table, and each `continue` has its own indirect `jmp`, to allow the branch predictor to more accurately
+        // use data patterns to predict future dispatches. The problem, however, is that LLVM emits fascinatingly
+        // bad asm for this. Not only does it not share the jump table -- which we really need it to do to prevent
+        // destroying the cache -- but it also actually generates slightly different jump tables for each case,
+        // and *a separate conditional branch beforehand* to handle dispatching back to the case we're currently
+        // within(!!).
+        //
+        // This asm is really, really, not what we want. As such, we will construct the jump table manually where
+        // appropriate (the values are dense and relatively few), and use it when lowering dispatches.
+
+        const jmp_table: ?SwitchDispatchInfo.JmpTable = jmp_table: {
+            if (!is_dispatch_loop) break :jmp_table null;
+            // On a 64-bit target, 1024 pointers in our jump table is about 8K of pointers. This seems just
+            // about acceptable - it won't fill L1d cache on most CPUs.
+            const max_table_len = 1024;
 
-        // This is not necessarily the actual `else` prong; it first contains conditionals
-        // for any range cases. It's just the `else` of the LLVM switch.
-        const llvm_else_block = try self.wip.block(1, "Default");
+            const cond_ty = self.typeOf(switch_br.operand);
+            switch (cond_ty.zigTypeTag(zcu)) {
+                .bool, .pointer => break :jmp_table null,
+                .@"enum", .int, .error_set => {},
+                else => unreachable,
+            }
 
-        const case_blocks = try self.gpa.alloc(Builder.Function.Block.Index, switch_br.cases_len);
-        defer self.gpa.free(case_blocks);
-        // We set incoming as 0 for now, and increment it as we construct the switch.
-        for (case_blocks) |*b| b.* = try self.wip.block(0, "Case");
+            if (cond_ty.intInfo(zcu).signedness == .signed) break :jmp_table null;
 
-        const llvm_usize = try o.lowerType(Type.usize);
-        const cond_int = if (cond.typeOfWip(&self.wip).isPointer(&o.builder))
-            try self.wip.cast(.ptrtoint, cond, llvm_usize, "")
-        else
-            cond;
+            // Don't worry about the size of the type -- it's irrelevant, because the prong values could be fairly dense.
+            // If they are, then we will construct a jump table.
+            const min, const max = self.switchCaseItemRange(switch_br);
+            const min_int = min.getUnsignedInt(zcu) orelse break :jmp_table null;
+            const max_int = max.getUnsignedInt(zcu) orelse break :jmp_table null;
+            const table_len = max_int - min_int + 1;
+            if (table_len > max_table_len) break :jmp_table null;
+
+            const table_elems = try self.gpa.alloc(Builder.Constant, @intCast(table_len));
+            defer self.gpa.free(table_elems);
 
-        const llvm_cases_len = llvm_cases_len: {
-            var len: u32 = 0;
+            // Set them all to the `else` branch, then iterate over the AIR switch
+            // and replace all values which correspond to other prongs.
+            @memset(table_elems, try o.builder.blockAddrConst(
+                self.wip.function,
+                case_blocks[case_blocks.len - 1],
+            ));
+            var item_count: u32 = 0;
             var it = switch_br.iterateCases();
-            while (it.next()) |case| len += @intCast(case.items.len);
-            break :llvm_cases_len len;
+            while (it.next()) |case| {
+                const case_block = case_blocks[case.idx];
+                const case_block_addr = try o.builder.blockAddrConst(
+                    self.wip.function,
+                    case_block,
+                );
+                for (case.items) |item| {
+                    const val = Value.fromInterned(item.toInterned().?);
+                    const table_idx = val.toUnsignedInt(zcu) - min_int;
+                    table_elems[@intCast(table_idx)] = case_block_addr;
+                    item_count += 1;
+                }
+                for (case.ranges) |range| {
+                    const low = Value.fromInterned(range[0].toInterned().?);
+                    const high = Value.fromInterned(range[1].toInterned().?);
+                    const low_idx = low.toUnsignedInt(zcu) - min_int;
+                    const high_idx = high.toUnsignedInt(zcu) - min_int;
+                    @memset(table_elems[@intCast(low_idx)..@intCast(high_idx + 1)], case_block_addr);
+                    item_count += @intCast(high_idx + 1 - low_idx);
+                }
+            }
+
+            const table_llvm_ty = try o.builder.arrayType(table_elems.len, .ptr);
+            const table_val = try o.builder.arrayConst(table_llvm_ty, table_elems);
+
+            const table_variable = try o.builder.addVariable(
+                try o.builder.strtabStringFmt("__jmptab_{d}", .{@intFromEnum(inst)}),
+                table_llvm_ty,
+                .default,
+            );
+            try table_variable.setInitializer(table_val, &o.builder);
+            table_variable.setLinkage(.internal, &o.builder);
+            table_variable.setUnnamedAddr(.unnamed_addr, &o.builder);
+
+            const table_includes_else = item_count != table_len;
+
+            break :jmp_table .{
+                .min = try o.lowerValue(min.toIntern()),
+                .max = try o.lowerValue(max.toIntern()),
+                .in_bounds_hint = if (table_includes_else) .none else switch (switch_br.getElseHint()) {
+                    .none, .cold => .none,
+                    .unpredictable => .unpredictable,
+                    .likely => .likely,
+                    .unlikely => .unlikely,
+                },
+                .table = table_variable.toConst(&o.builder),
+                .table_includes_else = table_includes_else,
+            };
         };
 
         const weights: Builder.Function.Instruction.BrCond.Weights = weights: {
+            if (jmp_table != null) break :weights .none; // not used
+
             // First pass. If any weights are `.unpredictable`, unpredictable.
             // If all are `.none` or `.cold`, none.
             var any_likely = false;
@@ -6304,6 +6626,13 @@ pub const FuncGen = struct {
             }
             if (!any_likely) break :weights .none;
 
+            const llvm_cases_len = llvm_cases_len: {
+                var len: u32 = 0;
+                var it = switch_br.iterateCases();
+                while (it.next()) |case| len += @intCast(case.items.len);
+                break :llvm_cases_len len;
+            };
+
             var weights = try self.gpa.alloc(Builder.Metadata, llvm_cases_len + 1);
             defer self.gpa.free(weights);
 
@@ -6336,75 +6665,66 @@ pub const FuncGen = struct {
             break :weights @enumFromInt(@intFromEnum(tuple));
         };
 
-        var wip_switch = try self.wip.@"switch"(cond_int, llvm_else_block, llvm_cases_len, weights);
-        defer wip_switch.finish(&self.wip);
+        const dispatch_info: SwitchDispatchInfo = .{
+            .case_blocks = case_blocks,
+            .switch_weights = weights,
+            .jmp_table = jmp_table,
+        };
+
+        if (is_dispatch_loop) {
+            try self.switch_dispatch_info.putNoClobber(self.gpa, inst, dispatch_info);
+        }
+        defer if (is_dispatch_loop) {
+            assert(self.switch_dispatch_info.remove(inst));
+        };
+
+        // Generate the initial dispatch.
+        // If this is a simple `switch_br`, this is the only dispatch.
+        try self.lowerSwitchDispatch(inst, switch_br.operand, dispatch_info);
 
+        // Iterate the cases and generate their bodies.
         var it = switch_br.iterateCases();
-        var any_ranges = false;
         while (it.next()) |case| {
-            if (case.ranges.len > 0) any_ranges = true;
             const case_block = case_blocks[case.idx];
-            case_block.ptr(&self.wip).incoming += @intCast(case.items.len);
-            // Handle scalar items, and generate the block.
-            // We'll generate conditionals for the ranges later on.
-            for (case.items) |item| {
-                const llvm_item = (try self.resolveInst(item)).toConst().?;
-                const llvm_int_item = if (llvm_item.typeOf(&o.builder).isPointer(&o.builder))
-                    try o.builder.castConst(.ptrtoint, llvm_item, llvm_usize)
-                else
-                    llvm_item;
-                try wip_switch.addCase(llvm_int_item, case_block, &self.wip);
-            }
             self.wip.cursor = .{ .block = case_block };
             if (switch_br.getHint(case.idx) == .cold) _ = try self.wip.callIntrinsicAssumeCold();
-            try self.genBodyDebugScope(null, case.body, .poi);
+            try self.genBodyDebugScope(null, case.body, .none);
         }
-
+        self.wip.cursor = .{ .block = case_blocks[case_blocks.len - 1] };
         const else_body = it.elseBody();
-        self.wip.cursor = .{ .block = llvm_else_block };
-        if (any_ranges) {
-            const cond_ty = self.typeOf(switch_br.operand);
-            // Add conditionals for the ranges, directing to the relevant bb.
-            // We don't need to consider `cold` branch hints since that information is stored
-            // in the target bb body, but we do care about likely/unlikely/unpredictable.
-            it = switch_br.iterateCases();
-            while (it.next()) |case| {
-                if (case.ranges.len == 0) continue;
-                const case_block = case_blocks[case.idx];
-                const hint = switch_br.getHint(case.idx);
-                case_block.ptr(&self.wip).incoming += 1;
-                const next_else_block = try self.wip.block(1, "Default");
-                var range_cond: ?Builder.Value = null;
-                for (case.ranges) |range| {
-                    const llvm_min = try self.resolveInst(range[0]);
-                    const llvm_max = try self.resolveInst(range[1]);
-                    const cond_part = try self.wip.bin(
-                        .@"and",
-                        try self.cmp(.normal, .gte, cond_ty, cond, llvm_min),
-                        try self.cmp(.normal, .lte, cond_ty, cond, llvm_max),
-                        "",
-                    );
-                    if (range_cond) |prev| {
-                        range_cond = try self.wip.bin(.@"or", prev, cond_part, "");
-                    } else range_cond = cond_part;
-                }
-                _ = try self.wip.brCond(range_cond.?, case_block, next_else_block, switch (hint) {
-                    .none, .cold => .none,
-                    .unpredictable => .unpredictable,
-                    .likely => .then_likely,
-                    .unlikely => .else_likely,
-                });
-                self.wip.cursor = .{ .block = next_else_block };
-            }
-        }
         if (switch_br.getElseHint() == .cold) _ = try self.wip.callIntrinsicAssumeCold();
-        if (else_body.len != 0) {
-            try self.genBodyDebugScope(null, else_body, .poi);
+        if (else_body.len > 0) {
+            try self.genBodyDebugScope(null, it.elseBody(), .none);
         } else {
             _ = try self.wip.@"unreachable"();
         }
+    }
 
-        // No need to reset the insert cursor since this instruction is noreturn.
+    fn switchCaseItemRange(self: *FuncGen, switch_br: Air.UnwrappedSwitch) [2]Value {
+        const zcu = self.ng.object.pt.zcu;
+        var it = switch_br.iterateCases();
+        var min: ?Value = null;
+        var max: ?Value = null;
+        while (it.next()) |case| {
+            for (case.items) |item| {
+                const val = Value.fromInterned(item.toInterned().?);
+                const low = if (min) |m| val.compareHetero(.lt, m, zcu) else true;
+                const high = if (max) |m| val.compareHetero(.gt, m, zcu) else true;
+                if (low) min = val;
+                if (high) max = val;
+            }
+            for (case.ranges) |range| {
+                const vals: [2]Value = .{
+                    Value.fromInterned(range[0].toInterned().?),
+                    Value.fromInterned(range[1].toInterned().?),
+                };
+                const low = if (min) |m| vals[0].compareHetero(.lt, m, zcu) else true;
+                const high = if (max) |m| vals[1].compareHetero(.gt, m, zcu) else true;
+                if (low) min = vals[0];
+                if (high) max = vals[1];
+            }
+        }
+        return .{ min.?, max.? };
     }
 
     fn airLoop(self: *FuncGen, inst: Air.Inst.Index) !void {
src/Liveness/Verify.zig
@@ -447,6 +447,16 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
 
                 try self.verifyMatchingLiveness(repeat.loop_inst, expected_live);
             },
+            .switch_dispatch => {
+                const br = data[@intFromEnum(inst)].br;
+
+                try self.verifyOperand(inst, br.operand, self.liveness.operandDies(inst, 0));
+
+                const expected_live = self.loops.get(br.block_inst) orelse
+                    return invalid("%{}: loop %{} not in scope", .{ @intFromEnum(inst), @intFromEnum(br.block_inst) });
+
+                try self.verifyMatchingLiveness(br.block_inst, expected_live);
+            },
             .block, .dbg_inline_block => |tag| {
                 const ty_pl = data[@intFromEnum(inst)].ty_pl;
                 const block_ty = ty_pl.ty.toType();
@@ -494,11 +504,11 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
 
                 // The same stuff should be alive after the loop as before it.
                 const gop = try self.loops.getOrPut(self.gpa, inst);
+                if (gop.found_existing) return invalid("%{}: loop already exists", .{@intFromEnum(inst)});
                 defer {
                     var live = self.loops.fetchRemove(inst).?;
                     live.value.deinit(self.gpa);
                 }
-                if (gop.found_existing) return invalid("%{}: loop already exists", .{@intFromEnum(inst)});
                 gop.value_ptr.* = try self.live.clone(self.gpa);
 
                 try self.verifyBody(loop_body);
@@ -528,7 +538,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
 
                 try self.verifyInst(inst);
             },
-            .switch_br => {
+            .switch_br, .loop_switch_br => {
                 const switch_br = self.air.unwrapSwitch(inst);
                 const switch_br_liveness = try self.liveness.getSwitchBr(
                     self.gpa,
@@ -539,13 +549,22 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
 
                 try self.verifyOperand(inst, switch_br.operand, self.liveness.operandDies(inst, 0));
 
-                var live = self.live.move();
-                defer live.deinit(self.gpa);
+                // Excluding the operand (which we just handled), the same stuff should be alive
+                // after the loop as before it.
+                {
+                    const gop = try self.loops.getOrPut(self.gpa, inst);
+                    if (gop.found_existing) return invalid("%{}: loop already exists", .{@intFromEnum(inst)});
+                    gop.value_ptr.* = self.live.move();
+                }
+                defer {
+                    var live = self.loops.fetchRemove(inst).?;
+                    live.value.deinit(self.gpa);
+                }
 
                 var it = switch_br.iterateCases();
                 while (it.next()) |case| {
                     self.live.deinit(self.gpa);
-                    self.live = try live.clone(self.gpa);
+                    self.live = try self.loops.get(inst).?.clone(self.gpa);
 
                     for (switch_br_liveness.deaths[case.idx]) |death| try self.verifyDeath(inst, death);
                     try self.verifyBody(case.body);
@@ -554,7 +573,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
                 const else_body = it.elseBody();
                 if (else_body.len > 0) {
                     self.live.deinit(self.gpa);
-                    self.live = try live.clone(self.gpa);
+                    self.live = try self.loops.get(inst).?.clone(self.gpa);
                     for (switch_br_liveness.deaths[switch_br.cases_len]) |death| try self.verifyDeath(inst, death);
                     try self.verifyBody(else_body);
                 }
src/Air.zig
@@ -429,6 +429,14 @@ pub const Inst = struct {
         /// Result type is always noreturn; no instructions in a block follow this one.
         /// Uses the `pl_op` field. Operand is the condition. Payload is `SwitchBr`.
         switch_br,
+        /// Switch branch which can dispatch back to itself with a different operand.
+        /// Result type is always noreturn; no instructions in a block follow this one.
+        /// Uses the `pl_op` field. Operand is the condition. Payload is `SwitchBr`.
+        loop_switch_br,
+        /// Dispatches back to a branch of a parent `loop_switch_br`.
+        /// Result type is always noreturn; no instructions in a block follow this one.
+        /// Uses the `br` field. `block_inst` is a `loop_switch_br` instruction.
+        switch_dispatch,
         /// Given an operand which is an error union, splits control flow. In
         /// case of error, control flow goes into the block that is part of this
         /// instruction, which is guaranteed to end with a return instruction
@@ -1454,6 +1462,8 @@ pub fn typeOfIndex(air: *const Air, inst: Air.Inst.Index, ip: *const InternPool)
         .br,
         .cond_br,
         .switch_br,
+        .loop_switch_br,
+        .switch_dispatch,
         .ret,
         .ret_safe,
         .ret_load,
@@ -1618,6 +1628,8 @@ pub fn mustLower(air: Air, inst: Air.Inst.Index, ip: *const InternPool) bool {
         .call_never_inline,
         .cond_br,
         .switch_br,
+        .loop_switch_br,
+        .switch_dispatch,
         .@"try",
         .try_cold,
         .try_ptr,
@@ -1903,7 +1915,10 @@ pub const UnwrappedSwitch = struct {
 
 pub fn unwrapSwitch(air: *const Air, switch_inst: Inst.Index) UnwrappedSwitch {
     const inst = air.instructions.get(@intFromEnum(switch_inst));
-    assert(inst.tag == .switch_br);
+    switch (inst.tag) {
+        .switch_br, .loop_switch_br => {},
+        else => unreachable, // assertion failure
+    }
     const pl_op = inst.data.pl_op;
     const extra = air.extraData(SwitchBr, pl_op.payload);
     const hint_bag_count = std.math.divCeil(usize, extra.data.cases_len + 1, 10) catch unreachable;
src/Liveness.zig
@@ -31,6 +31,7 @@ tomb_bits: []usize,
 ///  * `try`, `try_ptr` - points to a `CondBr` in `extra` at this index. The error path (the block
 ///    in the instruction) is considered the "else" path, and the rest of the block the "then".
 ///  * `switch_br` - points to a `SwitchBr` in `extra` at this index.
+///  * `loop_switch_br` - points to a `SwitchBr` in `extra` at this index.
 ///  * `block` - points to a `Block` in `extra` at this index.
 ///  * `asm`, `call`, `aggregate_init` - the value is a set of bits which are the extra tomb
 ///    bits of operands.
@@ -68,8 +69,8 @@ pub const Block = struct {
 /// Liveness analysis runs in several passes. Each pass iterates backwards over instructions in
 /// bodies, and recurses into bodies.
 const LivenessPass = enum {
-    /// In this pass, we perform some basic analysis of loops to gain information the main pass
-    /// needs. In particular, for every `loop`, we track the following information:
+    /// In this pass, we perform some basic analysis of loops to gain information the main pass needs.
+    /// In particular, for every `loop` and `loop_switch_br`, we track the following information:
     /// * Every outer block which the loop body contains a `br` to.
     /// * Every outer loop which the loop body contains a `repeat` to.
     /// * Every operand referenced within the loop body but created outside the loop.
@@ -91,7 +92,8 @@ fn LivenessPassData(comptime pass: LivenessPass) type {
         .loop_analysis => struct {
             /// The set of blocks which are exited with a `br` instruction at some point within this
             /// body and which we are currently within. Also includes `loop`s which are the target
-            /// of a `repeat` instruction.
+            /// of a `repeat` instruction, and `loop_switch_br`s which are the target of a
+            /// `switch_dispatch` instruction.
             breaks: std.AutoHashMapUnmanaged(Air.Inst.Index, void) = .{},
 
             /// The set of operands for which we have seen at least one usage but not their birth.
@@ -330,6 +332,7 @@ pub fn categorizeOperand(
         .trap,
         .breakpoint,
         .repeat,
+        .switch_dispatch,
         .dbg_stmt,
         .unreach,
         .ret_addr,
@@ -662,21 +665,17 @@ pub fn categorizeOperand(
 
             return .complex;
         },
-        .@"try", .try_cold => {
-            return .complex;
-        },
-        .try_ptr, .try_ptr_cold => {
-            return .complex;
-        },
-        .loop => {
-            return .complex;
-        },
-        .cond_br => {
-            return .complex;
-        },
-        .switch_br => {
-            return .complex;
-        },
+
+        .@"try",
+        .try_cold,
+        .try_ptr,
+        .try_ptr_cold,
+        .loop,
+        .cond_br,
+        .switch_br,
+        .loop_switch_br,
+        => return .complex,
+
         .wasm_memory_grow => {
             const pl_op = air_datas[@intFromEnum(inst)].pl_op;
             if (pl_op.operand == operand_ref) return matchOperandSmallIndex(l, inst, 0, .none);
@@ -1206,6 +1205,7 @@ fn analyzeInst(
 
         .br => return analyzeInstBr(a, pass, data, inst),
         .repeat => return analyzeInstRepeat(a, pass, data, inst),
+        .switch_dispatch => return analyzeInstSwitchDispatch(a, pass, data, inst),
 
         .assembly => {
             const extra = a.air.extraData(Air.Asm, inst_datas[@intFromEnum(inst)].ty_pl.payload);
@@ -1262,7 +1262,8 @@ fn analyzeInst(
         .@"try", .try_cold => return analyzeInstCondBr(a, pass, data, inst, .@"try"),
         .try_ptr, .try_ptr_cold => return analyzeInstCondBr(a, pass, data, inst, .try_ptr),
         .cond_br => return analyzeInstCondBr(a, pass, data, inst, .cond_br),
-        .switch_br => return analyzeInstSwitchBr(a, pass, data, inst),
+        .switch_br => return analyzeInstSwitchBr(a, pass, data, inst, false),
+        .loop_switch_br => return analyzeInstSwitchBr(a, pass, data, inst, true),
 
         .wasm_memory_grow => {
             const pl_op = inst_datas[@intFromEnum(inst)].pl_op;
@@ -1412,6 +1413,35 @@ fn analyzeInstRepeat(
     return analyzeOperands(a, pass, data, inst, .{ .none, .none, .none });
 }
 
+fn analyzeInstSwitchDispatch(
+    a: *Analysis,
+    comptime pass: LivenessPass,
+    data: *LivenessPassData(pass),
+    inst: Air.Inst.Index,
+) !void {
+    // This happens to be identical to `analyzeInstBr`, but is separated anyway for clarity.
+
+    const inst_datas = a.air.instructions.items(.data);
+    const br = inst_datas[@intFromEnum(inst)].br;
+    const gpa = a.gpa;
+
+    switch (pass) {
+        .loop_analysis => {
+            try data.breaks.put(gpa, br.block_inst, {});
+        },
+
+        .main_analysis => {
+            const block_scope = data.block_scopes.get(br.block_inst).?; // we should always be repeating an enclosing loop
+
+            const new_live_set = try block_scope.live_set.clone(gpa);
+            data.live_set.deinit(gpa);
+            data.live_set = new_live_set;
+        },
+    }
+
+    return analyzeOperands(a, pass, data, inst, .{ br.operand, .none, .none });
+}
+
 fn analyzeInstBlock(
     a: *Analysis,
     comptime pass: LivenessPass,
@@ -1482,109 +1512,133 @@ fn analyzeInstBlock(
     }
 }
 
-fn analyzeInstLoop(
+fn writeLoopInfo(
     a: *Analysis,
-    comptime pass: LivenessPass,
-    data: *LivenessPassData(pass),
+    data: *LivenessPassData(.loop_analysis),
     inst: Air.Inst.Index,
+    old_breaks: std.AutoHashMapUnmanaged(Air.Inst.Index, void),
+    old_live: std.AutoHashMapUnmanaged(Air.Inst.Index, void),
 ) !void {
-    const inst_datas = a.air.instructions.items(.data);
-    const extra = a.air.extraData(Air.Block, inst_datas[@intFromEnum(inst)].ty_pl.payload);
-    const body: []const Air.Inst.Index = @ptrCast(a.air.extra[extra.end..][0..extra.data.body_len]);
     const gpa = a.gpa;
 
-    try analyzeOperands(a, pass, data, inst, .{ .none, .none, .none });
+    // `loop`s are guaranteed to have at least one matching `repeat`.
+    // Similarly, `loop_switch_br`s have a matching `switch_dispatch`.
+    // However, we no longer care about repeats of this loop for resolving
+    // which operands must live within it.
+    assert(data.breaks.remove(inst));
 
-    switch (pass) {
-        .loop_analysis => {
-            var old_breaks = data.breaks.move();
-            defer old_breaks.deinit(gpa);
+    const extra_index: u32 = @intCast(a.extra.items.len);
 
-            var old_live = data.live_set.move();
-            defer old_live.deinit(gpa);
+    const num_breaks = data.breaks.count();
+    try a.extra.ensureUnusedCapacity(gpa, 1 + num_breaks);
 
-            try analyzeBody(a, pass, data, body);
+    a.extra.appendAssumeCapacity(num_breaks);
 
-            // `loop`s are guaranteed to have at least one matching `repeat`.
-            // However, we no longer care about repeats of this loop itself.
-            assert(data.breaks.remove(inst));
+    var it = data.breaks.keyIterator();
+    while (it.next()) |key| {
+        const block_inst = key.*;
+        a.extra.appendAssumeCapacity(@intFromEnum(block_inst));
+    }
+    log.debug("[{}] %{}: includes breaks to {}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.breaks) });
 
-            const extra_index: u32 = @intCast(a.extra.items.len);
+    // Now we put the live operands from the loop body in too
+    const num_live = data.live_set.count();
+    try a.extra.ensureUnusedCapacity(gpa, 1 + num_live);
 
-            const num_breaks = data.breaks.count();
-            try a.extra.ensureUnusedCapacity(gpa, 1 + num_breaks);
+    a.extra.appendAssumeCapacity(num_live);
+    it = data.live_set.keyIterator();
+    while (it.next()) |key| {
+        const alive = key.*;
+        a.extra.appendAssumeCapacity(@intFromEnum(alive));
+    }
+    log.debug("[{}] %{}: maintain liveness of {}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.live_set) });
 
-            a.extra.appendAssumeCapacity(num_breaks);
+    try a.special.put(gpa, inst, extra_index);
 
-            var it = data.breaks.keyIterator();
-            while (it.next()) |key| {
-                const block_inst = key.*;
-                a.extra.appendAssumeCapacity(@intFromEnum(block_inst));
-            }
-            log.debug("[{}] %{}: includes breaks to {}", .{ pass, inst, fmtInstSet(&data.breaks) });
+    // Add back operands which were previously alive
+    it = old_live.keyIterator();
+    while (it.next()) |key| {
+        const alive = key.*;
+        try data.live_set.put(gpa, alive, {});
+    }
 
-            // Now we put the live operands from the loop body in too
-            const num_live = data.live_set.count();
-            try a.extra.ensureUnusedCapacity(gpa, 1 + num_live);
+    // And the same for breaks
+    it = old_breaks.keyIterator();
+    while (it.next()) |key| {
+        const block_inst = key.*;
+        try data.breaks.put(gpa, block_inst, {});
+    }
+}
 
-            a.extra.appendAssumeCapacity(num_live);
-            it = data.live_set.keyIterator();
-            while (it.next()) |key| {
-                const alive = key.*;
-                a.extra.appendAssumeCapacity(@intFromEnum(alive));
-            }
-            log.debug("[{}] %{}: maintain liveness of {}", .{ pass, inst, fmtInstSet(&data.live_set) });
+/// When analyzing a loop in the main pass, sets up `data.live_set` to be the set
+/// of operands known to be alive when the loop repeats.
+fn resolveLoopLiveSet(
+    a: *Analysis,
+    data: *LivenessPassData(.main_analysis),
+    inst: Air.Inst.Index,
+) !void {
+    const gpa = a.gpa;
 
-            try a.special.put(gpa, inst, extra_index);
+    const extra_idx = a.special.fetchRemove(inst).?.value;
+    const num_breaks = data.old_extra.items[extra_idx];
+    const breaks: []const Air.Inst.Index = @ptrCast(data.old_extra.items[extra_idx + 1 ..][0..num_breaks]);
 
-            // Add back operands which were previously alive
-            it = old_live.keyIterator();
-            while (it.next()) |key| {
-                const alive = key.*;
-                try data.live_set.put(gpa, alive, {});
-            }
+    const num_loop_live = data.old_extra.items[extra_idx + num_breaks + 1];
+    const loop_live: []const Air.Inst.Index = @ptrCast(data.old_extra.items[extra_idx + num_breaks + 2 ..][0..num_loop_live]);
 
-            // And the same for breaks
-            it = old_breaks.keyIterator();
-            while (it.next()) |key| {
-                const block_inst = key.*;
-                try data.breaks.put(gpa, block_inst, {});
-            }
-        },
+    // This is necessarily not in the same control flow branch, because loops are noreturn
+    data.live_set.clearRetainingCapacity();
 
-        .main_analysis => {
-            const extra_idx = a.special.fetchRemove(inst).?.value; // remove because this data does not exist after analysis
+    try data.live_set.ensureUnusedCapacity(gpa, @intCast(loop_live.len));
+    for (loop_live) |alive| data.live_set.putAssumeCapacity(alive, {});
 
-            const num_breaks = data.old_extra.items[extra_idx];
-            const breaks: []const Air.Inst.Index = @ptrCast(data.old_extra.items[extra_idx + 1 ..][0..num_breaks]);
+    log.debug("[{}] %{}: block live set is {}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) });
 
-            const num_loop_live = data.old_extra.items[extra_idx + num_breaks + 1];
-            const loop_live: []const Air.Inst.Index = @ptrCast(data.old_extra.items[extra_idx + num_breaks + 2 ..][0..num_loop_live]);
+    for (breaks) |block_inst| {
+        // We might break to this block, so include every operand that the block needs alive
+        const block_scope = data.block_scopes.get(block_inst).?;
 
-            // This is necessarily not in the same control flow branch, because loops are noreturn
-            data.live_set.clearRetainingCapacity();
+        var it = block_scope.live_set.keyIterator();
+        while (it.next()) |key| {
+            const alive = key.*;
+            try data.live_set.put(gpa, alive, {});
+        }
+    }
 
-            try data.live_set.ensureUnusedCapacity(gpa, @intCast(loop_live.len));
-            for (loop_live) |alive| {
-                data.live_set.putAssumeCapacity(alive, {});
-            }
+    log.debug("[{}] %{}: loop live set is {}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) });
+}
 
-            log.debug("[{}] %{}: block live set is {}", .{ pass, inst, fmtInstSet(&data.live_set) });
+fn analyzeInstLoop(
+    a: *Analysis,
+    comptime pass: LivenessPass,
+    data: *LivenessPassData(pass),
+    inst: Air.Inst.Index,
+) !void {
+    const inst_datas = a.air.instructions.items(.data);
+    const extra = a.air.extraData(Air.Block, inst_datas[@intFromEnum(inst)].ty_pl.payload);
+    const body: []const Air.Inst.Index = @ptrCast(a.air.extra[extra.end..][0..extra.data.body_len]);
+    const gpa = a.gpa;
 
-            for (breaks) |block_inst| {
-                // We might break to this block, so include every operand that the block needs alive
-                const block_scope = data.block_scopes.get(block_inst).?;
+    try analyzeOperands(a, pass, data, inst, .{ .none, .none, .none });
 
-                var it = block_scope.live_set.keyIterator();
-                while (it.next()) |key| {
-                    const alive = key.*;
-                    try data.live_set.put(gpa, alive, {});
-                }
-            }
+    switch (pass) {
+        .loop_analysis => {
+            var old_breaks = data.breaks.move();
+            defer old_breaks.deinit(gpa);
+
+            var old_live = data.live_set.move();
+            defer old_live.deinit(gpa);
+
+            try analyzeBody(a, pass, data, body);
+
+            try writeLoopInfo(a, data, inst, old_breaks, old_live);
+        },
+
+        .main_analysis => {
+            try resolveLoopLiveSet(a, data, inst);
 
             // Now, `data.live_set` is the operands which must be alive when the loop repeats.
             // Move them into a block scope for corresponding `repeat` instructions to notice.
-            log.debug("[{}] %{}: loop live set is {}", .{ pass, inst, fmtInstSet(&data.live_set) });
             try data.block_scopes.putNoClobber(gpa, inst, .{
                 .live_set = data.live_set.move(),
             });
@@ -1720,6 +1774,7 @@ fn analyzeInstSwitchBr(
     comptime pass: LivenessPass,
     data: *LivenessPassData(pass),
     inst: Air.Inst.Index,
+    is_dispatch_loop: bool,
 ) !void {
     const inst_datas = a.air.instructions.items(.data);
     const pl_op = inst_datas[@intFromEnum(inst)].pl_op;
@@ -1730,6 +1785,17 @@ fn analyzeInstSwitchBr(
 
     switch (pass) {
         .loop_analysis => {
+            var old_breaks: std.AutoHashMapUnmanaged(Air.Inst.Index, void) = .{};
+            defer old_breaks.deinit(gpa);
+
+            var old_live: std.AutoHashMapUnmanaged(Air.Inst.Index, void) = .{};
+            defer old_live.deinit(gpa);
+
+            if (is_dispatch_loop) {
+                old_breaks = data.breaks.move();
+                old_live = data.live_set.move();
+            }
+
             var it = switch_br.iterateCases();
             while (it.next()) |case| {
                 try analyzeBody(a, pass, data, case.body);
@@ -1738,9 +1804,24 @@ fn analyzeInstSwitchBr(
                 const else_body = it.elseBody();
                 try analyzeBody(a, pass, data, else_body);
             }
+
+            if (is_dispatch_loop) {
+                try writeLoopInfo(a, data, inst, old_breaks, old_live);
+            }
         },
 
         .main_analysis => {
+            if (is_dispatch_loop) {
+                try resolveLoopLiveSet(a, data, inst);
+                try data.block_scopes.putNoClobber(gpa, inst, .{
+                    .live_set = data.live_set.move(),
+                });
+            }
+            defer if (is_dispatch_loop) {
+                log.debug("[{}] %{}: popped loop block scop", .{ pass, inst });
+                var scope = data.block_scopes.fetchRemove(inst).?.value;
+                scope.live_set.deinit(gpa);
+            };
             // This is, all in all, just a messier version of the `cond_br` logic. If you're trying
             // to understand it, I encourage looking at `analyzeInstCondBr` first.
 
src/print_air.zig
@@ -296,11 +296,12 @@ const Writer = struct {
             .aggregate_init => try w.writeAggregateInit(s, inst),
             .union_init => try w.writeUnionInit(s, inst),
             .br => try w.writeBr(s, inst),
+            .switch_dispatch => try w.writeBr(s, inst),
             .repeat => try w.writeRepeat(s, inst),
             .cond_br => try w.writeCondBr(s, inst),
             .@"try", .try_cold => try w.writeTry(s, inst),
             .try_ptr, .try_ptr_cold => try w.writeTryPtr(s, inst),
-            .switch_br => try w.writeSwitchBr(s, inst),
+            .loop_switch_br, .switch_br => try w.writeSwitchBr(s, inst),
             .cmpxchg_weak, .cmpxchg_strong => try w.writeCmpxchg(s, inst),
             .fence => try w.writeFence(s, inst),
             .atomic_load => try w.writeAtomicLoad(s, inst),
src/print_zir.zig
@@ -302,6 +302,7 @@ const Writer = struct {
 
             .@"break",
             .break_inline,
+            .switch_continue,
             => try self.writeBreak(stream, inst),
 
             .slice_start => try self.writeSliceStart(stream, inst),
src/Sema.zig
@@ -503,11 +503,21 @@ pub const Block = struct {
         /// to enable more precise compile errors.
         /// Same indexes, capacity, length as `results`.
         src_locs: std.ArrayListUnmanaged(?LazySrcLoc),
-
-        pub fn deinit(merges: *@This(), allocator: mem.Allocator) void {
+        /// Most blocks do not utilize this field. When it is used, its use is
+        /// contextual. The possible uses are as follows:
+        /// * for a `switch_block[_ref]`, this refers to dummy `br` instructions
+        ///   which correspond to `switch_continue` ZIR. The switch logic will
+        ///   rewrite these to appropriate AIR switch dispatches.
+        extra_insts: std.ArrayListUnmanaged(Air.Inst.Index) = .{},
+        /// Same indexes, capacity, length as `extra_insts`.
+        extra_src_locs: std.ArrayListUnmanaged(LazySrcLoc) = .{},
+
+        pub fn deinit(merges: *@This(), allocator: Allocator) void {
             merges.results.deinit(allocator);
             merges.br_list.deinit(allocator);
             merges.src_locs.deinit(allocator);
+            merges.extra_insts.deinit(allocator);
+            merges.extra_src_locs.deinit(allocator);
         }
     };
 
@@ -946,14 +956,21 @@ fn analyzeInlineBody(
         error.ComptimeBreak => {},
         else => |e| return e,
     }
-    const break_inst = sema.comptime_break_inst;
-    const break_data = sema.code.instructions.items(.data)[@intFromEnum(break_inst)].@"break";
-    const extra = sema.code.extraData(Zir.Inst.Break, break_data.payload_index).data;
+    const break_inst = sema.code.instructions.get(@intFromEnum(sema.comptime_break_inst));
+    switch (break_inst.tag) {
+        .switch_continue => {
+            // This is handled by separate logic.
+            return error.ComptimeBreak;
+        },
+        .break_inline, .@"break" => {},
+        else => unreachable,
+    }
+    const extra = sema.code.extraData(Zir.Inst.Break, break_inst.data.@"break".payload_index).data;
     if (extra.block_inst != break_target) {
         // This control flow goes further up the stack.
         return error.ComptimeBreak;
     }
-    return try sema.resolveInst(break_data.operand);
+    return try sema.resolveInst(break_inst.data.@"break".operand);
 }
 
 /// Like `analyzeInlineBody`, but if the body does not break with a value, returns
@@ -1571,6 +1588,13 @@ fn analyzeBodyInner(
                 i = 0;
                 continue;
             },
+            .switch_continue => if (block.is_comptime) {
+                sema.comptime_break_inst = inst;
+                return error.ComptimeBreak;
+            } else {
+                try sema.zirSwitchContinue(block, inst);
+                break;
+            },
             .loop => blk: {
                 if (!block.is_comptime) break :blk try sema.zirLoop(block, inst);
                 // Same as `block_inline`. TODO https://github.com/ziglang/zig/issues/8220
@@ -6531,6 +6555,56 @@ fn zirBreak(sema: *Sema, start_block: *Block, inst: Zir.Inst.Index) CompileError
     }
 }
 
+fn zirSwitchContinue(sema: *Sema, start_block: *Block, inst: Zir.Inst.Index) CompileError!void {
+    const tracy = trace(@src());
+    defer tracy.end();
+
+    const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].@"break";
+    const extra = sema.code.extraData(Zir.Inst.Break, inst_data.payload_index).data;
+    assert(extra.operand_src_node != Zir.Inst.Break.no_src_node);
+    const operand_src = start_block.nodeOffset(extra.operand_src_node);
+    const uncoerced_operand = try sema.resolveInst(inst_data.operand);
+    const switch_inst = extra.block_inst;
+
+    switch (sema.code.instructions.items(.tag)[@intFromEnum(switch_inst)]) {
+        .switch_block, .switch_block_ref => {},
+        else => unreachable, // assertion failure
+    }
+
+    const switch_payload_index = sema.code.instructions.items(.data)[@intFromEnum(switch_inst)].pl_node.payload_index;
+    const switch_operand_ref = sema.code.extraData(Zir.Inst.SwitchBlock, switch_payload_index).data.operand;
+    const switch_operand_ty = sema.typeOf(try sema.resolveInst(switch_operand_ref));
+
+    const operand = try sema.coerce(start_block, switch_operand_ty, uncoerced_operand, operand_src);
+
+    try sema.validateRuntimeValue(start_block, operand_src, operand);
+
+    // We want to generate a `switch_dispatch` instruction with the switch condition,
+    // possibly preceded by a store to the stack alloc containing the raw operand.
+    // However, to avoid too much special-case state in Sema, this is handled by the
+    // `switch` lowering logic. As such, we will find the `Block` corresponding to the
+    // parent `switch_block[_ref]` instruction, create a dummy `br`, and add a merge
+    // to signal to the switch logic to rewrite this into an appropriate dispatch.
+
+    var block = start_block;
+    while (true) {
+        if (block.label) |label| {
+            if (label.zir_block == switch_inst) {
+                const br_ref = try start_block.addBr(label.merges.block_inst, operand);
+                try label.merges.extra_insts.append(sema.gpa, br_ref.toIndex().?);
+                try label.merges.extra_src_locs.append(sema.gpa, operand_src);
+                block.runtime_index.increment();
+                if (block.runtime_cond == null and block.runtime_loop == null) {
+                    block.runtime_cond = start_block.runtime_cond orelse start_block.runtime_loop;
+                    block.runtime_loop = start_block.runtime_loop;
+                }
+                return;
+            }
+        }
+        block = block.parent.?;
+    }
+}
+
 fn zirDbgStmt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
     if (block.is_comptime or block.ownerModule().strip) return;
 
@@ -10940,12 +11014,7 @@ const SwitchProngAnalysis = struct {
     sema: *Sema,
     /// The block containing the `switch_block` itself.
     parent_block: *Block,
-    /// The raw switch operand value (*not* the condition). Always defined.
-    operand: Air.Inst.Ref,
-    /// May be `undefined` if no prong has a by-ref capture.
-    operand_ptr: Air.Inst.Ref,
-    /// The switch condition value. For unions, `operand` is the union and `cond` is its tag.
-    cond: Air.Inst.Ref,
+    operand: Operand,
     /// If this switch is on an error set, this is the type to assign to the
     /// `else` prong. If `null`, the prong should be unreachable.
     else_error_ty: ?Type,
@@ -10955,6 +11024,34 @@ const SwitchProngAnalysis = struct {
     /// undefined if no prong has a tag capture.
     tag_capture_inst: Zir.Inst.Index,
 
+    const Operand = union(enum) {
+        /// This switch will be dispatched only once, with the given operand.
+        simple: struct {
+            /// The raw switch operand value. Always defined.
+            by_val: Air.Inst.Ref,
+            /// The switch operand *pointer*. Defined only if there is a prong
+            /// with a by-ref capture.
+            by_ref: Air.Inst.Ref,
+            /// The switch condition value. For unions, `operand` is the union
+            /// and `cond` is its enum tag value.
+            cond: Air.Inst.Ref,
+        },
+        /// This switch may be dispatched multiple times with `continue` syntax.
+        /// As such, the operand is stored in an alloc if needed.
+        loop: struct {
+            /// The `alloc` containing the `switch` operand for the active dispatch.
+            /// Each prong must load from this `alloc` to get captures.
+            /// If there are no captures, this may be undefined.
+            operand_alloc: Air.Inst.Ref,
+            /// Whether `operand_alloc` contains a by-val operand or a by-ref
+            /// operand.
+            operand_is_ref: bool,
+            /// The switch condition value for the *initial* dispatch. For
+            /// unions, this is the enum tag value.
+            init_cond: Air.Inst.Ref,
+        },
+    };
+
     /// Resolve a switch prong which is determined at comptime to have no peers.
     /// Uses `resolveBlockBody`. Sets up captures as needed.
     fn resolveProngComptime(
@@ -11086,7 +11183,15 @@ const SwitchProngAnalysis = struct {
         const sema = spa.sema;
         const pt = sema.pt;
         const zcu = pt.zcu;
-        const operand_ty = sema.typeOf(spa.operand);
+        const operand_ty = switch (spa.operand) {
+            .simple => |s| sema.typeOf(s.by_val),
+            .loop => |l| ty: {
+                const alloc_ty = sema.typeOf(l.operand_alloc);
+                const alloc_child = alloc_ty.childType(zcu);
+                if (l.operand_is_ref) break :ty alloc_child.childType(zcu);
+                break :ty alloc_child;
+            },
+        };
         if (operand_ty.zigTypeTag(zcu) != .@"union") {
             const tag_capture_src: LazySrcLoc = .{
                 .base_node_inst = capture_src.base_node_inst,
@@ -11117,10 +11222,24 @@ const SwitchProngAnalysis = struct {
         const zir_datas = sema.code.instructions.items(.data);
         const switch_node_offset = zir_datas[@intFromEnum(spa.switch_block_inst)].pl_node.src_node;
 
-        const operand_ty = sema.typeOf(spa.operand);
-        const operand_ptr_ty = if (capture_byref) sema.typeOf(spa.operand_ptr) else undefined;
         const operand_src = block.src(.{ .node_offset_switch_operand = switch_node_offset });
 
+        const operand_val, const operand_ptr = switch (spa.operand) {
+            .simple => |s| .{ s.by_val, s.by_ref },
+            .loop => |l| op: {
+                const loaded = try sema.analyzeLoad(block, operand_src, l.operand_alloc, operand_src);
+                if (l.operand_is_ref) {
+                    const by_val = try sema.analyzeLoad(block, operand_src, loaded, operand_src);
+                    break :op .{ by_val, loaded };
+                } else {
+                    break :op .{ loaded, undefined };
+                }
+            },
+        };
+
+        const operand_ty = sema.typeOf(operand_val);
+        const operand_ptr_ty = if (capture_byref) sema.typeOf(operand_ptr) else undefined;
+
         if (inline_case_capture != .none) {
             const item_val = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, inline_case_capture, undefined) catch unreachable;
             if (operand_ty.zigTypeTag(zcu) == .@"union") {
@@ -11136,16 +11255,16 @@ const SwitchProngAnalysis = struct {
                             .address_space = operand_ptr_ty.ptrAddressSpace(zcu),
                         },
                     });
-                    if (try sema.resolveDefinedValue(block, operand_src, spa.operand_ptr)) |union_ptr| {
+                    if (try sema.resolveDefinedValue(block, operand_src, operand_ptr)) |union_ptr| {
                         return Air.internedToRef((try union_ptr.ptrField(field_index, pt)).toIntern());
                     }
-                    return block.addStructFieldPtr(spa.operand_ptr, field_index, ptr_field_ty);
+                    return block.addStructFieldPtr(operand_ptr, field_index, ptr_field_ty);
                 } else {
-                    if (try sema.resolveDefinedValue(block, operand_src, spa.operand)) |union_val| {
+                    if (try sema.resolveDefinedValue(block, operand_src, operand_val)) |union_val| {
                         const tag_and_val = ip.indexToKey(union_val.toIntern()).un;
                         return Air.internedToRef(tag_and_val.val);
                     }
-                    return block.addStructFieldVal(spa.operand, field_index, field_ty);
+                    return block.addStructFieldVal(operand_val, field_index, field_ty);
                 }
             } else if (capture_byref) {
                 return sema.uavRef(item_val.toIntern());
@@ -11156,17 +11275,17 @@ const SwitchProngAnalysis = struct {
 
         if (is_special_prong) {
             if (capture_byref) {
-                return spa.operand_ptr;
+                return operand_ptr;
             }
 
             switch (operand_ty.zigTypeTag(zcu)) {
                 .error_set => if (spa.else_error_ty) |ty| {
-                    return sema.bitCast(block, ty, spa.operand, operand_src, null);
+                    return sema.bitCast(block, ty, operand_val, operand_src, null);
                 } else {
                     try sema.analyzeUnreachable(block, operand_src, false);
                     return .unreachable_value;
                 },
-                else => return spa.operand,
+                else => return operand_val,
             }
         }
 
@@ -11265,19 +11384,19 @@ const SwitchProngAnalysis = struct {
                         };
                     };
 
-                    if (try sema.resolveDefinedValue(block, operand_src, spa.operand_ptr)) |op_ptr_val| {
+                    if (try sema.resolveDefinedValue(block, operand_src, operand_ptr)) |op_ptr_val| {
                         if (op_ptr_val.isUndef(zcu)) return pt.undefRef(capture_ptr_ty);
                         const field_ptr_val = try op_ptr_val.ptrField(first_field_index, pt);
                         return Air.internedToRef((try pt.getCoerced(field_ptr_val, capture_ptr_ty)).toIntern());
                     }
 
                     try sema.requireRuntimeBlock(block, operand_src, null);
-                    return block.addStructFieldPtr(spa.operand_ptr, first_field_index, capture_ptr_ty);
+                    return block.addStructFieldPtr(operand_ptr, first_field_index, capture_ptr_ty);
                 }
 
-                if (try sema.resolveDefinedValue(block, operand_src, spa.operand)) |operand_val| {
-                    if (operand_val.isUndef(zcu)) return pt.undefRef(capture_ty);
-                    const union_val = ip.indexToKey(operand_val.toIntern()).un;
+                if (try sema.resolveDefinedValue(block, operand_src, operand_val)) |operand_val_val| {
+                    if (operand_val_val.isUndef(zcu)) return pt.undefRef(capture_ty);
+                    const union_val = ip.indexToKey(operand_val_val.toIntern()).un;
                     if (Value.fromInterned(union_val.tag).isUndef(zcu)) return pt.undefRef(capture_ty);
                     const uncoerced = Air.internedToRef(union_val.val);
                     return sema.coerce(block, capture_ty, uncoerced, operand_src);
@@ -11286,7 +11405,7 @@ const SwitchProngAnalysis = struct {
                 try sema.requireRuntimeBlock(block, operand_src, null);
 
                 if (same_types) {
-                    return block.addStructFieldVal(spa.operand, first_field_index, capture_ty);
+                    return block.addStructFieldVal(operand_val, first_field_index, capture_ty);
                 }
 
                 // We may have to emit a switch block which coerces the operand to the capture type.
@@ -11300,7 +11419,7 @@ const SwitchProngAnalysis = struct {
                     }
                     // All fields are in-memory coercible to the resolved type!
                     // Just take the first field and bitcast the result.
-                    const uncoerced = try block.addStructFieldVal(spa.operand, first_field_index, first_field_ty);
+                    const uncoerced = try block.addStructFieldVal(operand_val, first_field_index, first_field_ty);
                     return block.addBitCast(capture_ty, uncoerced);
                 };
 
@@ -11364,7 +11483,7 @@ const SwitchProngAnalysis = struct {
 
                         const field_idx = field_indices[idx];
                         const field_ty = Type.fromInterned(union_obj.field_types.get(ip)[field_idx]);
-                        const uncoerced = try coerce_block.addStructFieldVal(spa.operand, field_idx, field_ty);
+                        const uncoerced = try coerce_block.addStructFieldVal(operand_val, field_idx, field_ty);
                         const coerced = try sema.coerce(&coerce_block, capture_ty, uncoerced, case_src);
                         _ = try coerce_block.addBr(capture_block_inst, coerced);
 
@@ -11388,7 +11507,7 @@ const SwitchProngAnalysis = struct {
                     const first_imc_item_idx = in_mem_coercible.findFirstSet().?;
                     const first_imc_field_idx = field_indices[first_imc_item_idx];
                     const first_imc_field_ty = Type.fromInterned(union_obj.field_types.get(ip)[first_imc_field_idx]);
-                    const uncoerced = try coerce_block.addStructFieldVal(spa.operand, first_imc_field_idx, first_imc_field_ty);
+                    const uncoerced = try coerce_block.addStructFieldVal(operand_val, first_imc_field_idx, first_imc_field_ty);
                     const coerced = try coerce_block.addBitCast(capture_ty, uncoerced);
                     _ = try coerce_block.addBr(capture_block_inst, coerced);
 
@@ -11404,21 +11523,47 @@ const SwitchProngAnalysis = struct {
                 const switch_br_inst: u32 = @intCast(sema.air_instructions.len);
                 try sema.air_instructions.append(sema.gpa, .{
                     .tag = .switch_br,
-                    .data = .{ .pl_op = .{
-                        .operand = spa.cond,
-                        .payload = sema.addExtraAssumeCapacity(Air.SwitchBr{
-                            .cases_len = @intCast(prong_count),
-                            .else_body_len = @intCast(else_body_len),
-                        }),
-                    } },
+                    .data = .{
+                        .pl_op = .{
+                            .operand = undefined, // set by switch below
+                            .payload = sema.addExtraAssumeCapacity(Air.SwitchBr{
+                                .cases_len = @intCast(prong_count),
+                                .else_body_len = @intCast(else_body_len),
+                            }),
+                        },
+                    },
                 });
                 sema.air_extra.appendSliceAssumeCapacity(cases_extra.items);
 
                 // Set up block body
-                sema.air_instructions.items(.data)[@intFromEnum(capture_block_inst)].ty_pl.payload = sema.addExtraAssumeCapacity(Air.Block{
-                    .body_len = 1,
-                });
-                sema.air_extra.appendAssumeCapacity(switch_br_inst);
+                switch (spa.operand) {
+                    .simple => |s| {
+                        const air_datas = sema.air_instructions.items(.data);
+                        air_datas[switch_br_inst].pl_op.operand = s.cond;
+                        air_datas[@intFromEnum(capture_block_inst)].ty_pl.payload = sema.addExtraAssumeCapacity(Air.Block{
+                            .body_len = 1,
+                        });
+                        sema.air_extra.appendAssumeCapacity(switch_br_inst);
+                    },
+                    .loop => {
+                        // The block must first extract the tag from the loaded union.
+                        const tag_inst: Air.Inst.Index = @enumFromInt(sema.air_instructions.len);
+                        try sema.air_instructions.append(sema.gpa, .{
+                            .tag = .get_union_tag,
+                            .data = .{ .ty_op = .{
+                                .ty = Air.internedToRef(union_obj.enum_tag_ty),
+                                .operand = operand_val,
+                            } },
+                        });
+                        const air_datas = sema.air_instructions.items(.data);
+                        air_datas[switch_br_inst].pl_op.operand = tag_inst.toRef();
+                        air_datas[@intFromEnum(capture_block_inst)].ty_pl.payload = sema.addExtraAssumeCapacity(Air.Block{
+                            .body_len = 2,
+                        });
+                        sema.air_extra.appendAssumeCapacity(@intFromEnum(tag_inst));
+                        sema.air_extra.appendAssumeCapacity(switch_br_inst);
+                    },
+                }
 
                 return capture_block_inst.toRef();
             },
@@ -11435,7 +11580,7 @@ const SwitchProngAnalysis = struct {
                 if (case_vals.len == 1) {
                     const item_val = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, case_vals[0], undefined) catch unreachable;
                     const item_ty = try pt.singleErrorSetType(item_val.getErrorName(zcu).unwrap().?);
-                    return sema.bitCast(block, item_ty, spa.operand, operand_src, null);
+                    return sema.bitCast(block, item_ty, operand_val, operand_src, null);
                 }
 
                 var names: InferredErrorSet.NameMap = .{};
@@ -11445,15 +11590,15 @@ const SwitchProngAnalysis = struct {
                     names.putAssumeCapacityNoClobber(err_val.getErrorName(zcu).unwrap().?, {});
                 }
                 const error_ty = try pt.errorSetFromUnsortedNames(names.keys());
-                return sema.bitCast(block, error_ty, spa.operand, operand_src, null);
+                return sema.bitCast(block, error_ty, operand_val, operand_src, null);
             },
             else => {
                 // In this case the capture value is just the passed-through value
                 // of the switch condition.
                 if (capture_byref) {
-                    return spa.operand_ptr;
+                    return operand_ptr;
                 } else {
-                    return spa.operand;
+                    return operand_val;
                 }
             },
         }
@@ -11686,9 +11831,13 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
     var spa: SwitchProngAnalysis = .{
         .sema = sema,
         .parent_block = block,
-        .operand = undefined, // must be set to the unwrapped error code before use
-        .operand_ptr = .none,
-        .cond = raw_operand_val,
+        .operand = .{
+            .simple = .{
+                .by_val = undefined, // must be set to the unwrapped error code before use
+                .by_ref = undefined,
+                .cond = raw_operand_val,
+            },
+        },
         .else_error_ty = else_error_ty,
         .switch_block_inst = inst,
         .tag_capture_inst = undefined,
@@ -11709,13 +11858,13 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
                     .name = operand_val.getErrorName(zcu).unwrap().?,
                 },
             }));
-            spa.operand = if (extra.data.bits.payload_is_ref)
+            spa.operand.simple.by_val = if (extra.data.bits.payload_is_ref)
                 try sema.analyzeErrUnionCodePtr(block, switch_operand_src, raw_operand_val)
             else
                 try sema.analyzeErrUnionCode(block, switch_operand_src, raw_operand_val);
 
             if (extra.data.bits.any_uses_err_capture) {
-                sema.inst_map.putAssumeCapacity(err_capture_inst, spa.operand);
+                sema.inst_map.putAssumeCapacity(err_capture_inst, spa.operand.simple.by_val);
             }
             defer if (extra.data.bits.any_uses_err_capture) assert(sema.inst_map.remove(err_capture_inst));
 
@@ -11723,7 +11872,7 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
                 sema,
                 spa,
                 &child_block,
-                try sema.switchCond(block, switch_operand_src, spa.operand),
+                try sema.switchCond(block, switch_operand_src, spa.operand.simple.by_val),
                 err_val,
                 operand_err_set_ty,
                 switch_src_node_offset,
@@ -11777,20 +11926,20 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
     const true_instructions = try sub_block.instructions.toOwnedSlice(gpa);
     defer gpa.free(true_instructions);
 
-    spa.operand = if (extra.data.bits.payload_is_ref)
+    spa.operand.simple.by_val = if (extra.data.bits.payload_is_ref)
         try sema.analyzeErrUnionCodePtr(&sub_block, switch_operand_src, raw_operand_val)
     else
         try sema.analyzeErrUnionCode(&sub_block, switch_operand_src, raw_operand_val);
 
     if (extra.data.bits.any_uses_err_capture) {
-        sema.inst_map.putAssumeCapacity(err_capture_inst, spa.operand);
+        sema.inst_map.putAssumeCapacity(err_capture_inst, spa.operand.simple.by_val);
     }
     defer if (extra.data.bits.any_uses_err_capture) assert(sema.inst_map.remove(err_capture_inst));
     _ = try sema.analyzeSwitchRuntimeBlock(
         spa,
         &sub_block,
         switch_src,
-        try sema.switchCond(block, switch_operand_src, spa.operand),
+        try sema.switchCond(block, switch_operand_src, spa.operand.simple.by_val),
         operand_err_set_ty,
         switch_operand_src,
         case_vals,
@@ -11859,17 +12008,63 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
     const special_prong_src = block.src(.{ .node_offset_switch_special_prong = src_node_offset });
     const extra = sema.code.extraData(Zir.Inst.SwitchBlock, inst_data.payload_index);
 
-    const raw_operand_val: Air.Inst.Ref, const raw_operand_ptr: Air.Inst.Ref = blk: {
+    const operand: SwitchProngAnalysis.Operand, const raw_operand_ty: Type = op: {
         const maybe_ptr = try sema.resolveInst(extra.data.operand);
-        if (operand_is_ref) {
-            const val = try sema.analyzeLoad(block, src, maybe_ptr, operand_src);
-            break :blk .{ val, maybe_ptr };
-        } else {
-            break :blk .{ maybe_ptr, undefined };
+        const val, const ref = if (operand_is_ref)
+            .{ try sema.analyzeLoad(block, src, maybe_ptr, operand_src), maybe_ptr }
+        else
+            .{ maybe_ptr, undefined };
+
+        const init_cond = try sema.switchCond(block, operand_src, val);
+
+        const operand_ty = sema.typeOf(val);
+
+        if (extra.data.bits.has_continue and !block.is_comptime) {
+            // Even if the operand is comptime-known, this `switch` is runtime.
+            if (try operand_ty.comptimeOnlySema(pt)) {
+                return sema.failWithOwnedErrorMsg(block, msg: {
+                    const msg = try sema.errMsg(operand_src, "operand of switch loop has comptime-only type '{}'", .{operand_ty.fmt(pt)});
+                    errdefer msg.destroy(gpa);
+                    try sema.errNote(operand_src, msg, "switch loops are evalauted at runtime outside of comptime scopes", .{});
+                    break :msg msg;
+                });
+            }
+            try sema.validateRuntimeValue(block, operand_src, maybe_ptr);
+            const operand_alloc = if (extra.data.bits.any_non_inline_capture) a: {
+                const operand_ptr_ty = try pt.singleMutPtrType(sema.typeOf(maybe_ptr));
+                const operand_alloc = try block.addTy(.alloc, operand_ptr_ty);
+                _ = try block.addBinOp(.store, operand_alloc, maybe_ptr);
+                break :a operand_alloc;
+            } else undefined;
+            break :op .{
+                .{ .loop = .{
+                    .operand_alloc = operand_alloc,
+                    .operand_is_ref = operand_is_ref,
+                    .init_cond = init_cond,
+                } },
+                operand_ty,
+            };
         }
+
+        // We always use `simple` in the comptime case, because as far as the dispatching logic
+        // is concerned, it really is dispatching a single prong. `resolveSwitchComptime` will
+        // be resposible for recursively resolving different prongs as needed.
+        break :op .{
+            .{ .simple = .{
+                .by_val = val,
+                .by_ref = ref,
+                .cond = init_cond,
+            } },
+            operand_ty,
+        };
     };
 
-    const operand = try sema.switchCond(block, operand_src, raw_operand_val);
+    const union_originally = raw_operand_ty.zigTypeTag(zcu) == .@"union";
+    const err_set = raw_operand_ty.zigTypeTag(zcu) == .error_set;
+    const cond_ty = switch (raw_operand_ty.zigTypeTag(zcu)) {
+        .@"union" => raw_operand_ty.unionTagType(zcu).?, // validated by `switchCond` above
+        else => raw_operand_ty,
+    };
 
     // AstGen guarantees that the instruction immediately preceding
     // switch_block(_ref) is a dbg_stmt
@@ -11919,9 +12114,6 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         },
     };
 
-    const maybe_union_ty = sema.typeOf(raw_operand_val);
-    const union_originally = maybe_union_ty.zigTypeTag(zcu) == .@"union";
-
     // Duplicate checking variables later also used for `inline else`.
     var seen_enum_fields: []?LazySrcLoc = &.{};
     var seen_errors = SwitchErrorSet.init(gpa);
@@ -11937,13 +12129,10 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
 
     var empty_enum = false;
 
-    const operand_ty = sema.typeOf(operand);
-    const err_set = operand_ty.zigTypeTag(zcu) == .error_set;
-
     var else_error_ty: ?Type = null;
 
     // Validate usage of '_' prongs.
-    if (special_prong == .under and (!operand_ty.isNonexhaustiveEnum(zcu) or union_originally)) {
+    if (special_prong == .under and !raw_operand_ty.isNonexhaustiveEnum(zcu)) {
         const msg = msg: {
             const msg = try sema.errMsg(
                 src,
@@ -11969,11 +12158,11 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
     }
 
     // Validate for duplicate items, missing else prong, and invalid range.
-    switch (operand_ty.zigTypeTag(zcu)) {
+    switch (cond_ty.zigTypeTag(zcu)) {
         .@"union" => unreachable, // handled in `switchCond`
         .@"enum" => {
-            seen_enum_fields = try gpa.alloc(?LazySrcLoc, operand_ty.enumFieldCount(zcu));
-            empty_enum = seen_enum_fields.len == 0 and !operand_ty.isNonexhaustiveEnum(zcu);
+            seen_enum_fields = try gpa.alloc(?LazySrcLoc, cond_ty.enumFieldCount(zcu));
+            empty_enum = seen_enum_fields.len == 0 and !cond_ty.isNonexhaustiveEnum(zcu);
             @memset(seen_enum_fields, null);
             // `range_set` is used for non-exhaustive enum values that do not correspond to any tags.
 
@@ -11991,7 +12180,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                         seen_enum_fields,
                         &range_set,
                         item_ref,
-                        operand_ty,
+                        cond_ty,
                         block.src(.{ .switch_case_item = .{
                             .switch_node_offset = src_node_offset,
                             .case_idx = .{ .kind = .scalar, .index = @intCast(scalar_i) },
@@ -12019,7 +12208,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                             seen_enum_fields,
                             &range_set,
                             item_ref,
-                            operand_ty,
+                            cond_ty,
                             block.src(.{ .switch_case_item = .{
                                 .switch_node_offset = src_node_offset,
                                 .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) },
@@ -12028,7 +12217,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                         ));
                     }
 
-                    try sema.validateSwitchNoRange(block, ranges_len, operand_ty, src_node_offset);
+                    try sema.validateSwitchNoRange(block, ranges_len, cond_ty, src_node_offset);
                 }
             }
             const all_tags_handled = for (seen_enum_fields) |seen_src| {
@@ -12036,7 +12225,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
             } else true;
 
             if (special_prong == .@"else") {
-                if (all_tags_handled and !operand_ty.isNonexhaustiveEnum(zcu)) return sema.fail(
+                if (all_tags_handled and !cond_ty.isNonexhaustiveEnum(zcu)) return sema.fail(
                     block,
                     special_prong_src,
                     "unreachable else prong; all cases already handled",
@@ -12053,9 +12242,9 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                     for (seen_enum_fields, 0..) |seen_src, i| {
                         if (seen_src != null) continue;
 
-                        const field_name = operand_ty.enumFieldName(i, zcu);
+                        const field_name = cond_ty.enumFieldName(i, zcu);
                         try sema.addFieldErrNote(
-                            operand_ty,
+                            cond_ty,
                             i,
                             msg,
                             "unhandled enumeration value: '{}'",
@@ -12063,15 +12252,15 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                         );
                     }
                     try sema.errNote(
-                        operand_ty.srcLoc(zcu),
+                        cond_ty.srcLoc(zcu),
                         msg,
                         "enum '{}' declared here",
-                        .{operand_ty.fmt(pt)},
+                        .{cond_ty.fmt(pt)},
                     );
                     break :msg msg;
                 };
                 return sema.failWithOwnedErrorMsg(block, msg);
-            } else if (special_prong == .none and operand_ty.isNonexhaustiveEnum(zcu) and !union_originally) {
+            } else if (special_prong == .none and cond_ty.isNonexhaustiveEnum(zcu) and !union_originally) {
                 return sema.fail(
                     block,
                     src,
@@ -12085,7 +12274,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
             block,
             &seen_errors,
             &case_vals,
-            operand_ty,
+            cond_ty,
             inst_data,
             scalar_cases_len,
             multi_cases_len,
@@ -12106,7 +12295,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                         block,
                         &range_set,
                         item_ref,
-                        operand_ty,
+                        cond_ty,
                         block.src(.{ .switch_case_item = .{
                             .switch_node_offset = src_node_offset,
                             .case_idx = .{ .kind = .scalar, .index = @intCast(scalar_i) },
@@ -12133,7 +12322,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                             block,
                             &range_set,
                             item_ref,
-                            operand_ty,
+                            cond_ty,
                             block.src(.{ .switch_case_item = .{
                                 .switch_node_offset = src_node_offset,
                                 .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) },
@@ -12155,7 +12344,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                             &range_set,
                             item_first,
                             item_last,
-                            operand_ty,
+                            cond_ty,
                             block.src(.{ .switch_case_item = .{
                                 .switch_node_offset = src_node_offset,
                                 .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) },
@@ -12171,9 +12360,9 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
             }
 
             check_range: {
-                if (operand_ty.zigTypeTag(zcu) == .int) {
-                    const min_int = try operand_ty.minInt(pt, operand_ty);
-                    const max_int = try operand_ty.maxInt(pt, operand_ty);
+                if (cond_ty.zigTypeTag(zcu) == .int) {
+                    const min_int = try cond_ty.minInt(pt, cond_ty);
+                    const max_int = try cond_ty.maxInt(pt, cond_ty);
                     if (try range_set.spans(min_int.toIntern(), max_int.toIntern())) {
                         if (special_prong == .@"else") {
                             return sema.fail(
@@ -12246,7 +12435,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                         ));
                     }
 
-                    try sema.validateSwitchNoRange(block, ranges_len, operand_ty, src_node_offset);
+                    try sema.validateSwitchNoRange(block, ranges_len, cond_ty, src_node_offset);
                 }
             }
             switch (special_prong) {
@@ -12278,7 +12467,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                     block,
                     src,
                     "else prong required when switching on type '{}'",
-                    .{operand_ty.fmt(pt)},
+                    .{cond_ty.fmt(pt)},
                 );
             }
 
@@ -12299,7 +12488,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                         block,
                         &seen_values,
                         item_ref,
-                        operand_ty,
+                        cond_ty,
                         block.src(.{ .switch_case_item = .{
                             .switch_node_offset = src_node_offset,
                             .case_idx = .{ .kind = .scalar, .index = @intCast(scalar_i) },
@@ -12326,7 +12515,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                             block,
                             &seen_values,
                             item_ref,
-                            operand_ty,
+                            cond_ty,
                             block.src(.{ .switch_case_item = .{
                                 .switch_node_offset = src_node_offset,
                                 .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) },
@@ -12335,7 +12524,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                         ));
                     }
 
-                    try sema.validateSwitchNoRange(block, ranges_len, operand_ty, src_node_offset);
+                    try sema.validateSwitchNoRange(block, ranges_len, cond_ty, src_node_offset);
                 }
             }
         },
@@ -12354,16 +12543,14 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         .comptime_float,
         .float,
         => return sema.fail(block, operand_src, "invalid switch operand type '{}'", .{
-            operand_ty.fmt(pt),
+            raw_operand_ty.fmt(pt),
         }),
     }
 
     const spa: SwitchProngAnalysis = .{
         .sema = sema,
         .parent_block = block,
-        .operand = raw_operand_val,
-        .operand_ptr = raw_operand_ptr,
-        .cond = operand,
+        .operand = operand,
         .else_error_ty = else_error_ty,
         .switch_block_inst = inst,
         .tag_capture_inst = tag_capture_inst,
@@ -12407,24 +12594,6 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
     defer child_block.instructions.deinit(gpa);
     defer merges.deinit(gpa);
 
-    if (try sema.resolveDefinedValue(&child_block, src, operand)) |operand_val| {
-        return resolveSwitchComptime(
-            sema,
-            spa,
-            &child_block,
-            operand,
-            operand_val,
-            operand_ty,
-            src_node_offset,
-            special,
-            case_vals,
-            scalar_cases_len,
-            multi_cases_len,
-            err_set,
-            empty_enum,
-        );
-    }
-
     if (scalar_cases_len + multi_cases_len == 0 and !special.is_inline) {
         if (empty_enum) {
             return .void_value;
@@ -12432,54 +12601,90 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         if (special_prong == .none) {
             return sema.fail(block, src, "switch must handle all possibilities", .{});
         }
-        if (err_set and try sema.maybeErrorUnwrap(block, special.body, operand, operand_src, false)) {
-            return .unreachable_value;
-        }
-        if (zcu.backendSupportsFeature(.is_named_enum_value) and block.wantSafety() and operand_ty.zigTypeTag(zcu) == .@"enum" and
-            (!operand_ty.isNonexhaustiveEnum(zcu) or union_originally))
+        const init_cond = switch (operand) {
+            .simple => |s| s.cond,
+            .loop => |l| l.init_cond,
+        };
+        if (zcu.backendSupportsFeature(.is_named_enum_value) and block.wantSafety() and
+            raw_operand_ty.zigTypeTag(zcu) == .@"enum" and !raw_operand_ty.isNonexhaustiveEnum(zcu))
         {
             try sema.zirDbgStmt(block, cond_dbg_node_index);
-            const ok = try block.addUnOp(.is_named_enum_value, operand);
+            const ok = try block.addUnOp(.is_named_enum_value, init_cond);
             try sema.addSafetyCheck(block, src, ok, .corrupt_switch);
         }
+        if (err_set and try sema.maybeErrorUnwrap(block, special.body, init_cond, operand_src, false)) {
+            return .unreachable_value;
+        }
+    }
 
-        return spa.resolveProngComptime(
-            &child_block,
-            .special,
-            special.body,
-            special.capture,
-            block.src(.{ .switch_capture = .{
-                .switch_node_offset = src_node_offset,
-                .case_idx = LazySrcLoc.Offset.SwitchCaseIndex.special,
-            } }),
-            undefined, // case_vals may be undefined for special prongs
-            .none,
-            false,
-            merges,
-        );
+    switch (operand) {
+        .loop => {}, // always runtime; evaluation in comptime scope uses `simple`
+        .simple => |s| {
+            if (try sema.resolveDefinedValue(&child_block, src, s.cond)) |cond_val| {
+                return resolveSwitchComptimeLoop(
+                    sema,
+                    spa,
+                    &child_block,
+                    if (operand_is_ref)
+                        sema.typeOf(s.by_ref)
+                    else
+                        raw_operand_ty,
+                    cond_ty,
+                    cond_val,
+                    src_node_offset,
+                    special,
+                    case_vals,
+                    scalar_cases_len,
+                    multi_cases_len,
+                    err_set,
+                    empty_enum,
+                    operand_is_ref,
+                );
+            }
+
+            if (scalar_cases_len + multi_cases_len == 0 and !special.is_inline and !extra.data.bits.has_continue) {
+                return spa.resolveProngComptime(
+                    &child_block,
+                    .special,
+                    special.body,
+                    special.capture,
+                    block.src(.{ .switch_capture = .{
+                        .switch_node_offset = src_node_offset,
+                        .case_idx = LazySrcLoc.Offset.SwitchCaseIndex.special,
+                    } }),
+                    undefined, // case_vals may be undefined for special prongs
+                    .none,
+                    false,
+                    merges,
+                );
+            }
+        },
     }
 
     if (child_block.is_comptime) {
-        _ = try sema.resolveConstDefinedValue(&child_block, operand_src, operand, .{
+        _ = try sema.resolveConstDefinedValue(&child_block, operand_src, operand.simple.cond, .{
             .needed_comptime_reason = "condition in comptime switch must be comptime-known",
             .block_comptime_reason = child_block.comptime_reason,
         });
         unreachable;
     }
 
-    _ = try sema.analyzeSwitchRuntimeBlock(
+    const air_switch_ref = try sema.analyzeSwitchRuntimeBlock(
         spa,
         &child_block,
         src,
-        operand,
-        operand_ty,
+        switch (operand) {
+            .simple => |s| s.cond,
+            .loop => |l| l.init_cond,
+        },
+        cond_ty,
         operand_src,
         case_vals,
         special,
         scalar_cases_len,
         multi_cases_len,
         union_originally,
-        maybe_union_ty,
+        raw_operand_ty,
         err_set,
         src_node_offset,
         special_prong_src,
@@ -12492,6 +12697,67 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         false,
     );
 
+    for (merges.extra_insts.items, merges.extra_src_locs.items) |placeholder_inst, dispatch_src| {
+        var replacement_block = block.makeSubBlock();
+        defer replacement_block.instructions.deinit(gpa);
+
+        assert(sema.air_instructions.items(.tag)[@intFromEnum(placeholder_inst)] == .br);
+        const new_operand_maybe_ref = sema.air_instructions.items(.data)[@intFromEnum(placeholder_inst)].br.operand;
+
+        if (extra.data.bits.any_non_inline_capture) {
+            _ = try replacement_block.addBinOp(.store, operand.loop.operand_alloc, new_operand_maybe_ref);
+        }
+
+        const new_operand_val = if (operand_is_ref)
+            try sema.analyzeLoad(&replacement_block, dispatch_src, new_operand_maybe_ref, dispatch_src)
+        else
+            new_operand_maybe_ref;
+
+        const new_cond = try sema.switchCond(&replacement_block, dispatch_src, new_operand_val);
+
+        if (zcu.backendSupportsFeature(.is_named_enum_value) and block.wantSafety() and
+            cond_ty.zigTypeTag(zcu) == .@"enum" and !cond_ty.isNonexhaustiveEnum(zcu) and
+            !try sema.isComptimeKnown(new_cond))
+        {
+            const ok = try replacement_block.addUnOp(.is_named_enum_value, new_cond);
+            try sema.addSafetyCheck(&replacement_block, src, ok, .corrupt_switch);
+        }
+
+        _ = try replacement_block.addInst(.{
+            .tag = .switch_dispatch,
+            .data = .{ .br = .{
+                .block_inst = air_switch_ref.toIndex().?,
+                .operand = new_cond,
+            } },
+        });
+
+        if (replacement_block.instructions.items.len == 1) {
+            // Optimization: we don't need a block!
+            sema.air_instructions.set(
+                @intFromEnum(placeholder_inst),
+                sema.air_instructions.get(@intFromEnum(replacement_block.instructions.items[0])),
+            );
+            continue;
+        }
+
+        // Replace placeholder with a block.
+        // No `br` is needed as the block is a switch dispatch so necessarily `noreturn`.
+        try sema.air_extra.ensureUnusedCapacity(
+            gpa,
+            @typeInfo(Air.Block).@"struct".fields.len + replacement_block.instructions.items.len,
+        );
+        sema.air_instructions.set(@intFromEnum(placeholder_inst), .{
+            .tag = .block,
+            .data = .{ .ty_pl = .{
+                .ty = .noreturn_type,
+                .payload = sema.addExtraAssumeCapacity(Air.Block{
+                    .body_len = @intCast(replacement_block.instructions.items.len),
+                }),
+            } },
+        });
+        sema.air_extra.appendSliceAssumeCapacity(@ptrCast(replacement_block.instructions.items));
+    }
+
     return sema.resolveAnalyzedBlock(block, src, &child_block, merges, false);
 }
 
@@ -13123,7 +13389,7 @@ fn analyzeSwitchRuntimeBlock(
     sema.air_extra.appendSliceAssumeCapacity(@ptrCast(else_body));
 
     return try child_block.addInst(.{
-        .tag = .switch_br,
+        .tag = if (spa.operand == .loop) .loop_switch_br else .switch_br,
         .data = .{ .pl_op = .{
             .operand = operand,
             .payload = payload_index,
@@ -13131,6 +13397,77 @@ fn analyzeSwitchRuntimeBlock(
     });
 }
 
+fn resolveSwitchComptimeLoop(
+    sema: *Sema,
+    init_spa: SwitchProngAnalysis,
+    child_block: *Block,
+    maybe_ptr_operand_ty: Type,
+    cond_ty: Type,
+    init_cond_val: Value,
+    switch_node_offset: i32,
+    special: SpecialProng,
+    case_vals: std.ArrayListUnmanaged(Air.Inst.Ref),
+    scalar_cases_len: u32,
+    multi_cases_len: u32,
+    err_set: bool,
+    empty_enum: bool,
+    operand_is_ref: bool,
+) CompileError!Air.Inst.Ref {
+    var spa = init_spa;
+    var cond_val = init_cond_val;
+
+    while (true) {
+        if (resolveSwitchComptime(
+            sema,
+            spa,
+            child_block,
+            spa.operand.simple.cond,
+            cond_val,
+            cond_ty,
+            switch_node_offset,
+            special,
+            case_vals,
+            scalar_cases_len,
+            multi_cases_len,
+            err_set,
+            empty_enum,
+        )) |result| {
+            return result;
+        } else |err| switch (err) {
+            error.ComptimeBreak => {
+                const break_inst = sema.code.instructions.get(@intFromEnum(sema.comptime_break_inst));
+                if (break_inst.tag != .switch_continue) return error.ComptimeBreak;
+                const extra = sema.code.extraData(Zir.Inst.Break, break_inst.data.@"break".payload_index).data;
+                if (extra.block_inst != spa.switch_block_inst) return error.ComptimeBreak;
+                // This is a `switch_continue` targeting this block. Change the operand and start over.
+                const src = child_block.nodeOffset(extra.operand_src_node);
+                const new_operand_uncoerced = try sema.resolveInst(break_inst.data.@"break".operand);
+                const new_operand = try sema.coerce(child_block, maybe_ptr_operand_ty, new_operand_uncoerced, src);
+
+                try sema.emitBackwardBranch(child_block, src);
+
+                const val, const ref = if (operand_is_ref)
+                    .{ try sema.analyzeLoad(child_block, src, new_operand, src), new_operand }
+                else
+                    .{ new_operand, undefined };
+
+                const cond_ref = try sema.switchCond(child_block, src, val);
+
+                cond_val = try sema.resolveConstDefinedValue(child_block, src, cond_ref, .{
+                    .needed_comptime_reason = "condition in comptime switch must be comptime-known",
+                    .block_comptime_reason = child_block.comptime_reason,
+                });
+                spa.operand = .{ .simple = .{
+                    .by_val = val,
+                    .by_ref = ref,
+                    .cond = cond_ref,
+                } };
+            },
+            else => |e| return e,
+        }
+    }
+}
+
 fn resolveSwitchComptime(
     sema: *Sema,
     spa: SwitchProngAnalysis,
@@ -13148,6 +13485,7 @@ fn resolveSwitchComptime(
 ) CompileError!Air.Inst.Ref {
     const merges = &child_block.label.?.merges;
     const resolved_operand_val = try sema.resolveLazyValue(operand_val);
+
     var extra_index: usize = special.end;
     {
         var scalar_i: usize = 0;
src/Value.zig
@@ -292,6 +292,7 @@ pub fn getUnsignedIntInner(
                 .none => 0,
                 else => |payload| Value.fromInterned(payload).getUnsignedIntInner(strat, zcu, tid),
             },
+            .enum_tag => |enum_tag| return Value.fromInterned(enum_tag.int).getUnsignedIntInner(strat, zcu, tid),
             else => null,
         },
     };
test/behavior/switch_loop.zig
@@ -0,0 +1,205 @@
+const builtin = @import("builtin");
+const std = @import("std");
+const expect = std.testing.expect;
+
+test "simple switch loop" {
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
+
+    const S = struct {
+        fn doTheTest() !void {
+            var start: u32 = undefined;
+            start = 32;
+            const result: u32 = s: switch (start) {
+                0 => 0,
+                1 => 1,
+                2 => 2,
+                3 => 3,
+                else => |x| continue :s x / 2,
+            };
+            try expect(result == 2);
+        }
+    };
+    try S.doTheTest();
+    try comptime S.doTheTest();
+}
+
+test "switch loop with ranges" {
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
+
+    const S = struct {
+        fn doTheTest() !void {
+            var start: u32 = undefined;
+            start = 32;
+            const result = s: switch (start) {
+                0...3 => |x| x,
+                else => |x| continue :s x / 2,
+            };
+            try expect(result == 2);
+        }
+    };
+    try S.doTheTest();
+    try comptime S.doTheTest();
+}
+
+test "switch loop on enum" {
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
+
+    const S = struct {
+        const E = enum { a, b, c };
+
+        fn doTheTest() !void {
+            var start: E = undefined;
+            start = .a;
+            const result: u32 = s: switch (start) {
+                .a => continue :s .b,
+                .b => continue :s .c,
+                .c => 123,
+            };
+            try expect(result == 123);
+        }
+    };
+    try S.doTheTest();
+    try comptime S.doTheTest();
+}
+
+test "switch loop on tagged union" {
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
+
+    const S = struct {
+        const U = union(enum) {
+            a: u32,
+            b: f32,
+            c: f32,
+        };
+
+        fn doTheTest() !void {
+            var start: U = undefined;
+            start = .{ .a = 80 };
+            const result = s: switch (start) {
+                .a => |x| switch (x) {
+                    0...49 => continue :s .{ .b = @floatFromInt(x) },
+                    50 => continue :s .{ .c = @floatFromInt(x) },
+                    else => continue :s .{ .a = x / 2 },
+                },
+                .b => |x| x,
+                .c => return error.TestFailed,
+            };
+            try expect(result == 40.0);
+        }
+    };
+    try S.doTheTest();
+    try comptime S.doTheTest();
+}
+
+test "switch loop dispatching instructions" {
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
+
+    const S = struct {
+        const Inst = union(enum) {
+            set: u32,
+            add: u32,
+            sub: u32,
+            end,
+        };
+
+        fn doTheTest() !void {
+            var insts: [5]Inst = undefined;
+            @memcpy(&insts, &[5]Inst{
+                .{ .set = 123 },
+                .{ .add = 100 },
+                .{ .sub = 50 },
+                .{ .sub = 10 },
+                .end,
+            });
+            var i: u32 = 0;
+            var cur: u32 = undefined;
+            eval: switch (insts[0]) {
+                .set => |x| {
+                    cur = x;
+                    i += 1;
+                    continue :eval insts[i];
+                },
+                .add => |x| {
+                    cur += x;
+                    i += 1;
+                    continue :eval insts[i];
+                },
+                .sub => |x| {
+                    cur -= x;
+                    i += 1;
+                    continue :eval insts[i];
+                },
+                .end => {},
+            }
+            try expect(cur == 163);
+        }
+    };
+    try S.doTheTest();
+    try comptime S.doTheTest();
+}
+
+test "switch loop with pointer capture" {
+    if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO
+    if (builtin.zig_backend == .stage2_spirv64) return error.SkipZigTest; // TODO
+
+    const S = struct {
+        const U = union(enum) {
+            a: u32,
+            b: u32,
+            c: u32,
+        };
+
+        fn doTheTest() !void {
+            var a: U = .{ .a = 100 };
+            var b: U = .{ .b = 200 };
+            var c: U = .{ .c = 300 };
+            inc: switch (a) {
+                .a => |*x| {
+                    x.* += 1;
+                    continue :inc b;
+                },
+                .b => |*x| {
+                    x.* += 10;
+                    continue :inc c;
+                },
+                .c => |*x| {
+                    x.* += 50;
+                },
+            }
+            try expect(a.a == 101);
+            try expect(b.b == 210);
+            try expect(c.c == 350);
+        }
+    };
+    try S.doTheTest();
+    try comptime S.doTheTest();
+}
test/behavior.zig
@@ -88,6 +88,7 @@ test {
     _ = @import("behavior/struct_contains_null_ptr_itself.zig");
     _ = @import("behavior/struct_contains_slice_of_itself.zig");
     _ = @import("behavior/switch.zig");
+    _ = @import("behavior/switch_loop.zig");
     _ = @import("behavior/switch_prong_err_enum.zig");
     _ = @import("behavior/switch_prong_implicit_cast.zig");
     _ = @import("behavior/switch_on_captured_error.zig");