commit 01546e68cd0d82ef78498a10649e6bc2937680da (tree)
parent 3a4a7d2ca378862dd6b31678a143315a9e306f8c
Author: Matthew Lugg <mlugg@mlugg.co.uk>
Date: Sun, 11 Jan 2026 14:27:00 +0000
compiler: handle switch rewrite review feedback
Diffstat:
11 files changed, 127 insertions(+), 114 deletions(-)
diff --git a/doc/langref.html.in b/doc/langref.html.in
@@ -2595,7 +2595,7 @@ or
{#header_close#}
- {#header_open|Switching on errors#}
+ {#header_open|Switching on Errors#}
<p>
When switching on errors, some special cases are allowed to simplify generic programming patterns:
</p>
diff --git a/doc/langref/test_switch_on_errors.zig b/doc/langref/test_switch_on_errors.zig
@@ -12,7 +12,10 @@ test "unreachable else prong" {
switch (openFile0()) {
error.AccessDenied, error.FileNotFound => |e| return e,
error.OutOfMemory => {},
- else => unreachable, // technically unreachable, but will still compile!
+ // 'openFile0' cannot return any more errors, so an 'else' prong would be
+ // statically known to be unreachable. Nonetheless, in this case, adding
+ // one does not raise an "unreachable else prong" compile error:
+ else => unreachable,
}
// Allowed unreachable else prongs are:
diff --git a/doc/langref/test_tagged_union.zig b/doc/langref/test_tagged_union.zig
@@ -20,7 +20,10 @@ test "switch on tagged union" {
}
switch (c) {
- .ok => |_, tag| try expect(tag == .ok),
+ .ok => |_, tag| {
+ // Because we're in the '.ok' prong, 'tag' is compile-time known to be '.ok':
+ comptime std.debug.assert(tag == .ok);
+ },
.not_ok => unreachable,
}
}
diff --git a/src/Air/Liveness.zig b/src/Air/Liveness.zig
@@ -176,10 +176,7 @@ pub fn analyze(zcu: *Zcu, air: Air, intern_pool: *InternPool) Allocator.Error!Li
data.old_extra = a.extra;
a.extra = .{};
try analyzeBody(&a, .main_analysis, &data, main_body);
- if (std.debug.runtime_safety and data.live_set.count() != 0) {
- log.debug("instructions still in live set after analysis: {f}", .{fmtInstSet(&data.live_set)});
- @panic("liveness analysis failed");
- }
+ assert(data.live_set.count() == 0);
}
return .{
diff --git a/src/Sema.zig b/src/Sema.zig
@@ -12591,6 +12591,7 @@ fn resolveSwitchProng(
sema.typeOf(operand.simple.by_val),
capture_src,
inline_case_capture,
+ kind,
);
sema.inst_map.putAssumeCapacity(tag_inst, tag_ref);
break :inst tag_inst;
@@ -12723,6 +12724,7 @@ fn analyzeSwitchProng(
operand_ty,
capture_src,
inline_case_capture,
+ kind,
);
sema.inst_map.putAssumeCapacity(tag_inst, tag_ref);
break :inst tag_inst;
@@ -12743,6 +12745,7 @@ fn analyzeSwitchTagCapture(
operand_ty: Type,
capture_src: LazySrcLoc,
inline_case_capture: Air.Inst.Ref,
+ kind: SwitchProngKind,
) CompileError!Air.Inst.Ref {
const pt = sema.pt;
const zcu = pt.zcu;
@@ -12760,6 +12763,10 @@ fn analyzeSwitchTagCapture(
if (inline_case_capture != .none) {
return inline_case_capture; // this already is the tag, it's what we're switching on!
}
+ switch (kind) {
+ .has_ranges, .special => {},
+ .item_refs => |refs| if (refs.len == 1) return refs[0],
+ }
const tag_ty = operand_ty.unionTagType(zcu).?;
return sema.unionToTag(case_block, tag_ty, operand_val, tag_capture_src);
}
@@ -13150,10 +13157,12 @@ fn analyzeSwitchPayloadCapture(
return sema.bitCast(case_block, error_ty, operand_val, operand_src, null);
},
else => {
- // In this case the capture value is just the passed-through value
- // of the switch condition.
+ // In this case the capture value is just the passed-through value of the
+ // switch condition. It is comptime-known if there is only one item.
if (capture_by_ref) {
return operand_ptr;
+ } else if (case_vals.len == 1) {
+ return case_vals[0];
} else {
return operand_val;
}
@@ -31361,17 +31370,19 @@ fn resolvePtrIsNonErrVal(
assert(ptr_ty.zigTypeTag(zcu) == .pointer);
const child_ty = ptr_ty.childType(zcu);
- const child_tag = child_ty.zigTypeTag(zcu);
- if (child_tag != .error_set and child_tag != .error_union) return .true;
- if (child_tag == .error_set) return .false;
- assert(child_tag == .error_union);
+ if (try sema.resolveIsNonErrFromType(block, src, child_ty)) |res| {
+ return res;
+ }
+ assert(child_ty.zigTypeTag(zcu) == .error_union);
- if (try sema.resolveValue(operand)) |ptr_val| {
- if (ptr_val.isUndef(zcu)) return .undef_bool;
- if (try sema.pointerDeref(block, src, ptr_val, ptr_ty)) |val| {
- return try sema.resolveIsNonErrVal(block, src, .fromValue(val));
+ if (try sema.resolveValue(operand)) |eu_ptr_val| {
+ if (eu_ptr_val.isUndef(zcu)) return .undef_bool;
+ if (try sema.pointerDeref(block, src, eu_ptr_val, ptr_ty)) |err_union| {
+ if (err_union.isUndef(zcu)) return .undef_bool;
+ return .makeBool(err_union.getErrorName(zcu) == .none);
}
}
+
return null;
}
@@ -31381,10 +31392,29 @@ fn resolveIsNonErrVal(
src: LazySrcLoc,
operand: Air.Inst.Ref,
) CompileError!?Value {
+ const zcu = sema.pt.zcu;
+ if (try sema.resolveIsNonErrFromType(block, src, sema.typeOf(operand))) |res| {
+ return res;
+ }
+ assert(sema.typeOf(operand).zigTypeTag(zcu) == .error_union);
+
+ if (try sema.resolveValue(operand)) |err_union| {
+ if (err_union.isUndef(zcu)) return .undef_bool;
+ return .makeBool(err_union.getErrorName(zcu) == .none);
+ }
+
+ return null;
+}
+
+fn resolveIsNonErrFromType(
+ sema: *Sema,
+ block: *Block,
+ src: LazySrcLoc,
+ operand_ty: Type,
+) CompileError!?Value {
const pt = sema.pt;
const zcu = pt.zcu;
const ip = &zcu.intern_pool;
- const operand_ty = sema.typeOf(operand);
const ot = operand_ty.zigTypeTag(zcu);
if (ot != .error_set and ot != .error_union) return .true;
if (ot == .error_set) return .false;
@@ -31395,15 +31425,6 @@ fn resolveIsNonErrVal(
return .false;
}
- if (operand == .undef) {
- return .undef_bool;
- } else if (@intFromEnum(operand) < InternPool.static_len) {
- // None of the ref tags can be errors.
- return .true;
- }
-
- const maybe_operand_val = try sema.resolveValue(operand);
-
// exception if the error union error set is known to be empty,
// we allow the comparison but always make it comptime-known.
const set_ty = ip.errorUnionSet(operand_ty.toIntern());
@@ -31419,9 +31440,6 @@ fn resolveIsNonErrVal(
else => |i| if (ip.indexToKey(i).error_set_type.names.len != 0) break :blk,
}
- if (maybe_operand_val != null) break :blk;
-
- // Try to avoid resolving inferred error set if possible.
if (ies.errors.count() != 0) return null;
switch (ies.resolved) {
.anyerror_type => return null,
@@ -31450,7 +31468,6 @@ fn resolveIsNonErrVal(
.none => {},
else => |i| if (ip.indexToKey(i).error_set_type.names.len != 0) break :blk,
}
- if (maybe_operand_val != null) break :blk;
if (sema.fn_ret_ty_ies) |ies| {
if (ies.func == func_index) {
// Try to avoid resolving inferred error set if possible.
@@ -31479,9 +31496,6 @@ fn resolveIsNonErrVal(
},
}
- if (maybe_operand_val) |err_union| {
- return if (err_union.isUndef(zcu)) .undef_bool else if (err_union.getErrorName(zcu) == .none) .true else .false;
- }
return null;
}
diff --git a/src/Zcu.zig b/src/Zcu.zig
@@ -2221,16 +2221,7 @@ pub const SrcLoc = struct {
continue;
}
return tree.nodeToSpan(item_node);
- } else {
- for (case.ast.values) |item_node| {
- const item_span = tree.nodeToSpan(item_node);
- std.debug.print("{s}\n", .{tree.source[item_span.start..item_span.end]});
- }
- std.debug.print("want_case_idx={any}\n", .{want_case_idx});
- std.debug.print("want_item_idx={any}\n", .{want_item_idx});
- unreachable;
- }
- // } else unreachable;
+ } else unreachable;
},
.range => {
var range_i: u32 = 0;
diff --git a/test/behavior/for.zig b/test/behavior/for.zig
@@ -525,7 +525,7 @@ test "for loop 0 length range" {
}
}
-test "labeled break from else prong" {
+test "labeled break from else" {
const S = struct {
fn doTheTest(x: u32) !void {
var y: u32 = 0;
diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig
@@ -1128,20 +1128,17 @@ test "decl literals as switch cases" {
const foo: @This() = @enumFromInt(0xa);
- fn doTheTest() !void {
- var e: @This() = .foo;
- _ = &e;
- const ok = switch (e) {
- .bar => false,
- .foo => true,
- else => false,
- };
- try expect(ok);
+ fn doTheTest(e: @This()) !void {
+ switch (e) {
+ .bar => return error.TestFailed,
+ .foo => {},
+ else => return error.TestFailed,
+ }
}
};
- try E.doTheTest();
- try comptime E.doTheTest();
+ try E.doTheTest(.foo);
+ try comptime E.doTheTest(.foo);
}
// TODO audit after #15909 and/or #19855 are decided/implemented
@@ -1152,32 +1149,30 @@ test "switch with uninstantiable union fields" {
b: noreturn,
c: error{},
- fn doTheTest() !void {
- var u: @This() = .ok;
- _ = &u;
- try expect(switch (u) {
- .ok => true,
+ fn doTheTest(u: @This()) void {
+ switch (u) {
+ .ok => {},
.a => comptime unreachable,
.b => comptime unreachable,
.c => comptime unreachable,
- });
- try expect(switch (u) {
- .ok => true,
+ }
+ switch (u) {
+ .ok => {},
.a, .b, .c => comptime unreachable,
- });
- try expect(switch (u) {
- .ok => true,
+ }
+ switch (u) {
+ .ok => {},
else => comptime unreachable,
- });
- try expect(switch (u) {
+ }
+ switch (u) {
.a => comptime unreachable,
- .ok, .b, .c => true,
- });
+ .ok, .b, .c => {},
+ }
}
};
- try U.doTheTest();
- try comptime U.doTheTest();
+ U.doTheTest(.ok);
+ comptime U.doTheTest(.ok);
}
test "switch with tag capture" {
@@ -1196,8 +1191,8 @@ test "switch with tag capture" {
fn doTheSwitch(u: @This()) !void {
switch (u) {
.a => |nothing, tag| {
- try expect(nothing == {});
- try expect(tag == .a);
+ comptime assert(nothing == {});
+ comptime assert(tag == .a);
try expect(@intFromEnum(tag) == @intFromEnum(@This().a));
},
.b, .d => |_, tag| {
@@ -1216,13 +1211,13 @@ test "switch with tag capture" {
}
switch (u) {
inline .a, .b, .c => |payload, tag| {
- if (@TypeOf(payload) == void) try expect(tag == .a);
- if (@TypeOf(payload) == i32) try expect(tag == .b);
- if (@TypeOf(payload) == u8) try expect(tag == .c);
+ if (@TypeOf(payload) == void) comptime assert(tag == .a);
+ if (@TypeOf(payload) == i32) comptime assert(tag == .b);
+ if (@TypeOf(payload) == u8) comptime assert(tag == .c);
},
inline else => |payload, tag| {
- if (@TypeOf(payload) == i32) try expect(tag == .d);
- try expect(tag != .e);
+ if (@TypeOf(payload) == i32) comptime assert(tag == .d);
+ comptime assert(tag != .e);
},
}
}
@@ -1232,7 +1227,7 @@ test "switch with tag capture" {
try comptime U.doTheTest();
}
-test "switch with advanced prong items" {
+test "switch with complex item expressions" {
const S = struct {
fn doTheTest() !void {
try doTheSwitch(2000, 20);
@@ -1278,19 +1273,11 @@ test "switch with advanced prong items" {
}
test "switch evaluation order" {
- const eval = comptime eval: {
- var eval = false;
- const eu: anyerror!u32 = 0;
- _ = eu catch |err| switch (err) {
- blk: {
- eval = true;
- break :blk error.MyError;
- } => {},
- else => unreachable,
- };
- break :eval eval;
+ const eu: anyerror!u32 = 0;
+ _ = eu catch |err| switch (err) {
+ if (true) @compileError("unreachable") => unreachable,
+ else => unreachable,
};
- try comptime expect(!eval);
}
test "switch resolves lazy values correctly" {
@@ -1298,13 +1285,25 @@ test "switch resolves lazy values correctly" {
a: u16,
b: i16,
};
- const ok1 = switch (@sizeOf(S)) {
- 4 => true,
- else => false,
- };
- const ok2 = switch (@sizeOf(S)) {
- 4 => true,
- else => false,
+ switch (@sizeOf(S)) {
+ 4 => {},
+ else => comptime unreachable,
+ }
+}
+
+test "single-item prong in switch on enum has comptime-known capture" {
+ const E = enum {
+ a,
+ b,
+ c,
+ fn doTheTest(e: @This()) !void {
+ switch (e) {
+ .a => |tag| comptime assert(tag == .a),
+ .b => return error.TestFailed,
+ .c => return error.TestFailed,
+ }
+ }
};
- try comptime expect(ok1 == ok2);
+ try E.doTheTest(.a);
+ try comptime E.doTheTest(.a);
}
diff --git a/test/behavior/switch_loop.zig b/test/behavior/switch_loop.zig
@@ -1,5 +1,6 @@
const builtin = @import("builtin");
const std = @import("std");
+const assert = std.debug.assert;
const expect = std.testing.expect;
test "simple switch loop" {
@@ -340,7 +341,7 @@ test "switch loop with single catch-all prong" {
continue :label .{ .b = 456 };
},
};
- try expect(ok);
+ comptime assert(ok);
}
};
try S.doTheTest();
@@ -411,8 +412,8 @@ test "switch loop with tag capture" {
fn doTheSwitch(u: @This()) !void {
const ok1 = label: switch (u) {
.a => |nothing, tag| {
- try expect(nothing == {});
- try expect(tag == .a);
+ comptime assert(nothing == {});
+ comptime assert(tag == .a);
try expect(@intFromEnum(tag) == @intFromEnum(@This().a));
continue :label .{ .d = 456 };
},
@@ -438,21 +439,21 @@ test "switch loop with tag capture" {
const ok2 = label: switch (u) {
inline .a, .b, .c => |payload, tag| {
if (@TypeOf(payload) == void) {
- try expect(tag == .a);
+ comptime assert(tag == .a);
continue :label .{ .b = 456 };
}
if (@TypeOf(payload) == i32) {
- try expect(tag == .b);
+ comptime assert(tag == .b);
continue :label .{ .d = payload };
}
if (@TypeOf(payload) == u8) {
- try expect(tag == .c);
+ comptime assert(tag == .c);
continue :label .{ .d = payload };
}
},
inline else => |payload, tag| {
- if (@TypeOf(payload) == i32) try expect(tag == .d);
- try expect(tag != .e);
+ if (@TypeOf(payload) == i32) comptime assert(tag == .d);
+ comptime assert(tag != .e);
if (payload == 0) break :label false;
break :label true;
},
diff --git a/test/behavior/switch_on_captured_error.zig b/test/behavior/switch_on_captured_error.zig
@@ -17,7 +17,9 @@ test "switch on error union catch capture" {
try testElse();
try testCapture();
try testInline();
+ try testEmptyErrSet();
try testUnreachableElseProng();
+ try testErrNotInSet();
try testAddressOf();
}
@@ -384,8 +386,11 @@ test "switch on error union if else capture" {
try testCapturePtr();
try testInline();
try testInlinePtr();
+ try testEmptyErrSet();
+ try testEmptyErrSetPtr();
try testUnreachableElseProng();
try testUnreachableElseProngPtr();
+ try testErrNotInSet();
try testAddressOf();
}
@@ -835,7 +840,7 @@ test "switch on error union if else capture" {
var a: error{}!u64 = 0;
_ = &a;
const b = if (a) |*x| x.* else |err| switch (err) {
- error.undefined => @compileError("unreachable"),
+ undefined => @compileError("unreachable"),
};
try expectEqual(@as(u64, 0), b);
}
diff --git a/test/behavior/while.zig b/test/behavior/while.zig
@@ -400,12 +400,12 @@ test "breaking from a loop in an if statement" {
_ = opt;
}
-test "labeled break from else prong" {
+test "labeled break from else" {
const S = struct {
fn doTheTest(x: u32) !void {
- var y: u32 = 0;
- const ok = label: while (y < x) : (y += 1) {
- if (y == 10) break :label false;
+ const arr: []const u32 = &.{ 1, 3, 10 };
+ const ok = label: for (arr) |y| {
+ if (y == x) break :label false;
} else {
break :label true;
};