authorgravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-06-15 01:27:09+02:00
committergravatar for justus@klausecker.deJustus Klausecker <justus@klausecker.de> 2026-06-15 02:06:17+02:00
log8c3b99a0bbc23fd48a85d445088108eb9d6e7dcf
tree3453cd5373b990bd9599a0eadc7526d9478145fa
parenta85cb728775375825afe4ebd62c60ae0b361d1e9

Sema: make switch prong item duplicate validation go faster

`RangeSet` used to do a linear search for any overlapping ranges on every insert, leading to O(n^2) comparisons. This had the 'advantage' that the entire set only needs to be sorted once, when checking whether the whole value range of a given type has been covered. It now instead keeps itself sorted at all times which means that we can use binary search and only perform O(n log n) comparisons. This turns out to be way faster for large amounts of switch prong items.

3 files changed, 82 insertions(+), 41 deletions(-)

src/RangeSet.zig+44-31
......@@ -1,6 +1,6 @@
11const RangeSet = @This();
22
3ranges: std.ArrayList(Range),
3ranges: std.MultiArrayList(Range),
44
55pub const Range = struct {
66 first: Value,
......@@ -22,15 +22,19 @@ pub fn ensureUnusedCapacity(self: *RangeSet, allocator: Allocator, additional_co
2222pub fn addAssumeCapacity(set: *RangeSet, new: Range, ty: Type, zcu: *Zcu) ?LazySrcLoc {
2323 assert(new.first.typeOf(zcu).eql(ty));
2424 assert(new.last.typeOf(zcu).eql(ty));
25 assert(new.first.compareScalar(.lte, new.last, ty, zcu));
2526
26 for (set.ranges.items) |range| {
27 if (new.last.compareScalar(.gte, range.first, ty, zcu) and
28 new.first.compareScalar(.lte, range.last, ty, zcu))
29 {
30 return range.src; // They overlap.
31 }
27 const idx = std.sort.lowerBound(Value, set.ranges.items(.last), @as(SearchCtx, .{
28 .val = new.first,
29 .zcu = zcu,
30 }), compare);
31
32 if (idx != set.ranges.len and // `new.first` is *not* greater than all `old.last`
33 new.last.compareScalar(.gte, set.ranges.items(.first)[idx], ty, zcu))
34 {
35 return set.ranges.items(.src)[idx]; // `new` overlaps with existing range.
3236 }
33 set.ranges.appendAssumeCapacity(new);
37 set.ranges.insertAssumeCapacity(idx, new);
3438 return null;
3539}
3640
......@@ -39,15 +43,6 @@ pub fn add(set: *RangeSet, allocator: Allocator, new: Range, ty: Type, zcu: *Zcu
3943 return set.addAssumeCapacity(new, ty, zcu);
4044}
4145
42const SortCtx = struct {
43 ty: Type,
44 zcu: *Zcu,
45};
46/// Assumes a and b do not overlap
47fn lessThan(ctx: SortCtx, a: Range, b: Range) bool {
48 return a.first.compareScalar(.lt, b.first, ctx.ty, ctx.zcu);
49}
50
5146pub fn spans(
5247 set: *RangeSet,
5348 allocator: Allocator,
......@@ -58,35 +53,36 @@ pub fn spans(
5853) Allocator.Error!bool {
5954 assert(first.typeOf(zcu).eql(ty));
6055 assert(last.typeOf(zcu).eql(ty));
61 if (set.ranges.items.len == 0) return false;
56 if (set.ranges.len == 0) return false;
6257
63 std.mem.sort(Range, set.ranges.items, SortCtx{ .ty = ty, .zcu = zcu }, lessThan);
58 assert(std.sort.isSorted(Value, set.ranges.items(.first), @as(SortCtx, .{ .ty = ty, .zcu = zcu }), lessThan));
59 assert(std.sort.isSorted(Value, set.ranges.items(.last), @as(SortCtx, .{ .ty = ty, .zcu = zcu }), lessThan));
6460
65 if (!set.ranges.items[0].first.eql(first, ty, zcu) or
66 !set.ranges.items[set.ranges.items.len - 1].last.eql(last, ty, zcu))
61 if (!set.ranges.items(.first)[0].eql(first, ty, zcu) or
62 !set.ranges.items(.last)[set.ranges.len - 1].eql(last, ty, zcu))
6763 {
6864 return false;
6965 }
7066
7167 const limbs = try allocator.alloc(
72 std.math.big.Limb,
73 std.math.big.int.calcTwosCompLimbCount(ty.intInfo(zcu).bits),
68 math.big.Limb,
69 math.big.int.calcTwosCompLimbCount(ty.intInfo(zcu).bits),
7470 );
7571 defer allocator.free(limbs);
76 var counter: std.math.big.int.Mutable = .init(limbs, 0);
72 var counter: math.big.int.Mutable = .init(limbs, 0);
7773
7874 var space: InternPool.Key.Int.Storage.BigIntSpace = undefined;
7975
8076 // look for gaps
81 for (set.ranges.items[1..], 0..) |cur, i| {
82 // i starts counting from the second item.
83 const prev = set.ranges.items[i];
84
85 // prev.last + 1 == cur.first
86 counter.copy(prev.last.toBigInt(&space, zcu));
77 for (
78 set.ranges.items(.first)[1..],
79 set.ranges.items(.last)[0 .. set.ranges.len - 1],
80 ) |cur_first, prev_last| {
81 // prev_last + 1 == cur_first
82 counter.copy(prev_last.toBigInt(&space, zcu));
8783 counter.addScalar(counter.toConst(), 1);
8884
89 const cur_start_int = cur.first.toBigInt(&space, zcu);
85 const cur_start_int = cur_first.toBigInt(&space, zcu);
9086 if (!cur_start_int.eql(counter.toConst())) {
9187 return false;
9288 }
......@@ -95,7 +91,24 @@ pub fn spans(
9591 return true;
9692}
9793
94const SearchCtx = struct {
95 val: Value,
96 zcu: *const Zcu,
97};
98fn compare(ctx: SearchCtx, other: Value) math.Order {
99 return ctx.val.order(other, ctx.zcu);
100}
101
102const SortCtx = struct {
103 ty: Type,
104 zcu: *Zcu,
105};
106fn lessThan(ctx: SortCtx, a: Value, b: Value) bool {
107 return a.compareScalar(.lt, b, ctx.ty, ctx.zcu);
108}
109
98110const std = @import("std");
111const math = std.math;
99112const assert = std.debug.assert;
100113const Allocator = std.mem.Allocator;
101114
src/Sema.zig+5-5
......@@ -10866,7 +10866,7 @@ fn finishSwitchBr(
1086610866const ValidatedSwitchBlock = struct {
1086710867 seen_enum_fields: []const ?LazySrcLoc,
1086810868 seen_errors: std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc),
10869 seen_ranges: []const RangeSet.Range,
10869 seen_ranges: std.MultiArrayList(RangeSet.Range).Slice,
1087010870 true_src: ?LazySrcLoc,
1087110871 false_src: ?LazySrcLoc,
1087210872 void_src: ?LazySrcLoc,
......@@ -10901,7 +10901,7 @@ const ValidatedSwitchBlock = struct {
1090110901 error_names: InternPool.NullTerminatedString.Slice,
1090210902 seen_enum_fields: []const ?LazySrcLoc,
1090310903 seen_errors: *const std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc),
10904 seen_ranges: []const RangeSet.Range,
10904 seen_ranges: std.MultiArrayList(RangeSet.Range).Slice,
1090510905 seen_true: bool,
1090610906 seen_false: bool,
1090710907 seen_void: bool,
......@@ -10938,13 +10938,13 @@ const ValidatedSwitchBlock = struct {
1093810938 else => unreachable,
1093910939 };
1094010940 while (it.next_idx < it.seen_ranges.len and
10941 cur_val.eql(it.seen_ranges[it.next_idx].first, int_ty, zcu))
10941 cur_val.eql(it.seen_ranges.items(.first)[it.next_idx], int_ty, zcu))
1094210942 {
1094310943 defer it.next_idx += 1;
1094410944 const incr = try arith.incrementDefinedInt(
1094510945 sema,
1094610946 int_ty,
10947 it.seen_ranges[it.next_idx].last,
10947 it.seen_ranges.items(.last)[it.next_idx],
1094810948 );
1094910949 if (incr.overflow) {
1095010950 it.next_val = null;
......@@ -11432,7 +11432,7 @@ fn validateSwitchBlock(
1143211432 return .{
1143311433 .seen_enum_fields = seen_enum_fields,
1143411434 .seen_errors = seen_errors,
11435 .seen_ranges = range_set.ranges.items,
11435 .seen_ranges = range_set.ranges.slice(),
1143611436 .true_src = true_src,
1143711437 .false_src = false_src,
1143811438 .void_src = void_src,
test/cases/compile_errors/switch_with_overlapping_case_ranges.zig+33-5
......@@ -1,12 +1,40 @@
1export fn entry() void {
2 var q: u8 = 0;
3 switch ((&q).*) {
1export fn entry1(x: u8) void {
2 switch (x) {
43 1...2 => {},
54 0...255 => {},
65 }
76}
87
8export fn entry2(x: i8) void {
9 switch (x) {
10 -128...5 => {},
11 5...127 => {},
12 }
13}
14
15export fn entry3(x: u8) void {
16 switch (x) {
17 0...5 => {},
18 5 => {},
19 6...255 => {},
20 }
21}
22
23export fn entry4(x: u8) void {
24 switch (x) {
25 0...5 => {},
26 6 => {},
27 6...255 => {},
28 }
29}
30
931// error
1032//
11// :5:10: error: duplicate switch value
12// :4:10: note: previous value here
33// :4:10: error: duplicate switch value
34// :3:10: note: previous value here
35// :11:10: error: duplicate switch value
36// :10:13: note: previous value here
37// :17:10: error: duplicate switch value
38// :18:9: note: previous value here
39// :27:10: error: duplicate switch value
40// :26:9: note: previous value here