From 047df44d71d6a3e82ba748900ceaffaea072738e Mon Sep 17 00:00:00 2001 From: Justus Klausecker Date: Wed, 18 Mar 2026 01:06:07 +0100 Subject: [PATCH] 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