authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-03-18 01:06:07+01:00
committergravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-03-18 15:47:18+01:00
log047df44d71d6a3e82ba748900ceaffaea072738e
treed255dd0b572cdb6651aecc5d10b5d40274cc4a0d
parent820308b3d44ed7ca2cabd07a11b2099992ed4e6b

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.

4 files changed, 256 insertions(+), 62 deletions(-)

lib/zig.h+14
...@@ -1981,6 +1981,20 @@ static inline zig_i128 zig_bit_reverse_i128(zig_i128 val, uint8_t bits) {...@@ -1981,6 +1981,20 @@ static inline zig_i128 zig_bit_reverse_i128(zig_i128 val, uint8_t bits) {
1981 return zig_bitCast_i128(zig_bit_reverse_u128(zig_bitCast_u128(val), bits));1981 return zig_bitCast_i128(zig_bit_reverse_u128(zig_bitCast_u128(val), bits));
1982}1982}
19831983
1984#if zig_has_int128
1985#define zig_switch_int128(operand) switch (operand)
1986#define zig_switch_prong_begin_int128()
1987#define zig_switch_case_int128(Type, operand, value) case value:
1988#define zig_switch_prong_end_int128()
1989#define zig_switch_default_int128() default:
1990#else // zig_has_int128
1991#define zig_switch_int128(operand)
1992#define zig_switch_prong_begin_int128() if (0
1993#define zig_switch_case_int128(Type, operand, value) || (zig_cmp_##Type(operand, value) == 0)
1994#define zig_switch_prong_end_int128() )
1995#define zig_switch_default_int128()
1996#endif // zig_has_int128
1997
1984/* ========================== Big Integer Support =========================== */1998/* ========================== Big Integer Support =========================== */
19851999
1986static inline uint16_t zig_int_bytes(uint16_t bits) {2000static inline uint16_t zig_int_bytes(uint16_t bits) {
src/codegen/c.zig+182-62
...@@ -4453,14 +4453,14 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void...@@ -4453,14 +4453,14 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
4453 const switch_br = f.air.unwrapSwitch(inst);4453 const switch_br = f.air.unwrapSwitch(inst);
4454 const init_condition = try f.resolveInst(switch_br.operand);4454 const init_condition = try f.resolveInst(switch_br.operand);
4455 try reap(f, inst, &.{switch_br.operand});4455 try reap(f, inst, &.{switch_br.operand});
4456 const condition_ty = f.typeOf(switch_br.operand);4456 const cond_ty = f.typeOf(switch_br.operand);
4457 const w = &f.code.writer;4457 const w = &f.code.writer;
44584458
4459 // For dispatches, we will create a local alloc to contain the condition value.4459 // For dispatches, we will create a local alloc to contain the condition value.
4460 // This may not result in optimal codegen for switch loops, but it minimizes the4460 // This may not result in optimal codegen for switch loops, but it minimizes the
4461 // amount of C code we generate, which is probably more desirable here (and is simpler).4461 // amount of C code we generate, which is probably more desirable here (and is simpler).
4462 const condition = if (is_dispatch_loop) cond: {4462 const cond_val = if (is_dispatch_loop) cond: {
4463 const new_local = try f.allocLocal(inst, condition_ty);4463 const new_local = try f.allocLocal(inst, cond_ty);
4464 try f.copyCValue(new_local, init_condition);4464 try f.copyCValue(new_local, init_condition);
4465 try w.print("zig_switch_{d}_loop:", .{@intFromEnum(inst)});4465 try w.print("zig_switch_{d}_loop:", .{@intFromEnum(inst)});
4466 try f.newline();4466 try f.newline();
...@@ -4472,26 +4472,38 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void...@@ -4472,26 +4472,38 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
4472 assert(f.loop_switch_conds.remove(inst));4472 assert(f.loop_switch_conds.remove(inst));
4473 };4473 };
44744474
4475 try w.writeAll("switch (");4475 const liveness = try f.liveness.getSwitchBr(gpa, inst, switch_br.cases_len + 1);
4476 defer gpa.free(liveness.deaths);
44764477
4477 const lowered_condition_ty: Type = if (condition_ty.toIntern() == .bool_type)4478 const lowered_cond_ty: Type = switch (cond_ty.zigTypeTag(zcu)) {
4478 .u14479 .@"enum", .error_set, .int, .@"struct", .@"union" => cond_ty,
4479 else if (condition_ty.isPtrAtRuntime(zcu))4480 .bool => .u1,
4480 .usize4481 .pointer => .usize,
4481 else4482 .void => unreachable, // OPV type, always lowered to block/loop
4482 condition_ty;4483 .comptime_int, .enum_literal, .@"fn", .type => unreachable, // comptime-only
4483 if (condition_ty.toIntern() != lowered_condition_ty.toIntern()) {4484 else => unreachable, // not supported by switch statement
4485 };
4486 const cond_cint = switch (CType.classifyInt(lowered_cond_ty, zcu)) {
4487 .void => unreachable, // OPV type, always lowered to block/loop
4488 .small => |small| small,
4489 .big => {
4490 return lowerSwitchToConditions(f, inst, cond_val, lowered_cond_ty, switch_br, liveness, is_dispatch_loop, false);
4491 },
4492 };
4493
4494 switch (cond_cint) {
4495 .zig_u128, .zig_i128 => try w.writeAll("zig_switch_int128("),
4496 else => try w.writeAll("switch ("),
4497 }
4498 if (cond_ty.toIntern() != lowered_cond_ty.toIntern()) {
4484 try w.writeByte('(');4499 try w.writeByte('(');
4485 try f.renderType(w, lowered_condition_ty);4500 try f.renderType(w, lowered_cond_ty);
4486 try w.writeByte(')');4501 try w.writeByte(')');
4487 }4502 }
4488 try f.writeCValue(w, condition, .other);4503 try f.writeCValue(w, cond_val, .other);
4489 try w.writeAll(") {");4504 try w.writeAll(") {");
4490 f.indent();4505 f.indent();
44914506
4492 const liveness = try f.liveness.getSwitchBr(gpa, inst, switch_br.cases_len + 1);
4493 defer gpa.free(liveness.deaths);
4494
4495 var any_range_cases = false;4507 var any_range_cases = false;
4496 var it = switch_br.iterateCases();4508 var it = switch_br.iterateCases();
4497 while (it.next()) |case| {4509 while (it.next()) |case| {
...@@ -4499,28 +4511,63 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void...@@ -4499,28 +4511,63 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
4499 any_range_cases = true;4511 any_range_cases = true;
4500 continue;4512 continue;
4501 }4513 }
4514
4515 switch (cond_cint) {
4516 .zig_u128, .zig_i128 => {
4517 try f.newline();
4518 try w.writeAll("zig_switch_prong_begin_int128()");
4519 },
4520 else => {},
4521 }
4522
4502 for (case.items) |item| {4523 for (case.items) |item| {
4503 try f.newline();4524 try f.newline();
4504 try w.writeAll("case ");4525 case: {
4526 switch (cond_cint) {
4527 .zig_u128 => try w.writeAll(" zig_switch_case_int128(u128, "),
4528 .zig_i128 => try w.writeAll(" zig_switch_case_int128(i128, "),
4529 else => {
4530 try w.writeAll("case ");
4531 break :case;
4532 },
4533 }
4534 if (cond_ty.toIntern() != lowered_cond_ty.toIntern()) {
4535 try w.writeByte('(');
4536 try f.renderType(w, lowered_cond_ty);
4537 try w.writeByte(')');
4538 }
4539 try f.writeCValue(w, cond_val, .other);
4540 try w.writeAll(", ");
4541 }
4505 const item_value = try f.air.value(item, pt);4542 const item_value = try f.air.value(item, pt);
4506 // If `item_value` is a pointer with a known integer address, print the address4543 // If `item_value` is a pointer with a known integer address, print the address
4507 // with no cast to avoid a warning.4544 // with no cast to avoid a warning.
4508 write_val: {4545 write_val: {
4509 if (condition_ty.isPtrAtRuntime(zcu)) {4546 if (cond_ty.zigTypeTag(zcu) == .pointer) {
4510 if (item_value.?.getUnsignedInt(zcu)) |item_int| {4547 if (item_value.?.getUnsignedInt(zcu)) |item_int| {
4511 try w.print("{f}", .{try f.fmtIntLiteralDec(try pt.intValue(lowered_condition_ty, item_int))});4548 try w.print("{f}", .{try f.fmtIntLiteralDec(try pt.intValue(lowered_cond_ty, item_int))});
4512 break :write_val;4549 break :write_val;
4513 }4550 }
4514 }
4515 if (condition_ty.isPtrAtRuntime(zcu)) {
4516 try w.writeByte('(');4551 try w.writeByte('(');
4517 try f.renderType(w, .usize);4552 try f.renderType(w, .usize);
4518 try w.writeByte(')');4553 try w.writeByte(')');
4519 }4554 }
4520 try f.dg.renderValue(w, (try f.air.value(item, pt)).?, .other);4555 try f.dg.renderValue(w, (try f.air.value(item, pt)).?, .other);
4521 }4556 }
4522 try w.writeByte(':');4557 switch (cond_cint) {
4558 .zig_u128, .zig_i128 => try w.writeByte(')'),
4559 else => try w.writeByte(':'),
4560 }
4523 }4561 }
4562
4563 switch (cond_cint) {
4564 .zig_u128, .zig_i128 => {
4565 try f.newline();
4566 try w.writeAll("zig_switch_prong_end_int128()");
4567 },
4568 else => {},
4569 }
4570
4524 try w.writeAll(" {");4571 try w.writeAll(" {");
4525 f.indent();4572 f.indent();
4526 try f.newline();4573 try f.newline();
...@@ -4537,56 +4584,24 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void...@@ -4537,56 +4584,24 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
4537 // The case body must be noreturn so we don't need to insert a break.4584 // The case body must be noreturn so we don't need to insert a break.
4538 }4585 }
45394586
4540 const else_body = it.elseBody();
4541 try f.newline();4587 try f.newline();
45424588
4543 try w.writeAll("default: ");4589 switch (cond_cint) {
4590 .zig_u128, .zig_i128 => try w.writeAll("zig_switch_default_int128() "),
4591 else => try w.writeAll("default: "),
4592 }
4544 if (any_range_cases) {4593 if (any_range_cases) {
4545 // We will iterate the cases again to handle those with ranges, and generate4594 // We will iterate the cases again to handle those with ranges, and generate
4546 // code using conditions rather than switch cases for such cases.4595 // code using conditions rather than switch cases for such cases.
4547 it = switch_br.iterateCases();4596 try lowerSwitchToConditions(f, inst, cond_val, lowered_cond_ty, switch_br, liveness, is_dispatch_loop, true);
4548 while (it.next()) |case| {
4549 if (case.ranges.len == 0) continue; // handled above
4550
4551 try w.writeAll("if (");
4552 for (case.items, 0..) |item, item_i| {
4553 if (item_i != 0) try w.writeAll(" || ");
4554 try f.writeCValue(w, condition, .other);
4555 try w.writeAll(" == ");
4556 try f.dg.renderValue(w, (try f.air.value(item, pt)).?, .other);
4557 }
4558 for (case.ranges, 0..) |range, range_i| {
4559 if (case.items.len != 0 or range_i != 0) try w.writeAll(" || ");
4560 // "(x >= lower && x <= upper)"
4561 try w.writeByte('(');
4562 try f.writeCValue(w, condition, .other);
4563 try w.writeAll(" >= ");
4564 try f.dg.renderValue(w, (try f.air.value(range[0], pt)).?, .other);
4565 try w.writeAll(" && ");
4566 try f.writeCValue(w, condition, .other);
4567 try w.writeAll(" <= ");
4568 try f.dg.renderValue(w, (try f.air.value(range[1], pt)).?, .other);
4569 try w.writeByte(')');
4570 }
4571 try w.writeAll(") {");
4572 f.indent();
4573 try f.newline();
4574 if (is_dispatch_loop) {
4575 try w.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), case.idx });
4576 }
4577 try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, true);
4578 try f.outdent();
4579 try w.writeByte('}');
4580 if (f.dg.expected_block) |_|
4581 return f.fail("runtime code not allowed in naked function", .{});
4582 }
4583 }4597 }
4584 if (is_dispatch_loop) {4598 if (is_dispatch_loop) {
4585 try w.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), switch_br.cases_len });4599 try w.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), switch_br.cases_len });
4586 }4600 }
4601 const else_body = it.elseBody();
4587 if (else_body.len > 0) {4602 if (else_body.len > 0) {
4588 // Note that this must be the last case, so we do not need to use `genBodyResolveState` since4603 // Note that this must be the last case, so we do not need to use `genBodyResolveState`
4589 // the parent block will do it (because the case body is noreturn).4604 // since the parent block will do it (because the case body is noreturn).
4590 for (liveness.deaths[liveness.deaths.len - 1]) |death| {4605 for (liveness.deaths[liveness.deaths.len - 1]) |death| {
4591 try die(f, inst, death.toRef());4606 try die(f, inst, death.toRef());
4592 }4607 }
...@@ -4598,6 +4613,111 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void...@@ -4598,6 +4613,111 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
4598 try f.outdent();4613 try f.outdent();
4599 try w.writeAll("}\n");4614 try w.writeAll("}\n");
4600}4615}
4616fn lowerSwitchToConditions(
4617 f: *Function,
4618 inst: Air.Inst.Index,
4619 cond_val: CValue,
4620 cond_ty: Type,
4621 switch_br: Air.UnwrappedSwitch,
4622 liveness: Air.Liveness.SwitchBrTable,
4623 is_dispatch_loop: bool,
4624 only_ranges: bool,
4625) !void {
4626 const w = &f.code.writer;
4627
4628 var it = switch_br.iterateCases();
4629 while (it.next()) |case| {
4630 if (case.ranges.len == 0 and only_ranges) continue;
4631
4632 try w.writeAll("if (");
4633 for (case.items, 0..) |item, item_i| {
4634 if (item_i != 0) {
4635 try f.newline();
4636 try w.writeAll(" || ");
4637 }
4638 try lowerSwitchCmp(f, cond_val, .eq, item, cond_ty);
4639 }
4640 for (case.ranges, 0..) |range, range_i| {
4641 if (case.items.len != 0 or range_i != 0) {
4642 try f.newline();
4643 try w.writeAll(" || ");
4644 }
4645 // "(x >= lower && x <= upper)"
4646 try w.writeByte('(');
4647 try lowerSwitchCmp(f, cond_val, .gte, range[0], cond_ty);
4648 try w.writeAll(" && ");
4649 try lowerSwitchCmp(f, cond_val, .lte, range[1], cond_ty);
4650 try w.writeByte(')');
4651 }
4652 try w.writeAll(") {");
4653 f.indent();
4654 try f.newline();
4655 if (is_dispatch_loop) {
4656 try w.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), case.idx });
4657 }
4658 try genBodyResolveState(f, inst, liveness.deaths[case.idx], case.body, true);
4659 try f.outdent();
4660 try w.writeByte('}');
4661 try f.newline();
4662 if (f.dg.expected_block) |_|
4663 return f.fail("runtime code not allowed in naked function", .{});
4664 }
4665
4666 if (!only_ranges) {
4667 if (is_dispatch_loop) {
4668 try w.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), switch_br.cases_len });
4669 }
4670 const else_body = it.elseBody();
4671 if (else_body.len > 0) {
4672 // Note that this must be the last case, so we do not need to use `genBodyResolveState`
4673 // since the parent block will do it (because the case body is noreturn).
4674 for (liveness.deaths[liveness.deaths.len - 1]) |death| {
4675 try die(f, inst, death.toRef());
4676 }
4677 try genBody(f, else_body);
4678 if (f.dg.expected_block) |_|
4679 return f.fail("runtime code not allowed in naked function", .{});
4680 } else try airUnreach(f);
4681 try f.newline();
4682 }
4683}
4684fn lowerSwitchCmp(
4685 f: *Function,
4686 cond_val: CValue,
4687 operator: std.math.CompareOperator,
4688 case_inst: Air.Inst.Ref,
4689 ty: Type,
4690) !void {
4691 const pt = f.dg.pt;
4692 const zcu = pt.zcu;
4693 const w = &f.code.writer;
4694
4695 const class = CType.classifyInt(ty, zcu);
4696 const use_builtin = switch (class) {
4697 .void => unreachable, // assertion failure
4698 .small => |small| switch (small) {
4699 .zig_u128, .zig_i128 => true,
4700 else => false,
4701 },
4702 .big => true,
4703 };
4704 if (use_builtin) {
4705 try w.writeAll("zig_cmp_");
4706 try f.dg.renderTypeForBuiltinFnName(w, ty);
4707 try w.writeByte('(');
4708 }
4709 if (class == .big) try w.writeByte('&');
4710 try f.writeCValue(w, cond_val, .other);
4711 try w.writeAll(if (use_builtin) ", " else compareOperatorC(operator));
4712 if (class == .big) try w.writeByte('&');
4713 try f.dg.renderValue(w, (try f.air.value(case_inst, pt)).?, .other);
4714 if (use_builtin) {
4715 try f.dg.renderBuiltinInfo(w, ty, if (class == .big) .bits else .none);
4716 try w.writeByte(')');
4717 try w.writeAll(compareOperatorC(operator));
4718 try w.writeByte('0');
4719 }
4720}
46014721
4602fn asmInputNeedsLocal(f: *Function, constraint: []const u8, value: CValue) bool {4722fn asmInputNeedsLocal(f: *Function, constraint: []const u8, value: CValue) bool {
4603 const dg = f.dg;4723 const dg = f.dg;
test/behavior/switch.zig+30
...@@ -1459,3 +1459,33 @@ test "switch on nested packed containers" {...@@ -1459,3 +1459,33 @@ test "switch on nested packed containers" {
1459 .p = .{ .a = 2, .b = 17 },1459 .p = .{ .a = 2, .b = 17 },
1460 });1460 });
1461}1461}
1462
1463test "switch on large types" {
1464 const S = struct {
1465 fn doTheTest(a: u128, b: i500) !void {
1466 switch (a) {
1467 0x0,
1468 0x3...0xFFFF_FFFF_FFFF_FFFF_FFFF_ABCD,
1469 0xFFFF_FFFF_FFFF_FFFF_FFFF_EF00,
1470 => return error.TestFailed,
1471 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_0000...0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFF0,
1472 => |val| {
1473 try expect(val == 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_1234);
1474 },
1475 else => return error.TestFailed,
1476 }
1477 switch (b) {
1478 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,
1479 => return error.TestFailed,
1480 0xFFFF_1234,
1481 0xFFFF_FFFF_FFFF_FFFF_FFFF_0123...0xFFFF_FFFF_FFFF_FFFF_FFFF_4567,
1482 => |val| {
1483 try expect(val == 0xFFFF_1234);
1484 },
1485 else => return error.TestFailed,
1486 }
1487 }
1488 };
1489 try S.doTheTest(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_1234, 0xFFFF_1234);
1490 try comptime S.doTheTest(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_1234, 0xFFFF_1234);
1491}
test/behavior/switch_loop.zig+30
...@@ -564,3 +564,33 @@ test "switch loop with packed unions with OPV" {...@@ -564,3 +564,33 @@ test "switch loop with packed unions with OPV" {
564 try P.doTheTest(.{ .a = 0 });564 try P.doTheTest(.{ .a = 0 });
565 try comptime P.doTheTest(.{ .a = 0 });565 try comptime P.doTheTest(.{ .a = 0 });
566}566}
567
568test "switch loop on large types" {
569 const S = struct {
570 fn doTheTest(a: u128, b: i500) !void {
571 label: switch (a) {
572 0x0,
573 0x3...0xFFFF_FFFF_FFFF_FFFF_FFFF_ABCD,
574 0xFFFF_FFFF_FFFF_FFFF_FFFF_EF00,
575 => return error.TestFailed,
576 0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_0000...0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFF0,
577 => |val| {
578 continue :label val + 1;
579 },
580 else => {},
581 }
582 label: switch (b) {
583 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,
584 => return error.TestFailed,
585 0xFFFF_1234,
586 0xFFFF_FFFF_FFFF_FFFF_FFFF_0123...0xFFFF_FFFF_FFFF_FFFF_FFFF_4567,
587 => |val| {
588 continue :label val + 1;
589 },
590 else => {},
591 }
592 }
593 };
594 try S.doTheTest(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FF00, 0xFFFF_FFFF_FFFF_FFFF_FFFF_4550);
595 try comptime S.doTheTest(0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FF00, 0xFFFF_FFFF_FFFF_FFFF_FFFF_4550);
596}