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 @@...@@ -1,6 +1,6 @@
1const RangeSet = @This();1const RangeSet = @This();
22
3ranges: std.ArrayList(Range),3ranges: std.MultiArrayList(Range),
44
5pub const Range = struct {5pub const Range = struct {
6 first: Value,6 first: Value,
...@@ -22,15 +22,19 @@ pub fn ensureUnusedCapacity(self: *RangeSet, allocator: Allocator, additional_co...@@ -22,15 +22,19 @@ pub fn ensureUnusedCapacity(self: *RangeSet, allocator: Allocator, additional_co
22pub fn addAssumeCapacity(set: *RangeSet, new: Range, ty: Type, zcu: *Zcu) ?LazySrcLoc {22pub fn addAssumeCapacity(set: *RangeSet, new: Range, ty: Type, zcu: *Zcu) ?LazySrcLoc {
23 assert(new.first.typeOf(zcu).eql(ty));23 assert(new.first.typeOf(zcu).eql(ty));
24 assert(new.last.typeOf(zcu).eql(ty));24 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 const idx = std.sort.lowerBound(Value, set.ranges.items(.last), @as(SearchCtx, .{
27 if (new.last.compareScalar(.gte, range.first, ty, zcu) and28 .val = new.first,
28 new.first.compareScalar(.lte, range.last, ty, zcu))29 .zcu = zcu,
29 {30 }), compare);
30 return range.src; // They overlap.31
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.
32 }36 }
33 set.ranges.appendAssumeCapacity(new);37 set.ranges.insertAssumeCapacity(idx, new);
34 return null;38 return null;
35}39}
3640
...@@ -39,15 +43,6 @@ pub fn add(set: *RangeSet, allocator: Allocator, new: Range, ty: Type, zcu: *Zcu...@@ -39,15 +43,6 @@ pub fn add(set: *RangeSet, allocator: Allocator, new: Range, ty: Type, zcu: *Zcu
39 return set.addAssumeCapacity(new, ty, zcu);43 return set.addAssumeCapacity(new, ty, zcu);
40}44}
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
51pub fn spans(46pub fn spans(
52 set: *RangeSet,47 set: *RangeSet,
53 allocator: Allocator,48 allocator: Allocator,
...@@ -58,35 +53,36 @@ pub fn spans(...@@ -58,35 +53,36 @@ pub fn spans(
58) Allocator.Error!bool {53) Allocator.Error!bool {
59 assert(first.typeOf(zcu).eql(ty));54 assert(first.typeOf(zcu).eql(ty));
60 assert(last.typeOf(zcu).eql(ty));55 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) or61 if (!set.ranges.items(.first)[0].eql(first, ty, zcu) or
66 !set.ranges.items[set.ranges.items.len - 1].last.eql(last, ty, zcu))62 !set.ranges.items(.last)[set.ranges.len - 1].eql(last, ty, zcu))
67 {63 {
68 return false;64 return false;
69 }65 }
7066
71 const limbs = try allocator.alloc(67 const limbs = try allocator.alloc(
72 std.math.big.Limb,68 math.big.Limb,
73 std.math.big.int.calcTwosCompLimbCount(ty.intInfo(zcu).bits),69 math.big.int.calcTwosCompLimbCount(ty.intInfo(zcu).bits),
74 );70 );
75 defer allocator.free(limbs);71 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
78 var space: InternPool.Key.Int.Storage.BigIntSpace = undefined;74 var space: InternPool.Key.Int.Storage.BigIntSpace = undefined;
7975
80 // look for gaps76 // look for gaps
81 for (set.ranges.items[1..], 0..) |cur, i| {77 for (
82 // i starts counting from the second item.78 set.ranges.items(.first)[1..],
83 const prev = set.ranges.items[i];79 set.ranges.items(.last)[0 .. set.ranges.len - 1],
8480 ) |cur_first, prev_last| {
85 // prev.last + 1 == cur.first81 // prev_last + 1 == cur_first
86 counter.copy(prev.last.toBigInt(&space, zcu));82 counter.copy(prev_last.toBigInt(&space, zcu));
87 counter.addScalar(counter.toConst(), 1);83 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);
90 if (!cur_start_int.eql(counter.toConst())) {86 if (!cur_start_int.eql(counter.toConst())) {
91 return false;87 return false;
92 }88 }
...@@ -95,7 +91,24 @@ pub fn spans(...@@ -95,7 +91,24 @@ pub fn spans(
95 return true;91 return true;
96}92}
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
98const std = @import("std");110const std = @import("std");
111const math = std.math;
99const assert = std.debug.assert;112const assert = std.debug.assert;
100const Allocator = std.mem.Allocator;113const Allocator = std.mem.Allocator;
101114
src/Sema.zig+5-5
...@@ -10866,7 +10866,7 @@ fn finishSwitchBr(...@@ -10866,7 +10866,7 @@ fn finishSwitchBr(
10866const ValidatedSwitchBlock = struct {10866const ValidatedSwitchBlock = struct {
10867 seen_enum_fields: []const ?LazySrcLoc,10867 seen_enum_fields: []const ?LazySrcLoc,
10868 seen_errors: std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc),10868 seen_errors: std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc),
10869 seen_ranges: []const RangeSet.Range,10869 seen_ranges: std.MultiArrayList(RangeSet.Range).Slice,
10870 true_src: ?LazySrcLoc,10870 true_src: ?LazySrcLoc,
10871 false_src: ?LazySrcLoc,10871 false_src: ?LazySrcLoc,
10872 void_src: ?LazySrcLoc,10872 void_src: ?LazySrcLoc,
...@@ -10901,7 +10901,7 @@ const ValidatedSwitchBlock = struct {...@@ -10901,7 +10901,7 @@ const ValidatedSwitchBlock = struct {
10901 error_names: InternPool.NullTerminatedString.Slice,10901 error_names: InternPool.NullTerminatedString.Slice,
10902 seen_enum_fields: []const ?LazySrcLoc,10902 seen_enum_fields: []const ?LazySrcLoc,
10903 seen_errors: *const std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc),10903 seen_errors: *const std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, LazySrcLoc),
10904 seen_ranges: []const RangeSet.Range,10904 seen_ranges: std.MultiArrayList(RangeSet.Range).Slice,
10905 seen_true: bool,10905 seen_true: bool,
10906 seen_false: bool,10906 seen_false: bool,
10907 seen_void: bool,10907 seen_void: bool,
...@@ -10938,13 +10938,13 @@ const ValidatedSwitchBlock = struct {...@@ -10938,13 +10938,13 @@ const ValidatedSwitchBlock = struct {
10938 else => unreachable,10938 else => unreachable,
10939 };10939 };
10940 while (it.next_idx < it.seen_ranges.len and10940 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))
10942 {10942 {
10943 defer it.next_idx += 1;10943 defer it.next_idx += 1;
10944 const incr = try arith.incrementDefinedInt(10944 const incr = try arith.incrementDefinedInt(
10945 sema,10945 sema,
10946 int_ty,10946 int_ty,
10947 it.seen_ranges[it.next_idx].last,10947 it.seen_ranges.items(.last)[it.next_idx],
10948 );10948 );
10949 if (incr.overflow) {10949 if (incr.overflow) {
10950 it.next_val = null;10950 it.next_val = null;
...@@ -11432,7 +11432,7 @@ fn validateSwitchBlock(...@@ -11432,7 +11432,7 @@ fn validateSwitchBlock(
11432 return .{11432 return .{
11433 .seen_enum_fields = seen_enum_fields,11433 .seen_enum_fields = seen_enum_fields,
11434 .seen_errors = seen_errors,11434 .seen_errors = seen_errors,
11435 .seen_ranges = range_set.ranges.items,11435 .seen_ranges = range_set.ranges.slice(),
11436 .true_src = true_src,11436 .true_src = true_src,
11437 .false_src = false_src,11437 .false_src = false_src,
11438 .void_src = void_src,11438 .void_src = void_src,
test/cases/compile_errors/switch_with_overlapping_case_ranges.zig+33-5
...@@ -1,12 +1,40 @@...@@ -1,12 +1,40 @@
1export fn entry() void {1export fn entry1(x: u8) void {
2 var q: u8 = 0;2 switch (x) {
3 switch ((&q).*) {
4 1...2 => {},3 1...2 => {},
5 0...255 => {},4 0...255 => {},
6 }5 }
7}6}
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
9// error31// error
10//32//
11// :5:10: error: duplicate switch value33// :4:10: error: duplicate switch value
12// :4:10: note: previous value here34// :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