Commit ba549a7d67

Justus Klausecker <justus@klausecker.de>
2025-07-10 01:58:02
Add support for both '_' and 'else' prongs at the same time in switch statements If both are used, 'else' handles named members and '_' handles unnamed members. In this case the 'else' prong will be unrolled to an explicit case containing all remaining named values.
1 parent 1d9b1c0
lib/std/zig/Ast.zig
@@ -2877,24 +2877,6 @@ pub const full = struct {
             arrow_token: TokenIndex,
             target_expr: Node.Index,
         };
-
-        /// Returns:
-        ///   `null` if case is not special
-        ///   `.none` if case is else prong
-        ///   Index of underscore otherwise
-        pub fn isSpecial(case: *const SwitchCase, tree: *const Ast) ?Node.OptionalIndex {
-            if (case.ast.values.len == 0) {
-                return .none;
-            }
-            for (case.ast.values) |val| {
-                if (tree.nodeTag(val) == .identifier and
-                    mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(val)), "_"))
-                {
-                    return val.toOptional();
-                }
-            }
-            return null;
-        }
     };
 
     pub const Asm = struct {
lib/std/zig/AstGen.zig
@@ -7662,11 +7662,12 @@ fn switchExpr(
     var scalar_cases_len: u32 = 0;
     var multi_cases_len: u32 = 0;
     var inline_cases_len: u32 = 0;
-    var special_prong: Zir.SpecialProng = .none;
-    var special_node: Ast.Node.OptionalIndex = .none;
+    var else_case_node: Ast.Node.OptionalIndex = .none;
     var else_src: ?Ast.TokenIndex = null;
-    var underscore_src: ?Ast.TokenIndex = null;
+    var underscore_case_node: Ast.Node.OptionalIndex = .none;
     var underscore_node: Ast.Node.OptionalIndex = .none;
+    var underscore_src: ?Ast.TokenIndex = null;
+    var underscore_additional_items: Zir.SpecialProngs.AdditionalItems = .none;
     for (case_nodes) |case_node| {
         const case = tree.fullSwitchCase(case_node).?;
         if (case.payload_token) |payload_token| {
@@ -7687,6 +7688,7 @@ fn switchExpr(
                 any_non_inline_capture = true;
             }
         }
+
         // Check for else prong.
         if (case.ast.values.len == 0) {
             const case_src = case.ast.arrow_token - 1;
@@ -7703,40 +7705,21 @@ fn switchExpr(
                         ),
                     },
                 );
-            } else if (underscore_src) |some_underscore| {
-                return astgen.failNodeNotes(
-                    node,
-                    "else and '_' prong in switch expression",
-                    .{},
-                    &[_]u32{
-                        try astgen.errNoteTok(
-                            case_src,
-                            "else prong here",
-                            .{},
-                        ),
-                        try astgen.errNoteTok(
-                            some_underscore,
-                            "'_' prong here",
-                            .{},
-                        ),
-                    },
-                );
             }
-            special_node = case_node.toOptional();
-            special_prong = .@"else";
+            else_case_node = case_node.toOptional();
             else_src = case_src;
             continue;
         }
 
         // Check for '_' prong.
-        var found_underscore = false;
+        var case_has_underscore = false;
         for (case.ast.values) |val| {
             switch (tree.nodeTag(val)) {
                 .identifier => if (mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(val)), "_")) {
-                    const case_src = case.ast.arrow_token - 1;
+                    const val_src = tree.nodeMainToken(val);
                     if (underscore_src) |src| {
                         return astgen.failTokNotes(
-                            case_src,
+                            val_src,
                             "multiple '_' prongs in switch expression",
                             .{},
                             &[_]u32{
@@ -7747,39 +7730,26 @@ fn switchExpr(
                                 ),
                             },
                         );
-                    } else if (else_src) |some_else| {
-                        return astgen.failNodeNotes(
-                            node,
-                            "else and '_' prong in switch expression",
-                            .{},
-                            &[_]u32{
-                                try astgen.errNoteTok(
-                                    some_else,
-                                    "else prong here",
-                                    .{},
-                                ),
-                                try astgen.errNoteTok(
-                                    case_src,
-                                    "'_' prong here",
-                                    .{},
-                                ),
-                            },
-                        );
                     }
                     if (case.inline_token != null) {
-                        return astgen.failTok(case_src, "cannot inline '_' prong", .{});
+                        return astgen.failTok(val_src, "cannot inline '_' prong", .{});
                     }
-                    special_node = case_node.toOptional();
-                    special_prong = if (case.ast.values.len == 1) .under else .absorbing_under;
-                    underscore_src = case_src;
+                    underscore_case_node = case_node.toOptional();
+                    underscore_src = val_src;
                     underscore_node = val.toOptional();
-                    found_underscore = true;
+                    underscore_additional_items = switch (case.ast.values.len) {
+                        0 => unreachable,
+                        1 => .none,
+                        2 => .one,
+                        else => .many,
+                    };
+                    case_has_underscore = true;
                 },
                 .string_literal => return astgen.failNode(val, "cannot switch on strings", .{}),
                 else => {},
             }
         }
-        if (found_underscore) continue;
+        if (case_has_underscore) continue;
 
         if (case.ast.values.len == 1 and tree.nodeTag(case.ast.values[0]) != .switch_range) {
             scalar_cases_len += 1;
@@ -7791,6 +7761,14 @@ fn switchExpr(
         }
     }
 
+    const special_prongs: Zir.SpecialProngs = .init(
+        else_src != null,
+        underscore_src != null,
+        underscore_additional_items,
+    );
+    const has_else = special_prongs.hasElse();
+    const has_under = special_prongs.hasUnder();
+
     const operand_ri: ResultInfo = .{ .rl = if (any_payload_is_ref) .ref else .none };
 
     astgen.advanceSourceCursorToNode(operand_node);
