From 047df44d71d6a3e82ba748900ceaffaea072738e Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 18 Mar 2026 01:06:07 +0100 Subject: [PATCH 1/4] 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. --- lib/zig.h | 14 ++ src/codegen/c.zig | 252 +++++++++++++++++++++++++--------- test/behavior/switch.zig | 30 ++++ test/behavior/switch_loop.zig | 30 ++++ 4 files changed, 260 insertions(+), 66 deletions(-) diff --git a/lib/zig.h b/lib/zig.h index 0b9c6e58ca..dc5666d894 100644 --- 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 index 97e624fd50..e126cf535b 100644 --- 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 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; diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index d95a5733f1..6db21357f0 100644 --- 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 index a653b6a638..4b8cdc71b5 100644 --- 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); +} From dbe787a98404ff3d4b02e3807995cdffd25d3d96 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 18 Mar 2026 01:19:08 +0100 Subject: [PATCH 2/4] Revert "test: disable switch behavior test switching on type >64bits for cbe" This reverts commit e91654b1e730ad8082eeb8bb0ae90a17ca69f679. --- test/behavior/switch.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index 6db21357f0..1cdbbda41e 100644 --- a/test/behavior/switch.zig +++ b/test/behavior/switch.zig @@ -1406,8 +1406,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, From 0b857eebeb9e348370ab88797743eeed9fdffbe6 Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 18 Mar 2026 15:44:27 +0100 Subject: [PATCH 3/4] test: disable 'switch on large types' behavior tests for wasm backend The self-hosted wasm backend doesn't properly support very large integers yet. --- test/behavior/switch.zig | 2 ++ test/behavior/switch_loop.zig | 2 ++ 2 files changed, 4 insertions(+) diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index 1cdbbda41e..2eb633f479 100644 --- a/test/behavior/switch.zig +++ b/test/behavior/switch.zig @@ -1459,6 +1459,8 @@ test "switch on nested packed containers" { } 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) { diff --git a/test/behavior/switch_loop.zig b/test/behavior/switch_loop.zig index 4b8cdc71b5..14794d2d12 100644 --- a/test/behavior/switch_loop.zig +++ b/test/behavior/switch_loop.zig @@ -566,6 +566,8 @@ test "switch loop with packed unions with OPV" { } 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) { From c10089d060e7c11a01e1bcc382235a24f8c48efa Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 18 Mar 2026 16:56:28 +0100 Subject: [PATCH 4/4] test: enable 'switch arbitrary int size' behavior test for cbe was disabled in b25d93e7d95 --- test/behavior/switch.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index 2eb633f479..95fd81e58f 100644 --- a/test/behavior/switch.zig +++ b/test/behavior/switch.zig @@ -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);