Merge pull request 'cbe: fix switch statements on large types' (#31557) from justusk/zig:cbe-switch-128 into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/31557
This commit is contained in:
Andrew Kelley
2026-03-18 20:57:04 +01:00
4 changed files with 264 additions and 70 deletions
+14
View File
@@ -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) {
+186 -66
View File
@@ -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 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()) {
try w.writeByte('(');
try f.renderType(w, lowered_condition_ty);
try w.writeByte(')');
}
try f.writeCValue(w, condition, .other);
try w.writeAll(") {");
f.indent();
const liveness = try f.liveness.getSwitchBr(gpa, inst, switch_br.cases_len + 1);
defer gpa.free(liveness.deaths);
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_cond_ty);
try w.writeByte(')');
}
try f.writeCValue(w, cond_val, .other);
try w.writeAll(") {");
f.indent();
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;
+32 -4
View File
@@ -49,8 +49,6 @@ test "switch arbitrary int size" {
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_c and builtin.os.tag == .windows) return error.SkipZigTest; // TODO
try expect(testSwitchArbInt(u64, 0) == 0);
try expect(testSwitchArbInt(u64, 12) == 1);
try expect(testSwitchArbInt(u64, maxInt(u64)) == 2);
@@ -1406,8 +1404,6 @@ test "switch on packed union" {
}
test "switch on nested packed containers" {
if (builtin.object_format == .c) return error.SkipZigTest; // https://codeberg.org/ziglang/zig/issues/31467
const P = packed struct {
iu: u17,
is: i31,
@@ -1459,3 +1455,35 @@ test "switch on nested packed containers" {
.p = .{ .a = 2, .b = 17 },
});
}
test "switch on large types" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest;
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);
}
+32
View File
@@ -564,3 +564,35 @@ 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" {
if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest;
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);
}