1 module nxt.filters;
2 
3 /// Growable flag.
4 enum Growable { no, yes }
5 
6 /// Copyable flag.
7 enum Copyable { no, yes }
8 
9 enum isDenseSetFilterable(E) = (is(typeof(cast(size_t)E.init)) // is castable to size_t
10                                 /* && cast(size_t)E.max <= uint.max */ );      // and small enough
11 
12 /** Store presence of elements of type `E` in a set in the range `0 .. length`.
13  *
14  * Can be seen as a generalization of `std.typecons.BitFlags` to integer types.
15  *
16  * Typically used to implement very fast membership checking. For instance in
17  * graph traversal algorithms, this filter is typically used as a temporary set
18  * node (integer) ids that checks if a node has been previously visisted or not.
19  */
20 struct DenseSetFilter(E,
21                       // TODO make these use the Flags template
22                       Growable growable = Growable.yes,
23                       Copyable copyable = Copyable.no)
24 if (isDenseSetFilterable!E)
25 {
26     import core.memory : malloc = pureMalloc, calloc = pureCalloc, realloc = pureRealloc;
27     import core.bitop : bts, btr, btc, bt;
28 
29     alias ElementType = E;
30 
31     @safe pure nothrow @nogc:
32 
33     static if (growable == Growable.no)
34     {
35         /// Maximum number of elements in filter.
36         enum elementMaxCount = cast(size_t)E.max + 1;
37 
38         /// Construct from inferred capacity and length `elementMaxCount`.
39         static typeof(this) withInferredLength()
40         {
41             version(D_Coverage) {} else pragma(inline, true);
42             return typeof(return)(elementMaxCount);
43         }
44     }
45 
46     /// Construct set to store at most `length` number of bits.
47     this(size_t length) @trusted
48     {
49         _blocksPtr = null;
50         static if (growable == Growable.yes)
51         {
52             _length = length;
53             _capacity = 0;
54             assureCapacity(length);
55         }
56         else
57         {
58             _capacity = length;
59             _blocksPtr = cast(Block*)calloc(blockCount, Block.sizeof);
60         }
61     }
62 
63     ~this() @nogc
64     {
65         version(D_Coverage) {} else pragma(inline, true);
66         release();
67     }
68 
69     /// Free storage.
70     private void release() @trusted @nogc
71     {
72         version(D_Coverage) {} else pragma(inline, true);
73         import nxt.qcmeman : free;
74         free(_blocksPtr);
75     }
76 
77     /// Clear contents.
78     void clear()() @nogc
79     {
80         version(D_Coverage) {} else pragma(inline, true);
81         release();
82         _blocksPtr = null;
83         static if (growable == Growable.yes)
84         {
85             _length = 0;
86         }
87         _capacity = 0;
88     }
89 
90     static if (copyable)
91     {
92         this(this) @trusted
93         {
94             Block* srcBlocksPtr = _blocksPtr;
95             _blocksPtr = cast(Block*)malloc(blockCount * Block.sizeof);
96             _blocksPtr[0 .. blockCount] = srcBlocksPtr[0 .. blockCount];
97         }
98     }
99     else
100     {
101         @disable this(this);
102 
103         /// Returns: shallow (and deep) duplicate of `this`.
104         typeof(this) dup()() @trusted // template-lazy
105         {
106             typeof(return) copy;
107             static if (growable == Growable.yes)
108             {
109                 copy._length = this._length;
110             }
111             copy._capacity = this._capacity;
112             copy._blocksPtr = cast(Block*)malloc(blockCount * Block.sizeof);
113             copy._blocksPtr[0 .. blockCount] = this._blocksPtr[0 .. blockCount];
114             return copy;
115         }
116     }
117 
118     @property:
119 
120     static if (growable == Growable.yes)
121     {
122         /// Expand to capacity to make room for at least `newLength`.
123         private void assureCapacity()(size_t newLength) @trusted // template-lazy
124         {
125             if (_capacity < newLength)
126             {
127                 const oldBlockCount = blockCount;
128                 import std.math : nextPow2;
129                 this._capacity = newLength.nextPow2;
130                 _blocksPtr = cast(Block*)realloc(_blocksPtr,
131                                                  blockCount * Block.sizeof);
132                 _blocksPtr[oldBlockCount .. blockCount] = 0;
133             }
134         }
135     }
136 
137     /** Insert element `e`.
138      *
139      * Returns: precense status of element before insertion.
140      */
141     bool insert()(in E e) @trusted // template-lazy
142     {
143         version(D_Coverage) {} else pragma(inline, true);
144         const ix = cast(size_t)e;
145         static if (growable == Growable.yes)
146         {
147             assureCapacity(ix + 1);
148             _length = ix + 1;
149         }
150         else
151         {
152             assert(ix < _capacity);
153         }
154         return bts(_blocksPtr, ix) != 0;
155     }
156     alias put = insert;         // OutputRange compatibility
157 
158     /** Remove element `e`.
159      *
160      * Returns: precense status of element before removal.
161      */
162     bool remove()(in E e) @trusted // template-lazy
163     {
164         version(D_Coverage) {} else pragma(inline, true);
165         const ix = cast(size_t)e;
166         static if (growable == Growable.yes)
167         {
168             assureCapacity(ix + 1);
169             _length = ix + 1;
170         }
171         else
172         {
173             assert(ix < _capacity);
174         }
175         return btr(_blocksPtr, ix) != 0;
176     }
177 
178     /** Insert element `e` if it's present otherwise remove it.
179      *
180      * Returns: `true` if elements was zeroed, `false` otherwise.
181      */
182     bool complement()(in E e) @trusted // template-laze
183     {
184         version(D_Coverage) {} else pragma(inline, true);
185         const ix = cast(size_t)e;
186         static if (growable == Growable.yes)
187         {
188             assureCapacity(ix + 1);
189             _length = ix + 1;
190         }
191         else
192         {
193             assert(ix < _capacity);
194         }
195         return btc(_blocksPtr, ix) != 0;
196     }
197 
198     /// Check if element `e` is stored/contained.
199     bool contains()(in E e) @trusted const // template-lazy
200     {
201         version(D_Coverage) {} else pragma(inline, true);
202         const ix = cast(size_t)e;
203         static if (growable == Growable.yes)
204         {
205             return ix < _length && bt(_blocksPtr, ix) != 0;
206         }
207         else
208         {
209             return ix < _capacity && bt(_blocksPtr, ix) != 0;
210         }
211     }
212     /// ditto
213     bool opBinaryRight(string op)(in E e) const
214         if (op == "in")
215     {
216         version(D_Coverage) {} else pragma(inline, true);
217         return contains(e);
218     }
219 
220     /// ditto
221     typeof(this) opBinary(string op)(auto ref in typeof(this) e) const
222         if (op == "|" || op == "&" || op == "^")
223     {
224         typeof(return) result;
225         mixin(`result._blocks[] = _blocks[] ` ~ op ~ ` e._blocks[];`);
226         return result;
227     }
228 
229     /** Get current capacity in number of elements (bits).
230         If `growable` is `Growable.yes` then capacity is variable, otherwise it's constant.
231     */
232     @property size_t capacity() const
233     {
234         version(D_Coverage) {} else pragma(inline, true);
235         return _capacity;
236     }
237 
238 private:
239     @property size_t blockCount() const
240     {
241         version(D_Coverage) {} else pragma(inline, true);
242         return _capacity / Block.sizeof + (_capacity % Block.sizeof ? 1 : 0);
243     }
244 
245     alias Block = size_t;       /// Allocated block type.
246     Block* _blocksPtr;          /// Pointer to blocks of bits.
247     static if (growable == Growable.yes)
248     {
249         size_t _length;         /// Offset + 1 of highest set bit.
250         size_t _capacity;       /// Number of bits allocated.
251     }
252     else
253     {
254         size_t _capacity;       /// Number of bits allocated.
255     }
256 }
257 
258 ///
259 @safe pure nothrow @nogc unittest
260 {
261     alias E = uint;
262 
263     import std.range.primitives : isOutputRange;
264     alias Set = DenseSetFilter!(E, Growable.no);
265     static assert(isOutputRange!(Set, E));
266 
267     const set0 = Set();
268     assert(set0.capacity == 0);
269 
270     const length = 2^^6;
271     auto set = DenseSetFilter!(E, Growable.no)(2*length);
272     const y = set.dup;
273     assert(y.capacity == 2*length);
274 
275     foreach (const ix; 0 .. length)
276     {
277         assert(!set.contains(ix));
278         assert(ix !in set);
279 
280         assert(!set.insert(ix));
281         assert(set.contains(ix));
282         assert(ix in set);
283 
284         assert(set.complement(ix));
285         assert(!set.contains(ix));
286         assert(ix !in set);
287 
288         assert(!set.complement(ix));
289         assert(set.contains(ix));
290         assert(ix in set);
291 
292         assert(!set.contains(ix + 1));
293     }
294 
295     auto z = set.dup;
296     foreach (const ix; 0 .. length)
297     {
298         assert(z.contains(ix));
299         assert(ix in z);
300     }
301 
302     foreach (const ix; 0 .. length)
303     {
304         assert(set.contains(ix));
305         assert(ix in set);
306     }
307 
308     foreach (const ix; 0 .. length)
309     {
310         assert(set.contains(ix));
311         set.remove(ix);
312         assert(!set.contains(ix));
313     }
314 }
315 
316 ///
317 @safe pure nothrow @nogc unittest
318 {
319     alias E = uint;
320 
321     auto set = DenseSetFilter!(E, Growable.yes)();
322     assert(set._length == 0);
323 
324     const length = 2^^16;
325     foreach (const ix; 0 .. length)
326     {
327         assert(!set.contains(ix));
328         assert(ix !in set);
329 
330         assert(!set.insert(ix));
331         assert(set.contains(ix));
332         assert(ix in set);
333 
334         assert(set.complement(ix));
335         assert(!set.contains(ix));
336         assert(ix !in set);
337 
338         assert(!set.complement(ix));
339         assert(set.contains(ix));
340         assert(ix in set);
341 
342         assert(!set.contains(ix + 1));
343     }
344 }
345 
346 /// test `RefCounted` storage
347 nothrow @nogc unittest          // TODO pure when https://github.com/dlang/phobos/pull/4692/files has been merged
348 {
349     import std.typecons : RefCounted;
350     alias E = int;
351 
352     RefCounted!(DenseSetFilter!(E, Growable.yes)) set;
353 
354     assert(set._length == 0);
355     assert(set.capacity == 0);
356 
357     assert(!set.insert(0));
358     assert(set._length == 1);
359     assert(set.capacity == 2);
360 
361     const y = set;
362 
363     foreach (const e; 1 .. 1000)
364     {
365         assert(!set.insert(e));
366         assert(set._length == e + 1);
367         assert(y._length == e + 1);
368     }
369 
370     const set1 = RefCounted!(DenseSetFilter!(E, Growable.yes))(42);
371     assert(set1._length == 42);
372     assert(set1.capacity == 64);
373 }
374 
375 ///
376 @safe pure nothrow @nogc unittest
377 {
378     enum E : ubyte { a, b, c, d, dAlias = d }
379 
380     auto set = DenseSetFilter!(E, Growable.yes)();
381 
382     assert(set._length == 0);
383 
384     import std.traits : EnumMembers;
385     foreach (const lang; [EnumMembers!E])
386     {
387         assert(!set.contains(lang));
388     }
389     foreach (const lang; [EnumMembers!E])
390     {
391         set.insert(lang);
392         assert(set.contains(lang));
393     }
394 
395 }
396 
397 ///
398 @safe pure nothrow @nogc unittest
399 {
400     enum E : ubyte { a, b, c, d, dAlias = d }
401 
402     auto set = DenseSetFilter!(E, Growable.no).withInferredLength(); // TODO use instantiator function here
403     assert(set.capacity == typeof(set).elementMaxCount);
404 
405     static assert(!__traits(compiles, { assert(set.contains(0)); }));
406     static assert(!__traits(compiles, { assert(set.insert(0)); }));
407     static assert(!__traits(compiles, { assert(0 in set); }));
408 
409     import std.traits : EnumMembers;
410     foreach (const lang; [EnumMembers!E])
411     {
412         assert(!set.contains(lang));
413         assert(lang !in set);
414     }
415     foreach (const lang; [EnumMembers!E])
416     {
417         set.insert(lang);
418         assert(set.contains(lang));
419         assert(lang in set);
420     }
421 
422 }
423 
424 /** Check if `E` is filterable in `StaticDenseSetFilter`, that is castable to
425     `uint` and castable from unsigned int zero.
426 */
427 template isStaticDenseFilterableType(E)
428 {
429     import std.traits : hasIndirections, isUnsigned, isSomeChar;
430     static if (is(E == enum) ||
431                isUnsigned!E ||
432                isSomeChar!E)
433     {
434         enum isStaticDenseFilterableType = true;
435     }
436     else static if (is(typeof(E.init.toUnsigned)))
437     {
438         alias UnsignedType = typeof(E.init.toUnsigned());
439         enum isStaticDenseFilterableType = (isUnsigned!UnsignedType &&
440                                             is(typeof(E.fromUnsigned(UnsignedType.init))) &&
441                                             !hasIndirections!E);
442     }
443     else
444     {
445         enum isStaticDenseFilterableType = false;
446     }
447 }
448 
449 @safe pure nothrow @nogc unittest
450 {
451     static assert(isStaticDenseFilterableType!uint);
452     static assert(!isStaticDenseFilterableType!string);
453     static assert(isStaticDenseFilterableType!char);
454     static assert(!isStaticDenseFilterableType!(char*));
455 
456     enum E { a, b }
457     static assert(isStaticDenseFilterableType!E);
458 }
459 
460 /** Store presence of elements of type `E` in a set in the range `0 .. length`.
461     Can be seen as a generalization of `std.typecons.BitFlags` to integer types.
462 
463     Typically used to implement very fast membership checking in sets of
464     enumerators.
465 
466     TODO Add operators for bitwise `and` and `or` operations similar to
467     https://dlang.org/library/std/typecons/bit_flags.html
468  */
469 struct StaticDenseSetFilter(E,
470                             bool requestPacked = true)
471 if (isStaticDenseFilterableType!E)
472 {
473     import std.range.primitives : ElementType;
474     import std.traits : isIterable, isAssignable, isUnsigned;
475     import core.bitop : bts, btr, btc, bt;
476 
477     alias ElementType = E;
478     alias This = typeof(this);
479 
480     @safe pure:
481 
482     string toString() const @property @trusted
483     {
484         import std.array : Appender;
485         import std.conv : to;
486 
487         Appender!(typeof(return)) str = This.stringof ~ "([";
488         bool other = false;
489 
490         void putElement(in E e)
491         {
492             version(LDC) pragma(inline, true);
493             if (contains(e))
494             {
495                 if (other)
496                 {
497                     str.put(", ");
498                 }
499                 else
500                 {
501                     other = true;
502                 }
503                 str.put(e.to!string);
504             }
505         }
506 
507         static if (is(E == enum))
508         {
509             import std.traits : EnumMembers;
510             foreach (const e; [EnumMembers!E])
511             {
512                 putElement(e);
513             }
514         }
515         else
516         {
517             foreach (const i; E.unsignedMin .. E.unsignedMax + 1)
518             {
519                 putElement(E.fromUnsigned(i));
520             }
521         }
522 
523         str.put("])");
524         return str.data;
525     }
526 
527     nothrow @nogc:
528 
529     /** Construct from elements `r`.
530      */
531     private this(R)(R r)
532         if (isIterable!R &&
533             isAssignable!(E, ElementType!R))
534     {
535         foreach (const ref e; r)
536         {
537             insert(e);
538         }
539     }
540 
541     /** Construct from `r` if `r` is non-empty, otherwise construct a full set.
542      */
543     static typeof(this) withValuesOrFull(R)(R r)
544         if (isIterable!R &&
545             isAssignable!(E, ElementType!R))
546     {
547         import std.range.primitives : empty;
548         if (r.empty)
549         {
550             return asFull();
551         }
552         return typeof(return)(r);
553     }
554 
555     pragma(inline, true):
556 
557     /// Construct a full set .
558     static typeof(this) asFull()() // template-lazy
559     {
560         typeof(return) that = void;
561         that._blocks[] = Block.max;
562         return that;
563     }
564 
565     /** Insert element `e`.
566      */
567     void insert()(in E e) @trusted // template-lazy
568     {
569         import nxt.bitop_ex : setBit;
570         static if (isPackedInScalar)
571         {
572             _blocks[0].setBit(cast(size_t)e);
573         }
574         else
575         {
576             bts(_blocksPtr, cast(size_t)e);
577         }
578     }
579     alias put = insert;         // OutputRange compatibility
580 
581     /** Remove element `e`.
582      */
583     void remove()(in E e) @trusted // template-lazy
584     {
585         import nxt.bitop_ex : resetBit;
586         static if (isPackedInScalar)
587         {
588             _blocks[0].resetBit(cast(size_t)e);
589         }
590         else
591         {
592             btr(_blocksPtr, cast(size_t)e);
593         }
594     }
595 
596     @property:
597 
598     /** Check if element `e` is present/stored/contained.
599      */
600     bool contains()(in E e) @trusted const // template-lazy
601     {
602         import nxt.bitop_ex : testBit;
603         static if (isPackedInScalar)
604         {
605             auto y = _blocks[0].testBit(cast(size_t)e);
606             return y;
607         }
608         else
609         {
610             return bt(_blocksPtr, cast(size_t)e) != 0;
611         }
612     }
613 
614     /// ditto
615     bool opBinaryRight(string op)(in E e) const
616         if (op == "in")
617     {
618         return contains(e);
619     }
620 
621     /// ditto
622     typeof(this) opBinary(string op)(auto ref in typeof(this) e) const
623         if (op == "|" || op == "&" || op == "^")
624     {
625         typeof(return) result;
626         mixin(`result._blocks[] = _blocks[] ` ~ op ~ ` e._blocks[];`);
627         return result;
628     }
629 
630     /// ditto
631     typeof(this) opOpAssign(string op)(auto ref in typeof(this) e)
632         if (op == "|" || op == "&" || op == "^")
633     {
634         mixin(`_blocks[] ` ~ op ~ `= e._blocks[];`);
635         return this;
636     }
637 
638 private:
639     static if (is(E == enum) || isUnsigned!E) // has normal
640     {
641         /// Maximum number of elements in filter.
642         enum elementMaxCount = E.max - E.min + 1;
643     }
644     else
645     {
646         /// Maximum number of elements in filter.
647         enum elementMaxCount = E.unsignedMax - E.unsignedMin + 1;
648     }
649 
650     static if (requestPacked)
651     {
652         static      if (elementMaxCount <= 8*ubyte.sizeof)
653         {
654             enum isPackedInScalar = true;
655             alias Block = ubyte;
656         }
657         else static if (elementMaxCount <= 8*ushort.sizeof)
658         {
659             enum isPackedInScalar = true;
660             alias Block = ushort;
661         }
662         else static if (elementMaxCount <= 8*uint.sizeof)
663         {
664             enum isPackedInScalar = true;
665             alias Block = uint;
666         }
667         else
668         {
669             enum isPackedInScalar = false;
670             alias Block = size_t;
671         }
672     }
673     else
674     {
675         enum isPackedInScalar = false;
676         alias Block = size_t;
677     }
678 
679     /** Number of bits per `Block`. */
680     enum bitsPerBlock = 8*Block.sizeof;
681 
682     /** Number of `Block`s. */
683     enum blockCount = (elementMaxCount + (bitsPerBlock-1)) / bitsPerBlock;
684 
685     Block[blockCount] _blocks;          /// Pointer to blocks of bits.
686     inout(Block)* _blocksPtr() @trusted inout { return _blocks.ptr; }
687 }
688 
689 version(unittest)
690 {
691     import std.traits : EnumMembers;
692 }
693 
694 ///
695 @safe pure nothrow @nogc unittest
696 {
697     enum E : ubyte { a, b, c, d, dAlias = d }
698 
699     auto set = StaticDenseSetFilter!(E)();
700     static assert(set.sizeof == 1);
701 
702     static assert(!__traits(compiles, { assert(set.contains(0)); }));
703     static assert(!__traits(compiles, { assert(set.insert(0)); }));
704     static assert(!__traits(compiles, { assert(0 in set); }));
705 
706     // initially empty
707     foreach (const lang; [EnumMembers!E])
708     {
709         assert(!set.contains(lang));
710         assert(lang !in set);
711     }
712 
713     // insert
714     foreach (const lang; [EnumMembers!E])
715     {
716         set.insert(lang);
717         assert(set.contains(lang));
718         assert(lang in set);
719     }
720 
721     // remove
722     foreach (const lang; [EnumMembers!E])
723     {
724         set.remove(lang);
725         assert(!set.contains(lang));
726         assert(lang !in set);
727     }
728 }
729 
730 /// assignment from range
731 @safe pure nothrow @nogc unittest
732 {
733     enum E : ubyte { a, b, c, d, dAlias = d }
734 
735     const E[2] es = [E.a, E.c];
736     auto set = StaticDenseSetFilter!(E)(es[]);
737     static assert(set.sizeof == 1);
738 
739     foreach (const ref e; es)
740     {
741         assert(set.contains(e));
742     }
743 }
744 
745 /// assignment from range
746 @safe pure nothrow @nogc unittest
747 {
748     enum E : ubyte { a, b, c, d }
749 
750     const E[] es = [];
751     auto set = StaticDenseSetFilter!(E).withValuesOrFull(es[]);
752     static assert(set.sizeof == 1);
753 
754     foreach (const e; [EnumMembers!E])
755     {
756         assert(set.contains(e));
757         assert(e in set);
758         set.remove(e);
759         assert(!set.contains(e));
760         assert(e !in set);
761     }
762 }
763 
764 /// assignment from range
765 @safe pure nothrow @nogc unittest
766 {
767     enum E : ubyte { a, b, c, d }
768 
769     const E[2] es = [E.a, E.c];
770     auto set = StaticDenseSetFilter!(E).withValuesOrFull(es[]);
771     static assert(set.sizeof == 1);
772 
773     foreach (const ref e; es)
774     {
775         assert(set.contains(e));
776         set.remove(e);
777         assert(!set.contains(e));
778     }
779 }
780 
781 /// assignment from range
782 @safe pure nothrow @nogc unittest
783 {
784     enum E : ubyte { a, b, c, d }
785 
786     auto set = StaticDenseSetFilter!(E).asFull;
787     static assert(set.sizeof == 1);
788 
789     foreach (const e; [EnumMembers!E])
790     {
791         assert(set.contains(e));
792         set.remove(e);
793         assert(!set.contains(e));
794     }
795 }
796 
797 /// assignment from range
798 @safe pure nothrow @nogc unittest
799 {
800     enum E : ubyte
801     {
802         a, b, c, d, e, f, g, h,
803     }
804 
805     auto set = StaticDenseSetFilter!(E).asFull;
806     static assert(set.sizeof == 1);
807     static assert(set.isPackedInScalar);
808 
809     foreach (const e; [EnumMembers!E])
810     {
811         assert(set.contains(e));
812         set.remove(e);
813         assert(!set.contains(e));
814     }
815 }
816 
817 /// assignment from range
818 @safe pure nothrow @nogc unittest
819 {
820     enum E : ubyte
821     {
822         a, b, c, d, e, f, g, h,
823         i,
824     }
825 
826     auto set = StaticDenseSetFilter!(E).asFull;
827     static assert(set.sizeof == 2);
828     static assert(set.isPackedInScalar);
829 
830     foreach (const e; [EnumMembers!E])
831     {
832         assert(set.contains(e));
833         set.remove(e);
834         assert(!set.contains(e));
835     }
836 }
837 
838 /// assignment from range
839 @safe pure nothrow @nogc unittest
840 {
841     enum E : ubyte
842     {
843         a, b, c, d, e, f, g, h,
844         i, j, k, l, m, n, o, p,
845     }
846 
847     auto set = StaticDenseSetFilter!(E).asFull;
848     static assert(set.sizeof == 2);
849     static assert(set.isPackedInScalar);
850 
851     foreach (const e; [EnumMembers!E])
852     {
853         assert(set.contains(e));
854         set.remove(e);
855         assert(!set.contains(e));
856     }
857 }
858 
859 /// assignment from range
860 @safe pure nothrow @nogc unittest
861 {
862     enum E : ubyte
863     {
864         a, b, c, d, e, f, g, h,
865         i, j, k, l, m, n, o, p,
866         r,
867     }
868 
869     auto set = StaticDenseSetFilter!(E).asFull;
870     static assert(set.sizeof == 4);
871     static assert(set.isPackedInScalar);
872 
873     foreach (const e; [EnumMembers!E])
874     {
875         assert(set.contains(e));
876         set.remove(e);
877         assert(!set.contains(e));
878     }
879 }
880 
881 /// assignment from range
882 @safe pure nothrow @nogc unittest
883 {
884     enum E : ubyte
885     {
886         a, b, c, d, e, f, g, h,
887         i, j, k, l, m, n, o, p,
888         r, s, t, u, v, w, x, y,
889         z, a1, a2, a3, a4, a5, a6, a7,
890         a8
891     }
892 
893     auto set = StaticDenseSetFilter!(E).asFull;
894     static assert(set.sizeof == 8);
895     static assert(!set.isPackedInScalar);
896 
897     foreach (const e; [EnumMembers!E])
898     {
899         assert(set.contains(e));
900         set.remove(e);
901         assert(!set.contains(e));
902     }
903 }
904 
905 version(unittest)
906 {
907     enum Rel : ubyte { unknown, subClassOf, instanceOf, memberOf }
908     import nxt.bit_traits : packedBitSizeOf;
909     static assert(packedBitSizeOf!Rel == 2);
910 
911     struct Role
912     {
913         Rel rel;
914         bool reversion;
915 
916         enum unsignedMin = 0;
917         enum unsignedMax = 2^^(1 +  // reversion
918                                packedBitSizeOf!Rel // relation
919             ) - 1;
920 
921         alias UnsignedType = ushort;
922         static assert(typeof(this).sizeof == UnsignedType.sizeof);
923         static assert(Role.sizeof == 2);
924 
925         pragma(inline, true)
926         @safe pure nothrow @nogc:
927 
928         /// Create from `UnsignedType` `u`.
929         static typeof(this) fromUnsigned(in UnsignedType u) @trusted
930         {
931             typeof(return) that;
932             that.reversion = (u >> 0) & 1;
933             that.rel = cast(Rel)(u >> 1);
934             return that;
935         }
936 
937         /// Convert to `UnsignedType` `u`.
938         UnsignedType toUnsigned() const @trusted
939         {
940             return cast(UnsignedType)((cast(UnsignedType)reversion << 0) |
941                                       (cast(UnsignedType)rel << 1));
942         }
943 
944         UnsignedType opCast(UnsignedType)() const @nogc
945         {
946             return toUnsigned;
947         }
948     }
949 
950     static assert(is(typeof(Role.init.toUnsigned)));
951     static assert(is(typeof(Role.init.toUnsigned()) == Role.UnsignedType));
952     static assert(isStaticDenseFilterableType!Role);
953 }
954 
955 /// assignment from range
956 @safe pure unittest
957 {
958     auto set = StaticDenseSetFilter!(Role)();
959     static assert(set.sizeof == 1);
960 
961     assert(set.toString == "StaticDenseSetFilter!(Role, true)([])");
962 
963     // inserts
964     foreach (const rel; [EnumMembers!Rel])
965     {
966         assert(!set.contains(Role(rel)));
967         assert(!set.contains(Role(rel, true)));
968 
969         set.insert(Role(rel));
970         assert(set.contains(Role(rel)));
971 
972         assert(!set.contains(Role(rel, true)));
973         set.insert(Role(rel, true));
974 
975         assert(set.contains(Role(rel)));
976         assert(set.contains(Role(rel, true)));
977     }
978 
979     assert(set.toString == "StaticDenseSetFilter!(Role, true)([const(Role)(unknown, false), const(Role)(unknown, true), const(Role)(subClassOf, false), const(Role)(subClassOf, true), const(Role)(instanceOf, false), const(Role)(instanceOf, true), const(Role)(memberOf, false), const(Role)(memberOf, true)])");
980 
981     // removes
982     foreach (const rel; [EnumMembers!Rel])
983     {
984         assert(set.contains(Role(rel)));
985         assert(set.contains(Role(rel, true)));
986 
987         set.remove(Role(rel));
988         assert(!set.contains(Role(rel)));
989 
990         assert(set.contains(Role(rel, true)));
991         set.remove(Role(rel, true));
992 
993         assert(!set.contains(Role(rel)));
994         assert(!set.contains(Role(rel, true)));
995     }
996 
997     assert(set.toString == "StaticDenseSetFilter!(Role, true)([])");
998 
999     auto fullSet = StaticDenseSetFilter!(Role).asFull;
1000 
1001     foreach (const rel; [EnumMembers!Rel])
1002     {
1003         assert(fullSet.contains(Role(rel)));
1004         assert(fullSet.contains(Role(rel, true)));
1005     }
1006 
1007     auto emptySet = StaticDenseSetFilter!(Role)();
1008 
1009     foreach (const rel; [EnumMembers!Rel])
1010     {
1011         assert(!emptySet.contains(Role(rel)));
1012         assert(!emptySet.contains(Role(rel, true)));
1013     }
1014 }
1015 
1016 /// set operations
1017 @safe pure nothrow @nogc unittest
1018 {
1019     import nxt.array_help : s;
1020 
1021     enum E : ubyte { a, b, c, d, dAlias = d }
1022 
1023     auto a = StaticDenseSetFilter!(E)([E.a].s[]);
1024     auto b = StaticDenseSetFilter!(E)([E.b].s[]);
1025     auto c = StaticDenseSetFilter!(E)([E.b].s[]);
1026 
1027     auto a_or_b = StaticDenseSetFilter!(E)([E.a, E.b].s[]);
1028     auto a_and_b = StaticDenseSetFilter!(E)();
1029 
1030     assert((a | b | c) == a_or_b);
1031     assert((a & b) == a_and_b);
1032     assert((a & a & b) == a_and_b);
1033 
1034     a |= b;
1035     assert(a == a_or_b);
1036 }