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