authorgravatar for david@vortan.devDavid Rubin <david@vortan.dev> 2025-10-04 12:04:42-07:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-01-11 11:37:16+00:00
logaa2b178029fc466f847c0cb663c3b32c7809a357
treee8f6a3b8b6b2416aef0476ce67fdceb998ad8f00
parentc4345991340af5ff2e0155a9832f4eed9ec677fc
signaturelock-open Commit is signed but in an unrecognized format.

disallow switch case capture discards

Previously Zig allowed you to write something like, ```zig switch (x) { .y => |_| { ``` This seems a bit strange because in other cases, such as when capturing the tag in a switch case, ```zig switch (x) { .y => |_, _| { ``` this produces an error. The only usecase I can think of for the previous behaviour is if you wanted to assert that all union payloads are able to coerce, ```zig const X = union(enum) { y: u8, z: f32 }; switch (x) { .y, .z => |_| { ``` This will compile-error with the `|_|` and pass without it. I don't believe this usecase is strong enough to keep the current behaviour; it was never used in the Zig codebase and I cannot find a single usage of this behaviour in the real world, searching through Sourcegraph.

5 files changed, 56 insertions(+), 5 deletions(-)

lib/std/testing.zig+1-1
...@@ -813,7 +813,7 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe...@@ -813,7 +813,7 @@ fn expectEqualDeepInner(comptime T: type, expected: T, actual: T) error{TestExpe
813 }813 }
814 },814 },
815815
816 .array => |_| {816 .array => {
817 if (expected.len != actual.len) {817 if (expected.len != actual.len) {
818 print("Array len not the same, expected {d}, found {d}\n", .{ expected.len, actual.len });818 print("Array len not the same, expected {d}, found {d}\n", .{ expected.len, actual.len });
819 return error.TestExpectedEqual;819 return error.TestExpectedEqual;
lib/std/zig/AstGen.zig+9-3
...@@ -7882,8 +7882,10 @@ fn switchExpr(...@@ -7882,8 +7882,10 @@ fn switchExpr(
7882 var payload_sub_scope: *Scope = undefined;7882 var payload_sub_scope: *Scope = undefined;
7883 if (mem.eql(u8, ident_slice, "_")) {7883 if (mem.eql(u8, ident_slice, "_")) {
7884 if (capture_is_ref) {7884 if (capture_is_ref) {
7885 // |*_, tag| is invalid, so we can fail early
7885 return astgen.failTok(payload_token, "pointer modifier invalid on discard", .{});7886 return astgen.failTok(payload_token, "pointer modifier invalid on discard", .{});
7886 }7887 }
7888 capture = .none;
7887 payload_sub_scope = &case_scope.base;7889 payload_sub_scope = &case_scope.base;
7888 } else {7890 } else {
7889 const capture_name = try astgen.identAsString(ident);7891 const capture_name = try astgen.identAsString(ident);
...@@ -7903,11 +7905,15 @@ fn switchExpr(...@@ -7903,11 +7905,15 @@ fn switchExpr(
79037905
7904 const tag_token = if (tree.tokenTag(ident + 1) == .comma)7906 const tag_token = if (tree.tokenTag(ident + 1) == .comma)
7905 ident + 27907 ident + 2
7906 else7908 else if (capture == .none) {
7907 break :blk payload_sub_scope;7909 // discarding the capture is only valid iff the tag is captured
7910 // whether the tag capture is discarded is handled below
7911 return astgen.failTok(payload_token, "discard of capture; omit it instead", .{});
7912 } else break :blk payload_sub_scope;
7913
7908 const tag_slice = tree.tokenSlice(tag_token);7914 const tag_slice = tree.tokenSlice(tag_token);
7909 if (mem.eql(u8, tag_slice, "_")) {7915 if (mem.eql(u8, tag_slice, "_")) {
7910 try astgen.appendErrorTok(tag_token, "discard of tag capture; omit it instead", .{});7916 return astgen.failTok(tag_token, "discard of tag capture; omit it instead", .{});
7911 } else if (case.inline_token == null) {7917 } else if (case.inline_token == null) {
7912 return astgen.failTok(tag_token, "tag capture on non-inline prong", .{});7918 return astgen.failTok(tag_token, "tag capture on non-inline prong", .{});
7913 }7919 }
src/Sema.zig+3-1
...@@ -11546,7 +11546,9 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r...@@ -11546,7 +11546,9 @@ fn zirSwitchBlock(sema: *Sema, block: *Block, inst: Zir.Inst.Index, operand_is_r
11546 });11546 });
11547 }11547 }
11548 try sema.validateRuntimeValue(block, operand_src, maybe_ptr);11548 try sema.validateRuntimeValue(block, operand_src, maybe_ptr);
11549 const operand_alloc = if (extra.data.bits.any_non_inline_capture) a: {11549 const operand_alloc = if (extra.data.bits.any_non_inline_capture or
11550 extra.data.bits.any_has_tag_capture)
11551 a: {
11550 const operand_ptr_ty = try pt.singleMutPtrType(sema.typeOf(maybe_ptr));11552 const operand_ptr_ty = try pt.singleMutPtrType(sema.typeOf(maybe_ptr));
11551 const operand_alloc = try block.addTy(.alloc, operand_ptr_ty);11553 const operand_alloc = try block.addTy(.alloc, operand_ptr_ty);
11552 _ = try block.addBinOp(.store, operand_alloc, maybe_ptr);11554 _ = try block.addBinOp(.store, operand_alloc, maybe_ptr);
test/behavior/switch_loop.zig+27
...@@ -270,3 +270,30 @@ test "switch loop on non-exhaustive enum" {...@@ -270,3 +270,30 @@ test "switch loop on non-exhaustive enum" {
270 try S.doTheTest();270 try S.doTheTest();
271 try comptime S.doTheTest();271 try comptime S.doTheTest();
272}272}
273
274test "switch loop with discarded tag capture" {
275 if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest;
276 if (builtin.zig_backend == .stage2_c) return error.SkipZigTest;
277 if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
278
279 const S = struct {
280 const U = union(enum) {
281 a: u32,
282 b: u32,
283 c: u32,
284 };
285
286 fn doTheTest() void {
287 const a: U = .{ .a = 10 };
288 blk: switch (a) {
289 inline .b => |_, tag| {
290 _ = tag;
291 continue :blk .{ .c = 20 };
292 },
293 else => {},
294 }
295 }
296 };
297 S.doTheTest();
298 comptime S.doTheTest();
299}
test/cases/compile_errors/switch_loop_discarded_capture.zig created+16
...@@ -0,0 +1,16 @@
1export fn foo() void {
2 const S = struct {
3 fn doTheTest() void {
4 blk: switch (@as(u8, 'a')) {
5 '1' => |_| continue :blk '1',
6 else => {},
7 }
8 }
9 };
10 S.doTheTest();
11 comptime S.doTheTest();
12}
13
14// error
15//
16// :5:25: error: discard of capture; omit it instead