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) {
19811981 return zig_bitCast_i128(zig_bit_reverse_u128(zig_bitCast_u128(val), bits));
19821982}
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
19841998/* ========================== Big Integer Support =========================== */
19851999
19862000static 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
44534453 const switch_br = f.air.unwrapSwitch(inst);
44544454 const init_condition = try f.resolveInst(switch_br.operand);
44554455 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);
44574457 const w = &f.code.writer;
44584458
44594459 // For dispatches, we will create a local alloc to contain the condition value.
44604460 // This may not result in optimal codegen for switch loops, but it minimizes the
44614461 // amount of C code we generate, which is probably more desirable here (and is simpler).
4462 const condition = if (is_dispatch_loop) cond: {
4463 const new_local = try f.allocLocal(inst, condition_ty);
4462 const cond_val = if (is_dispatch_loop) cond: {
4463 const new_local = try f.allocLocal(inst, cond_ty);
44644464 try f.copyCValue(new_local, init_condition);
44654465 try w.print("zig_switch_{d}_loop:", .{@intFromEnum(inst)});
44664466 try f.newline();
......@@ -4472,26 +4472,38 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
44724472 assert(f.loop_switch_conds.remove(inst));
44734473 };
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 .u1
4479 else if (condition_ty.isPtrAtRuntime(zcu))
4480 .usize
4481 else
4482 condition_ty;
4483 if (condition_ty.toIntern() != lowered_condition_ty.toIntern()) {
4478 const lowered_cond_ty: Type = switch (cond_ty.zigTypeTag(zcu)) {
4479 .@"enum", .error_set, .int, .@"struct", .@"union" => cond_ty,
4480 .bool => .u1,
4481 .pointer => .usize,
4482 .void => unreachable, // OPV type, always lowered to block/loop
4483 .comptime_int, .enum_literal, .@"fn", .type => unreachable, // comptime-only
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()) {
44844499 try w.writeByte('(');
4485 try f.renderType(w, lowered_condition_ty);
4500 try f.renderType(w, lowered_cond_ty);
44864501 try w.writeByte(')');
44874502 }
4488 try f.writeCValue(w, condition, .other);
4503 try f.writeCValue(w, cond_val, .other);
44894504 try w.writeAll(") {");
44904505 f.indent();
44914506
4492 const liveness = try f.liveness.getSwitchBr(gpa, inst, switch_br.cases_len + 1);
4493 defer gpa.free(liveness.deaths);
4494
44954507 var any_range_cases = false;
44964508 var it = switch_br.iterateCases();
44974509 while (it.next()) |case| {
......@@ -4499,28 +4511,63 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
44994511 any_range_cases = true;
45004512 continue;
45014513 }
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
45024523 for (case.items) |item| {
45034524 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 }
45054542 const item_value = try f.air.value(item, pt);
45064543 // If `item_value` is a pointer with a known integer address, print the address
45074544 // with no cast to avoid a warning.
45084545 write_val: {
4509 if (condition_ty.isPtrAtRuntime(zcu)) {
4546 if (cond_ty.zigTypeTag(zcu) == .pointer) {
45104547 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))});
45124549 break :write_val;
45134550 }
4514 }
4515 if (condition_ty.isPtrAtRuntime(zcu)) {
45164551 try w.writeByte('(');
45174552 try f.renderType(w, .usize);
45184553 try w.writeByte(')');
45194554 }
45204555 try f.dg.renderValue(w, (try f.air.value(item, pt)).?, .other);
45214556 }
4522 try w.writeByte(':');
4557 switch (cond_cint) {
4558 .zig_u128, .zig_i128 => try w.writeByte(')'),
4559 else => try w.writeByte(':'),
4560 }
45234561 }
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
45244571 try w.writeAll(" {");
45254572 f.indent();
45264573 try f.newline();
......@@ -4537,56 +4584,24 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
45374584 // The case body must be noreturn so we don't need to insert a break.
45384585 }
45394586
4540 const else_body = it.elseBody();
45414587 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 }
45444593 if (any_range_cases) {
45454594 // We will iterate the cases again to handle those with ranges, and generate
45464595 // code using conditions rather than switch cases for such cases.
4547 it = switch_br.iterateCases();
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 }
4596 try lowerSwitchToConditions(f, inst, cond_val, lowered_cond_ty, switch_br, liveness, is_dispatch_loop, true);
45834597 }
45844598 if (is_dispatch_loop) {
45854599 try w.print("zig_switch_{d}_dispatch_{d}: ", .{ @intFromEnum(inst), switch_br.cases_len });
45864600 }
4601 const else_body = it.elseBody();
45874602 if (else_body.len > 0) {
4588 // Note that this must be the last case, so we do not need to use `genBodyResolveState` since
4589 // the parent block will do it (because the case body is noreturn).
4603 // Note that this must be the last case, so we do not need to use `genBodyResolveState`
4604 // since the parent block will do it (because the case body is noreturn).
45904605 for (liveness.deaths[liveness.deaths.len - 1]) |death| {
45914606 try die(f, inst, death.toRef());
45924607 }
......@@ -4598,6 +4613,111 @@ fn airSwitchBr(f: *Function, inst: Air.Inst.Index, is_dispatch_loop: bool) !void
45984613 try f.outdent();
45994614 try w.writeAll("}\n");
46004615}
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
46024722fn asmInputNeedsLocal(f: *Function, constraint: []const u8, value: CValue) bool {
46034723 const dg = f.dg;
test/behavior/switch.zig+30
......@@ -1459,3 +1459,33 @@ test "switch on nested packed containers" {
14591459 .p = .{ .a = 2, .b = 17 },
14601460 });
14611461}
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" {
564564 try P.doTheTest(.{ .a = 0 });
565565 try comptime P.doTheTest(.{ .a = 0 });
566566}
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}