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 	pure nothrow @safe @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 		this(this) @disable;
94 
95 		/// Returns: shallow (and deep) duplicate of `this`.
96 		typeof(this) dup()() @trusted /*tlm*/
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 /*tlm*/
114 		{
115 			if (_capacity < newCapacity)
116 			{
117 				const oldBlockCount = blockCount;
118 				import std.math.algebraic : 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 /*tlm*/
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 /*tlm*/
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 /*tlm*/
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 /*tlm*/
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 pure nothrow @safe @nogc unittest {
226 	alias E = uint;
227 
228 	import std.range.primitives : isOutputRange;
229 	alias Set = DynamicDenseSetFilter!(E, Growable.no);
230 	static assert(isOutputRange!(Set, E));
231 
232 	const set0 = Set();
233 	assert(set0.capacity == 0);
234 
235 	const length = 2^^6;
236 	auto set = DynamicDenseSetFilter!(E, Growable.no)(2*length);
237 	const y = set.dup;
238 	assert(y.capacity == 2*length);
239 
240 	foreach (const ix; 0 .. length)
241 	{
242 		assert(!set.contains(ix));
243 		assert(ix !in set);
244 
245 		assert(!set.insert(ix));
246 		assert(set.contains(ix));
247 		assert(ix in set);
248 
249 		assert(set.complement(ix));
250 		assert(!set.contains(ix));
251 		assert(ix !in set);
252 
253 		assert(!set.complement(ix));
254 		assert(set.contains(ix));
255 		assert(ix in set);
256 
257 		assert(!set.contains(ix + 1));
258 	}
259 
260 	auto z = set.dup;
261 	foreach (const ix; 0 .. length)
262 	{
263 		assert(z.contains(ix));
264 		assert(ix in z);
265 	}
266 
267 	foreach (const ix; 0 .. length)
268 	{
269 		assert(set.contains(ix));
270 		assert(ix in set);
271 	}
272 
273 	foreach (const ix; 0 .. length)
274 	{
275 		assert(set.contains(ix));
276 		set.remove(ix);
277 		assert(!set.contains(ix));
278 	}
279 }
280 
281 ///
282 pure nothrow @safe @nogc unittest {
283 	alias E = uint;
284 
285 	auto set = DynamicDenseSetFilter!(E, Growable.yes)();
286 	assert(set._length == 0);
287 
288 	const length = 2^^16;
289 	foreach (const ix; 0 .. length)
290 	{
291 		assert(!set.contains(ix));
292 		assert(ix !in set);
293 
294 		assert(!set.insert(ix));
295 		assert(set.contains(ix));
296 		assert(ix in set);
297 
298 		assert(set.complement(ix));
299 		assert(!set.contains(ix));
300 		assert(ix !in set);
301 
302 		assert(!set.complement(ix));
303 		assert(set.contains(ix));
304 		assert(ix in set);
305 
306 		assert(!set.contains(ix + 1));
307 	}
308 }
309 
310 /// test `RefCounted` storage
311 nothrow @nogc unittest		  /+ TODO: pure when https://github.com/dlang/phobos/pull/4692/files has been merged +/
312 {
313 	import std.typecons : RefCounted;
314 	alias E = uint;
315 
316 	RefCounted!(DynamicDenseSetFilter!(E, Growable.yes)) set;
317 
318 	assert(set._length == 0);
319 	assert(set.capacity == 0);
320 
321 	assert(!set.insert(0));
322 	assert(set._length == 1);
323 	assert(set.capacity == 2);
324 
325 	const y = set;
326 
327 	foreach (const e; 1 .. 1000)
328 	{
329 		assert(!set.insert(e));
330 		assert(set._length == e + 1);
331 		assert(y._length == e + 1);
332 	}
333 
334 	const set1 = RefCounted!(DynamicDenseSetFilter!(E, Growable.yes))(42);
335 	assert(set1._length == 42);
336 	assert(set1.capacity == 64);
337 }
338 
339 ///
340 pure nothrow @safe @nogc unittest {
341 	enum E : ubyte { a, b, c, d, dAlias = d }
342 
343 	auto set = DynamicDenseSetFilter!(E, Growable.yes)();
344 
345 	assert(set._length == 0);
346 
347 	import std.traits : EnumMembers;
348 	foreach (const lang; [EnumMembers!E])
349 		assert(!set.contains(lang));
350 	foreach (const lang; [EnumMembers!E])
351 	{
352 		set.insert(lang);
353 		assert(set.contains(lang));
354 	}
355 
356 }
357 
358 ///
359 pure nothrow @safe @nogc unittest {
360 	enum E : ubyte { a, b, c, d, dAlias = d }
361 
362 	auto set = DynamicDenseSetFilter!(E, Growable.no).withInferredLength(); /+ TODO: use instantiator function here +/
363 	assert(set.capacity == typeof(set).elementMaxCount);
364 
365 	static assert(!__traits(compiles, { assert(set.contains(0)); }));
366 	static assert(!__traits(compiles, { assert(set.insert(0)); }));
367 	static assert(!__traits(compiles, { assert(0 in set); }));
368 
369 	import std.traits : EnumMembers;
370 	foreach (const lang; [EnumMembers!E])
371 	{
372 		assert(!set.contains(lang));
373 		assert(lang !in set);
374 	}
375 	foreach (const lang; [EnumMembers!E])
376 	{
377 		set.insert(lang);
378 		assert(set.contains(lang));
379 		assert(lang in set);
380 	}
381 
382 }
383 
384 /** Check if `E` is filterable in `StaticDenseSetFilter`, that is castable to
385 	`uint` and castable from unsigned int zero.
386 */
387 template isStaticDenseFilterableType(E)
388 {
389 	static if (is(E == enum))
390 		enum isStaticDenseFilterableType = true;
391 	else
392 	{
393 		import std.traits : hasIndirections, isUnsigned, isSomeChar;
394 		static if (isUnsigned!E ||
395 				   isSomeChar!E)
396 			enum isStaticDenseFilterableType = true;
397 		else static if (is(typeof(E.init.toUnsigned)))
398 		{
399 			alias UnsignedType = typeof(E.init.toUnsigned());
400 			enum isStaticDenseFilterableType = (isUnsigned!UnsignedType &&
401 												is(typeof(E.fromUnsigned(UnsignedType.init))) &&
402 												!hasIndirections!E);
403 		}
404 		else
405 			enum isStaticDenseFilterableType = false;
406 	}
407 }
408 
409 pure nothrow @safe @nogc unittest {
410 	static assert(isStaticDenseFilterableType!uint);
411 	static assert(!isStaticDenseFilterableType!string);
412 	static assert(isStaticDenseFilterableType!char);
413 	static assert(!isStaticDenseFilterableType!(char*));
414 
415 	enum E { a, b }
416 	static assert(isStaticDenseFilterableType!E);
417 }
418 
419 /** Store presence of elements of type `E` in a set in the range `0 .. length`.
420  *
421  * Can be seen as a generalization of `std.typecons.BitFlags` to integer types.
422  *
423  * Typically used to implement very fast membership checking in sets of
424  * enumerators.
425  *
426  * TODO: Add operators for bitwise `and` and `or` operations similar to
427  * https://dlang.org/library/std/typecons/bit_flags.html
428  */
429 struct StaticDenseSetFilter(E, bool requestPacked = true)
430 if (isStaticDenseFilterableType!E)
431 {
432 	import std.range.primitives : StdElementType = ElementType;
433 	import std.traits : isIterable, isAssignable, isUnsigned;
434 	import core.bitop : bts, btr, btc, bt;
435 
436 	alias ElementType = E;
437 	alias This = typeof(this);
438 
439 	@safe pure:
440 
441 	string toString() const @property @trusted
442 	{
443 		import std.array : Appender;
444 		import std.conv : to;
445 
446 		Appender!(typeof(return)) str = This.stringof ~ "([";
447 		bool other = false;
448 
449 		void putElement(in E e)
450 		{
451 			version (LDC) pragma(inline, true);
452 			if (contains(e))
453 			{
454 				if (other)
455 					str.put(", ");
456 				else
457 					other = true;
458 				str.put(e.to!string);
459 			}
460 		}
461 
462 		static if (is(E == enum))
463 		{
464 			import std.traits : EnumMembers;
465 			foreach (const e; [EnumMembers!E])
466 				putElement(e);
467 		}
468 		else
469 		{
470 			foreach (const i; E.unsignedMin .. E.unsignedMax + 1)
471 				putElement(E.fromUnsigned(i));
472 		}
473 
474 		str.put("])");
475 		return str.data;
476 	}
477 
478 	nothrow @nogc:
479 
480 	/** Construct from elements `r`.
481 	 */
482 	private this(R)(R r)
483 		if (isIterable!R &&
484 			isAssignable!(E, StdElementType!R))
485 	{
486 		foreach (const ref e; r)
487 			insert(e);
488 	}
489 
490 	/** Construct from `r` if `r` is non-empty, otherwise construct a full set.
491 	 */
492 	static typeof(this) withValuesOrFull(R)(R r)
493 		if (isIterable!R &&
494 			isAssignable!(E, StdElementType!R))
495 	{
496 		import std.range.primitives : empty;
497 		if (r.empty)
498 			return asFull();
499 		return typeof(return)(r);
500 	}
501 
502 	pragma(inline, true):
503 
504 	/// Construct a full set .
505 	static typeof(this) asFull()() /*tlm*/
506 	{
507 		typeof(return) that = void;
508 		that._blocks[] = Block.max;
509 		return that;
510 	}
511 
512 	/** Insert element `e`.
513 	 */
514 	void insert()(in E e) @trusted /*tlm*/
515 	{
516 		import nxt.bitop_ex : setBit;
517 		static if (isPackedInScalar)
518 			_blocks[0].setBit(cast(size_t)e);
519 		else
520 			bts(_blocksPtr, cast(size_t)e);
521 	}
522 	alias put = insert;		 // OutputRange compatibility
523 
524 	/** Remove element `e`.
525 	 */
526 	void remove()(in E e) @trusted /*tlm*/
527 	{
528 		import nxt.bitop_ex : resetBit;
529 		static if (isPackedInScalar)
530 			_blocks[0].resetBit(cast(size_t)e);
531 		else
532 			btr(_blocksPtr, cast(size_t)e);
533 	}
534 
535 	@property:
536 
537 	/** Check if element `e` is present/stored/contained.
538 	 */
539 	bool contains()(in E e) @trusted const /*tlm*/
540 	{
541 		import nxt.bitop_ex : testBit;
542 		static if (isPackedInScalar)
543 			return _blocks[0].testBit(cast(size_t)e);
544 		else
545 			return bt(_blocksPtr, cast(size_t)e) != 0;
546 	}
547 
548 	/// ditto
549 	bool opBinaryRight(string op)(in E e) const if (op == "in")
550 		=> contains(e);
551 
552 	/// ditto
553 	typeof(this) opBinary(string op)(in typeof(this) e) const
554 	if (op == "|" || op == "&" || op == "^")
555 	{
556 		typeof(return) result;
557 		mixin(`result._blocks[] = _blocks[] ` ~ op ~ ` e._blocks[];`);
558 		return result;
559 	}
560 
561 	/// ditto
562 	typeof(this) opOpAssign(string op)(in typeof(this) e)
563 	if (op == "|" || op == "&" || op == "^")
564 	{
565 		version (DigitalMars) pragma(inline, false);
566 		mixin(`_blocks[] ` ~ op ~ `= e._blocks[];`);
567 		return this;
568 	}
569 
570 private:
571 	static if (is(E == enum) || isUnsigned!E) // has normal
572 	{
573 		/// Maximum number of elements in filter.
574 		enum elementMaxCount = E.max - E.min + 1;
575 	}
576 	else
577 	{
578 		/// Maximum number of elements in filter.
579 		enum elementMaxCount = E.unsignedMax - E.unsignedMin + 1;
580 	}
581 
582 	static if (requestPacked)
583 	{
584 		static	  if (elementMaxCount <= 8*ubyte.sizeof)
585 		{
586 			enum isPackedInScalar = true;
587 			alias Block = ubyte;
588 		}
589 		else static if (elementMaxCount <= 8*ushort.sizeof)
590 		{
591 			enum isPackedInScalar = true;
592 			alias Block = ushort;
593 		}
594 		else static if (elementMaxCount <= 8*uint.sizeof)
595 		{
596 			enum isPackedInScalar = true;
597 			alias Block = uint;
598 		}
599 		else
600 		{
601 			enum isPackedInScalar = false;
602 			alias Block = size_t;
603 		}
604 	}
605 	else
606 	{
607 		enum isPackedInScalar = false;
608 		alias Block = size_t;
609 	}
610 
611 	/** Number of bits per `Block`. */
612 	enum bitsPerBlock = 8*Block.sizeof;
613 
614 	/** Number of `Block`s. */
615 	enum blockCount = (elementMaxCount + (bitsPerBlock-1)) / bitsPerBlock;
616 
617 	Block[blockCount] _blocks;		  /// Pointer to blocks of bits.
618 	inout(Block)* _blocksPtr() @trusted inout
619 		=> _blocks.ptr;
620 }
621 
622 version (unittest)
623 {
624 	import std.traits : EnumMembers;
625 }
626 
627 ///
628 pure nothrow @safe @nogc unittest {
629 	enum E : ubyte { a, b, c, d, dAlias = d }
630 
631 	auto set = StaticDenseSetFilter!(E)();
632 	static assert(set.sizeof == 1);
633 
634 	static assert(!__traits(compiles, { assert(set.contains(0)); }));
635 	static assert(!__traits(compiles, { assert(set.insert(0)); }));
636 	static assert(!__traits(compiles, { assert(0 in set); }));
637 
638 	// initially empty
639 	foreach (const lang; [EnumMembers!E])
640 	{
641 		assert(!set.contains(lang));
642 		assert(lang !in set);
643 	}
644 
645 	// insert
646 	foreach (const lang; [EnumMembers!E])
647 	{
648 		set.insert(lang);
649 		assert(set.contains(lang));
650 		assert(lang in set);
651 	}
652 
653 	// remove
654 	foreach (const lang; [EnumMembers!E])
655 	{
656 		set.remove(lang);
657 		assert(!set.contains(lang));
658 		assert(lang !in set);
659 	}
660 }
661 
662 /// assignment from range
663 pure nothrow @safe @nogc unittest {
664 	enum E : ubyte { a, b, c, d, dAlias = d }
665 
666 	const E[2] es = [E.a, E.c];
667 	auto set = StaticDenseSetFilter!(E)(es[]);
668 	static assert(set.sizeof == 1);
669 
670 	foreach (const ref e; es)
671 	{
672 		assert(set.contains(e));
673 	}
674 }
675 
676 /// assignment from range
677 pure nothrow @safe @nogc unittest {
678 	enum E : ubyte { a, b, c, d }
679 
680 	const E[] es = [];
681 	auto set = StaticDenseSetFilter!(E).withValuesOrFull(es[]);
682 	static assert(set.sizeof == 1);
683 
684 	foreach (const e; [EnumMembers!E])
685 	{
686 		assert(set.contains(e));
687 		assert(e in set);
688 		set.remove(e);
689 		assert(!set.contains(e));
690 		assert(e !in set);
691 	}
692 }
693 
694 /// assignment from range
695 pure nothrow @safe @nogc unittest {
696 	enum E : ubyte { a, b, c, d }
697 
698 	const E[2] es = [E.a, E.c];
699 	auto set = StaticDenseSetFilter!(E).withValuesOrFull(es[]);
700 	static assert(set.sizeof == 1);
701 
702 	foreach (const ref e; es)
703 	{
704 		assert(set.contains(e));
705 		set.remove(e);
706 		assert(!set.contains(e));
707 	}
708 }
709 
710 /// assignment from range
711 pure nothrow @safe @nogc unittest {
712 	enum E : ubyte { a, b, c, d }
713 
714 	auto set = StaticDenseSetFilter!(E).asFull;
715 	static assert(set.sizeof == 1);
716 
717 	foreach (const e; [EnumMembers!E])
718 	{
719 		assert(set.contains(e));
720 		set.remove(e);
721 		assert(!set.contains(e));
722 	}
723 }
724 
725 /// assignment from range
726 pure nothrow @safe @nogc unittest {
727 	enum E : ubyte
728 	{
729 		a, b, c, d, e, f, g, h,
730 	}
731 
732 	auto set = StaticDenseSetFilter!(E).asFull;
733 	static assert(set.sizeof == 1);
734 	static assert(set.isPackedInScalar);
735 
736 	foreach (const e; [EnumMembers!E])
737 	{
738 		assert(set.contains(e));
739 		set.remove(e);
740 		assert(!set.contains(e));
741 	}
742 }
743 
744 /// assignment from range
745 pure nothrow @safe @nogc unittest {
746 	enum E : ubyte
747 	{
748 		a, b, c, d, e, f, g, h,
749 		i,
750 	}
751 
752 	auto set = StaticDenseSetFilter!(E).asFull;
753 	static assert(set.sizeof == 2);
754 	static assert(set.isPackedInScalar);
755 
756 	foreach (const e; [EnumMembers!E])
757 	{
758 		assert(set.contains(e));
759 		set.remove(e);
760 		assert(!set.contains(e));
761 	}
762 }
763 
764 /// assignment from range
765 pure nothrow @safe @nogc unittest {
766 	enum E : ubyte
767 	{
768 		a, b, c, d, e, f, g, h,
769 		i, j, k, l, m, n, o, p,
770 	}
771 
772 	auto set = StaticDenseSetFilter!(E).asFull;
773 	static assert(set.sizeof == 2);
774 	static assert(set.isPackedInScalar);
775 
776 	foreach (const e; [EnumMembers!E])
777 	{
778 		assert(set.contains(e));
779 		set.remove(e);
780 		assert(!set.contains(e));
781 	}
782 }
783 
784 /// assignment from range
785 pure nothrow @safe @nogc unittest {
786 	enum E : ubyte
787 	{
788 		a, b, c, d, e, f, g, h,
789 		i, j, k, l, m, n, o, p,
790 		r,
791 	}
792 
793 	auto set = StaticDenseSetFilter!(E).asFull;
794 	static assert(set.sizeof == 4);
795 	static assert(set.isPackedInScalar);
796 
797 	foreach (const e; [EnumMembers!E])
798 	{
799 		assert(set.contains(e));
800 		set.remove(e);
801 		assert(!set.contains(e));
802 	}
803 }
804 
805 /// assignment from range
806 pure nothrow @safe @nogc unittest {
807 	enum E : ubyte
808 	{
809 		a, b, c, d, e, f, g, h,
810 		i, j, k, l, m, n, o, p,
811 		r, s, t, u, v, w, x, y,
812 		z, a1, a2, a3, a4, a5, a6, a7,
813 		a8
814 	}
815 
816 	auto set = StaticDenseSetFilter!(E).asFull;
817 	static assert(set.sizeof == 8);
818 	static assert(!set.isPackedInScalar);
819 
820 	foreach (const e; [EnumMembers!E])
821 	{
822 		assert(set.contains(e));
823 		set.remove(e);
824 		assert(!set.contains(e));
825 	}
826 }
827 
828 version (unittest)
829 {
830 	enum Rel : ubyte { unknown, subClassOf, instanceOf, memberOf }
831 	import nxt.bit_traits : packedBitSizeOf;
832 	static assert(packedBitSizeOf!Rel == 2);
833 
834 	struct Role
835 	{
836 		Rel rel;
837 		bool reversion;
838 
839 		enum unsignedMin = 0;
840 		enum unsignedMax = 2^^(1 +  // reversion
841 							   packedBitSizeOf!Rel // relation
842 			) - 1;
843 
844 		alias UnsignedType = ushort;
845 		static assert(typeof(this).sizeof == UnsignedType.sizeof);
846 		static assert(Role.sizeof == 2);
847 
848 		pragma(inline, true)
849 		pure nothrow @safe @nogc:
850 
851 		/// Create from `UnsignedType` `u`.
852 		static typeof(this) fromUnsigned(in UnsignedType u) @trusted
853 		{
854 			typeof(return) that;
855 			that.reversion = (u >> 0) & 1;
856 			that.rel = cast(Rel)(u >> 1);
857 			return that;
858 		}
859 
860 		/// Convert to `UnsignedType` `u`.
861 		UnsignedType toUnsigned() const @trusted
862 			=> cast(UnsignedType)((cast(UnsignedType)reversion << 0) |
863 								  (cast(UnsignedType)rel << 1));
864 
865 		UnsignedType opCast(UnsignedType)() const @nogc
866 			=> toUnsigned;
867 	}
868 
869 	static assert(is(typeof(Role.init.toUnsigned)));
870 	static assert(is(typeof(Role.init.toUnsigned()) == Role.UnsignedType));
871 	static assert(isStaticDenseFilterableType!Role);
872 }
873 
874 /// assignment from range
875 pure @safe unittest {
876 	auto set = StaticDenseSetFilter!(Role)();
877 	static assert(set.sizeof == 1);
878 
879 	assert(set.toString == "StaticDenseSetFilter!(Role, true)([])");
880 
881 	// inserts
882 	foreach (const rel; [EnumMembers!Rel])
883 	{
884 		assert(!set.contains(Role(rel)));
885 		assert(!set.contains(Role(rel, true)));
886 
887 		set.insert(Role(rel));
888 		assert(set.contains(Role(rel)));
889 
890 		assert(!set.contains(Role(rel, true)));
891 		set.insert(Role(rel, true));
892 
893 		assert(set.contains(Role(rel)));
894 		assert(set.contains(Role(rel, true)));
895 	}
896 
897 	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)])");
898 
899 	// removes
900 	foreach (const rel; [EnumMembers!Rel])
901 	{
902 		assert(set.contains(Role(rel)));
903 		assert(set.contains(Role(rel, true)));
904 
905 		set.remove(Role(rel));
906 		assert(!set.contains(Role(rel)));
907 
908 		assert(set.contains(Role(rel, true)));
909 		set.remove(Role(rel, true));
910 
911 		assert(!set.contains(Role(rel)));
912 		assert(!set.contains(Role(rel, true)));
913 	}
914 
915 	assert(set.toString == "StaticDenseSetFilter!(Role, true)([])");
916 
917 	auto fullSet = StaticDenseSetFilter!(Role).asFull;
918 
919 	foreach (const rel; [EnumMembers!Rel])
920 	{
921 		assert(fullSet.contains(Role(rel)));
922 		assert(fullSet.contains(Role(rel, true)));
923 	}
924 
925 	auto emptySet = StaticDenseSetFilter!(Role)();
926 
927 	foreach (const rel; [EnumMembers!Rel])
928 	{
929 		assert(!emptySet.contains(Role(rel)));
930 		assert(!emptySet.contains(Role(rel, true)));
931 	}
932 }
933 
934 /// set operations
935 pure nothrow @safe @nogc unittest {
936 	import nxt.array_help : s;
937 
938 	enum E : ubyte { a, b, c, d, dAlias = d }
939 
940 	auto a = StaticDenseSetFilter!(E)([E.a].s[]);
941 	auto b = StaticDenseSetFilter!(E)([E.b].s[]);
942 	auto c = StaticDenseSetFilter!(E)([E.b].s[]);
943 
944 	auto a_or_b = StaticDenseSetFilter!(E)([E.a, E.b].s[]);
945 	auto a_and_b = StaticDenseSetFilter!(E)();
946 
947 	assert((a | b | c) == a_or_b);
948 	assert((a & b) == a_and_b);
949 
950 	a |= b;
951 	assert(a == a_or_b);
952 }