commit 047df44d71d6a3e82ba748900ceaffaea072738e (tree)
parent 820308b3d44ed7ca2cabd07a11b2099992ed4e6b
Author: Justus Klausecker <justus@klausecker.de>
Date: Wed, 18 Mar 2026 01:06:07 +0100
cbe: fix `switch` statements on large types
`switch` statements on types >128bits are now lowered to conditionals.
This is necessary because Zig lowers integers with more than 128 bits to
bigints, which are not 'native' integers and thus cannot be used as case
values.
128-bit integers get special treatment because Zig will emit actual 128bit
ints if they are supported by the target. They still *may* be lowered to
bigints though, e.g. for 32-bit targets or MSVC. To solve this, this commit
adds a bunch of switch macros to `zig.h` which will either resolve to an
actual `switch` statement or to conditionals. The `if` statements this
approach can generate are not as optimal as they could be but I think this
is a good trade-off since the generated `switch` statements are still the
same as the ones generated for smaller integers. Also the macros result
in pretty readable code.
Diffstat:
4 files changed, 256 insertions(+), 62 deletions(-)
diff --git a/lib/zig.h b/lib/zig.h
@@ -1981,6 +1981,20 @@ static inline zig_i128 zig_bit_reverse_i128(zig_i128 val, uint8_t bits) {
return zig_bitCast_i128(zig_bit_reverse_u128(zig_bitCast_u128(val), bits));
}
+#if zig_has_int128
+#define zig_switch_int128(operand) switch (operand)
+#define zig_switch_prong_begin_int128()
+#define zig_switch_case_int128(Type, operand, value) case value:
+#define zig_switch_prong_end_int128()
+#define zig_switch_default_int128() default:
+#else // zig_has_int128
+#define zig_switch_int128(operand)
+#define zig_switch_prong_begin_int128() if (0
+#define zig_switch_case_int128(Type, operand, value) || (zig_cmp_##Type(operand, value) == 0)
+#define zig_switch_prong_end_int128() )
+#define zig_switch_default_int128()
+#endif // zig_has_int128
+
/* ========================== Big Integer Support =========================== */
static inline uint16_t zig_int_bytes(uint16_t bits) {
diff --git a/src/codegen/c.zig b/src/codegen/c.zig
@@ -4453,14 +4453,14 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
const switch_br = f.air.unwrapSwitch(inst);
const init_condition = try f.resolveInst(switch_br.operand);
try reap(f, inst, &.{switch_br.operand});
- const condition_ty = f.typeOf(switch_br.operand);
+ const cond_ty = f.typeOf(switch_br.operand);
const w = &f.code.writer;
// For dispatches, we will create a local alloc to contain the condition value.
// This may not result in optimal codegen for switch loops, but it minimizes the
// amount of C code we generate, which is probably more desirable here (and is simpler).
- const condition = if (is_dispatch_loop) cond: {
- const new_local = try f.allocLocal(inst, condition_ty);
+ const cond_val = if (is_dispatch_loop) cond: {
+ const new_local = try f.allocLocal(inst, cond_ty);
try f.copyCValue(new_local, init_condition);
try w.print("zig_switch_{d}_loop:", .{@intFromEnum(inst)});
try f.newline();
@@ -4472,26 +4472,38 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
assert(f.loop_switch_conds.remove(inst));
};
- try w.writeAll("switch (");
+ const liveness = try f.liveness.getSwitchBr(gpa, inst, switch_br.cases_len + 1);
+ defer gpa.free(liveness.deaths);
- const lowered_condition_ty: Type = if (condition_ty.toIntern() == .bool_type)
- .u1
- else if (condition_ty.isPtrAtRuntime(zcu))
- .usize
- else
- condition_ty;
- if (condition_ty.toIntern() != lowered_condition_ty.toIntern()) {
+ const lowered_cond_ty: Type = switch (cond_ty.zigTypeTag(zcu)) {
+ .@"enum", .error_set, .int, .@"struct", .@"union" => cond_ty,
+ .bool => .u1,
+ .pointer => .usize,
+ .void => unreachable, // OPV type, always lowered to block/loop
+ .comptime_int, .enum_literal, .@"fn", .type => unreachable, // comptime-only
+ else => unreachable, // not supported by switch statement
+ };
+ const cond_cint = switch (CType.classifyInt(lowered_cond_ty, zcu)) {
+ .void => unreachable, // OPV type, always lowered to block/loop
+ .small => |small| small,
+ .big => {
+ return lowerSwitchToConditions(f, inst, cond_val, lowered_cond_ty, switch_br, liveness, is_dispatch_loop, false);
+ },
+ };
+
+ switch (cond_cint) {
+ .zig_u128, .zig_i128 => try w.writeAll("zig_switch_int128("),
+ else => try w.writeAll("switch ("),
+ }
+ if (cond_ty.toIntern() != lowered_cond_ty.toIntern()) {
try w.writeByte('(');
- try f.renderType(w, lowered_condition_ty);
+ try f.renderType(w, lowered_cond_ty);
try w.writeByte(')');
}
- try f.writeCValue(w, condition, .other);
+ try f.writeCValue(w, cond_val, .other);
try w.writeAll(") {");
f.indent();
- const liveness = try f.liveness.getSwitchBr(gpa, inst, switch_br.cases_len + 1);
- defer gpa.free(liveness.deaths);
-
var any_range_cases = false;
var it = switch_br.iterateCases();
while (it.next()) |case| {
@@ -4499,28 +4511,63 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
any_range_cases = true;
continue;
}
+
+ switch (cond_cint) {
+ .zig_u128, .zig_i128 => {
+ try f.newline();
+ try w.writeAll("zig_switch_prong_begin_int128()");
+ },
+ else => {},
+ }
+
for (case.items) |item| {
try f.newline();
- try w.writeAll("case ");
+ case: {
+ switch (cond_cint) {
+ .zig_u128 => try w.writeAll(" zig_switch_case_int128(u128, "),
+ .zig_i128 => try w.writeAll(" zig_switch_case_int128(i128, "),
+ else => {
+ try w.writeAll("case ");
+ break :case;
+ },
+ }
+ if (cond_ty.toIntern() != lowered_cond_ty.toIntern()) {
+ try w.writeByte('(');
+ try f.renderType(w, lowered_cond_ty);
+ try w.writeByte(')');
+ }
+ try f.writeCValue(w, cond_val, .other);
+ try w.writeAll(", ");
+ }
const item_value = try f.air.value(item, pt);
// If `item_value` is a pointer with a known integer address, print the address
// with no cast to avoid a warning.
write_val: {
- if (condition_ty.isPtrAtRuntime(zcu)) {
+ if (cond_ty.zigTypeTag(zcu) == .pointer) {
if (item_value.?.getUnsignedInt(zcu)) |item_int| {
- try w.print("{f}", .{try f.fmtIntLiteralDec(try pt.intValue(lowered_condition_ty, item_int))});
+ try w.print("{f}", .{try f.fmtIntLiteralDec(try pt.intValue(lowered_cond_ty, item_int))});
break :write_val;
}
- }
- if (condition_ty.isPtrAtRuntime(zcu)) {
try w.writeByte('(');
try f.renderType(w, .usize);
try w.writeByte(')');
}
try f.dg.renderValue(w, (try f.air.value(item, pt)).?, .other);
}
- try w.writeByte(':');
+ switch (cond_cint) {
+ .zig_u128, .zig_i128 => try w.writeByte(')'),
+ else => try w.writeByte(':'),
+ }
}
+
+ switch (cond_cint) {
+ .zig_u128, .zig_i128 => {
+ try f.newline();
+ try w.writeAll("zig_switch_prong_end_int128()");
+ },
+ else => {},
+ }
+
try w.writeAll(" {");
f.indent();
try f.newline();
@@ -4537,56 +4584,24 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
// The case body must be noreturn so we don't need to insert a break.
}
- const else_body = it.elseBody();
try f.newline();
- try w.writeAll("default: ");
+ switch (cond_cint) {
+ .zig_u128, .zig_i128 => try w.writeAll("zig_switch_default_int128() "),
+ else => try w.writeAll("default: "),
+ }
if (any_range_cases) {
// We will iterate the cases again to handle those with ranges, and generate
// code using conditions rather than switch cases for such cases.
- it = switch_br.iterateCases();
- while (it.next()) |case| {
- if (case.ranges.len == 0) continue; // handled above
-
- try w.writeAll("if (");
- for (case.items, 0..) |item, item_i| {
- if (item_i != 0) try w.writeAll(" || ");
- try f.writeCValue(w, condition, .other);
- try w.writeAll(" == ");
- try f.dg.renderValue(w, (try f.air.value(item, pt)).?, .other);
- }
- for (case.ranges, 0..) |range, range_i| {
- if (case.items.len != 0 or range_i != 0) try w.writeAll(" || ");
- // "(x >= lower && x <= upper)"
- try w.writeByte('(');
- try f.writeCValue(w, condition, .other);
- try w.writeAll(" >= ");
- try f.dg.renderValue(w, (try f.air.value(range[0], pt)).?, .other);
- try w.writeAll(" && ");
- try f.writeCValue(w, condition, .other);
- try w.writeAll(" <= ");
- try f.dg.renderValue(w, (try f.air.value(range[1], pt)).?, .other);
- try w.writeByte(')');
- }
- try w.writeAll(") {");
- f.indent();
- try f.newline();
- if (is_dispatch_loop) {
- try w.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), case.idx });
- }
- try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, true);
- try f.outdent();
- try w.writeByte('}');
- if (f.dg.expected_block) |_|
- return f.fail("runtime code not allowed in naked function", .{});
- }
+ try lowerSwitchToConditions(f, inst, cond_val, lowered_cond_ty, switch_br, liveness, is_dispatch_loop, true);
}
if (is_dispatch_loop) {
try w.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), switch_br.cases_len });
}
+ const else_body = it.elseBody();
if (else_body.len > 0) {
- // Note that this must be the last case, so we do not need to use `genBodyResolveState` since
- // the parent block will do it (because the case body is noreturn).
+ // Note that this must be the last case, so we do not need to use `genBodyResolveState`
+ // since the parent block will do it (because the case body is noreturn).
for (liveness.deaths[liveness.deaths.len - 1]) |death| {
try die(f, inst, death.toRef());
}
@@ -4598,6 +4613,111 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
try f.outdent();
try w.writeAll("}\n");
}
+fn lowerSwitchToConditions(
+ f: *Function,
+ inst: Air.Inst.Index,
+ cond_val: CValue,
+ cond_ty: Type,
+ switch_br: Air.UnwrappedSwitch,
+ liveness: Air.Liveness.SwitchBrTable,
+ is_dispatch_loop: bool,
+ only_ranges: bool,
+) !void {
+ const w = &f.code.writer;
+
+ var it = switch_br.iterateCases();
+ while (it.next()) |case| {
+ if (case.ranges.len == 0 and only_ranges) continue;
+
+ try w.writeAll("if (");
+ for (case.items, 0..) |item, item_i| {
+ if (item_i != 0) {
+ try f.newline();
+ try w.writeAll(" || ");
+ }
+ try lowerSwitchCmp(f, cond_val, .eq, item, cond_ty);
+ }
+ for (case.ranges, 0..) |range, range_i| {
+ if (case.items.len != 0 or range_i != 0) {
+ try f.newline();
+ try w.writeAll(" || ");
+ }
+ // "(x >= lower && x <= upper)"
+ try w.writeByte('(');
+ try lowerSwitchCmp(f, cond_val, .gte, range[0], cond_ty);
+ try w.writeAll(" && ");
+ try lowerSwitchCmp(f, cond_val, .lte, range[1], cond_ty);
+ try w.writeByte(')');
+ }
+ try w.writeAll(") {");
+ f.indent();
+ try f.newline();
+ if (is_dispatch_loop) {
+ try w.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), case.idx });
+ }
+ try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, true);
+ try f.outdent();
+ try w.writeByte('}');
+ try f.newline();
+ if (f.dg.expected_block) |_|
+ return f.fail("runtime code not allowed in naked function", .{});
+ }
+
+ if (!only_ranges) {
+ if (is_dispatch_loop) {
+ try w.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), switch_br.cases_len });
+ }
+ const else_body = it.elseBody();
+ if (else_body.len > 0) {
+ // Note that this must be the last case, so we do not need to use `genBodyResolveState`
+ // since the parent block will do it (because the case body is noreturn).
+ for (liveness.deaths[liveness.deaths.len - 1]) |death| {
+ try die(f, inst, death.toRef());
+ }
+ try genBody(f, else_body);
+ if (f.dg.expected_block) |_|
+ return f.fail("runtime code not allowed in naked function", .{});
+ } else try airUnreach(f);
+ try f.newline();
+ }
+}
+fn lowerSwitchCmp(
+ f: *Function,
+ cond_val: CValue,
+ operator: std.math.CompareOperator,
+ case_inst: Air.Inst.Ref,
+ ty: Type,
+) !void {
+ const pt = f.dg.pt;
+ const zcu = pt.zcu;
+ const w = &f.code.writer;
+
+ const class = CType.classifyInt(ty, zcu);
+ const use_builtin = switch (class) {
+ .void => unreachable, // assertion failure
+ .small => |small| switch (small) {
+ .zig_u128, .zig_i128 => true,
+ else => false,
+ },
+ .big => true,
+ };
+ if (use_builtin) {
+ try w.writeAll("zig_cmp_");
+ try f.dg.renderTypeForBuiltinFnName(w, ty);
+ try w.writeByte('(');
+ }
+ if (class == .big) try w.writeByte('&');
+ try f.writeCValue(w, cond_val, .other);
+ try w.writeAll(if (use_builtin) ", " else compareOperatorC(operator));
+ if (class == .big) try w.writeByte('&');
+ try f.dg.renderValue(w, (try f.air.value(case_inst, pt)).?, .other);
+ if (use_builtin) {
+ try f.dg.renderBuiltinInfo(w, ty, if (class == .big) .bits else .none);
+ try w.writeByte(')');
+ try w.writeAll(compareOperatorC(operator));
+ try w.writeByte('0');
+ }
+}
fn asmInputNeedsLocal(f: *Function, constraint: []const u8, value: CValue) bool {
const dg = f.dg;
diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig
@@ -1459,3 +1459,33 @@ test "switch on nested packed containers" {
.p = .{ .a = 2, .b = 17 },
});
}
+
+test "switch on large types" {
+ const S = struct {
+ fn doTheTest(a: u128, b: i500) !void {
+ switch (a) {
+ 0x0,
+ 0x3...0xFFFF_FFFF_FFFF_FFFF_FFFF_ABCD,
+ 0xFFFF_FFFF_FFFF_FFFF_FFFF_EF00,
+ => return error.TestFailed,
+ 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_0000...0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFF0,
+ => |val| {
+ try expect(val == 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_1234);
+ },
+ else => return error.TestFailed,
+ }
+ switch (b) {
+ 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_0000...0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_1234,
+ => return error.TestFailed,
+ 0xFFFF_1234,
+ 0xFFFF_FFFF_FFFF_FFFF_FFFF_0123...0xFFFF_FFFF_FFFF_FFFF_FFFF_4567,
+ => |val| {
+ try expect(val == 0xFFFF_1234);
+ },
+ else => return error.TestFailed,
+ }
+ }
+ };
+ try S.doTheTest(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_1234, 0xFFFF_1234);
+ try comptime S.doTheTest(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_1234, 0xFFFF_1234);
+}
diff --git a/test/behavior/switch_loop.zig b/test/behavior/switch_loop.zig
@@ -564,3 +564,33 @@ test "switch loop with packed unions with OPV" {
try P.doTheTest(.{ .a = 0 });
try comptime P.doTheTest(.{ .a = 0 });
}
+
+test "switch loop on large types" {
+ const S = struct {
+ fn doTheTest(a: u128, b: i500) !void {
+ label: switch (a) {
+ 0x0,
+ 0x3...0xFFFF_FFFF_FFFF_FFFF_FFFF_ABCD,
+ 0xFFFF_FFFF_FFFF_FFFF_FFFF_EF00,
+ => return error.TestFailed,
+ 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_0000...0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFF0,
+ => |val| {
+ continue :label val + 1;
+ },
+ else => {},
+ }
+ label: switch (b) {
+ 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_0000...0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_1234,
+ => return error.TestFailed,
+ 0xFFFF_1234,
+ 0xFFFF_FFFF_FFFF_FFFF_FFFF_0123...0xFFFF_FFFF_FFFF_FFFF_FFFF_4567,
+ => |val| {
+ continue :label val + 1;
+ },
+ else => {},
+ }
+ }
+ };
+ try S.doTheTest(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FF00, 0xFFFF_FFFF_FFFF_FFFF_FFFF_4550);
+ try comptime S.doTheTest(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FF00, 0xFFFF_FFFF_FFFF_FFFF_FFFF_4550);
+}