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 0b9c6e58ca6d9f1800fd8059e5db9d3636faa35c..dc5666d8947f8ad36648ab4481b4b384db1114e0 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 97e624fd506a9cbce4b226b3f83815a4977b9c54..e126cf535b08ee7fef804e051455e4f419c5a32f 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 d95a5733f19f907b3cb6f9d945c3adb4b5b1d5d6..6db21357f00de6515085cbdbd99f57b12e931cca 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 a653b6a638762d290bef062ce7ec43d925379390..4b8cdc71b5e0e458b878577d673390eb60739057 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); +} -- 2.54.0 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 6db21357f00de6515085cbdbd99f57b12e931cca..1cdbbda41e880645a1a4526cbc1f0563926bf40f 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, -- 2.54.0 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 1cdbbda41e880645a1a4526cbc1f0563926bf40f..2eb633f479c2439abec828a20b9c3aba9a90b548 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 4b8cdc71b5e0e458b878577d673390eb60739057..14794d2d120852838b94b3ba7d8e956fbd827441 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) { -- 2.54.0 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 2eb633f479c2439abec828a20b9c3aba9a90b548..95fd81e58fa6a26244f6cbcc9a01f4b6aa38e370 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); -- 2.54.0