-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathProgram.cs
More file actions
1357 lines (1173 loc) · 66.2 KB
/
Copy pathProgram.cs
File metadata and controls
1357 lines (1173 loc) · 66.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Native AOT smoke test for Celerity (#32).
//
// This console app exercises every collection shape and a representative spread
// of hashers so that `dotnet publish /p:PublishAot=true` is forced to compile
// each generic instantiation down to native code. It is run by the AOT CI job:
// a non-zero exit code (any failed assertion) fails the build, proving the
// library works end-to-end under Native AOT, not just that the static analyzers
// are happy.
using Celerity;
using Celerity.Collections;
using Celerity.Hashing;
using Celerity.Primitives;
int failures = 0;
void Check(bool condition, string message)
{
if (!condition)
{
Console.Error.WriteLine($"FAIL: {message}");
failures++;
}
}
// IntDictionary (default Int32WangNaiveHasher) — indexer, TryAdd/Add, TryGetValue,
// Remove, zero-key out-of-band slot, struct enumerator.
{
var d = new IntDictionary<int>();
d[42] = 1;
d[42]++;
Check(d.TryAdd(7, 100), "IntDictionary.TryAdd new key");
Check(!d.TryAdd(7, 999), "IntDictionary.TryAdd duplicate");
d.Add(8, 200);
d[0] = 99; // zero key is a legitimate value, not the empty sentinel
Check(d.TryGetValue(42, out var v) && v == 2, "IntDictionary indexer round-trip");
Check(d[0] == 99, "IntDictionary zero-key round-trip");
Check(d.Remove(7), "IntDictionary.Remove");
var sum = 0;
foreach (var kvp in d) sum += kvp.Value;
Check(sum == 2 + 200 + 99, "IntDictionary enumeration");
Check(d.Count == 3, "IntDictionary count");
}
// LongDictionary (default Int64WangNaiveHasher) — upper-32-bits distinctness.
{
var d = new LongDictionary<string>();
d[1L << 40] = "high";
d[1L] = "low";
Check(d.Count == 2, "LongDictionary distinct upper/lower bits");
Check(d.TryGetValue(1L << 40, out var v) && v == "high", "LongDictionary round-trip");
}
// CelerityDictionary with a spread of hashers.
{
var byGuid = new CelerityDictionary<Guid, string, GuidHasher>();
var id = Guid.NewGuid();
byGuid[id] = "alice";
byGuid[Guid.Empty] = "empty"; // out-of-band default-key slot
Check(byGuid[id] == "alice", "CelerityDictionary<Guid> round-trip");
Check(byGuid[Guid.Empty] == "empty", "CelerityDictionary<Guid> empty-key slot");
var byStr = new CelerityDictionary<string, int, StringMurmur3Hasher>();
byStr["hello"] = 1;
byStr["Ł"] = 2; // non-ASCII, distinct from low-byte-equal chars
Check(byStr.TryGetValue("hello", out var hv) && hv == 1, "CelerityDictionary<string> round-trip");
var fnv = new CelerityDictionary<string, int, StringFnV1AHasher>();
fnv["a"] = 1;
Check(fnv.ContainsKey("a"), "CelerityDictionary<string, StringFnV1AHasher>");
var fnv1 = new CelerityDictionary<string, int, StringFnV1Hasher>();
fnv1["A"] = 1;
fnv1["Ł"] = 2; // FNV-1 full-width fold keeps upper-byte-distinct keys separate
Check(fnv1.ContainsKey("Ł") && fnv1.Count == 2,
"CelerityDictionary<string, StringFnV1Hasher>");
var fnvFull = new CelerityDictionary<string, int, StringFnV1AFullHasher>();
fnvFull["A"] = 1;
fnvFull["Ł"] = 2; // full-width fold keeps upper-byte-distinct keys separate
Check(fnvFull.ContainsKey("Ł") && fnvFull.Count == 2,
"CelerityDictionary<string, StringFnV1AFullHasher>");
var fnv64 = new CelerityDictionary<string, int, StringFnV1A64Hasher>();
fnv64["A"] = 1;
fnv64["Ł"] = 2; // 64-bit full-width fold keeps upper-byte-distinct keys separate
Check(fnv64.ContainsKey("Ł") && fnv64.Count == 2,
"CelerityDictionary<string, StringFnV1A64Hasher>");
var fnv1_64 = new CelerityDictionary<string, int, StringFnV164Hasher>();
fnv1_64["A"] = 1;
fnv1_64["Ł"] = 2; // FNV-1 64-bit full-width fold keeps upper-byte-distinct keys separate
Check(fnv1_64.ContainsKey("Ł") && fnv1_64.Count == 2,
"CelerityDictionary<string, StringFnV164Hasher>");
var oaat = new CelerityDictionary<string, int, StringJenkinsOaatHasher>();
oaat["A"] = 1;
oaat["Ł"] = 2; // one-at-a-time full-width mix keeps upper-byte-distinct keys separate
Check(oaat.ContainsKey("Ł") && oaat.Count == 2,
"CelerityDictionary<string, StringJenkinsOaatHasher>");
var djb2 = new CelerityDictionary<string, int, StringDjb2Hasher>();
djb2["A"] = 1;
djb2["Ł"] = 2; // djb2 full-width fold keeps upper-byte-distinct keys separate
Check(djb2.ContainsKey("Ł") && djb2.Count == 2,
"CelerityDictionary<string, StringDjb2Hasher>");
var djb2a = new CelerityDictionary<string, int, StringDjb2AHasher>();
djb2a["A"] = 1;
djb2a["Ł"] = 2; // djb2a full-width fold keeps upper-byte-distinct keys separate
Check(djb2a.ContainsKey("Ł") && djb2a.Count == 2,
"CelerityDictionary<string, StringDjb2AHasher>");
var sdbm = new CelerityDictionary<string, int, StringSdbmHasher>();
sdbm["A"] = 1;
sdbm["Ł"] = 2; // sdbm full-width fold keeps upper-byte-distinct keys separate
Check(sdbm.ContainsKey("Ł") && sdbm.Count == 2,
"CelerityDictionary<string, StringSdbmHasher>");
var elf = new CelerityDictionary<string, int, StringElfHasher>();
elf["A"] = 1;
elf["Ł"] = 2; // ELF full-width fold keeps upper-byte-distinct keys separate
Check(elf.ContainsKey("Ł") && elf.Count == 2,
"CelerityDictionary<string, StringElfHasher>");
var crc32 = new CelerityDictionary<string, int, StringCrc32Hasher>();
crc32["A"] = 1;
crc32["Ł"] = 2; // CRC-32 full-width fold keeps upper-byte-distinct keys separate
Check(crc32.ContainsKey("Ł") && crc32.Count == 2,
"CelerityDictionary<string, StringCrc32Hasher>");
var adler32 = new CelerityDictionary<string, int, StringAdler32Hasher>();
adler32["A"] = 1;
adler32["Ł"] = 2; // Adler-32 full-width fold keeps upper-byte-distinct keys separate
Check(adler32.ContainsKey("Ł") && adler32.Count == 2,
"CelerityDictionary<string, StringAdler32Hasher>");
var murmur2 = new CelerityDictionary<string, int, StringMurmur2Hasher>();
murmur2["A"] = 1;
murmur2["Ł"] = 2; // MurmurHash2 full-width fold keeps upper-byte-distinct keys separate
Check(murmur2.ContainsKey("Ł") && murmur2.Count == 2,
"CelerityDictionary<string, StringMurmur2Hasher>");
var xxh32 = new CelerityDictionary<string, int, StringXxHash32Hasher>();
xxh32["A"] = 1;
xxh32["Ł"] = 2; // xxHash32 full-width fold keeps upper-byte-distinct keys separate
Check(xxh32.ContainsKey("Ł") && xxh32.Count == 2,
"CelerityDictionary<string, StringXxHash32Hasher>");
var xxh64 = new CelerityDictionary<string, int, StringXxHash64Hasher>();
xxh64["A"] = 1;
xxh64["Ł"] = 2; // xxHash64 full-width fold keeps upper-byte-distinct keys separate
Check(xxh64.ContainsKey("Ł") && xxh64.Count == 2,
"CelerityDictionary<string, StringXxHash64Hasher>");
var metro64 = new CelerityDictionary<string, int, StringMetroHash64Hasher>();
metro64["A"] = 1;
metro64["Ł"] = 2; // MetroHash64 full-width fold keeps upper-byte-distinct keys separate
Check(metro64.ContainsKey("Ł") && metro64.Count == 2,
"CelerityDictionary<string, StringMetroHash64Hasher>");
var city64 = new CelerityDictionary<string, int, StringCityHash64Hasher>();
city64["A"] = 1;
city64["Ł"] = 2; // CityHash64 full-width fold keeps upper-byte-distinct keys separate
Check(city64.ContainsKey("Ł") && city64.Count == 2,
"CelerityDictionary<string, StringCityHash64Hasher>");
var sip13 = new CelerityDictionary<string, int, StringSipHash13Hasher>();
sip13["A"] = 1;
sip13["Ł"] = 2; // SipHash-1-3 full-width fold keeps upper-byte-distinct keys separate
Check(sip13.ContainsKey("Ł") && sip13.Count == 2,
"CelerityDictionary<string, StringSipHash13Hasher>");
var sip24 = new CelerityDictionary<string, int, StringSipHash24Hasher>();
sip24["A"] = 1;
sip24["Ł"] = 2; // SipHash-2-4 full-width fold keeps upper-byte-distinct keys separate
Check(sip24.ContainsKey("Ł") && sip24.Count == 2,
"CelerityDictionary<string, StringSipHash24Hasher>");
var halfSip24 = new CelerityDictionary<string, int, StringHalfSipHash24Hasher>();
halfSip24["A"] = 1;
halfSip24["Ł"] = 2; // HalfSipHash-2-4 full-width fold keeps upper-byte-distinct keys separate
Check(halfSip24.ContainsKey("Ł") && halfSip24.Count == 2,
"CelerityDictionary<string, StringHalfSipHash24Hasher>");
var highway64 = new CelerityDictionary<string, int, StringHighwayHash64Hasher>();
highway64["A"] = 1;
highway64["Ł"] = 2; // HighwayHash64 full-width fold keeps upper-byte-distinct keys separate
Check(highway64.ContainsKey("Ł") && highway64.Count == 2,
"CelerityDictionary<string, StringHighwayHash64Hasher>");
var xxh3 = new CelerityDictionary<string, int, StringXxHash3Hasher>();
xxh3["A"] = 1;
xxh3["Ł"] = 2; // XXH3 full-width fold keeps upper-byte-distinct keys separate
Check(xxh3.ContainsKey("Ł") && xxh3.Count == 2,
"CelerityDictionary<string, StringXxHash3Hasher>");
// DefaultHasher<T> routes through EqualityComparer<T>.Default — the most
// AOT-sensitive path in the library.
var def = new CelerityDictionary<int, int, DefaultHasher<int>>();
def[5] = 50;
Check(def[5] == 50, "CelerityDictionary<int, DefaultHasher<int>>");
var u32 = new CelerityDictionary<uint, int, UInt32Hasher>();
u32[3000000000u] = 1;
Check(u32.ContainsKey(3000000000u), "CelerityDictionary<uint, UInt32Hasher>");
var u32w = new CelerityDictionary<uint, int, UInt32WangHasher>();
u32w[3000000000u] = 1;
Check(u32w.ContainsKey(3000000000u), "CelerityDictionary<uint, UInt32WangHasher>");
var u32m = new CelerityDictionary<uint, int, UInt32Murmur3Hasher>();
u32m[3000000000u] = 1;
Check(u32m.ContainsKey(3000000000u), "CelerityDictionary<uint, UInt32Murmur3Hasher>");
var u64 = new CelerityDictionary<ulong, int, UInt64Hasher>();
u64[ulong.MaxValue] = 1;
Check(u64.ContainsKey(ulong.MaxValue), "CelerityDictionary<ulong, UInt64Hasher>");
var u64w = new CelerityDictionary<ulong, int, UInt64WangHasher>();
u64w[ulong.MaxValue] = 1;
Check(u64w.ContainsKey(ulong.MaxValue), "CelerityDictionary<ulong, UInt64WangHasher>");
var u64wn = new CelerityDictionary<ulong, int, UInt64WangNaiveHasher>();
u64wn[ulong.MaxValue] = 1;
Check(u64wn.ContainsKey(ulong.MaxValue), "CelerityDictionary<ulong, UInt64WangNaiveHasher>");
var murmurInt = new CelerityDictionary<int, int, Int32Murmur3Hasher>();
murmurInt[1] = 1;
Check(murmurInt.ContainsKey(1), "CelerityDictionary<int, Int32Murmur3Hasher>");
// Identity hashers — the zero-work floor. Exercise the out-of-band zero-key
// slot (Hash(0) == 0 == EMPTY_KEY) plus a dense sequential fill, the shape
// identity is designed for.
var identInt = new IntDictionary<string, Int32IdentityHasher>();
identInt[0] = "zero";
identInt[1] = "one";
identInt[-1] = "neg-one";
Check(identInt[0] == "zero" && identInt[1] == "one" && identInt[-1] == "neg-one"
&& !identInt.ContainsKey(999), "IntDictionary<string, Int32IdentityHasher>");
var identIntSet = new IntSet<Int32IdentityHasher>();
for (int i = 0; i < 256; i++) identIntSet.Add(i);
Check(identIntSet.Count == 256 && identIntSet.Contains(0) && identIntSet.Contains(255)
&& !identIntSet.Contains(256), "IntSet<Int32IdentityHasher>");
var identLong = new LongDictionary<string, Int64IdentityHasher>();
identLong[0L] = "zero";
identLong[1L] = "one";
identLong[-1L] = "neg-one";
Check(identLong[0L] == "zero" && identLong[1L] == "one" && identLong[-1L] == "neg-one"
&& !identLong.ContainsKey(999L), "LongDictionary<string, Int64IdentityHasher>");
var identLongSet = new LongSet<Int64IdentityHasher>();
for (long i = 0; i < 256; i++) identLongSet.Add(i);
Check(identLongSet.Count == 256 && identLongSet.Contains(0L) && identLongSet.Contains(255L)
&& !identLongSet.Contains(256L), "LongSet<Int64IdentityHasher>");
var wangInt = new CelerityDictionary<int, int, Int32WangHasher>();
wangInt[1] = 1;
Check(wangInt.ContainsKey(1), "CelerityDictionary<int, Int32WangHasher>");
var wangLong = new CelerityDictionary<long, int, Int64WangHasher>();
wangLong[1L] = 1;
Check(wangLong.ContainsKey(1L), "CelerityDictionary<long, Int64WangHasher>");
var murmurLong = new CelerityDictionary<long, int, Int64Murmur3Hasher>();
murmurLong[1L] = 1;
Check(murmurLong.ContainsKey(1L), "CelerityDictionary<long, Int64Murmur3Hasher>");
}
// PooledCelerityDictionary — ArrayPool-backed, disposable dictionary. Exercise the
// full surface plus the rent/return lifecycle (Dispose) so the AOT publish compiles
// the new generic instantiations and the ArrayPool<T?> code paths.
{
using (var pooled = new PooledCelerityDictionary<int, int, Int32WangNaiveHasher>())
{
pooled[42] = 1;
pooled[42]++;
pooled[0] = 99; // out-of-band default key
Check(pooled.TryAdd(7, 100), "PooledCelerityDictionary.TryAdd new key");
Check(!pooled.TryAdd(7, 999), "PooledCelerityDictionary.TryAdd duplicate");
Check(pooled[42] == 2 && pooled[0] == 99, "PooledCelerityDictionary round-trip");
Check(pooled.Remove(7), "PooledCelerityDictionary.Remove");
var sum = 0;
foreach (var kvp in pooled) sum += kvp.Value;
Check(sum == 2 + 99, "PooledCelerityDictionary enumeration");
Check(pooled.Count == 2, "PooledCelerityDictionary count");
}
// Reference-type key/value instantiation exercises the clear-on-return path.
using var pooledStr = new PooledCelerityDictionary<string, string, StringFnV1AHasher>();
pooledStr[null!] = "null-key"; // out-of-band null key
pooledStr["a"] = "alpha";
Check(pooledStr[null!] == "null-key" && pooledStr["a"] == "alpha",
"PooledCelerityDictionary<string, string> null-key + round-trip");
}
// Sets — IntSet, LongSet, CeleritySet.
{
var s = new IntSet();
s.Add(1);
Check(s.TryAdd(2), "IntSet.TryAdd new");
Check(!s.TryAdd(1), "IntSet.TryAdd duplicate");
s.Add(0); // zero element out-of-band
Check(s.Contains(0) && s.Contains(1), "IntSet.Contains");
Check(s.Remove(2), "IntSet.Remove");
var count = 0;
foreach (var _ in s) count++;
Check(count == s.Count && s.Count == 2, "IntSet enumeration/count");
var ls = new LongSet();
ls.Add(1L << 40);
ls.Add(1L);
Check(ls.Count == 2 && ls.Contains(1L << 40), "LongSet upper-bits distinctness");
var gs = new CeleritySet<Guid, GuidHasher>();
var g = Guid.NewGuid();
gs.Add(g);
gs.Add(Guid.Empty);
Check(gs.Contains(g) && gs.Contains(Guid.Empty), "CeleritySet<Guid>");
}
// IEnumerable constructors (collection-count sizing path).
{
var source = new Dictionary<int, int> { [1] = 1, [2] = 2, [3] = 3 };
var d = new IntDictionary<int>(source);
Check(d.Count == 3, "IntDictionary IEnumerable ctor");
var setSource = new[] { 1, 2, 2, 3 };
var set = new IntSet(setSource);
Check(set.Count == 3, "IntSet IEnumerable ctor dedupe");
}
// FrozenCelerityDictionary — build-once perfect-hash dictionary (default and
// custom-hasher generic instantiations), the out-of-band null key, and the
// base-hash-collision fallback path ('A' / 'Ł' under the low-byte FNV-1a hasher).
{
var frozen = new FrozenCelerityDictionary<int>(new[]
{
new KeyValuePair<string, int>("alice", 1),
new KeyValuePair<string, int>("bob", 2),
new KeyValuePair<string, int>(null!, 99),
});
Check(frozen.Count == 3 && frozen["alice"] == 1 && frozen[null!] == 99,
"FrozenCelerityDictionary<int> build + null key");
var frozenMurmur = new FrozenCelerityDictionary<int, StringMurmur3Hasher>(new[]
{
new KeyValuePair<string, int>("x", 10),
new KeyValuePair<string, int>("y", 20),
});
Check(frozenMurmur["y"] == 20 && !frozenMurmur.ContainsKey("z"),
"FrozenCelerityDictionary<int, StringMurmur3Hasher>");
var frozenFallback = new FrozenCelerityDictionary<int, StringFnV1AHasher>(new[]
{
new KeyValuePair<string, int>("A", 1),
new KeyValuePair<string, int>("Ł", 2),
});
Check(frozenFallback["A"] == 1 && frozenFallback["Ł"] == 2,
"FrozenCelerityDictionary fallback keeps base-hash-colliding keys distinct");
}
// FrozenCeleritySet — build-once perfect-hash set (default and custom-hasher
// generic instantiations), the out-of-band null element, the IReadOnlySet surface,
// and the base-hash-collision fallback path ('A' / 'Ł' under the low-byte FNV-1a).
{
var frozen = new FrozenCeleritySet(new[] { "alice", "bob", null! });
Check(frozen.Count == 3 && frozen.Contains("alice") && frozen.Contains(null!),
"FrozenCeleritySet build + null element");
Check(frozen.IsSupersetOf(new[] { "alice" }) && frozen.Overlaps(new[] { "bob", "z" }),
"FrozenCeleritySet IReadOnlySet surface");
var frozenMurmur = new FrozenCeleritySet<StringMurmur3Hasher>(new[] { "x", "y" });
Check(frozenMurmur.Contains("y") && !frozenMurmur.Contains("z"),
"FrozenCeleritySet<StringMurmur3Hasher>");
var frozenFallbackSet = new FrozenCeleritySet<StringFnV1AHasher>(new[] { "A", "Ł" });
Check(frozenFallbackSet.Contains("A") && frozenFallbackSet.Contains("Ł") && frozenFallbackSet.Count == 2,
"FrozenCeleritySet fallback keeps base-hash-colliding elements distinct");
}
// CelerityMultiMap — one-to-many map (default and custom-hasher generic
// instantiations), grouping Adds, the out-of-band default-key group, the two
// removal shapes, and the ILookup<,> surface.
{
var multi = new CelerityMultiMap<string, int, StringFnV1AHasher>();
multi.Add("a", 1);
multi.Add("a", 2);
multi.Add("b", 3);
multi.Add(null!, 99); // out-of-band default-key group
Check(multi.Count == 3 && multi.ValueCount == 4, "CelerityMultiMap counts");
Check(multi["a"].Count == 2 && multi[null!][0] == 99, "CelerityMultiMap group + null key");
Check(multi.Remove("a", 1) && multi["a"].Count == 1, "CelerityMultiMap.Remove single value");
Check(multi.RemoveAll("b") && !multi.ContainsKey("b"), "CelerityMultiMap.RemoveAll");
System.Linq.ILookup<string, int> lookup = multi;
Check(lookup.Contains("a") && System.Linq.Enumerable.Count(lookup["a"]) == 1,
"CelerityMultiMap ILookup surface");
var multiGuid = new CelerityMultiMap<System.Guid, int, GuidHasher>();
multiGuid.Add(System.Guid.Empty, 7);
Check(multiGuid[System.Guid.Empty][0] == 7, "CelerityMultiMap<Guid, int, GuidHasher>");
}
// CelerityMultiSet — counting multiset (element -> multiplicity): counting Adds,
// the out-of-band default/null element, the two removal shapes, SetCount, and the
// (element, count) enumeration.
{
var bag = new CelerityMultiSet<string, StringFnV1AHasher>();
bag.Add("a");
bag.Add("a");
bag.Add("b", 3);
bag.Add(null!, 2); // out-of-band default element
Check(bag.Count == 3 && bag.TotalCount == 7, "CelerityMultiSet counts");
Check(bag["a"] == 2 && bag[null!] == 2, "CelerityMultiSet multiplicity + null element");
Check(bag.Remove("a") && bag["a"] == 1, "CelerityMultiSet.Remove decrements");
Check(bag.RemoveAll("b") && !bag.Contains("b"), "CelerityMultiSet.RemoveAll");
Check(bag.SetCount("c", 5) == 0 && bag["c"] == 5, "CelerityMultiSet.SetCount creates");
Check(bag.SetCount("c", 0) == 5 && !bag.Contains("c"), "CelerityMultiSet.SetCount removes");
int distinct = 0;
foreach (var pair in bag) distinct += pair.Value > 0 ? 1 : 0;
Check(distinct == bag.Count, "CelerityMultiSet enumeration");
var bagFromSeq = new CelerityMultiSet<int, Int32WangNaiveHasher>(new[] { 1, 1, 2 });
Check(bagFromSeq[1] == 2 && bagFromSeq[2] == 1, "CelerityMultiSet IEnumerable<T> counting ctor");
}
// LruCache — fixed-capacity least-recently-used cache. Exercise put/get, the
// recency-preserving eviction, a promoting read sparing an entry, peek/remove, the
// out-of-band default/zero key, and the MRU->LRU struct enumerator.
{
var cache = new LruCache<int, string, Int32WangNaiveHasher>(3);
cache[0] = "zero"; // out-of-band default key
cache[1] = "one";
cache[2] = "two";
Check(cache.Count == 3 && cache[0] == "zero", "LruCache put/get + default key");
_ = cache[0]; // promote 0 -> MRU..LRU = 0, 2, 1
cache[3] = "three"; // evicts the least-recently-used (1), not 0
Check(!cache.ContainsKey(1) && cache.ContainsKey(0), "LruCache evicts LRU, spares read");
Check(cache.TryPeek(0, out string? peeked) && peeked == "zero", "LruCache TryPeek");
Check(cache.TryPeekLeastRecentlyUsed(out int lruKey, out _) && lruKey == 2, "LruCache peek LRU");
Check(cache.Remove(2, out string? removed) && removed == "two", "LruCache Remove out value");
var order = new List<int>();
foreach (var kvp in cache) order.Add(kvp.Key);
Check(order.Count == 2 && order[0] == 3, "LruCache MRU-first enumeration");
var seeded = new LruCache<int, int, Int32WangNaiveHasher>(2,
new[] { new KeyValuePair<int, int>(1, 10), new KeyValuePair<int, int>(2, 20), new KeyValuePair<int, int>(3, 30) });
Check(seeded.Count == 2 && !seeded.ContainsKey(1) && seeded.ContainsKey(3), "LruCache source ctor evicts oldest");
}
// Deque — growable double-ended queue over a circular buffer. Exercise both-ends push/pop,
// the front-relative indexer, wrap-around growth, Try* peeks, the front-to-back struct
// enumerator, and the IEnumerable constructor.
{
var dq = new Deque<int>(new[] { 1, 2, 3 }); // front-to-back: 1, 2, 3
dq.PushFront(0); // [0, 1, 2, 3]
dq.PushBack(4); // [0, 1, 2, 3, 4]
Check(dq.Count == 5 && dq[0] == 0 && dq[4] == 4, "Deque push both ends + indexer");
Check(dq.PopFront() == 0 && dq.PopBack() == 4, "Deque pop both ends");
Check(dq.PeekFront() == 1 && dq.PeekBack() == 3, "Deque peek both ends");
// Force wrap-around and growth over a small buffer.
var churn = new Deque<int>(4);
for (int i = 0; i < 100; i++) churn.PushBack(i);
for (int i = 0; i < 50; i++) Check(churn.PopFront() == i, "Deque wrap-around FIFO churn");
Check(churn.Count == 50 && churn[0] == 50, "Deque count after churn");
Check(churn.TryPeekFront(out int f) && f == 50, "Deque TryPeekFront");
var empty = new Deque<int>();
Check(!empty.TryPopBack(out _), "Deque TryPopBack on empty");
var order = new List<int>();
var seq = new Deque<int>();
seq.PushBack(2); seq.PushFront(1); seq.PushBack(3);
foreach (int x in seq) order.Add(x);
Check(order.Count == 3 && order[0] == 1 && order[2] == 3, "Deque front-to-back enumeration");
}
// DisjointSet — union-find over arbitrary elements. Exercise add, auto-adding union, the
// merge/no-op return, representative find, connectivity queries, component sizing, the set
// count, growth across many singletons, grouped components, and the struct enumerator.
{
var ds = new DisjointSet<int>(new[] { 1, 2, 3, 4 });
Check(ds.Count == 4 && ds.SetCount == 4, "DisjointSet seeds singletons");
Check(ds.Union(1, 2) && ds.Union(3, 4), "DisjointSet union merges");
Check(ds.Union(2, 3), "DisjointSet union joins two components");
Check(!ds.Union(1, 4), "DisjointSet union of already-connected is a no-op");
Check(ds.SetCount == 1 && ds.Connected(1, 4), "DisjointSet all connected");
Check(ds.ComponentSize(1) == 4 && ds.Find(1).Equals(ds.Find(4)), "DisjointSet component size + shared representative");
Check(ds.Union(10, 20), "DisjointSet union auto-adds missing elements");
Check(ds.Contains(10) && !ds.Connected(1, 10), "DisjointSet distinct components");
var grown = new DisjointSet<int>(0);
for (int i = 1; i < 500; i++) grown.Union(i - 1, i);
Check(grown.Count == 500 && grown.SetCount == 1 && grown.ComponentSize(0) == 500, "DisjointSet chain union across growth");
var comps = ds.GetComponents();
Check(comps.Count == ds.SetCount, "DisjointSet GetComponents count");
var order = new List<int>();
foreach (int x in new DisjointSet<int>(new[] { 7, 8, 9 })) order.Add(x);
Check(order.Count == 3 && order[0] == 7 && order[2] == 9, "DisjointSet insertion-order enumeration");
}
// IndexedPriorityQueue — addressable binary min-heap. Exercise enqueue/peek/dequeue
// min-order, the decrease-key Update, arbitrary Remove, priority lookups, and growth.
{
var pq = new IndexedPriorityQueue<int, int, Int32WangNaiveHasher>();
pq.Enqueue(1, 30);
pq.Enqueue(2, 10);
pq.Enqueue(3, 20);
Check(pq.Count == 3 && pq.Peek() == 2, "IndexedPriorityQueue min at top");
Check(!pq.TryEnqueue(2, 5), "IndexedPriorityQueue rejects duplicate element");
pq.Update(3, 1); // decrease-key
Check(pq.Peek() == 3 && pq.GetPriority(3) == 1, "IndexedPriorityQueue decrease-key");
Check(pq.Remove(1, out int removed) && removed == 30, "IndexedPriorityQueue remove arbitrary out value");
Check(pq.TryGetPriority(2, out int p2) && p2 == 10 && !pq.Contains(1), "IndexedPriorityQueue priority lookup + absence");
Check(pq.Dequeue() == 3 && pq.Dequeue() == 2 && pq.Count == 0, "IndexedPriorityQueue dequeue order");
var grown = new IndexedPriorityQueue<int, int, Int32WangNaiveHasher>(0);
for (int i = 500; i > 0; i--) grown.Enqueue(i, i);
Check(grown.Count == 500 && grown.Peek() == 1, "IndexedPriorityQueue enqueue across growth");
var prev = int.MinValue;
var monotonic = true;
while (grown.TryDequeue(out _, out int pr)) { if (pr < prev) monotonic = false; prev = pr; }
Check(monotonic, "IndexedPriorityQueue drains in ascending priority order");
var maxHeap = new IndexedPriorityQueue<string, int, DefaultHasher<string>>(
Comparer<int>.Create((a, b) => b.CompareTo(a)));
maxHeap.Enqueue("a", 1);
maxHeap.Enqueue("b", 3);
maxHeap.Enqueue("c", 2);
Check(maxHeap.Dequeue() == "b", "IndexedPriorityQueue custom comparer (max-heap)");
}
// SparseSet — bounded-universe sparse integer set (Briggs–Torczon). Exercise add /
// contains / swap-remove, the out-of-range rejection, the O(1) clear-then-reuse path
// (which must reject stale sparse entries), and the dense-array enumerator.
{
var ss = new SparseSet(64);
for (int i = 0; i < 10; i++) ss.Add(i);
Check(ss.Count == 10 && ss.Universe == 64 && ss.Contains(0) && ss.Contains(9), "SparseSet add + contains");
Check(!ss.TryAdd(5), "SparseSet.TryAdd duplicate");
Check(!ss.Contains(64) && !ss.Contains(-1), "SparseSet out-of-range reads absent");
Check(ss.Remove(5) && !ss.Contains(5) && ss.Contains(9), "SparseSet swap-remove keeps survivors");
ss.Clear();
Check(ss.Count == 0 && !ss.Contains(0) && !ss.Contains(9), "SparseSet O(1) clear rejects stale entries");
ss.Add(9); // 9 was present before Clear — must not false-positive until re-added
Check(ss.Count == 1 && ss.Contains(9) && !ss.Contains(0), "SparseSet reusable after clear");
var reached = new SparseSet(128, new[] { 3, 3, 7, 1, 7 }); // dedupes
var seen = new List<int>();
foreach (int x in reached) seen.Add(x);
Check(reached.Count == 3 && seen.Count == 3, "SparseSet source ctor dedupe + enumeration");
((ISet<int>)reached).UnionWith(new[] { 1, 2 });
Check(reached.Count == 4 && reached.Contains(2), "SparseSet ISet<int> union within universe");
}
// FenwickTree — Binary Indexed Tree over a numeric sequence. This is the one collection
// built on generic math (INumber<T>), so the static abstract interface members resolve
// through constrained calls the AOT compiler must specialize per T — worth pinning here
// over more than one T. Exercise the O(n) seeded build, point update, prefix / range sums,
// the indexer round-trip, the no-op update, clear-then-reuse, and the struct enumerator.
{
var ft = new FenwickTree<long>(new long[] { 3, 1, 4, 1, 5, 9 });
Check(ft.Count == 6 && ft.Total == 23, "FenwickTree seeded build + total");
Check(ft.PrefixSum(0) == 0 && ft.PrefixSum(3) == 8 && ft.PrefixSum(6) == 23, "FenwickTree prefix sums");
Check(ft.RangeSum(2, 5) == 10 && ft.RangeSum(4, 4) == 0, "FenwickTree range sum + empty range");
ft.Add(0, 10);
Check(ft[0] == 13 && ft.Total == 33, "FenwickTree point update");
ft[1] = 100;
Check(ft[1] == 100 && ft.Total == 132, "FenwickTree indexer set");
var before = new List<long>();
foreach (long v in ft) before.Add(v);
Check(before.Count == 6 && before[0] == 13 && before[1] == 100, "FenwickTree enumerates logical values");
ft.Add(2, 0); // no-op: must not invalidate the enumerator below
var during = 0;
foreach (long _ in ft) { ft[3] = ft[3]; during++; } // no-op assignment mid-enumeration
Check(during == 6, "FenwickTree no-op update does not invalidate enumerators");
ft.Clear();
Check(ft.Count == 6 && ft.Total == 0 && ft[0] == 0, "FenwickTree clear resets values, keeps length");
// A second T (and a larger tree) so the generic-math instantiation is exercised twice.
var wide = new FenwickTree<int>(1000);
for (int i = 0; i < 1000; i++) wide.Add(i, i);
Check(wide.Total == 499_500 && wide.PrefixSum(10) == 45, "FenwickTree int instantiation at scale");
}
// SmallDictionary — flat-array, linear-scan dictionary (default key inline, no
// hasher). Exercise the indexer, TryAdd/Add, TryGetValue, Remove, the swap-remove
// path, the inline default/zero key, and the struct enumerator.
{
var d = new SmallDictionary<int, int>();
d[42] = 1;
d[42]++;
Check(d.TryAdd(7, 100), "SmallDictionary.TryAdd new key");
Check(!d.TryAdd(7, 999), "SmallDictionary.TryAdd duplicate");
d.Add(8, 200);
d[0] = 99; // zero key is an ordinary inline entry, not a sentinel
Check(d.TryGetValue(42, out var v) && v == 2, "SmallDictionary indexer round-trip");
Check(d[0] == 99, "SmallDictionary zero-key round-trip");
Check(d.Remove(7), "SmallDictionary.Remove");
var sum = 0;
foreach (var kvp in d) sum += kvp.Value;
Check(sum == 2 + 200 + 99, "SmallDictionary enumeration");
Check(d.Count == 3, "SmallDictionary count");
var byStr = new SmallDictionary<string, int>(new[]
{
new KeyValuePair<string, int>("a", 1),
new KeyValuePair<string, int>("b", 2),
});
byStr[null!] = 99; // null key is an ordinary inline entry
Check(byStr["a"] == 1 && byStr[null!] == 99 && byStr.Count == 3,
"SmallDictionary<string, int> IEnumerable ctor + null key");
}
// EnumMap — dense array-backed dictionary for enum keys (the .NET EnumMap). Exercise
// the indexer, TryAdd/Add, TryGetValue, Remove, the parallel occupancy vector
// (default value distinct from absent), and the ascending-order struct enumerator.
// DayOfWeek (0..6, contiguous) is a supported small non-negative enum; the switch on
// Unsafe.SizeOf<TEnum>() and the Unsafe.As reinterpret cast must compile to native
// code under AOT.
{
var m = new EnumMap<DayOfWeek, string>();
m[DayOfWeek.Monday] = "mon";
Check(m.TryAdd(DayOfWeek.Tuesday, "tue"), "EnumMap.TryAdd new key");
Check(!m.TryAdd(DayOfWeek.Tuesday, "x"), "EnumMap.TryAdd duplicate");
m.Add(DayOfWeek.Sunday, "sun"); // Sunday == 0 is an ordinary key, not a sentinel
Check(m.TryGetValue(DayOfWeek.Monday, out var v) && v == "mon", "EnumMap indexer round-trip");
Check(m[DayOfWeek.Sunday] == "sun", "EnumMap zero-valued key round-trip");
Check(m.Remove(DayOfWeek.Tuesday), "EnumMap.Remove");
Check(m.Count == 2, "EnumMap count");
var keys = new List<DayOfWeek>();
foreach (var kvp in m) keys.Add(kvp.Key);
Check(keys.Count == 2 && keys[0] == DayOfWeek.Sunday && keys[1] == DayOfWeek.Monday,
"EnumMap ascending-order enumeration");
bool enumMapRejectsOutOfRange = false;
try { _ = new EnumMap<DateTimeKind, int>() { [(DateTimeKind)999] = 1 }; }
catch (ArgumentOutOfRangeException) { enumMapRejectsOutOfRange = true; }
Check(enumMapRejectsOutOfRange, "EnumMap rejects out-of-range key");
}
// SwissDictionary — SIMD group-probing dictionary (default key out-of-band, like
// the other hash-table dictionaries). Exercise the indexer, TryAdd/Add,
// TryGetValue, Remove (tombstone path), the out-of-band zero / null key, resize
// under collision, and the struct enumerator, across a spread of hashers so the
// Vector128 group-compare path is compiled to native code under AOT.
{
var d = new SwissDictionary<int, int, Int32WangNaiveHasher>();
d[42] = 1;
d[42]++;
Check(d.TryAdd(7, 100), "SwissDictionary.TryAdd new key");
Check(!d.TryAdd(7, 999), "SwissDictionary.TryAdd duplicate");
d.Add(8, 200);
d[0] = 99; // zero key stored out-of-band, never hashed
Check(d.TryGetValue(42, out var v) && v == 2, "SwissDictionary indexer round-trip");
Check(d[0] == 99, "SwissDictionary zero-key round-trip");
Check(d.Remove(7), "SwissDictionary.Remove (tombstone)");
var sum = 0;
foreach (var kvp in d) sum += kvp.Value;
Check(sum == 2 + 200 + 99, "SwissDictionary enumeration");
Check(d.Count == 3, "SwissDictionary count");
// Force several resizes / group overflows to compile the rehash + SIMD probe.
var grow = new SwissDictionary<int, int, Int32WangNaiveHasher>(capacity: 16);
for (int i = 1; i <= 500; i++) grow[i] = i * 3;
bool ok = true;
for (int i = 1; i <= 500; i++) ok &= grow[i] == i * 3;
Check(ok && grow.Count == 500, "SwissDictionary resize round-trip");
var byStr = new SwissDictionary<string, int, StringMurmur3Hasher>(new[]
{
new KeyValuePair<string, int>("alice", 1),
new KeyValuePair<string, int>("bob", 2),
});
byStr[null!] = 99; // null key stored out-of-band
Check(byStr["alice"] == 1 && byStr[null!] == 99 && byStr.Count == 3,
"SwissDictionary<string, int> IEnumerable ctor + null key");
var byGuid = new SwissDictionary<Guid, string, GuidHasher>();
byGuid[Guid.Empty] = "empty"; // out-of-band default-key slot
var gid = Guid.NewGuid();
byGuid[gid] = "alice";
Check(byGuid[gid] == "alice" && byGuid[Guid.Empty] == "empty",
"SwissDictionary<Guid> round-trip + empty-key slot");
}
// HashCachingDictionary — struct-of-arrays dictionary with a cached-fingerprint
// side array (default key out-of-band, like the other hash-table dictionaries).
// Exercise the indexer, TryAdd/Add, TryGetValue, Remove (backward-shift path),
// the out-of-band zero / null key, resize under collision, and the struct
// enumerator, across a spread of hashers so the fingerprint probe path is
// compiled to native code under AOT.
{
var d = new HashCachingDictionary<int, int, Int32WangNaiveHasher>();
d[42] = 1;
d[42]++;
Check(d.TryAdd(7, 100), "HashCachingDictionary.TryAdd new key");
Check(!d.TryAdd(7, 999), "HashCachingDictionary.TryAdd duplicate");
d.Add(8, 200);
d[0] = 99; // zero key stored out-of-band, never hashed
Check(d.TryGetValue(42, out var v) && v == 2, "HashCachingDictionary indexer round-trip");
Check(d[0] == 99, "HashCachingDictionary zero-key round-trip");
Check(d.Remove(7), "HashCachingDictionary.Remove (backward-shift)");
var sum = 0;
foreach (var kvp in d) sum += kvp.Value;
Check(sum == 2 + 200 + 99, "HashCachingDictionary enumeration");
Check(d.Count == 3, "HashCachingDictionary count");
// Force several resizes / collision clusters to compile the rehash + probe.
var grow = new HashCachingDictionary<int, int, Int32WangNaiveHasher>(capacity: 16);
for (int i = 1; i <= 500; i++) grow[i] = i * 3;
bool ok = true;
for (int i = 1; i <= 500; i++) ok &= grow[i] == i * 3;
Check(ok && grow.Count == 500, "HashCachingDictionary resize round-trip");
var byStr = new HashCachingDictionary<string, int, StringMurmur3Hasher>(new[]
{
new KeyValuePair<string, int>("alice", 1),
new KeyValuePair<string, int>("bob", 2),
});
byStr[null!] = 99; // null key stored out-of-band
Check(byStr["alice"] == 1 && byStr[null!] == 99 && byStr.Count == 3,
"HashCachingDictionary<string, int> IEnumerable ctor + null key");
var byGuid = new HashCachingDictionary<Guid, string, GuidHasher>();
byGuid[Guid.Empty] = "empty"; // out-of-band default-key slot
var gid = Guid.NewGuid();
byGuid[gid] = "alice";
Check(byGuid[gid] == "alice" && byGuid[Guid.Empty] == "empty",
"HashCachingDictionary<Guid> round-trip + empty-key slot");
}
// BloomFilter — probabilistic membership filter (no out-of-band slot; default(T) is
// an ordinary element, a null reference is mapped to a fixed base hash so the hasher
// is never called with null). Exercise Add / Contains / Clear / Count / UnionWith and
// the IEnumerable ctor across int / Guid / string instantiations so the AOT publish
// compiles the double-hashing probe path and the popcount-based fill estimate.
{
var filter = new BloomFilter<int, Int32WangNaiveHasher>(1000);
filter.Add(42);
filter.Add(0); // zero is an ordinary element, not a sentinel
Check(filter.Contains(42) && filter.Contains(0), "BloomFilter add/contains");
Check(!filter.Contains(7), "BloomFilter negative lookup");
Check(filter.Count == 2, "BloomFilter count");
Check(filter.BitCount >= 64 && (filter.BitCount & (filter.BitCount - 1)) == 0,
"BloomFilter power-of-two bit count");
Check(filter.HashCount >= 1, "BloomFilter hash count");
// No false negatives across a larger fill.
var big = new BloomFilter<int, Int32WangNaiveHasher>(1000);
for (int i = 1; i <= 500; i++) big.Add(i * 3);
bool noFalseNegatives = true;
for (int i = 1; i <= 500; i++) noFalseNegatives &= big.Contains(i * 3);
Check(noFalseNegatives, "BloomFilter no false negatives");
Check(big.CurrentFalsePositiveProbability > 0d, "BloomFilter current FP probability");
// UnionWith merges two equally-sized filters.
var other = new BloomFilter<int, Int32WangNaiveHasher>(1000);
other.Add(99999);
filter.UnionWith(other);
Check(filter.Contains(99999), "BloomFilter UnionWith");
filter.Clear();
Check(filter.Count == 0 && !filter.Contains(42), "BloomFilter clear");
// String elements via the IEnumerable ctor, plus the out-of-band null reference
// (StringFnV1AHasher throws on null; BloomFilter must not call it with null).
var strFilter = new BloomFilter<string, StringFnV1AHasher>(new[] { "alice", "bob" });
strFilter.Add(null!);
Check(strFilter.Contains("alice") && strFilter.Contains("bob") && strFilter.Contains(null!),
"BloomFilter<string> ctor + null element");
var guidFilter = new BloomFilter<Guid, GuidHasher>(100);
guidFilter.Add(Guid.Empty); // ordinary element, no out-of-band slot
Check(guidFilter.Contains(Guid.Empty), "BloomFilter<Guid> empty-guid element");
}
// CuckooFilter — probabilistic membership filter that, unlike BloomFilter, supports
// deletion. Exercise Add / TryAdd / Contains / Remove / Clear / Count / UnionWith and
// the IEnumerable ctor across int / Guid / string instantiations so the AOT publish
// compiles the partial-key cuckoo probe + eviction path and the fingerprint masking.
{
var filter = new CuckooFilter<int, Int32Murmur3Hasher>(1000);
filter.Add(42);
filter.Add(0); // zero is an ordinary element, not a sentinel
Check(filter.Contains(42) && filter.Contains(0), "CuckooFilter add/contains");
Check(!filter.Contains(7), "CuckooFilter negative lookup");
Check(filter.Count == 2, "CuckooFilter count");
Check(filter.BucketCount >= 1 && (filter.BucketCount & (filter.BucketCount - 1)) == 0,
"CuckooFilter power-of-two bucket count");
Check(filter.FingerprintBits is >= 1 and <= 16, "CuckooFilter fingerprint width");
// Remove — the differentiator from BloomFilter — deletes without false negatives.
Check(filter.Remove(42) && !filter.Contains(42), "CuckooFilter remove");
Check(filter.Count == 1, "CuckooFilter count after remove");
// No false negatives across a larger fill.
var big = new CuckooFilter<int, Int32Murmur3Hasher>(2000);
for (int i = 1; i <= 500; i++) big.Add(i * 3);
bool noFalseNegatives = true;
for (int i = 1; i <= 500; i++) noFalseNegatives &= big.Contains(i * 3);
Check(noFalseNegatives, "CuckooFilter no false negatives");
Check(big.LoadFactor > 0d, "CuckooFilter load factor");
// UnionWith merges two equally-sized filters.
var other = new CuckooFilter<int, Int32Murmur3Hasher>(1000);
other.Add(99999);
filter.UnionWith(other);
Check(filter.Contains(99999), "CuckooFilter UnionWith");
filter.Clear();
Check(filter.Count == 0 && !filter.Contains(0), "CuckooFilter clear");
// String elements via the IEnumerable ctor, plus the out-of-band null reference
// (StringFnV1AHasher throws on null; CuckooFilter must not call it with null).
var strFilter = new CuckooFilter<string, StringFnV1AHasher>(new[] { "alice", "bob" });
strFilter.Add(null!);
Check(strFilter.Contains("alice") && strFilter.Contains("bob") && strFilter.Contains(null!),
"CuckooFilter<string> ctor + null element");
Check(strFilter.Remove(null!) && !strFilter.Contains(null!),
"CuckooFilter<string> remove null element");
var guidFilter = new CuckooFilter<Guid, GuidHasher>(100);
guidFilter.Add(Guid.Empty); // ordinary element, no out-of-band slot
Check(guidFilter.Contains(Guid.Empty), "CuckooFilter<Guid> empty-guid element");
}
// XorFilter — build-once, immutable probabilistic membership filter (no out-of-band
// slot; default(T) is an ordinary element, a null reference is mapped to a fixed base
// hash so the hasher is never called with null). Exercise the IEnumerable ctor across
// int / Guid / string instantiations so the AOT publish compiles the peeling
// construction and the three-probe query path, plus the empty-filter short-circuit.
{
var filter = new XorFilter<int, Int32WangNaiveHasher>(new[] { 42, 0, 7, 100, -3 });
Check(filter.Contains(42) && filter.Contains(0) && filter.Contains(-3), "XorFilter build/contains");
Check(filter.Count == 5, "XorFilter count");
Check(filter.SlotCount % 3 == 0 && filter.SlotCount >= filter.Count, "XorFilter slot count");
Check(filter.FingerprintBits == 8, "XorFilter fingerprint width");
// No false negatives across a larger fill (exercises the peel + reseed path).
var big = new XorFilter<int, Int32WangNaiveHasher>(Enumerable.Range(1, 2000).Select(i => i * 3).ToArray());
bool noFalseNegatives = true;
for (int i = 1; i <= 2000; i++) noFalseNegatives &= big.Contains(i * 3);
Check(noFalseNegatives, "XorFilter no false negatives");
Check(big.BitsPerElement > 8d && big.BitsPerElement < 12d, "XorFilter bits/element");
// Empty filter reports everything absent via the _count == 0 short-circuit.
var empty = new XorFilter<int, Int32WangNaiveHasher>(Array.Empty<int>());
Check(empty.Count == 0 && !empty.Contains(1), "XorFilter empty reports absent");
// String elements via the IEnumerable ctor, plus the out-of-band null reference
// (StringFnV1AHasher throws on null; XorFilter must not call it with null).
var strFilter = new XorFilter<string, StringFnV1AHasher>(new[] { "alice", "bob", null! });
Check(strFilter.Contains("alice") && strFilter.Contains("bob") && strFilter.Contains(null!),
"XorFilter<string> ctor + null element");
var guidXor = new XorFilter<Guid, GuidHasher>(new[] { Guid.Empty, Guid.NewGuid() });
Check(guidXor.Contains(Guid.Empty), "XorFilter<Guid> empty-guid element");
}
// BitSet — dense exact bit vector. Exercise Set / Get / Flip / SetAll / Count
// (popcount), the SIMD-accelerated bulk And / Or / Xor / Not, the tail-bit masking
// past Length, and both enumerators so the AOT publish compiles the Vector<ulong>
// bulk paths and the TrailingZeroCount set-bit walk.
{
var bits = new BitSet(130); // 3 words, 62 tail bits past Length
bits.Set(0, true);
bits[64] = true;
bits[129] = true;
Check(bits.Length == 130 && bits.Count == 3, "BitSet set + popcount");
Check(bits[0] && bits[64] && bits[129] && !bits[1], "BitSet get");
Check(bits.Flip(1) && bits[1], "BitSet flip");
bits.Set(1, false);
bits.SetAll(true);
Check(bits.Count == 130 && bits.All(), "BitSet SetAll + tail masking");
bits.Not();
Check(bits.Count == 0 && bits.None(), "BitSet Not");
var a = new BitSet(1000);
var b = new BitSet(1000);
for (int i = 0; i < 1000; i += 2) a[i] = true; // evens
for (int i = 0; i < 1000; i += 3) b[i] = true; // multiples of 3
var union = new BitSet(1000);
union.Or(a).Or(b);
bool orOk = true;
for (int i = 0; i < 1000; i++) orOk &= union[i] == (i % 2 == 0 || i % 3 == 0);
Check(orOk, "BitSet SIMD Or");
var inter = new BitSet((bool[])ToBoolArray(a));
inter.And(b);
bool andOk = true;
for (int i = 0; i < 1000; i++) andOk &= inter[i] == (i % 2 == 0 && i % 3 == 0);
Check(andOk, "BitSet SIMD And");
var sparse = new BitSet(300);
sparse[7] = true;
sparse[256] = true;
var setBits = new List<int>();
foreach (int idx in sparse.EnumerateSetBits()) setBits.Add(idx);
Check(setBits.Count == 2 && setBits[0] == 7 && setBits[1] == 256, "BitSet EnumerateSetBits");
int trueCount = 0;
foreach (bool bit in sparse) if (bit) trueCount++;
Check(trueCount == 2, "BitSet value enumerator");
static bool[] ToBoolArray(BitSet src)
{
var arr = new bool[src.Length];
for (int i = 0; i < src.Length; i++) arr[i] = src[i];
return arr;
}
}
// HyperLogLog — probabilistic cardinality estimator (no out-of-band slot; default(T)
// is an ordinary element, a null reference is mapped to a fixed base hash so the hasher
// is never called with null). Exercise Add / EstimateCardinality / Clear / UnionWith
// and the IEnumerable ctor across int / Guid / string instantiations so the AOT publish
// compiles the SplitMix64 avalanche, the LeadingZeroCount rank path, and the harmonic-
// mean estimate with linear-counting correction.
{
var hll = new HyperLogLog<int, Int32WangNaiveHasher>();
hll.Add(42);
hll.Add(0); // zero is an ordinary element, not a sentinel
hll.Add(42); // duplicate collapses
Check(hll.EstimateCardinality() == 2, "HyperLogLog distinct count");
Check(hll.Precision == HyperLogLog<int, Int32WangNaiveHasher>.DEFAULT_PRECISION,
"HyperLogLog default precision");
Check(hll.RegisterCount == 1 << 14, "HyperLogLog register count");
Check(hll.StandardError > 0d, "HyperLogLog standard error");
// Larger fill: estimate must land within a few standard errors of the truth.
var big = new HyperLogLog<int, Int32WangNaiveHasher>();
for (int i = 0; i < 50_000; i++) big.Add(i);
long estimate = big.EstimateCardinality();
double relErr = Math.Abs(estimate - 50_000) / 50_000.0;
Check(relErr <= big.StandardError * 4 + 0.01, "HyperLogLog estimate within bound");
// UnionWith merges two equal-precision estimators (disjoint streams).
var other = new HyperLogLog<int, Int32WangNaiveHasher>();
for (int i = 50_000; i < 100_000; i++) other.Add(i);
big.UnionWith(other);
long union = big.EstimateCardinality();
Check(Math.Abs(union - 100_000) / 100_000.0 <= big.StandardError * 4 + 0.01,
"HyperLogLog UnionWith");
hll.Clear();
Check(hll.EstimateCardinality() == 0, "HyperLogLog clear");
// String elements via the IEnumerable ctor, plus the out-of-band null reference
// (StringFnV1AHasher throws on null; HyperLogLog must not call it with null).
var strHll = new HyperLogLog<string, StringFnV1AHasher>(new[] { "alice", "bob", "alice" });
strHll.Add(null!);
Check(strHll.EstimateCardinality() == 3, "HyperLogLog<string> ctor + null element");
var guidHll = new HyperLogLog<Guid, GuidHasher>();
guidHll.Add(Guid.Empty); // ordinary element, no out-of-band slot
Check(guidHll.EstimateCardinality() == 1, "HyperLogLog<Guid> empty-guid element");
}
// CountMinSketch — probabilistic frequency estimator (no out-of-band slot; default(T)
// is an ordinary element, a null reference is mapped to a fixed base hash so the hasher
// is never called with null). Exercise Add / Add(count) / EstimateCount / Clear /
// UnionWith and the IEnumerable ctor across int / Guid / string instantiations so the
// AOT publish compiles the SplitMix64 avalanche and the double-hashing column probe.
{
var cms = new CountMinSketch<int, Int32WangNaiveHasher>();
cms.Add(42, 5);
cms.Add(0, 3); // zero is an ordinary element, not a sentinel
cms.Add(42); // 42 now totals 6
Check(cms.EstimateCount(42) >= 6, "CountMinSketch never underestimates");
Check(cms.EstimateCount(0) >= 3, "CountMinSketch zero-element count");
Check(cms.TotalCount == 9, "CountMinSketch total count");
Check(cms.Width >= 4 && (cms.Width & (cms.Width - 1)) == 0, "CountMinSketch power-of-two width");
Check(cms.Depth >= 1, "CountMinSketch positive depth");
// No underestimates across a larger skewed fill.
var big = new CountMinSketch<int, Int32WangNaiveHasher>(0.001, 0.01);
var truth = new Dictionary<int, long>();
for (int i = 0; i < 50_000; i++)