@@ -7811,7 +7789,9 @@ fn switchExpr(
     const payloads = &astgen.scratch;
     const scratch_top = astgen.scratch.items.len;
     const case_table_start = scratch_top;
-    const scalar_case_table = case_table_start + @intFromBool(special_prong != .none);
+    const else_case_index = if (has_else) case_table_start else undefined;
+    const under_case_index = if (has_under) case_table_start + @intFromBool(has_else) else undefined;
+    const scalar_case_table = case_table_start + @intFromBool(has_else) + @intFromBool(has_under);
     const multi_case_table = scalar_case_table + scalar_cases_len;
     const case_table_end = multi_case_table + multi_cases_len;
     try astgen.scratch.resize(gpa, case_table_end);
@@ -7943,9 +7923,19 @@ fn switchExpr(
 
         const header_index: u32 = @intCast(payloads.items.len);
         const body_len_index = if (is_multi_case) blk: {
-            if (case_node.toOptional() == special_node) {
-                assert(special_prong == .absorbing_under);
-                payloads.items[case_table_start] = header_index;
+            if (case_node.toOptional() == underscore_case_node) {
+                payloads.items[under_case_index] = header_index;
+                if (special_prongs.hasOneAdditionalItem()) {
+                    try payloads.resize(gpa, header_index + 2); // item, body_len
+                    const maybe_item_node = case.ast.values[0];
+                    const item_node = if (maybe_item_node.toOptional() == underscore_node)
+                        case.ast.values[1]
+                    else
+                        maybe_item_node;
+                    const item_inst = try comptimeExpr(parent_gz, scope, item_ri, item_node, .switch_item);
+                    payloads.items[header_index] = @intFromEnum(item_inst);
+                    break :blk header_index + 1;
+                }
             } else {
                 payloads.items[multi_case_table + multi_case_index] = header_index;
                 multi_case_index += 1;
@@ -7985,9 +7975,13 @@ fn switchExpr(
             payloads.items[header_index] = items_len;
             payloads.items[header_index + 1] = ranges_len;
             break :blk header_index + 2;
-        } else if (case_node.toOptional() == special_node) blk: {
-            assert(special_prong != .absorbing_under);
-            payloads.items[case_table_start] = header_index;
+        } else if (case_node.toOptional() == else_case_node) blk: {
+            payloads.items[else_case_index] = header_index;
+            try payloads.resize(gpa, header_index + 1); // body_len
+            break :blk header_index;
+        } else if (case_node.toOptional() == underscore_case_node) blk: {
+            assert(!special_prongs.hasAdditionalItems());
+            payloads.items[under_case_index] = header_index;
             try payloads.resize(gpa, header_index + 1); // body_len
             break :blk header_index;
         } else blk: {
@@ -8048,7 +8042,7 @@ fn switchExpr(
         .operand = raw_operand,
         .bits = Zir.Inst.SwitchBlock.Bits{
             .has_multi_cases = multi_cases_len != 0,
-            .special_prong = special_prong,
+            .special_prongs = special_prongs,
             .any_has_tag_capture = any_has_tag_capture,
             .any_non_inline_capture = any_non_inline_capture,
             .has_continue = switch_full.label_token != null and block_scope.label.?.used_for_continue,
@@ -8067,29 +8061,40 @@ fn switchExpr(
     const zir_datas = astgen.instructions.items(.data);
     zir_datas[@intFromEnum(switch_block)].pl_node.payload_index = payload_index;
 
-    var normal_case_table_start = case_table_start;
-    if (special_prong != .none) {
-        normal_case_table_start += 1;
-
-        const start_index = payloads.items[case_table_start];
+    if (has_else) {
+        const start_index = payloads.items[else_case_index];
+        var end_index = start_index + 1;
+        const prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(payloads.items[start_index]);
+        end_index += prong_info.body_len;
+        astgen.extra.appendSliceAssumeCapacity(payloads.items[start_index..end_index]);
+    }
+    if (has_under) {
+        const start_index = payloads.items[under_case_index];
         var body_len_index = start_index;
         var end_index = start_index;
-        if (special_prong == .absorbing_under) {
-            body_len_index += 2;
-            const items_len = payloads.items[start_index];
-            const ranges_len = payloads.items[start_index + 1];
-            end_index += 3 + items_len + 2 * ranges_len;
-        } else {
-            end_index += 1;
+        switch (underscore_additional_items) {
+            .none => {
+                end_index += 1;
+            },
+            .one => {
+                body_len_index += 1;
+                end_index += 2;
+            },
+            .many => {
+                body_len_index += 2;
+                const items_len = payloads.items[start_index];
+                const ranges_len = payloads.items[start_index + 1];
+                end_index += 3 + items_len + 2 * ranges_len;
+            },
         }
         const prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(payloads.items[body_len_index]);
         end_index += prong_info.body_len;
         astgen.extra.appendSliceAssumeCapacity(payloads.items[start_index..end_index]);
     }
-    for (payloads.items[normal_case_table_start..case_table_end], 0..) |start_index, i| {
+    for (payloads.items[scalar_case_table..case_table_end], 0..) |start_index, i| {
         var body_len_index = start_index;
         var end_index = start_index;
-        const table_index = normal_case_table_start + i;
+        const table_index = scalar_case_table + i;
         if (table_index < multi_case_table) {
             body_len_index += 1;
             end_index += 2;
lib/std/zig/Zir.zig
@@ -3226,9 +3226,14 @@ pub const Inst = struct {
 
     /// 0. multi_cases_len: u32 // If has_multi_cases is set.
     /// 1. tag_capture_inst: u32 // If any_has_tag_capture is set. Index of instruction prongs use to refer to the inline tag capture.
-    /// 2. else_body { // If special_prong != .none
-    ///        items_len: u32, // If special_prong == .absorbing_under
-    ///        ranges_len: u32, // If special_prong == .absorbing_under
+    /// 2. else_body { // If special_prong.hasElse() is set.
+    ///        info: ProngInfo,
+    ///        body member Index for every info.body_len
+    ///     }
+    /// 3. under_body { // If special_prong.hasUnder() is set.
+    ///        item: Ref, // If special_prong.hasOneAdditionalItem() is set.
+    ///        items_len: u32, // If special_prong.hasManyAdditionalItems() is set.
+    ///        ranges_len: u32, // If special_prong.hasManyAdditionalItems() is set.
     ///        info: ProngInfo,
     ///        item: Ref, // for every items_len
     ///        ranges: { // for every ranges_len
@@ -3237,12 +3242,12 @@ pub const Inst = struct {
     ///        }
     ///        body member Index for every info.body_len
     ///     }
-    /// 3. scalar_cases: { // for every scalar_cases_len
+    /// 4. scalar_cases: { // for every scalar_cases_len
     ///        item: Ref,
     ///        info: ProngInfo,
     ///        body member Index for every info.body_len
     ///     }
-    /// 4. multi_cases: { // for every multi_cases_len
+    /// 5. multi_cases: { // for every multi_cases_len
     ///        items_len: u32,
     ///        ranges_len: u32,
     ///        info: ProngInfo,
@@ -3283,16 +3288,17 @@ pub const Inst = struct {
             /// If true, one or more prongs have multiple items.
             has_multi_cases: bool,
             /// Information about the special prong.
-            special_prong: SpecialProng,
+            special_prongs: SpecialProngs,
             /// If true, at least one prong has an inline tag capture.
             any_has_tag_capture: bool,
             /// If true, at least one prong has a capture which may not
             /// be comptime-known via `inline`.
             any_non_inline_capture: bool,
+            /// If true, at least one prong contains a `continue`.
             has_continue: bool,
             scalar_cases_len: ScalarCasesLen,
 
-            pub const ScalarCasesLen = u26;
+            pub const ScalarCasesLen = u25;
         };
 
         pub const MultiProng = struct {
@@ -3868,17 +3874,67 @@ pub const Inst = struct {
     };
 };
 
-pub const SpecialProng = enum(u2) {
-    none,
-    /// Simple else prong.
-    /// `else => {}`
-    @"else",
-    /// Simple '_' prong.
-    /// `_ => {}`
-    under,
-    /// '_' prong with additional items.
-    /// `a, _, b => {}`
-    absorbing_under,
+pub const SpecialProngs = enum(u3) {
+    none = 0b000,
+    /// Simple `else` prong.
+    /// `else => {},`
+    @"else" = 0b001,
+    /// Simple `_` prong.
+    /// `_ => {},`
+    under = 0b010,
+    /// Both an `else` and a `_` prong.
+    /// `else => {},`
+    /// `_ => {},`
+    under_and_else = 0b011,
+    /// `_` prong with 1 additional item.
+    /// `a, _ => {},`
+    under_one_item = 0b100,
+    /// Both an `else` and a `_` prong with 1 additional item.
+    /// `else => {},`
+    /// `a, _ => {},`
+    under_one_item_and_else = 0b101,
+    /// `_` prong with >1 additional items.
+    /// `a, _, b => {},`
+    under_many_items = 0b110,
+    /// Both an `else` and a `_` prong with >1 additional items.
+    /// `else => {},`
+    /// `a, _, b => {},`
+    under_many_items_and_else = 0b111,
+
+    pub const AdditionalItems = enum(u3) {
+        none = @intFromEnum(SpecialProngs.under),
+        one = @intFromEnum(SpecialProngs.under_one_item),
+        many = @intFromEnum(SpecialProngs.under_many_items),
+    };
+
+    pub fn init(has_else: bool, has_under: bool, additional_items: AdditionalItems) SpecialProngs {
+        const else_bit: u3 = @intFromBool(has_else);
+        const under_bits: u3 = if (has_under)
+            @intFromEnum(additional_items)
+        else
+            @intFromEnum(SpecialProngs.none);
+        return @enumFromInt(else_bit | under_bits);
+    }
+
+    pub fn hasElse(special_prongs: SpecialProngs) bool {
+        return (@intFromEnum(special_prongs) & 0b001) != 0;
+    }
+
+    pub fn hasUnder(special_prongs: SpecialProngs) bool {
+        return (@intFromEnum(special_prongs) & 0b110) != 0;
+    }
+
+    pub fn hasAdditionalItems(special_prongs: SpecialProngs) bool {
+        return (@intFromEnum(special_prongs) & 0b100) != 0;
+    }
+
+    pub fn hasOneAdditionalItem(special_prongs: SpecialProngs) bool {
+        return (@intFromEnum(special_prongs) & 0b110) == @intFromEnum(SpecialProngs.under_one_item);
+    }
+
+    pub fn hasManyAdditionalItems(special_prongs: SpecialProngs) bool {
+        return (@intFromEnum(special_prongs) & 0b110) == @intFromEnum(SpecialProngs.under_many_items);
+    }
 };
 
 pub const DeclIterator = struct {
@@ -4723,7 +4779,7 @@ fn findTrackableSwitch(
     }
 
     const has_special = switch (kind) {
-        .normal => extra.data.bits.special_prong != .none,
+        .normal => extra.data.bits.special_prongs != .none,
         .err_union => has_special: {
             // Handle `non_err_body` first.
             const prong_info: Inst.SwitchBlock.ProngInfo = @bitCast(zir.extra[extra_index]);
@@ -4738,29 +4794,40 @@ fn findTrackableSwitch(
     };
 
     if (has_special) {
-        if (kind == .normal) {
-            if (extra.data.bits.special_prong == .absorbing_under) {
-                const items_len = zir.extra[extra_index];
-                extra_index += 1;
-                const ranges_len = zir.extra[extra_index];
-                extra_index += 1;
-                const prong_info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(zir.extra[extra_index]);
-                extra_index += 1;
+        const has_else = if (kind == .normal)
+            extra.data.bits.special_prongs.hasElse()
+        else
+            true;
+        if (has_else) {
+            const prong_info: Inst.SwitchBlock.ProngInfo = @bitCast(zir.extra[extra_index]);
+            extra_index += 1;
+            const body = zir.bodySlice(extra_index, prong_info.body_len);
+            extra_index += body.len;
 
-                extra_index += items_len + ranges_len * 2;
+            try zir.findTrackableBody(gpa, contents, defers, body);
+        }
+        if (kind == .normal) {
+            const special_prongs = extra.data.bits.special_prongs;
 
+            if (special_prongs.hasUnder()) {
+                var trailing_items_len: u32 = 0;
+                if (special_prongs.hasOneAdditionalItem()) {
+                    extra_index += 1;
+                } else if (special_prongs.hasManyAdditionalItems()) {
+                    const items_len = zir.extra[extra_index];
+                    extra_index += 1;
+                    const ranges_len = zir.extra[extra_index];
+                    extra_index += 1;
+                    trailing_items_len = items_len + ranges_len * 2;
+                }
+                const prong_info: Inst.SwitchBlock.ProngInfo = @bitCast(zir.extra[extra_index]);
+                extra_index += 1 + trailing_items_len;
                 const body = zir.bodySlice(extra_index, prong_info.body_len);
                 extra_index += body.len;
 
                 try zir.findTrackableBody(gpa, contents, defers, body);
             }
         }
-        const prong_info: Inst.SwitchBlock.ProngInfo = @bitCast(zir.extra[extra_index]);
-        extra_index += 1;
-        const body = zir.bodySlice(extra_index, prong_info.body_len);
-        extra_index += body.len;
-
-        try zir.findTrackableBody(gpa, contents, defers, body);
     }
 
     {
src/print_zir.zig
@@ -2087,19 +2087,40 @@ const Writer = struct {
 
         self.indent += 2;
 
-        else_prong: {
-            const special_prong = extra.data.bits.special_prong;
-            if (special_prong == .none) break :else_prong;
+        const special_prongs = extra.data.bits.special_prongs;
 
+        if (special_prongs.hasElse()) {
+            const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(self.code.extra[extra_index]);
+            const capture_text = switch (info.capture) {
+                .none => "",
+                .by_val => "by_val ",
+                .by_ref => "by_ref ",
+            };
+            const inline_text = if (info.is_inline) "inline " else "";
+            extra_index += 1;
+            const body = self.code.bodySlice(extra_index, info.body_len);
+            extra_index += body.len;
+
+            try stream.writeAll(",\n");
+            try stream.splatByteAll(' ', self.indent);
+            try stream.print("{s}{s}else => ", .{ capture_text, inline_text });
+            try self.writeBracedBody(stream, body);
+        }
+
+        if (special_prongs.hasUnder()) {
+            var single_item_ref: Zir.Inst.Ref = .none;
             var items_len: u32 = 0;
             var ranges_len: u32 = 0;
-            if (special_prong == .absorbing_under) {
+            if (special_prongs.hasOneAdditionalItem()) {
+                single_item_ref = @enumFromInt(self.code.extra[extra_index]);
+                extra_index += 1;
+            } else if (special_prongs.hasManyAdditionalItems()) {
                 items_len = self.code.extra[extra_index];
                 extra_index += 1;
                 ranges_len = self.code.extra[extra_index];
                 extra_index += 1;
             }
-            const info = @as(Zir.Inst.SwitchBlock.ProngInfo, @bitCast(self.code.extra[extra_index]));
+            const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(self.code.extra[extra_index]);
             extra_index += 1;
             const items = self.code.refSlice(extra_index, items_len);
             extra_index += items_len;
@@ -2112,12 +2133,12 @@ const Writer = struct {
                 .by_ref => try stream.writeAll("by_ref "),
             }
             if (info.is_inline) try stream.writeAll("inline ");
-            switch (special_prong) {
-                .@"else" => try stream.writeAll("else"),
-                .under, .absorbing_under => try stream.writeAll("_"),
-                .none => unreachable,
-            }
 
+            try stream.writeAll("_");
+            if (single_item_ref != .none) {
+                try stream.writeAll(", ");
+                try self.writeInstRef(stream, single_item_ref);
+            }
             for (items) |item_ref| {
                 try stream.writeAll(", ");
                 try self.writeInstRef(stream, item_ref);
@@ -2125,9 +2146,9 @@ const Writer = struct {
 
             var range_i: usize = 0;
             while (range_i < ranges_len) : (range_i += 1) {
-                const item_first = @as(Zir.Inst.Ref, @enumFromInt(self.code.extra[extra_index]));
+                const item_first: Zir.Inst.Ref = @enumFromInt(self.code.extra[extra_index]);
                 extra_index += 1;
-                const item_last = @as(Zir.Inst.Ref, @enumFromInt(self.code.extra[extra_index]));
+                const item_last: Zir.Inst.Ref = @enumFromInt(self.code.extra[extra_index]);
                 extra_index += 1;
 
                 try stream.writeAll(", ");
@@ -2146,9 +2167,9 @@ const Writer = struct {
             const scalar_cases_len = extra.data.bits.scalar_cases_len;
             var scalar_i: usize = 0;
             while (scalar_i < scalar_cases_len) : (scalar_i += 1) {
-                const item_ref = @as(Zir.Inst.Ref, @enumFromInt(self.code.extra[extra_index]));
+                const item_ref: Zir.Inst.Ref = @enumFromInt(self.code.extra[extra_index]);
                 extra_index += 1;
-                const info = @as(Zir.Inst.SwitchBlock.ProngInfo, @bitCast(self.code.extra[extra_index]));
+                const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(self.code.extra[extra_index]);
                 extra_index += 1;
                 const body = self.code.bodySlice(extra_index, info.body_len);
                 extra_index += info.body_len;
@@ -2173,7 +2194,7 @@ const Writer = struct {
                 extra_index += 1;
                 const ranges_len = self.code.extra[extra_index];
                 extra_index += 1;
-                const info = @as(Zir.Inst.SwitchBlock.ProngInfo, @bitCast(self.code.extra[extra_index]));
+                const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(self.code.extra[extra_index]);
                 extra_index += 1;
                 const items = self.code.refSlice(extra_index, items_len);
                 extra_index += items_len;
@@ -2194,9 +2215,9 @@ const Writer = struct {
 
                 var range_i: usize = 0;
                 while (range_i < ranges_len) : (range_i += 1) {
-                    const item_first = @as(Zir.Inst.Ref, @enumFromInt(self.code.extra[extra_index]));
+                    const item_first: Zir.Inst.Ref = @enumFromInt(self.code.extra[extra_index]);
                     extra_index += 1;
-                    const item_last = @as(Zir.Inst.Ref, @enumFromInt(self.code.extra[extra_index]));
+                    const item_last: Zir.Inst.Ref = @enumFromInt(self.code.extra[extra_index]);
                     extra_index += 1;
 
                     if (range_i != 0 or items.len != 0) {
src/Sema.zig
@@ -10928,7 +10928,7 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
     const switch_src = block.nodeOffset(inst_data.src_node);
     const switch_src_node_offset = inst_data.src_node;
     const switch_operand_src = block.src(.{ .node_offset_switch_operand = switch_src_node_offset });
-    const else_prong_src = block.src(.{ .node_offset_switch_special_prong = switch_src_node_offset });
+    const else_prong_src = block.src(.{ .node_offset_switch_else_prong = switch_src_node_offset });
     const extra = sema.code.extraData(Zir.Inst.SwitchBlockErrUnion, inst_data.payload_index);
     const main_operand_src = block.src(.{ .node_offset_if_cond = extra.data.main_src_node_offset });
     const main_src = block.src(.{ .node_offset_main_token = extra.data.main_src_node_offset });
@@ -11122,6 +11122,7 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
                 err_val,
                 operand_err_set_ty,
                 switch_src_node_offset,
+                null,
                 .{
                     .body = else_case.body,
                     .end = else_case.end,
@@ -11129,6 +11130,7 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
                     .is_inline = else_case.is_inline,
                     .has_tag_capture = false,
                 },
+                false,
                 case_vals,
                 scalar_cases_len,
                 multi_cases_len,
@@ -11200,6 +11202,7 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
         true,
         switch_src_node_offset,
         else_prong_src,
+        false,
         undefined,
         seen_errors,
         undefined,
@@ -11207,6 +11210,10 @@ fn zirSwitchBlockErrUnion(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Comp
         undefined,
         cond_dbg_node_index,
         true,
+        null,
+        undefined,
+        &.{},
+        &.{},
     );
 
     try sema.air_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.CondBr).@"struct".fields.len +
@@ -11243,12 +11250,14 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
 
     const pt = sema.pt;
     const zcu = pt.zcu;
+    const ip = &zcu.intern_pool;
     const gpa = sema.gpa;
     const inst_data = sema.code.instructions.items(.data)[@intFromEnum(inst)].pl_node;
     const src = block.nodeOffset(inst_data.src_node);
     const src_node_offset = inst_data.src_node;
     const operand_src = block.src(.{ .node_offset_switch_operand = src_node_offset });
-    const special_prong_src = block.src(.{ .node_offset_switch_special_prong = src_node_offset });
+    const else_prong_src = block.src(.{ .node_offset_switch_else_prong = src_node_offset });
+    const under_prong_src = block.src(.{ .node_offset_switch_under_prong = src_node_offset });
     const extra = sema.code.extraData(Zir.Inst.SwitchBlock, inst_data.payload_index);
 
     const operand: SwitchProngAnalysis.Operand, const raw_operand_ty: Type = op: {
@@ -11335,50 +11344,63 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
     var case_vals = try std.ArrayListUnmanaged(Air.Inst.Ref).initCapacity(gpa, scalar_cases_len + 2 * multi_cases_len);
     defer case_vals.deinit(gpa);
 
+    var single_absorbed_item: Zir.Inst.Ref = .none;
     var absorbed_items: []const Zir.Inst.Ref = &.{};
     var absorbed_ranges: []const Zir.Inst.Ref = &.{};
 
-    const special_prong = extra.data.bits.special_prong;
-    const special: SpecialProng = switch (special_prong) {
-        .none => .{
-            .body = &.{},
-            .end = header_extra_index,
-            .capture = .none,
-            .is_inline = false,
-            .has_tag_capture = false,
-        },
-        .under, .@"else" => blk: {
-            const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[header_extra_index]);
-            const extra_body_start = header_extra_index + 1;
-            break :blk .{
-                .body = sema.code.bodySlice(extra_body_start, info.body_len),
-                .end = extra_body_start + info.body_len,
-                .capture = info.capture,
-                .is_inline = info.is_inline,
-                .has_tag_capture = info.has_tag_capture,
-            };
-        },
-        .absorbing_under => blk: {
-            var extra_index = header_extra_index;
+    const special_prongs = extra.data.bits.special_prongs;
+    const has_else = special_prongs.hasElse();
+    const has_under = special_prongs.hasUnder();
+    const special_else: SpecialProng = if (has_else) blk: {
+        const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[header_extra_index]);
+        const extra_body_start = header_extra_index + 1;
+        break :blk .{
+            .body = sema.code.bodySlice(extra_body_start, info.body_len),
+            .end = extra_body_start + info.body_len,
+            .capture = info.capture,
+            .is_inline = info.is_inline,
+            .has_tag_capture = info.has_tag_capture,
+        };
+    } else .{
+        .body = &.{},
+        .end = header_extra_index,
+        .capture = .none,
+        .is_inline = false,
+        .has_tag_capture = false,
+    };
+    const special_under: SpecialProng = if (has_under) blk: {
+        var extra_index = special_else.end;
+        var trailing_items_len: usize = 0;
+        if (special_prongs.hasOneAdditionalItem()) {
+            single_absorbed_item = @enumFromInt(sema.code.extra[extra_index]);
+            extra_index += 1;
+            absorbed_items = @ptrCast(&single_absorbed_item);
+        } else if (special_prongs.hasManyAdditionalItems()) {
             const items_len = sema.code.extra[extra_index];
             extra_index += 1;
             const ranges_len = sema.code.extra[extra_index];
             extra_index += 1;
-            const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]);
-            extra_index += 1;
-            absorbed_items = sema.code.refSlice(extra_index, items_len);
-            extra_index += items_len;
-            absorbed_ranges = sema.code.refSlice(extra_index, ranges_len * 2);
-            extra_index += ranges_len * 2;
-            break :blk .{
-                .body = sema.code.bodySlice(extra_index, info.body_len),
-                .end = extra_index + info.body_len,
-                .capture = info.capture,
-                .is_inline = info.is_inline,
-                .has_tag_capture = info.has_tag_capture,
-            };
-        },
+            absorbed_items = sema.code.refSlice(extra_index + 1, items_len);
+            absorbed_ranges = sema.code.refSlice(extra_index + 1 + items_len, ranges_len * 2);
+            trailing_items_len = items_len + ranges_len * 2;
+        }
+        const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]);
+        extra_index += 1 + trailing_items_len;
+        break :blk .{
+            .body = sema.code.bodySlice(extra_index, info.body_len),
+            .end = extra_index + info.body_len,
+            .capture = info.capture,
+            .is_inline = info.is_inline,
+            .has_tag_capture = info.has_tag_capture,
+        };
+    } else .{
+        .body = &.{},
+        .end = special_else.end,
+        .capture = .none,
+        .is_inline = false,
+        .has_tag_capture = false,
     };
+    const special_end = special_under.end;
 
     // Duplicate checking variables later also used for `inline else`.
     var seen_enum_fields: []?LazySrcLoc = &.{};
@@ -11398,9 +11420,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
     var else_error_ty: ?Type = null;
 
     // Validate usage of '_' prongs.
-    if ((special_prong == .under or special_prong == .absorbing_under) and
-        !raw_operand_ty.isNonexhaustiveEnum(zcu))
-    {
+    if (has_under and !raw_operand_ty.isNonexhaustiveEnum(zcu)) {
         const msg = msg: {
             const msg = try sema.errMsg(
                 src,
@@ -11409,7 +11429,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
             );
             errdefer msg.destroy(gpa);
             try sema.errNote(
-                special_prong_src,
+                under_prong_src,
                 msg,
                 "'_' prong here",
                 .{},
@@ -11443,14 +11463,14 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                     cond_ty,
                     block.src(.{ .switch_case_item = .{
                         .switch_node_offset = src_node_offset,
-                        .case_idx = .special,
+                        .case_idx = .special_under,
                         .item_idx = .{ .kind = .single, .index = @intCast(item_i) },
                     } }),
                 );
             }
             try sema.validateSwitchNoRange(block, @intCast(absorbed_ranges.len), cond_ty, src_node_offset);
 
-            var extra_index: usize = special.end;
+            var extra_index: usize = special_end;
             {
                 var scalar_i: u32 = 0;
                 while (scalar_i < scalar_cases_len) : (scalar_i += 1) {
@@ -11508,13 +11528,22 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                 if (seen_src == null) break false;
             } else true;
 
-            if (special_prong == .@"else") {
-                if (all_tags_handled and !cond_ty.isNonexhaustiveEnum(zcu)) return sema.fail(
-                    block,
-                    special_prong_src,
-                    "unreachable else prong; all cases already handled",
-                    .{},
-                );
+            if (has_else) {
+                if (all_tags_handled) {
+                    if (cond_ty.isNonexhaustiveEnum(zcu)) {
+                        if (has_under) return sema.fail(
+                            block,
+                            else_prong_src,
+                            "unreachable else prong; all explicit cases already handled",
+                            .{},
+                        );
+                    } else return sema.fail(
+                        block,
+                        else_prong_src,
+                        "unreachable else prong; all cases already handled",
+                        .{},
+                    );
+                }
             } else if (!all_tags_handled) {
                 const msg = msg: {
                     const msg = try sema.errMsg(
@@ -11532,7 +11561,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                             i,
                             msg,
                             "unhandled enumeration value: '{f}'",
-                            .{field_name.fmt(&zcu.intern_pool)},
+                            .{field_name.fmt(ip)},
                         );
                     }
                     try sema.errNote(
@@ -11544,11 +11573,11 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                     break :msg msg;
                 };
                 return sema.failWithOwnedErrorMsg(block, msg);
-            } else if (special_prong == .none and cond_ty.isNonexhaustiveEnum(zcu) and !union_originally) {
+            } else if (special_prongs == .none and cond_ty.isNonexhaustiveEnum(zcu) and !union_originally) {
                 return sema.fail(
                     block,
                     src,
-                    "switch on non-exhaustive enum must include 'else' or '_' prong",
+                    "switch on non-exhaustive enum must include 'else' or '_' prong or both",
                     .{},
                 );
             }
@@ -11562,11 +11591,11 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
             inst_data,
             scalar_cases_len,
             multi_cases_len,
-            .{ .body = special.body, .end = special.end, .src = special_prong_src },
-            special_prong == .@"else",
+            .{ .body = special_else.body, .end = special_else.end, .src = else_prong_src },
+            has_else,
         ),
         .int, .comptime_int => {
-            var extra_index: usize = special.end;
+            var extra_index: usize = special_end;
             {
                 var scalar_i: u32 = 0;
                 while (scalar_i < scalar_cases_len) : (scalar_i += 1) {
@@ -11648,10 +11677,10 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                     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") {
+                        if (has_else) {
                             return sema.fail(
                                 block,
-                                special_prong_src,
+                                else_prong_src,
                                 "unreachable else prong; all cases already handled",
                                 .{},
                             );
@@ -11659,7 +11688,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                         break :check_range;
                     }
                 }
-                if (special_prong != .@"else") {
+                if (special_prongs == .none) {
                     return sema.fail(
                         block,
                         src,
@@ -11670,7 +11699,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
             }
         },
         .bool => {
-            var extra_index: usize = special.end;
+            var extra_index: usize = special_end;
             {
                 var scalar_i: u32 = 0;
                 while (scalar_i < scalar_cases_len) : (scalar_i += 1) {
@@ -11722,31 +11751,28 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                     try sema.validateSwitchNoRange(block, ranges_len, cond_ty, src_node_offset);
                 }
             }
-            switch (special_prong) {
-                .@"else" => {
-                    if (true_count + false_count == 2) {
-                        return sema.fail(
-                            block,
-                            special_prong_src,
-                            "unreachable else prong; all cases already handled",
-                            .{},
-                        );
-                    }
-                },
-                .under, .absorbing_under, .none => {
-                    if (true_count + false_count < 2) {
-                        return sema.fail(
-                            block,
-                            src,
-                            "switch must handle all possibilities",
-                            .{},
-                        );
-                    }
-                },
+            if (has_else) {
+                if (true_count + false_count == 2) {
+                    return sema.fail(
+                        block,
+                        else_prong_src,
+                        "unreachable else prong; all cases already handled",
+                        .{},
+                    );
+                }
+            } else {
+                if (true_count + false_count < 2) {
+                    return sema.fail(
+                        block,
+                        src,
+                        "switch must handle all possibilities",
+                        .{},
+                    );
+                }
             }
         },
         .enum_literal, .void, .@"fn", .pointer, .type => {
-            if (special_prong != .@"else") {
+            if (!has_else) {
                 return sema.fail(
                     block,
                     src,
@@ -11758,7 +11784,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
             var seen_values = ValueSrcMap{};
             defer seen_values.deinit(gpa);
 
-            var extra_index: usize = special.end;
+            var extra_index: usize = special_end;
             {
                 var scalar_i: u32 = 0;
                 while (scalar_i < scalar_cases_len) : (scalar_i += 1) {
@@ -11831,6 +11857,16 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         }),
     }
 
+    var special_members_only: ?SpecialProng = null;
+    var special_members_only_src: LazySrcLoc = undefined;
+    const special_generic, const special_generic_src = if (has_under) b: {
+        if (has_else) {
+            special_members_only = special_else;
+            special_members_only_src = else_prong_src;
+        }
+        break :b .{ special_under, under_prong_src };
+    } else .{ special_else, else_prong_src };
+
     const spa: SwitchProngAnalysis = .{
         .sema = sema,
         .parent_block = block,
@@ -11877,11 +11913,14 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
     defer child_block.instructions.deinit(gpa);
     defer merges.deinit(gpa);
 
-    if (scalar_cases_len + multi_cases_len == 0 and !special.is_inline) {
+    if (scalar_cases_len + multi_cases_len == 0 and
+        special_members_only == null and
+        !special_generic.is_inline)
+    {
         if (empty_enum) {
             return .void_value;
         }
-        if (special_prong == .none) {
+        if (special_prongs == .none) {
             return sema.fail(block, src, "switch must handle all possibilities", .{});
         }
         const init_cond = switch (operand) {
@@ -11895,7 +11934,7 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
             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)) {
+        if (err_set and try sema.maybeErrorUnwrap(block, special_generic.body, init_cond, operand_src, false)) {
             return .unreachable_value;
         }
     }
@@ -11915,7 +11954,9 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                     cond_ty,
                     cond_val,
                     src_node_offset,
-                    special,
+                    special_members_only,
+                    special_generic,
+                    has_under,
                     case_vals,
                     scalar_cases_len,
                     multi_cases_len,
@@ -11925,15 +11966,19 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
                 );
             }
 
-            if (scalar_cases_len + multi_cases_len == 0 and !special.is_inline and !extra.data.bits.has_continue) {
+            if (scalar_cases_len + multi_cases_len == 0 and
+                special_members_only == null and
+                !special_generic.is_inline and
+                !extra.data.bits.has_continue)
+            {
                 return spa.resolveProngComptime(
                     &child_block,
                     .special,
-                    special.body,
-                    special.capture,
+                    special_generic.body,
+                    special_generic.capture,
                     block.src(.{ .switch_capture = .{
                         .switch_node_offset = src_node_offset,
-                        .case_idx = .special,
+                        .case_idx = if (has_under) .special_under else .special_else,
                     } }),
                     undefined, // case_vals may be undefined for special prongs
                     .none,
@@ -11949,6 +11994,88 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         unreachable;
     }
 
+    var extra_case_vals: struct {
+        items: std.ArrayListUnmanaged(Air.Inst.Ref),
+        ranges: std.ArrayListUnmanaged([2]Air.Inst.Ref),
+    } = .{ .items = .empty, .ranges = .empty };
+    defer {
+        extra_case_vals.items.deinit(gpa);
+        extra_case_vals.ranges.deinit(gpa);
+    }
+
+    // Runtime switch, if we have a special_members_only prong we need to unroll
+    // it to a prong with explicit items.
+    // Although this is potentially the same as `inline else` it does not count
+    // towards the backward branch quota because it's an implementation detail.
+    if (special_members_only) |special| gen: {
+        assert(cond_ty.isNonexhaustiveEnum(zcu));
+        _ = special;
+
+        var min_i: usize = math.maxInt(usize);
+        var max_i: usize = 0;
+        var seen_field_count: usize = 0;
+        for (seen_enum_fields, 0..) |seen, enum_i| {
+            if (seen != null) {
+                seen_field_count += 1;
+            } else {
+                min_i = @min(min_i, enum_i);
+                max_i = @max(max_i, enum_i);
+            }
+        }
+        if (min_i == max_i) {
+            seen_enum_fields[min_i] = special_members_only_src;
+            const item_val = try pt.enumValueFieldIndex(cond_ty, @intCast(min_i));
+            const item_ref = Air.internedToRef(item_val.toIntern());
+            try extra_case_vals.items.append(gpa, item_ref);
+            break :gen;
+        }
+        const missing_field_count = seen_enum_fields.len - seen_field_count;
+
+        extra_case_vals.items = try .initCapacity(gpa, missing_field_count / 2);
+        extra_case_vals.ranges = try .initCapacity(gpa, missing_field_count / 4);
+        const int_ty = cond_ty.intTagType(zcu);
+
+        var last_val = try pt.enumValueFieldIndex(cond_ty, @intCast(min_i));
+        var first_ref = Air.internedToRef(last_val.toIntern());
+        seen_enum_fields[min_i] = special_members_only_src;
+        for (seen_enum_fields[(min_i + 1)..(max_i + 1)], (min_i + 1)..) |seen, enum_i| {
+            if (seen != null) continue;
+            seen_enum_fields[enum_i] = special_members_only_src;
+
+            const item_val = try pt.enumValueFieldIndex(cond_ty, @intCast(enum_i));
+            const item_ref = Air.internedToRef(item_val.toIntern());
+
+            const is_next = is_next: {
+                const prev_int = ip.indexToKey(last_val.toIntern()).enum_tag.int;
+
+                const result = try arith.incrementDefinedInt(sema, int_ty, .fromInterned(prev_int));
+                if (result.overflow) break :is_next false;
+
+                const item_int = ip.indexToKey(item_val.toIntern()).enum_tag.int;
+                break :is_next try sema.valuesEqual(.fromInterned(item_int), result.val, int_ty);
+            };
+
+            if (is_next) {
+                last_val = item_val;
+            } else {
+                const last_ref = Air.internedToRef(last_val.toIntern());
+                if (first_ref == last_ref) {
+                    try extra_case_vals.items.append(gpa, first_ref);
+                } else {
+                    try extra_case_vals.ranges.append(gpa, .{ first_ref, last_ref });
+                }
+                first_ref = item_ref;
+                last_val = item_val;
+            }
+        }
+        const last_ref = Air.internedToRef(last_val.toIntern());
+        if (first_ref == last_ref) {
+            try extra_case_vals.items.append(gpa, first_ref);
+        } else {
+            try extra_case_vals.ranges.append(gpa, .{ first_ref, last_ref });
+        }
+    }
+
     const air_switch_ref = try sema.analyzeSwitchRuntimeBlock(
         spa,
         &child_block,
@@ -11960,14 +12087,15 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         cond_ty,
         operand_src,
         case_vals,
-        special,
+        special_generic,
         scalar_cases_len,
         multi_cases_len,
         union_originally,
         raw_operand_ty,
         err_set,
         src_node_offset,
-        special_prong_src,
+        special_generic_src,
+        has_under,
         seen_enum_fields,
         seen_errors,
         range_set,
@@ -11975,6 +12103,10 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
         false_count,
         cond_dbg_node_index,
         false,
+        special_members_only,
+        special_members_only_src,
+        extra_case_vals.items.items,
+        extra_case_vals.ranges.items,
     );
 
     for (merges.extra_insts.items, merges.extra_src_locs.items) |placeholder_inst, dispatch_src| {
@@ -12058,14 +12190,15 @@ fn analyzeSwitchRuntimeBlock(
     operand_ty: Type,
     operand_src: LazySrcLoc,
     case_vals: std.ArrayListUnmanaged(Air.Inst.Ref),
-    special: SpecialProng,
+    else_prong: SpecialProng,
     scalar_cases_len: usize,
     multi_cases_len: usize,
     union_originally: bool,
     maybe_union_ty: Type,
     err_set: bool,
     switch_node_offset: std.zig.Ast.Node.Offset,
-    special_prong_src: LazySrcLoc,
+    else_prong_src: LazySrcLoc,
+    else_prong_is_underscore: bool,
     seen_enum_fields: []?LazySrcLoc,
     seen_errors: SwitchErrorSet,
     range_set: RangeSet,
@@ -12073,6 +12206,11 @@ fn analyzeSwitchRuntimeBlock(
     false_count: u8,
     cond_dbg_node_index: Zir.Inst.Index,
     allow_err_code_unwrap: bool,
+    extra_prong: ?SpecialProng,
+    /// May be `undefined` if `extra_prong` is `null`
+    extra_prong_src: LazySrcLoc,
+    extra_prong_items: []const Air.Inst.Ref,
+    extra_prong_ranges: []const [2]Air.Inst.Ref,
 ) CompileError!Air.Inst.Ref {
     const pt = sema.pt;
     const zcu = pt.zcu;
@@ -12096,7 +12234,7 @@ fn analyzeSwitchRuntimeBlock(
     case_block.need_debug_scope = null; // this body is emitted regardless
     defer case_block.instructions.deinit(gpa);
 
-    var extra_index: usize = special.end;
+    var extra_index: usize = else_prong.end;
 
     var scalar_i: usize = 0;
     while (scalar_i < scalar_cases_len) : (scalar_i += 1) {
@@ -12158,23 +12296,42 @@ fn analyzeSwitchRuntimeBlock(
 
     var cases_len = scalar_cases_len;
     var case_val_idx: usize = scalar_cases_len;
+    const multi_cases_len_with_extra_prong = multi_cases_len + @intFromBool(extra_prong != null);
     var multi_i: u32 = 0;
-    while (multi_i < multi_cases_len) : (multi_i += 1) {
-        const items_len = sema.code.extra[extra_index];
-        extra_index += 1;
-        const ranges_len = sema.code.extra[extra_index];
-        extra_index += 1;
-        const info: Zir.Inst.SwitchBlock.ProngInfo = @bitCast(sema.code.extra[extra_index]);
-        extra_index += 1 + items_len + 2 * ranges_len;
+    while (multi_i < multi_cases_len_with_extra_prong) : (multi_i += 1) {
+        const is_extra_prong = multi_i == multi_cases_len;
+        var items: []const Air.Inst.Ref = undefined;
+        var info: Zir.Inst.SwitchBlock.ProngInfo = undefined;
+        var ranges: []const [2]Air.Inst.Ref = undefined;
+        var body: []const Zir.Inst.Index = undefined;
+        if (is_extra_prong) {
+            const prong = extra_prong.?;
+            items = extra_prong_items;
+            ranges = extra_prong_ranges;
+            body = prong.body;
+            info = .{
+                .body_len = undefined,
+                .capture = prong.capture,
+                .is_inline = prong.is_inline,
+                .has_tag_capture = prong.has_tag_capture,
+            };
+        } else {
+            @branchHint(.likely);
+            const items_len = sema.code.extra[extra_index];
+            extra_index += 1;
+            const ranges_len = sema.code.extra[extra_index];
+            extra_index += 1;
+            info = @bitCast(sema.code.extra[extra_index]);
+            extra_index += 1 + items_len + ranges_len * 2;
 
-        const items = case_vals.items[case_val_idx..][0..items_len];
-        case_val_idx += items_len;
-        // TODO: @ptrCast slice once Sema supports it
-        const ranges: []const [2]Air.Inst.Ref = @as([*]const [2]Air.Inst.Ref, @ptrCast(case_vals.items[case_val_idx..]))[0..ranges_len];
-        case_val_idx += ranges_len * 2;
+            items = case_vals.items[case_val_idx..][0..items_len];
+            case_val_idx += items_len;
+            ranges = @ptrCast(case_vals.items[case_val_idx..][0 .. ranges_len * 2]);
+            case_val_idx += ranges_len * 2;
 
-        const body = sema.code.bodySlice(extra_index, info.body_len);
-        extra_index += info.body_len;
+            body = sema.code.bodySlice(extra_index, info.body_len);
+            extra_index += info.body_len;
+        }
 
         case_block.instructions.shrinkRetainingCapacity(0);
         case_block.error_return_trace_index = child_block.error_return_trace_index;
@@ -12184,14 +12341,29 @@ fn analyzeSwitchRuntimeBlock(
             var emit_bb = false;
 
             for (ranges, 0..) |range_items, range_i| {
-                var item = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, range_items[0], undefined) catch unreachable;
-                const item_last = sema.resolveConstDefinedValue(block, LazySrcLoc.unneeded, range_items[1], undefined) catch unreachable;
+                var item = sema.resolveConstDefinedValue(block, .unneeded, range_items[0], undefined) catch unreachable;
+                const item_last = sema.resolveConstDefinedValue(block, .unneeded, range_items[1], undefined) catch unreachable;
 
                 while (item.compareScalar(.lte, item_last, operand_ty, zcu)) : ({
                     // Previous validation has resolved any possible lazy values.
-                    const result = try arith.incrementDefinedInt(sema, operand_ty, item);
+                    const int_val: Value, const int_ty: Type = switch (operand_ty.zigTypeTag(zcu)) {
+                        .int => .{ item, operand_ty },
+                        .@"enum" => b: {
+                            const int_val = Value.fromInterned(ip.indexToKey(item.toIntern()).enum_tag.int);
+                            break :b .{ int_val, int_val.typeOf(zcu) };
+                        },
+                        else => unreachable,
+                    };
+                    const result = try arith.incrementDefinedInt(sema, int_ty, int_val);
                     assert(!result.overflow);
-                    item = result.val;
+                    item = switch (operand_ty.zigTypeTag(zcu)) {
+                        .int => result.val,
+                        .@"enum" => .fromInterned(try pt.intern(.{ .enum_tag = .{
+                            .ty = operand_ty.toIntern(),
+                            .int = result.val.toIntern(),
+                        } })),
+                        else => unreachable,
+                    };
                 }) {
                     cases_len += 1;
 
@@ -12200,11 +12372,14 @@ fn analyzeSwitchRuntimeBlock(
                     case_block.instructions.shrinkRetainingCapacity(0);
                     case_block.error_return_trace_index = child_block.error_return_trace_index;
 
-                    if (emit_bb) try sema.emitBackwardBranch(block, block.src(.{ .switch_case_item = .{
-                        .switch_node_offset = switch_node_offset,
-                        .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) },
-                        .item_idx = .{ .kind = .range, .index = @intCast(range_i) },
-                    } }));
+                    if (emit_bb) {
+                        const bb_src = if (is_extra_prong) extra_prong_src else block.src(.{ .switch_case_item = .{
+                            .switch_node_offset = switch_node_offset,
+                            .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) },
+                            .item_idx = .{ .kind = .range, .index = @intCast(range_i) },
+                        } });
+                        try sema.emitBackwardBranch(block, bb_src);
+                    }
                     emit_bb = true;
 
                     const prong_hint = try spa.analyzeProngRuntime(
@@ -12249,11 +12424,14 @@ fn analyzeSwitchRuntimeBlock(
                     break :blk field_ty.zigTypeTag(zcu) != .noreturn;
                 } else true;
 
-                if (emit_bb) try sema.emitBackwardBranch(block, block.src(.{ .switch_case_item = .{
-                    .switch_node_offset = switch_node_offset,
-                    .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) },
-                    .item_idx = .{ .kind = .single, .index = @intCast(item_i) },
-                } }));
+                if (emit_bb) {
+                    const bb_src = if (is_extra_prong) extra_prong_src else block.src(.{ .switch_case_item = .{
+                        .switch_node_offset = switch_node_offset,
+                        .case_idx = .{ .kind = .multi, .index = @intCast(multi_i) },
+                        .item_idx = .{ .kind = .single, .index = @intCast(item_i) },
+                    } });
+                    try sema.emitBackwardBranch(block, bb_src);
+                }
                 emit_bb = true;
 
                 const prong_hint: std.builtin.BranchHint = if (analyze_body) h: {
@@ -12329,11 +12507,11 @@ fn analyzeSwitchRuntimeBlock(
         try branch_hints.append(gpa, prong_hint);
 
         try cases_extra.ensureUnusedCapacity(gpa, @typeInfo(Air.SwitchBr.Case).@"struct".fields.len +
-            items.len + 2 * ranges_len +
+            items.len + ranges.len * 2 +
             case_block.instructions.items.len);
         cases_extra.appendSliceAssumeCapacity(&payloadToExtraItems(Air.SwitchBr.Case{
             .items_len = @intCast(items.len),
-            .ranges_len = @intCast(ranges_len),
+            .ranges_len = @intCast(ranges.len),
             .body_len = @intCast(case_block.instructions.items.len),
         }));
 
@@ -12350,12 +12528,14 @@ fn analyzeSwitchRuntimeBlock(
         cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items));
     }
 
-    const else_body: []const Air.Inst.Index = if (special.body.len != 0 or case_block.wantSafety()) else_body: {
+    const else_body: []const Air.Inst.Index = if (else_prong.body.len != 0 or case_block.wantSafety()) else_body: {
         var emit_bb = false;
-        if (special.is_inline) switch (operand_ty.zigTypeTag(zcu)) {
+        // If this is true we must have a 'true' else prong and not an underscore because
+        // underscore prongs can never be inlined. We've already checked for this.
+        if (else_prong.is_inline) switch (operand_ty.zigTypeTag(zcu)) {
             .@"enum" => {
                 if (operand_ty.isNonexhaustiveEnum(zcu) and !union_originally) {
-                    return sema.fail(block, special_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{
+                    return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{
                         operand_ty.fmt(pt),
                     });
                 }
@@ -12374,22 +12554,22 @@ fn analyzeSwitchRuntimeBlock(
                         break :blk field_ty.zigTypeTag(zcu) != .noreturn;
                     } else true;
 
-                    if (emit_bb) try sema.emitBackwardBranch(block, special_prong_src);
+                    if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src);
                     emit_bb = true;
 
                     const prong_hint: std.builtin.BranchHint = if (analyze_body) h: {
                         break :h try spa.analyzeProngRuntime(
                             &case_block,
                             .special,
-                            special.body,
-                            special.capture,
+                            else_prong.body,
+                            else_prong.capture,
                             child_block.src(.{ .switch_capture = .{
                                 .switch_node_offset = switch_node_offset,
-                                .case_idx = .special,
+                                .case_idx = .special_else,
                             } }),
                             &.{item_ref},
                             item_ref,
-                            special.has_tag_capture,
+                            else_prong.has_tag_capture,
                         );
                     } else h: {
                         _ = try case_block.addNoOp(.unreach);
@@ -12411,7 +12591,7 @@ fn analyzeSwitchRuntimeBlock(
             },
             .error_set => {
                 if (operand_ty.isAnyError(zcu)) {
-                    return sema.fail(block, special_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{
+                    return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{
                         operand_ty.fmt(pt),
                     });
                 }
@@ -12430,21 +12610,21 @@ fn analyzeSwitchRuntimeBlock(
                     case_block.instructions.shrinkRetainingCapacity(0);
                     case_block.error_return_trace_index = child_block.error_return_trace_index;
 
-                    if (emit_bb) try sema.emitBackwardBranch(block, special_prong_src);
+                    if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src);
                     emit_bb = true;
 
                     const prong_hint = try spa.analyzeProngRuntime(
                         &case_block,
                         .special,
-                        special.body,
-                        special.capture,
+                        else_prong.body,
+                        else_prong.capture,
                         child_block.src(.{ .switch_capture = .{
                             .switch_node_offset = switch_node_offset,
-                            .case_idx = .special,
+                            .case_idx = .special_else,
                         } }),
                         &.{item_ref},
                         item_ref,
-                        special.has_tag_capture,
+                        else_prong.has_tag_capture,
                     );
                     try branch_hints.append(gpa, prong_hint);
 
@@ -12470,21 +12650,21 @@ fn analyzeSwitchRuntimeBlock(
                     case_block.instructions.shrinkRetainingCapacity(0);
                     case_block.error_return_trace_index = child_block.error_return_trace_index;
 
-                    if (emit_bb) try sema.emitBackwardBranch(block, special_prong_src);
+                    if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src);
                     emit_bb = true;
 
                     const prong_hint = try spa.analyzeProngRuntime(
                         &case_block,
                         .special,
-                        special.body,
-                        special.capture,
+                        else_prong.body,
+                        else_prong.capture,
                         child_block.src(.{ .switch_capture = .{
                             .switch_node_offset = switch_node_offset,
-                            .case_idx = .special,
+                            .case_idx = .special_else,
                         } }),
                         &.{item_ref},
                         item_ref,
-                        special.has_tag_capture,
+                        else_prong.has_tag_capture,
                     );
                     try branch_hints.append(gpa, prong_hint);
 
@@ -12507,21 +12687,21 @@ fn analyzeSwitchRuntimeBlock(
                     case_block.instructions.shrinkRetainingCapacity(0);
                     case_block.error_return_trace_index = child_block.error_return_trace_index;
 
-                    if (emit_bb) try sema.emitBackwardBranch(block, special_prong_src);
+                    if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src);
                     emit_bb = true;
 
                     const prong_hint = try spa.analyzeProngRuntime(
                         &case_block,
                         .special,
-                        special.body,
-                        special.capture,
+                        else_prong.body,
+                        else_prong.capture,
                         child_block.src(.{ .switch_capture = .{
                             .switch_node_offset = switch_node_offset,
-                            .case_idx = .special,
+                            .case_idx = .special_else,
                         } }),
                         &.{.bool_true},
                         .bool_true,
-                        special.has_tag_capture,
+                        else_prong.has_tag_capture,
                     );
                     try branch_hints.append(gpa, prong_hint);
 
@@ -12542,21 +12722,21 @@ fn analyzeSwitchRuntimeBlock(
                     case_block.instructions.shrinkRetainingCapacity(0);
                     case_block.error_return_trace_index = child_block.error_return_trace_index;
 
-                    if (emit_bb) try sema.emitBackwardBranch(block, special_prong_src);
+                    if (emit_bb) try sema.emitBackwardBranch(block, else_prong_src);
                     emit_bb = true;
 
                     const prong_hint = try spa.analyzeProngRuntime(
                         &case_block,
                         .special,
-                        special.body,
-                        special.capture,
+                        else_prong.body,
+                        else_prong.capture,
                         child_block.src(.{ .switch_capture = .{
                             .switch_node_offset = switch_node_offset,
-                            .case_idx = .special,
+                            .case_idx = .special_else,
                         } }),
                         &.{.bool_false},
                         .bool_false,
-                        special.has_tag_capture,
+                        else_prong.has_tag_capture,
                     );
                     try branch_hints.append(gpa, prong_hint);
 
@@ -12572,7 +12752,7 @@ fn analyzeSwitchRuntimeBlock(
                     cases_extra.appendSliceAssumeCapacity(@ptrCast(case_block.instructions.items));
                 }
             },
-            else => return sema.fail(block, special_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{
+            else => return sema.fail(block, else_prong_src, "cannot enumerate values of type '{f}' for 'inline else'", .{
                 operand_ty.fmt(pt),
             }),
         };
@@ -12581,7 +12761,7 @@ fn analyzeSwitchRuntimeBlock(
         case_block.error_return_trace_index = child_block.error_return_trace_index;
 
         if (zcu.backendSupportsFeature(.is_named_enum_value) and
-            special.body.len != 0 and block.wantSafety() and
+            else_prong.body.len != 0 and block.wantSafety() and
             operand_ty.zigTypeTag(zcu) == .@"enum" and
             (!operand_ty.isNonexhaustiveEnum(zcu) or union_originally))
         {
@@ -12590,7 +12770,12 @@ fn analyzeSwitchRuntimeBlock(
             try sema.addSafetyCheck(&case_block, src, ok, .corrupt_switch);
         }
 
-        const analyze_body = if (union_originally and !special.is_inline)
+        const else_src_idx: LazySrcLoc.Offset.SwitchCaseIndex = if (else_prong_is_underscore)
+            .special_under
+        else
+            .special_else;
+
+        const analyze_body = if (union_originally and !else_prong.is_inline)
             for (seen_enum_fields, 0..) |seen_field, index| {
                 if (seen_field != null) continue;
                 const union_obj = zcu.typeToUnion(maybe_union_ty).?;
@@ -12599,20 +12784,20 @@ fn analyzeSwitchRuntimeBlock(
             } else false
         else
             true;
-        const else_hint: std.builtin.BranchHint = if (special.body.len != 0 and err_set and
-            try sema.maybeErrorUnwrap(&case_block, special.body, operand, operand_src, allow_err_code_unwrap))
+        const else_hint: std.builtin.BranchHint = if (else_prong.body.len != 0 and err_set and
+            try sema.maybeErrorUnwrap(&case_block, else_prong.body, operand, operand_src, allow_err_code_unwrap))
         h: {
             // nothing to do here. weight against error branch
             break :h .unlikely;
-        } else if (special.body.len != 0 and analyze_body and !special.is_inline) h: {
+        } else if (else_prong.body.len != 0 and analyze_body and !else_prong.is_inline) h: {
             break :h try spa.analyzeProngRuntime(
                 &case_block,
                 .special,
-                special.body,
-                special.capture,
+                else_prong.body,
+                else_prong.capture,
                 child_block.src(.{ .switch_capture = .{
                     .switch_node_offset = switch_node_offset,
-                    .case_idx = .special,
+                    .case_idx = else_src_idx,
                 } }),
                 undefined, // case_vals may be undefined for special prongs
                 .none,
@@ -12686,7 +12871,9 @@ fn resolveSwitchComptimeLoop(
     cond_ty: Type,
     init_cond_val: Value,
     switch_node_offset: std.zig.Ast.Node.Offset,
-    special: SpecialProng,
+    special_members_only: ?SpecialProng,
+    special_generic: SpecialProng,
+    special_generic_is_under: bool,
     case_vals: std.ArrayListUnmanaged(Air.Inst.Ref),
     scalar_cases_len: u32,
     multi_cases_len: u32,
@@ -12706,7 +12893,9 @@ fn resolveSwitchComptimeLoop(
             cond_val,
             cond_ty,
             switch_node_offset,
-            special,
+            special_members_only,
+            special_generic,
+            special_generic_is_under,
             case_vals,
             scalar_cases_len,
             multi_cases_len,
@@ -12754,17 +12943,20 @@ fn resolveSwitchComptime(
     operand_val: Value,
     operand_ty: Type,
     switch_node_offset: std.zig.Ast.Node.Offset,
-    special: SpecialProng,
+    special_members_only: ?SpecialProng,
+    special_generic: SpecialProng,
+    special_generic_is_under: bool,
     case_vals: std.ArrayListUnmanaged(Air.Inst.Ref),
     scalar_cases_len: u32,
     multi_cases_len: u32,
     err_set: bool,
     empty_enum: bool,
 ) CompileError!Air.Inst.Ref {
+    const zcu = sema.pt.zcu;
     const merges = &child_block.label.?.merges;
     const resolved_operand_val = try sema.resolveLazyValue(operand_val);
 
-    var extra_index: usize = special.end;
+    var extra_index: usize = special_generic.end;
     {
         var scalar_i: usize = 0;
         while (scalar_i < scalar_cases_len) : (scalar_i += 1) {
@@ -12865,23 +13057,45 @@ fn resolveSwitchComptime(
             extra_index += info.body_len;
         }
     }
-    if (err_set) try sema.maybeErrorUnwrapComptime(child_block, special.body, cond_operand);
+    if (err_set) try sema.maybeErrorUnwrapComptime(child_block, special_generic.body, cond_operand);
     if (empty_enum) {
         return .void_value;
     }
+    if (special_members_only) |special| {
+        assert(operand_ty.isNonexhaustiveEnum(zcu));
+        if (operand_ty.enumTagFieldIndex(operand_val, zcu)) |_| {
+            return spa.resolveProngComptime(
+                child_block,
+                .special,
+                special.body,
+                special.capture,
+                child_block.src(.{ .switch_capture = .{
+                    .switch_node_offset = switch_node_offset,
+                    .case_idx = .special_else,
+                } }),
+                undefined, // case_vals may be undefined for special prongs
+                if (special.is_inline) cond_operand else .none,
+                special.has_tag_capture,
+                merges,
+            );
+        }
+    }
 
     return spa.resolveProngComptime(
         child_block,
         .special,
-        special.body,
-        special.capture,
+        special_generic.body,
+        special_generic.capture,
         child_block.src(.{ .switch_capture = .{
             .switch_node_offset = switch_node_offset,
-            .case_idx = .special,
+            .case_idx = if (special_generic_is_under)
+                .special_under
+            else
+                .special_else,
         } }),
         undefined, // case_vals may be undefined for special prongs
-        if (special.is_inline) cond_operand else .none,
-        special.has_tag_capture,
+        if (special_generic.is_inline) cond_operand else .none,
+        special_generic.has_tag_capture,
         merges,
     );
 }
src/Zcu.zig
@@ -1677,34 +1677,47 @@ pub const SrcLoc = struct {
                 return tree.nodeToSpan(condition);
             },
 
-            .node_offset_switch_special_prong => |node_off| {
+            .node_offset_switch_else_prong => |node_off| {
                 const tree = try src_loc.file_scope.getTree(zcu);
                 const switch_node = node_off.toAbsolute(src_loc.base_node);
                 _, const extra_index = tree.nodeData(switch_node).node_and_extra;
                 const case_nodes = tree.extraDataSlice(tree.extraData(extra_index, Ast.Node.SubRange), Ast.Node.Index);
                 for (case_nodes) |case_node| {
                     const case = tree.fullSwitchCase(case_node).?;
-                    if (case.isSpecial(tree)) |special_node| {
-                        return tree.tokensToSpan(
-                            tree.firstToken(case_node),
-                            tree.lastToken(case_node),
-                            tree.nodeMainToken(special_node.unwrap() orelse case_node),
-                        );
+                    if (case.ast.values.len == 0) {
+                        return tree.nodeToSpan(case_node);
                     }
                 } else unreachable;
             },
 
-            .node_offset_switch_range => |node_off| {
+            .node_offset_switch_under_prong => |node_off| {
                 const tree = try src_loc.file_scope.getTree(zcu);
                 const switch_node = node_off.toAbsolute(src_loc.base_node);
                 _, const extra_index = tree.nodeData(switch_node).node_and_extra;
                 const case_nodes = tree.extraDataSlice(tree.extraData(extra_index, Ast.Node.SubRange), Ast.Node.Index);
                 for (case_nodes) |case_node| {
                     const case = tree.fullSwitchCase(case_node).?;
-                    if (case.isSpecial(tree)) |maybe_else| {
-                        if (maybe_else == .none) continue;
+                    for (case.ast.values) |val| {
+                        if (tree.nodeTag(val) == .identifier and
+                            mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(val)), "_"))
+                        {
+                            return tree.tokensToSpan(
+                                tree.firstToken(case_node),
+                                tree.lastToken(case_node),
+                                tree.nodeMainToken(val),
+                            );
+                        }
                     }
+                } else unreachable;
+            },
 
+            .node_offset_switch_range => |node_off| {
+                const tree = try src_loc.file_scope.getTree(zcu);
+                const switch_node = node_off.toAbsolute(src_loc.base_node);
+                _, const extra_index = tree.nodeData(switch_node).node_and_extra;
+                const case_nodes = tree.extraDataSlice(tree.extraData(extra_index, Ast.Node.SubRange), Ast.Node.Index);
+                for (case_nodes) |case_node| {
+                    const case = tree.fullSwitchCase(case_node).?;
                     for (case.ast.values) |item_node| {
                         if (tree.nodeTag(item_node) == .switch_range) {
                             return tree.nodeToSpan(item_node);
@@ -2109,32 +2122,35 @@ pub const SrcLoc = struct {
 
                 var multi_i: u32 = 0;
                 var scalar_i: u32 = 0;
-                var found_special = false;
                 var underscore_node: Ast.Node.OptionalIndex = .none;
-                const case = for (case_nodes) |case_node| {
+                const case = case: for (case_nodes) |case_node| {
                     const case = tree.fullSwitchCase(case_node).?;
-                    const is_special = special: {
-                        if (found_special) break :special false;
-                        if (case.isSpecial(tree)) |special_node| {
-                            underscore_node = special_node;
-                            found_special = true;
-                            break :special true;
+                    if (case.ast.values.len == 0) {
+                        if (want_case_idx == LazySrcLoc.Offset.SwitchCaseIndex.special_else) {
+                            break :case case;
                         }
-                        break :special false;
-                    };
-                    if (is_special) {
-                        if (want_case_idx == LazySrcLoc.Offset.SwitchCaseIndex.special) {
-                            break case;
-                        }
-                        continue;
+                        continue :case;
                     }
+                    if (underscore_node == .none) for (case.ast.values) |val_node| {
+                        if (tree.nodeTag(val_node) == .identifier and
+                            mem.eql(u8, tree.tokenSlice(tree.nodeMainToken(val_node)), "_"))
+                        {
+                            underscore_node = val_node.toOptional();
+                            if (want_case_idx == LazySrcLoc.Offset.SwitchCaseIndex.special_under) {
+                                break :case case;
+                            }
+                            continue :case;
+                        }
+                    };
 
                     const is_multi = case.ast.values.len != 1 or
                         tree.nodeTag(case.ast.values[0]) == .switch_range;
 
                     switch (want_case_idx.kind) {
-                        .scalar => if (!is_multi and want_case_idx.index == scalar_i) break case,
-                        .multi => if (is_multi and want_case_idx.index == multi_i) break case,
+                        .scalar => if (!is_multi and want_case_idx.index == scalar_i)
+                            break :case case,
+                        .multi => if (is_multi and want_case_idx.index == multi_i)
+                            break :case case,
                     }
 
                     if (is_multi) {
@@ -2148,7 +2164,10 @@ pub const SrcLoc = struct {
                     .switch_case_item,
                     .switch_case_item_range_first,
                     .switch_case_item_range_last,
-                    => |x| x.item_idx,
+                    => |x| item_idx: {
+                        assert(want_case_idx != LazySrcLoc.Offset.SwitchCaseIndex.special_else);
+                        break :item_idx x.item_idx;
+                    },
                     .switch_capture, .switch_tag_capture => {
                         const start = switch (src_loc.lazy) {
                             .switch_capture => case.payload_token.?,
@@ -2369,10 +2388,14 @@ pub const LazySrcLoc = struct {
         /// by taking this AST node index offset from the containing base node,
         /// which points to a switch expression AST node. Next, navigate to the operand.
         node_offset_switch_operand: Ast.Node.Offset,
-        /// The source location points to the else/`_` prong of a switch expression, found
+        /// The source location points to the else prong of a switch expression, found
+        /// by taking this AST node index offset from the containing base node,
+        /// which points to a switch expression AST node. Next, navigate to the else prong.
+        node_offset_switch_else_prong: Ast.Node.Offset,
+        /// The source location points to the `_` prong of a switch expression, found
         /// by taking this AST node index offset from the containing base node,
-        /// which points to a switch expression AST node. Next, navigate to the else/`_` prong.
-        node_offset_switch_special_prong: Ast.Node.Offset,
+        /// which points to a switch expression AST node. Next, navigate to the `_` prong.
+        node_offset_switch_under_prong: Ast.Node.Offset,
         /// The source location points to all the ranges of a switch expression, found
         /// by taking this AST node index offset from the containing base node,
         /// which points to a switch expression AST node. Next, navigate to any of the
@@ -2568,7 +2591,8 @@ pub const LazySrcLoc = struct {
             kind: enum(u1) { scalar, multi },
             index: u31,
 
-            pub const special: SwitchCaseIndex = @bitCast(@as(u32, std.math.maxInt(u32)));
+            pub const special_else: SwitchCaseIndex = @bitCast(@as(u32, std.math.maxInt(u32)));
+            pub const special_under: SwitchCaseIndex = @bitCast(@as(u32, std.math.maxInt(u32) - 1));
         };
 
         pub const SwitchItemIndex = packed struct(u32) {
test/behavior/switch.zig
@@ -1075,26 +1075,50 @@ test "switch on 8-bit mod result" {
 }
 
 test "switch on non-exhaustive enum" {
-    const E = enum(u32) {
+    if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; // TODO
+
+    const E = enum(u4) {
         a,
         b,
         c,
         _,
+
+        fn doTheTest(e: @This()) !void {
+            switch (e) {
+                .a, .b => {},
+                else => return error.TestFailed,
+            }
+            switch (e) {
+                .a, .b => {},
+                .c => return error.TestFailed,
+                _ => return error.TestFailed,
+            }
+            switch (e) {
+                .a, .b => {},
+                .c, _ => return error.TestFailed,
+            }
+            switch (e) {
+                .a => {},
+                .b, .c, _ => return error.TestFailed,
+            }
+            switch (e) {
+                .b => return error.TestFailed,
+                else => {},
+                _ => return error.TestFailed,
+            }
+            switch (e) {
+                else => {},
+                _ => return error.TestFailed,
+            }
+            switch (e) {
+                inline else => {},
+                _ => return error.TestFailed,
+            }
+        }
     };
 
     var e: E = .a;
     _ = &e;
-    switch (e) {
-        .a, .b => {},
-        else => return error.TestFailed,
-    }
-    switch (e) {
-        .a, .b => {},
-        .c => return error.TestFailed,
-        _ => return error.TestFailed,
-    }
-    switch (e) {
-        .a, .b => {},
-        .c, _ => return error.TestFailed,
-    }
+    try E.doTheTest(e);
+    try comptime E.doTheTest(.a);
 }
test/behavior/switch_loop.zig
@@ -249,3 +249,27 @@ test "switch loop on larger than pointer integer" {
     }
     try expect(entry == 3);
 }
+
+test "switch loop on non-exhaustive enum" {
+    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_spirv) return error.SkipZigTest; // TODO
+
+    const S = struct {
+        const E = enum(u8) { a, b, c, _ };
+
+        fn doTheTest() !void {
+            var start: E = undefined;
+            start = .a;
+            const result: u32 = s: switch (start) {
+                .a => continue :s .c,
+                else => continue :s @enumFromInt(123),
+                .b, _ => |x| break :s @intFromEnum(x),
+            };
+            try expect(result == 123);
+        }
+    };
+    try S.doTheTest();
+    try comptime S.doTheTest();
+}
test/cases/compile_errors/switch_expression-non_exhaustive_inline.zig
@@ -0,0 +1,27 @@
+const E = enum(u8) {
+    a,
+    b,
+    _,
+};
+
+export fn f(e: E) void {
+    switch (e) {
+        .a => {},
+        inline _ => {},
+    }
+}
+
+export fn g(e: E) void {
+    switch (e) {
+        .a => {},
+        else => {},
+        inline _ => {},
+    }
+}
+
+// error
+// backend=stage2
+// target=native
+//
+// :10:16: error: cannot inline '_' prong
+// :18:16: error: cannot inline '_' prong
test/cases/compile_errors/switch_expression-non_exhaustive_unreachable_else.zig
@@ -0,0 +1,18 @@
+const E = enum(u8) {
+    a,
+    b,
+    _,
+};
+
+export fn f(e: E) void {
+    switch (e) {
+        .a, .b, _ => {},
+        else => {},
+    }
+}
+
+// error
+// backend=stage2
+// target=native
+//
+// :10:14: error: unreachable else prong; all explicit cases already handled
test/cases/compile_errors/switching_with_non-exhaustive_enums.zig
@@ -37,7 +37,7 @@ pub export fn entry3() void {
 // :12:5: error: switch must handle all possibilities
 // :3:5: note: unhandled enumeration value: 'b'
 // :1:11: note: enum 'tmp.E' declared here
-// :19:5: error: switch on non-exhaustive enum must include 'else' or '_' prong
+// :19:5: error: switch on non-exhaustive enum must include 'else' or '_' prong or both
 // :26:5: error: '_' prong only allowed when switching on non-exhaustive enums
 // :29:9: note: '_' prong here
 // :26:5: note: consider using 'else'