1 module nxt.container.hybrid_hashmap;
2 
3 import core.internal.hash : hashOf;
4 import nxt.nullable_traits : isNullable;
5 import nxt.allocator_traits : isAllocator;
6 import nxt.my_mallocator : Mallocator;
7 
8 @safe:
9 
10 /** Hash table/map with open-addressing and hybrid storage, storing `keys` of
11 	type `K` and values of type `V`. Setting `V` to `void` makes the map turn
12 	into a set.
13 
14 	Keys are immutable except for when they are `class`es in which case they are
15 	head-const (through bin reinterpretation to `KeyValueType`), This can be
16 	overridden by setting `keyEqualPred` to, for instance, `a == b` for `class`
17 	keys.
18 
19 	Uses quadratic probing (using triangular numbers) unless `usePrimeCapacity`
20 	in which case a simpler probing is used.
21 
22 	Deletion/Removal of elements is lazy via the sentinel bitmap `_holesPtr` or
23 	through assignment of reserved value of `KeyType` when `KeyType` has
24 	hole/tombstone-sentinel-support via trait `isHoleable`. The terms
25 	"tombstone" and "sentinel" are used at, for instance,
26 	https://engineering.fb.com/2019/04/25/developer-tools/f14/ and
27 	https://www.youtube.com/watch?v=ncHmEUmJZf4&t=2837s.
28 
29 	Element iteration via
30 	- either `byKey`, `byValue` or `byKeyValue` over `HybridHashMap` and
31 	- `byElement` over `HybridHashSet`
32 	respects taking the container argument either as an l-value or r-value using
33 	detected using `auto ref`-qualified parameter introspected using `(__traits(isRef, y))`.
34 	In the r-value case no reference counting is needed.
35 	In the l-value case setting `borrowChecked` to `true` adds run-time
36 	support for dynamic Rust-style ownership and borrowing between the range and the container.
37 
38 	Params:
39 	    K = key type
40 	    V = value type
41 	    hasher = hash function or std.digest Hash
42         Allocator = memory allocator for bin array
43         borrowChecked = only activate when it's certain that this won't be moved via std.algorithm.mutation.move()
44         useSmallLinearSearch = Use linear search instead probing when `_store` is smaller than `linearSearchMaxSize`
45         usePrimeCapacity = Use prime numbers as capacity of hash table enabling better performance of simpler hash-functions
46 
47 	See_Also: https://engineering.fb.com/2019/04/25/developer-tools/f14/
48     See_Also: https://github.com/abseil/abseil-cpp/blob/master/absl/container/flat_hash_map.h
49 	See_Also: https://www.sebastiansylvan.com/post/robin-hood-hashing-should-be-your-default-hash-table-implementation/
50 	See_Also: https://arxiv.org/abs/1605.04031
51 	See_Also: https://github.com/Tessil/robin-map
52 	See_Also: https://github.com/martinus/robin-hood-hashing
53 	See_Also: https://probablydance.com/2017/02/26/i-wrote-the-fastest-hashtable/
54 	See_Also: https://en.wikipedia.org/wiki/Lazy_deletion
55 	See_Also: https://forum.dlang.org/post/ejqhcsvdyyqtntkgzgae@forum.dlang.org
56 	See_Also: https://gankro.github.io/blah/hashbrown-insert/
57 
58     TODO: Add support for opApply by copying opApply members in
59     ~/.dub/packages/emsi_containers-0.8.0/emsi_containers/src/containers/hashmap.d
60 
61 	TODO: Replace `withCapacity` with `void capacity(size_t)` like D arrays.
62 
63 	TODO: Replace remove branching on `passElementByValue` when
64 	https://github.com/dlang/dmd/pull/11000 has been merged and use `in` instead
65 
66 	TODO: Implement `foreach (const k, const v; _)` support. See
67 	https://forum.dlang.org/post/tj700o$1412$1@digitalmars.com
68 
69 	TODO: Disable pragma(inline, true) and rebenchmark
70 
71 	TODO: tests fails when `useSmallLinearSearch` is set to `false`
72 
73 	TODO: Use set of `Flag`s (defined here) as template params
74 
75 	TODO: Robin-hood case introspect key type for storage of parts of hash
76 	alongside nullValue and holeValue. Typically for address-types that doesn't
77 	need scanning. Example is `Address` for implementation of GC. This is a D
78 	showcase for code that is difficult to write in C++.
79 
80 	TODO: group `nxt.probing` functions in `Prober` struct given as type template
81 	param to `HybridHashMap`
82 
83 	TODO: Make load factor dependent on current capacity or length and perhaps
84 	also type and hash-function to get memory efficiency when it matters. Similar
85 	to what is recommended in https://ticki.github.io/blog/horrible/.
86 
87 	TODO: For copyable types replace `auto ref` with logic that only passes by
88 	`ref` when it's faster to do so. See_Also:
89 	https://github.com/dlang/dmd/pull/11000. When `-preview=in` has been made
90 	the default this complexity can be removed. Or replace `ref` with `in`.
91 
92 	TODO: Remove use of `static if (__traits(isCopyable, ...))` and `static if
93 	(__traits(isPOD, ...))` in cases where compiler can handle more moves
94 
95 	TODO: Use mmap allocator when `_store.sizeof` is larger than at least 8 pages
96 
97 	TODO: Use `StoreK` in store and cast between it and `KeyType`
98 
99 	TODO: Allocate _holesPtr array together with _store to reduce size of
100 	`HybridHashMap` to 3 words when element type doesn't support it
101 
102 	TODO: Fix bug in `growInPlaceWithCapacity` and benchmark
103 
104 	TODO: Modify existing unittest for `struct Rel { const string name; }`
105 
106 	TODO: Use allocator.dispose() instead of allocator.deallocate() as in
107 	https://github.com/dlang-community/containers
108 
109 	TODO: if hash-function is cast(size_t)(classInstance) always use prime length
110 	and shift pointer before hash based on alignof (might not be needed when
111 	module prime) to maximize memory locality when adding successively allocated
112 	pointers
113 
114 	TODO: Add extractElement that moves it out similar to
115 	http://en.cppreference.com/w/cpp/container/unordered_set/extract
116 
117 	TODO: Add merge or union algorithm here or into container/common.d. See
118 	also: http://en.cppreference.com/w/cpp/container/unordered_set/merge. this
119 	algorithm moves elements from source if they are not already in `this`
120 
121 	TODO: Robin-Hood-hashing
122 
123 	TODO: Enable `borrowChecked` unconditionally in version(debug) if and when
124 	`opPostMove` is implemented, in which case opPostMove() should assert false if this
125 	is borrowed. See: https://github.com/dlang/DIPs/pull/109
126 
127 	TODO: Save one word by making `_store.length` be inferred by
128 	`prime_modulo.primeConstants[_primeIndex]` if this is not too costly.
129 
130 	TODO: Only add one extra element to capacity when `assumeNonFullHaystack` is `true`
131 */
132 struct HybridHashMap(K, V = void,
133                    alias hasher = hashOf,
134                    string keyEqualPred = defaultKeyEqualPredOf!(K),
135                    Allocator = Mallocator,
136                    bool borrowChecked = false,
137                    bool useSmallLinearSearch = true,
138                    bool usePrimeCapacity = false)
139 if (isNullable!K /*&& !hasAliasing!K */ && isAllocator!Allocator)
140 {
141 	version(none) {				// TODO: Define and use on global scope or remove
142 		enum BorrowCheckFlag : ubyte { no, yes }
143 		enum Flag {
144 			borrowChecked = 1 << 0,
145 			useSmallLinearSearch = 1 << 1,
146 			usePrimeCapacity = 1 << 2,
147 		}
148 		import std.typecons : BitFlags;
149 		alias Flags = BitFlags!Flag;
150 	}
151 
152     // pragma(msg, K.stringof, " => ", V.stringof);
153     import core.exception : onOutOfMemoryError;
154     import core.internal.traits : hasElaborateDestructor, Unqual;
155     import core.lifetime : move;
156     import std.traits : hasIndirections, hasFunctionAttributes;
157     import std.typecons : Nullable;
158 
159     import nxt.nullable_traits : defaultNullKeyConstantOf, isNull, nullify;
160     import nxt.container.traits : mustAddGCRange;
161     import nxt.qcmeman : gc_addRange, gc_removeRange;
162 
163     static if (usePrimeCapacity)
164         import nxt.prime_modulo : PrimeIndex, ceilingPrime, moduloPrimeIndex;
165     else
166     {
167         static if (__VERSION__ >= 2098) import std.math.algebraic : nextPow2; else import std.math : nextPow2;
168         import nxt.probing : triangularProbeFromIndex, triangularProbeFromIndexIncludingHoles,
169 			triangularProbeCountFromIndex;
170         /// Setting this `true` doesn't give measurable speedups so set it to `false` for now.
171         enum bool assumeNonFullHaystack = false;
172     }
173 
174     static if (is(typeof(keyEqualPred) : string))
175     {
176         import std.functional : binaryFun;
177         alias keyEqualPredFn = binaryFun!keyEqualPred;
178     }
179     else
180         alias keyEqualPredFn = keyEqualPred;
181 
182 	/// Is true iff `T` is an array (slice).
183 	private enum isSlice(T) = is(T : const(E)[], E);
184 	/// Is true iff `T` is a pointer.
185 	private enum isPointer(T) = is(T == U*, U);
186 
187     static if ((is(K == class)) &&
188                keyEqualPred == `a is b`) // TODO: use better predicate compare?
189         alias StoreK = void*;
190     else static if (isPointer!K &&
191 					// TODO: use better predicate compare?
192 					(keyEqualPred == `a == b` ||
193 					 keyEqualPred == `a is b`))
194 		alias StoreK = void*;
195 	else
196 		alias StoreK = K;
197 
198 	/// Is `true` iff `this` is borrow-checked.
199     enum isBorrowChecked = borrowChecked;
200 
201     /** In the hash map case, `V` is non-void, and a value is stored alongside
202      * the key of type `K`.
203      */
204     enum hasValue = !is(V == void);
205 
206     /** Is `true` iff `K` is an address, in which case holes/tombstones are represented by
207      * a specific value `holeKeyConstant`.
208      */
209     enum hasAddressLikeKey = (isAddress!K || isSlice!K);
210 
211     /** Stores less than or equal to this size will be searched using linear search. */
212     private enum linearSearchMaxSize = 64; // one cache-line for now
213 
214     static if (hasAddressLikeKey)
215     {
216         enum hasHoleableKey = true;
217         enum holeKeyOffset = 0x1; // TODO: is this a good value? Or is 0xffff_ffff_ffff_ffff better?
218         @trusted enum holeKeyAddress = cast(void*)holeKeyOffset;
219 
220         /**
221          * See_Also: https://forum.dlang.org/post/p7726n$2apd$1@digitalmars.com
222          * TODO: test if ulong.max gives better performance
223          */
224         static K holeKeyConstant() @trusted pure nothrow @nogc
225         {
226             version(D_Coverage) {} else pragma(inline, true);
227             // TODO: note that cast(size_t*) will give address 0x8 instead of 0x1
228             static if (isSlice!K)
229             {
230                 alias E = typeof(K.init[0])*; // array element type
231                 auto ptr = cast(E)((cast(void*)null) + holeKeyOffset); // indicates a lazily deleted key
232                 return ptr[0 .. 0];
233             }
234             else
235                 return cast(K)((cast(void*)null) + holeKeyOffset); // indicates a lazily deleted key
236         }
237 
238         /** Returns: true iff `key` is a hole/tombstone key constant. */
239         static bool isHoleKeyConstant(const scope K key) @trusted pure nothrow @nogc
240         {
241             version(D_Coverage) {} else pragma(inline, true);
242             static if (isSlice!K) // for slices
243                 // suffice to compare pointer part
244                 return (key.ptr is holeKeyAddress);
245             else
246                 return (cast(const(void)*)key is holeKeyAddress);
247         }
248 
249         /** TODO: make these work
250          */
251         // enum K holeKey_1 = cast(K)((cast(size_t*)null));
252         // static immutable K holeKey_2 = cast(K)((cast(size_t*)null));
253     }
254     else static if (isHoleable!K)
255     {
256         enum hasHoleableKey = true;
257 		pragma(inline, true)
258         static K holeKeyConstant() @safe pure nothrow @nogc
259 			=> K.holeValue;
260         static bool isHoleKeyConstant(const scope K key) @safe pure nothrow @nogc
261         {
262             version(D_Coverage) {} else pragma(inline, true);
263             static if (__traits(hasMember, K, "isHole"))
264                 // typically faster by asserting value of member of aggregate `K`
265                 return key.isHole;
266             else
267                 return key is K.holeValue;
268         }
269     }
270     else static if (__traits(hasMember, K, "nullifier"))
271     {
272         alias Nullifier = typeof(K.init.nullifier);
273         // TODO: pragma(msg, K, " has nullifier ", Nullifier);
274         static if (isHoleable!Nullifier)
275         {
276             // TODO: pragma(msg, K, " has holeable nullifier ", Nullifier);
277             enum hasHoleableKey = true;
278             static K holeKeyConstant() @trusted pure nothrow @nogc
279             {
280                 K k;
281                 k.nullifier = Nullifier.holeValue;
282                 return k;
283             }
284             static bool isHoleKeyConstant(const scope K key) @trusted pure nothrow @nogc
285             	=> key.nullfier == Nullifier.holeValue;
286         }
287         else
288         {
289             enum hasHoleableKey = false;
290             // pragma(msg, "Need explicit hole/tombstone bitset for non-address-like key: ", K);
291             import core.bitop : bts, bt, btr;
292             import nxt.array_help : makeUninitializedBitArray, makeZeroedBitArray, makeReallocatedBitArrayZeroPadded;
293         }
294     }
295     else
296     {
297         enum hasHoleableKey = false;
298         // pragma(msg, "Need explicit hole/tombstone bitset for non-address-like key: ", K);
299         import core.bitop : bts, bt, btr;
300         import nxt.array_help : makeUninitializedBitArray, makeZeroedBitArray, makeReallocatedBitArrayZeroPadded;
301     }
302 
303     /// Element type.
304     static if (hasValue)
305     {
306         /** Map insertion status.
307          */
308         enum InsertionStatus
309         {
310             added,              ///< Element was added.
311             modified,           ///< Value of element was changed (map only).
312             unmodified          ///< Element was left unchanged.
313         }
314 
315         /// Mutable element reference with mutable constant key and value.
316         struct T
317         {
318             K key;
319             V value;
320         }
321 
322         /// Get key part of element.
323 		pragma(inline, true) static     inout(K) keyOf(SomeElement)(    scope        inout(SomeElement) element) => element.key;
324 		pragma(inline, true) static ref inout(K) keyOf(SomeElement)(ref scope return inout(SomeElement) element) => element.key;
325 
326         /// Get value part of element.
327         pragma(inline, true) static     inout(V) valueOf()(    scope        inout(T) element) => element.value;
328         pragma(inline, true) static ref inout(V) valueOf()(ref scope return inout(T) element) => element.value;
329 
330         /** Type of key stored. */
331         public alias KeyType = K;
332 
333         /** Type of value stored. */
334         public alias ValueType = V;
335 
336         enum nullKeyElement = T(defaultNullKeyConstantOf!K, V.init);
337 
338         /// Key-value element reference with head-const for `class` keys and mutable value.
339         static private struct KeyValueType
340         {
341             static if (isAddress!K) // for reference types
342             {
343                 K _key;          // no const because
344                 /** Key access is head-const. */
345                 inout(K) key() @property inout @safe pure nothrow @nogc => _key;
346             }
347             else
348                 const K key;
349             V value;
350         }
351 
352         /// Get key part.
353 		pragma(inline, true)
354         static auto ref inout(K) keyOf()(auto ref return scope inout(KeyValueType) element) @trusted
355 			=> cast(typeof(return))element.key; // needed for case: `inout(const(K)) => inout(K)`
356     }
357     else                        // HashSet
358     {
359         /** Set insertion status.
360          */
361         enum InsertionStatus
362         {
363             added,              ///< Element was added.
364             unmodified          ///< Element was left unchanged.
365         }
366 
367         alias T = K;            // short name for element type
368 
369         /// Get key part of element.
370 		pragma(inline, true)
371         static auto ref inout(SomeElement) keyOf(SomeElement)(auto ref return inout(SomeElement) element)
372 			=> element;
373 
374         enum nullKeyElement = defaultNullKeyConstantOf!K;
375     }
376 
377     /** TODO remove when https://github.com/dlang/dmd/pull/11000 has been merged
378      *
379      * @kinke: "The builtin `__argTypes` is (currently) only used/populated for Posix x64
380      * (and AArch64 for LDC), and not used by GDC at all AFAIK".
381      *
382      * See_Also: https://github.com/dlang/dmd/pull/11000#issuecomment-671103778
383      */
384     enum passElementByValue = (__traits(isCopyable, T) && // TODO: use isPOD instead?
385                                is(T U == __argTypes) &&
386                                U.length >= 1);
387 
388     /** Is `true` if an instance of `SomeKey` that can be implictly cast to `K`.
389      *
390      * For instance `const(char)[]` can be `@trusted`ly cast to `string` in a
391      * temporary scope.
392      */
393     template isScopedKeyType(SomeKey)
394     {
395         static if (is(SomeKey == class))
396             enum isScopedKeyType = (is(const(SomeKey) : const(K)));
397         else
398             enum isScopedKeyType = (is(K : SomeKey) || // `K is` implicitly convertible from `SomeKey`
399                                     is(SomeKey : U[], U) && // is array
400                                     is(typeof(K(SomeKey.init))));
401     }
402 
403     alias ElementType = T;
404 
405     /** Make with room for storing at least `minimumCapacity` number of elements.
406      *
407      * See_Also:
408      * https://forum.dlang.org/post/nyngzsaeqxzzuumivtze@forum.dlang.org
409      */
410     static typeof(this) withCapacity()(in size_t minimumCapacity) /* template-lazy */
411     {
412         static if (usePrimeCapacity)
413         {
414             PrimeIndex primeIndex;
415             immutable initialCapacity = minimumCapacity == 0 ? 1 : ceilingPrime(minimumCapacity + 1, primeIndex);
416             assert(minimumCapacity < initialCapacity); // need at least one vacancy
417             return typeof(return)(makeDefaultInitializedStoreOfCapacity(initialCapacity), primeIndex, 0);
418         }
419         else
420         {
421             immutable initialCapacity = minimumCapacity == 0 ? 1 : nextPow2(minimumCapacity);
422             assert(minimumCapacity < initialCapacity); // need at least one vacancy
423             return typeof(return)(makeDefaultInitializedStoreOfCapacity(initialCapacity), 0);
424         }
425     }
426 
427     /** Make default-initialized store with `capacity` number of slots.
428      */
429     static private T[] makeDefaultInitializedStoreOfCapacity()(in size_t capacity) @trusted pure nothrow @nogc /* template-lazy */
430     {
431         static if (usePrimeCapacity)
432         {
433             // TODO: check that capacity is prime?
434         }
435         else
436         {
437             debug import std.math : isPowerOf2;
438             debug assert(isPowerOf2(capacity)); // quadratic probing needs power of two capacity (`_store.length`)
439         }
440 
441         // TODO: cannot use makeArray here because it cannot handle uncopyable types
442         // import std.experimental.allocator : makeArray;
443         // auto store = allocator.makeArray!T(capacity, nullKeyElement);
444 
445         import nxt.bit_traits : isAllZeroBits;
446 
447         immutable byteCount = T.sizeof*capacity;
448 
449         static if (hasAddressLikeKey ||
450                    (__traits(isZeroInit, K)  &&
451                     __traits(hasMember, K, "nullifier")) ||
452                    // TODO: add check for __traits(isZeroInit, K) and member `K.nullValue` == `K.init`
453                    (__traits(hasMember, K, `nullValue`) && // if key has a null value
454                     __traits(compiles, { enum _ = isAllZeroBits!(K, K.nullValue); }) && // prevent strange error given when `K` is `knet.data.Data`
455                     isAllZeroBits!(K, K.nullValue))) // check that it's zero bits only
456         {
457             // pragma(msg, "zero-allocate:", "K:", K, " V:", V);
458             // TODO: use std.experimental.allocator.makeArray instead of this which handles clever checking for isZeroInit
459             import nxt.container.traits : makeInitZeroArray;
460             auto store = makeInitZeroArray!(T, allocator)(capacity);
461             if (store.ptr is null &&
462 				capacity >= 1)
463                 onOutOfMemoryError();
464         }
465         else                    // when default null key is not represented by zeros
466         {
467             // pragma(msg, "emplace:", "K:", K, " V:", V);
468             auto store = cast(T[])allocator.allocate(byteCount);
469             if (store.ptr is null &&
470 				byteCount >= 1)
471                 onOutOfMemoryError();
472             foreach (ref bin; store)
473             {
474                 import core.lifetime : emplace;
475                 enum hasNullValueKey = __traits(hasMember, K, `nullValue`);
476                 static if (hasNullValueKey &&
477                            !is(typeof(emplace(&keyOf(bin), K.nullValue)))) // __traits(compiles) fails here when building knet
478                     pragma(msg, __FILE__, ":", __LINE__, ":warning: emplace fails for null-Value key type ", K);
479 
480                 // initialize key
481                 static if (hasNullValueKey &&
482                            is(typeof(emplace(&keyOf(bin), K.nullValue))))
483                     emplace(&keyOf(bin), K.nullValue); // initialize in-place with explicit `K.nullValue`
484                 else
485                 {
486                     emplace(&keyOf(bin)); // initialize in-place with default value
487                     keyOf(bin).nullify(); // moveEmplace doesn't init source of type `Nullable`
488                 }
489 
490                 // initialize value
491                 static if (hasValue)
492                 {
493                     static if (hasElaborateDestructor!V)
494                         emplace(&valueOf(bin)); // initialize in-place
495                     else static if (mustAddGCRange!V)
496                         valueOf(bin) = V.init;
497                     else {}	// ok for this case to have uninitialized value part
498                 }
499             }
500         }
501 
502         static if (mustAddGCRange!T)
503             gc_addRange(store.ptr, byteCount);
504 
505         return store;
506     }
507 
508     static private T[] allocateUninitializedStore()(in size_t capacity) @trusted pure nothrow @nogc /* template-lazy */
509     {
510         immutable byteCount = T.sizeof*capacity;
511         auto store = cast(typeof(return))allocator.allocate(byteCount);
512         static if (mustAddGCRange!T)
513             gc_addRange(store.ptr, byteCount);
514         if (store.ptr is null &&
515             byteCount >= 1)
516             onOutOfMemoryError();
517         return store;
518     }
519 
520     import std.range.primitives : StdElementType = ElementType;
521     import std.traits : isIterable, isAssignable;
522 
523     /** Make with the element `element`. */
524     this(T element)
525     {
526         static if (usePrimeCapacity)
527         {
528             _primeIndex = PrimeIndex.init;
529             _store = makeDefaultInitializedStoreOfCapacity(ceilingPrime(1 + 1, _primeIndex));
530         }
531         else
532             _store = makeDefaultInitializedStoreOfCapacity(nextPow2(1));
533         _count = 0;
534         static if (__traits(isPOD, T))
535             insertWithoutGrowthNoStatus(element);
536         else
537             insertWithoutGrowthNoStatus(move(element));
538     }
539 
540     static if (hasHoleableKey)
541     {
542         private this(T[] store, in size_t count)
543         {
544             _store = store;
545             _count = count;
546         }
547     }
548     else
549     {
550         private this(T[] store, in size_t count, size_t* holesPtr = null)
551         {
552             _store = store;
553             _count = count;
554             _holesPtr = holesPtr;
555         }
556     }
557 
558     /** Make with the elements `elements`. */
559     static typeof(this) withElements(R)(R elements)
560     if (isIterable!R &&
561         isAssignable!(T, StdElementType!R))
562     {
563         import std.range.primitives : hasLength;
564         static if (hasLength!R)
565         {
566             typeof(this) that = withCapacity(elements.length);
567             foreach (ref element; elements)
568                 that.insertWithoutGrowthNoStatus(element);
569         }
570         else
571         {
572             typeof(this) that;
573             foreach (ref element; elements)
574                 that.insert(element);
575         }
576         return that;
577     }
578 
579     /// Destruct.
580     ~this() nothrow @nogc
581     {
582         release();
583     }
584 
585     /// No copying.
586     @disable this(this);
587 
588     /// Returns: a shallow duplicate of `this`.
589     typeof(this) dup()() const @trusted /* template-lazy */
590     {
591         T[] storeCopy = allocateUninitializedStore(_store.length); // unsafe
592 
593         foreach (immutable i, ref bin; _store)
594             if (isOccupiedAtIndex(i)) // normal case
595             {
596                 static if (hasValue) // map
597                 {
598                     duplicateEmplace(bin.key, storeCopy[i].key);
599                     duplicateEmplace(bin.value, storeCopy[i].value);
600                 }
601                 else            // set
602                     duplicateEmplace(bin, storeCopy[i]);
603             }
604             else
605             {
606                 import core.lifetime : emplace;
607                 emplace(&storeCopy[i]); // TODO: only emplace key and not value
608                 keyOf(storeCopy[i]).nullify();
609             }
610 
611         static if (!hasHoleableKey)
612             if (_holesPtr)
613             {
614                 immutable wordCount = holesWordCount(_store.length);
615                 auto holesPtrCopy = makeUninitializedBitArray!Allocator(_store.length);
616                 holesPtrCopy[0 .. wordCount] = _holesPtr[0 .. wordCount]; // TODO: use memcpy instead?
617 
618                 return typeof(return)(storeCopy, _count, holesPtrCopy);
619             }
620 
621         return typeof(return)(storeCopy, _count);
622     }
623 
624     /// Equality.
625     bool opEquals()(const scope auto ref typeof(this) rhs) const
626     {
627         if (_count != rhs._count)
628             return false;       // quick discardal
629 
630         foreach (immutable i, const ref bin; _store)
631             if (isOccupiedAtIndex(i))
632             {
633                 static if (hasValue)
634                 {
635                     auto valuePtr = bin.key in rhs;
636                     if (!valuePtr)
637                         return false;
638                     // TODO: make != a parameter that can also be typically !is. TODO: ask forum about this
639                     if ((*valuePtr) != bin.value)
640                         return false;
641                 }
642                 else
643                     if (!rhs.contains(bin))
644                         return false;
645             }
646 
647         return true;
648     }
649 
650     static if (true)
651     {
652     private:
653 
654         static if (!hasHoleableKey)
655         {
656 			/// Deallocate holes/tombstones.
657             void deallocateHoles() @trusted
658             {
659                 if (_holesPtr)
660                 {
661                     static if (__traits(hasMember, Allocator, "deallocatePtr"))
662                         allocator.deallocatePtr(_holesPtr);
663                     else
664                         allocator.deallocate(_holesPtr[0 .. holesWordCount(_store.length)]);
665                 }
666             }
667 
668 			/// Clear holes/tombstones.
669             static size_t* reallocateHoles(size_t[] holes, in size_t byteCount) @trusted
670             {
671                 auto rawHoles = cast(void[])holes;
672                 immutable ok = allocator.reallocate(rawHoles, byteCount);
673                 assert(ok, "couldn't reallocate holes");
674                 return cast(typeof(return))rawHoles.ptr;
675             }
676 
677 			/// Clear holes/tombstones.
678             void clearHoles()
679             {
680                 deallocateHoles();
681                 _holesPtr = null;
682             }
683 
684 			/// Number of bytes in a word.
685 			enum wordBytes = size_t.sizeof;
686 
687 			/// Number of bits in a word.
688 			enum wordBits = 8*wordBytes;
689 
690             /// Returns: number of words (`size_t`) needed to represent `binCount` holes.
691 			pragma(inline, true)
692             static size_t holesWordCount(in size_t binCount)
693 				=> (binCount / wordBits +
694 					(binCount % wordBits ? 1 : 0));
695 
696 			pragma(inline, true)
697             static size_t binBlockBytes(in size_t binCount) => wordBytes*holesWordCount(binCount);
698 
699 			/// Untag hole/tombstone at index `index`.
700             void untagHoleAtIndex(in size_t index) @trusted
701             {
702                 pragma(inline, true);
703                 version(unittest) assert(index < _store.length);
704                 if (_holesPtr !is null)
705                     btr(_holesPtr, index);
706             }
707 
708 			pragma(inline, true)
709             static bool hasHoleAtPtrIndex(const scope size_t* holesPtr, in size_t index) @trusted
710             	=> (holesPtr &&
711 					bt(holesPtr, index) != 0);
712         }
713 
714 		/// Tag hole/tombstone at index `index`.
715         void tagHoleAtIndex(in size_t index) @trusted
716         {
717             pragma(inline, true);
718             version(unittest) assert(index < _store.length);
719             static if (hasHoleableKey)
720                 keyOf(_store[index]) = holeKeyConstant;
721             else
722             {
723                 if (_holesPtr is null) // lazy allocation
724                     _holesPtr = makeZeroedBitArray!Allocator(_store.length);
725                 bts(_holesPtr, index);
726             }
727         }
728     }
729 
730     static if (borrowChecked)
731         static immutable borrowedErrorMessage = "cannot mutate this when it's borrowed";
732 
733     /// Empty.
734     void clear()()              /* template-lazy */
735     {
736         static if (borrowChecked) { debug assert(!isBorrowed, borrowedErrorMessage); }
737         release();
738         _store = typeof(_store).init;
739         static if (usePrimeCapacity)
740             _primeIndex = 0;
741         static if (!hasHoleableKey)
742             _holesPtr = null;
743         _count = 0;
744     }
745 
746     /// Release internal allocations.
747     private void release() scope
748     {
749         releaseBinElements();
750         releaseStoreAndHolesSlices();
751     }
752 
753     /// Release bin elements.
754     private void releaseBinElements() scope
755     {
756         foreach (ref bin; _store)
757         {
758             static if (hasElaborateDestructor!T)
759                 .destroy(bin);
760             else static if (mustAddGCRange!T)
761                 bin = T.init;
762         }
763     }
764 
765     /// Release store slice and holes.
766     private void releaseStoreAndHolesSlices() scope
767     {
768         releaseStoreSlice(_store);
769         static if (!hasHoleableKey)
770             deallocateHoles();
771     }
772 
773     /// Release store slice.
774     static private void releaseStoreSlice(T[] store) @trusted
775     {
776         if (store.ptr is null) { return; } // `gc_removeRange` fails for null input
777         static if (mustAddGCRange!T)
778             gc_removeRange(store.ptr); // `gc_removeRange` fails for null input
779         static if (__traits(hasMember, Allocator, "deallocatePtr"))
780             allocator.deallocatePtr(store.ptr);
781         else
782             allocator.deallocate(store);
783     }
784 
785     /// Adjust `key`.
786     private auto adjustKey(SomeKey)(const return scope SomeKey key) const scope @trusted
787     {
788         pragma(inline, true);            // must be inlined
789         static if (is(SomeKey : U[], U)) // is array (slice)
790             /* because return value is used only temporarily it's ok to cast to
791              * `immutable` to prevent GC-allocations in types such as
792              * `sso_string.SSOString` */
793             return cast(immutable(typeof(key[0]))[])key;
794         else
795             return key;
796     }
797 
798     /** Check if `element` is stored.
799      *
800      * Parameter `key` may be non-immutable, for instance const(char)[]
801      * eventhough key type `K` is `string`.
802      *
803      * Returns: `true` if element is present, `false` otherwise.
804      */
805     bool contains(SomeKey)(const scope SomeKey key) const scope @trusted /* template-lazy , `auto ref` here makes things slow */
806     if (isScopedKeyType!(typeof(key)))
807 	in(!key.isNull)
808     {
809         // pragma(msg, SomeKey.stringof ~ " " ~ K.stringof, " ", is(K : SomeKey), " ", is(SomeKey : K));
810         // debug static assert(isScopedKeyType!(typeof(key)), SomeKey.stringof ~ " " ~ K.stringof);
811         version(LDC) pragma(inline, true);
812 
813         static if (hasHoleableKey) { assert(!isHoleKeyConstant(cast(const(K))adjustKey(key))); }
814 
815         static if (useSmallLinearSearch)
816             if (_store.length * T.sizeof <= linearSearchMaxSize)
817                 return containsUsingLinearSearch(key);
818 
819         immutable hitIndex = indexOfKeyOrVacancySkippingHoles(cast(const(K))adjustKey(key)); // cast scoped `key` is @trusted
820         return (hitIndex != _store.length &&
821                 isOccupiedAtIndex(hitIndex));
822     }
823 
824     /** Check if `element` is stored.
825      *
826      * Uses linear search instead of hashing plus probing and may be faster for
827      * for small tables with complicated hash functions.
828      *
829      * Parameter `key` may be non-immutable, for instance const(char)[]
830      * eventhough key type `K` is `string`.
831      *
832      * Returns: `true` if element is present, `false` otherwise.
833      */
834     bool containsUsingLinearSearch(SomeKey)(const scope SomeKey key) const scope @trusted /* template-lazy, `auto ref` here makes things slow */
835     if (isScopedKeyType!(typeof(key)))
836     in(!key.isNull)
837     {
838         static if (hasHoleableKey) { assert(!isHoleKeyConstant(cast(const(K))adjustKey(key))); }
839         static if (isInstanceOf!(Nullable, SomeKey))
840         {
841             import std.algorithm.searching : canFind;
842             import std.traits : TemplateArgsOf;
843             alias args = TemplateArgsOf!(SomeKey);
844             debug static assert(args.length == 2,
845                           "linear search for Nullable without nullValue is slower than default `this.contains()` and is not allowed");
846             alias UnderlyingType = args[0];
847             return length >= 1 && (cast(UnderlyingType[])_store).canFind!keyEqualPredFn(key.get());
848         }
849         else
850         {
851             foreach (const ref bin; _store)
852                 if (keyEqualPredFn(keyOf(bin), key))
853                     return true;
854             return false;
855         }
856     }
857 
858     /** Check if `element` is stored. Move found element to a hole if possible.
859         Returns: `true` if element is present, `false` otherwise.
860     */
861     bool containsWithHoleMoving()(const scope K key) /* template-lazy, `auto ref` here makes things slow */
862     in(!key.isNull)
863     {
864         version(LDC) pragma(inline, true);
865 
866         static if (hasHoleableKey) { assert(!isHoleKeyConstant(key)); }
867         static if (borrowChecked) { debug assert(!isBorrowed, borrowedErrorMessage); }
868 
869         immutable hitIndex = indexOfKeyOrVacancySkippingHoles(adjustKey(key));
870         // TODO: update holes
871         return (hitIndex != _store.length &&
872                 isOccupiedAtIndex(hitIndex));
873     }
874 
875     /** Insert `element`, being either a key-value (map-case) or a just a key
876      * (set-case).
877      *
878      * If `element` is a nullable type and it is null an `AssertError` is thrown.
879      */
880     InsertionStatus insert()(const T element) @trusted /* template-lazy. need `T` to be `const` in `class` case */
881 	in(!keyOf(element).isNull)
882     {
883         version(LDC) pragma(inline, true);
884 
885         static if (hasHoleableKey) { debug assert(!isHoleKeyConstant(keyOf(element))); }
886         static if (borrowChecked) { debug assert(!isBorrowed, borrowedErrorMessage); }
887 
888         reserveExtra(1);
889         size_t hitIndex = 0;
890         static if (__traits(isPOD, T))
891             return insertWithoutGrowth(element, hitIndex);
892         else
893             return insertWithoutGrowth(move(*cast(T*)&element), hitIndex);
894     }
895 
896     /** Insert `element`, being either a key-value (map-case) or a just a key
897      * (set-case).
898      *
899      * If `element` is a nullable type and it is null an `AssertError` is thrown.
900      *
901      * Returns: reference to existing element if present, otherwise new `element`.
902      *
903      * Can be used for implementing, for instance, caching of typically strings.
904      */
905     ref T insertAndReturnElement(SomeElement)(scope SomeElement element) return /* template-lazy */
906     in(!keyOf(element).isNull)
907     {
908         version(LDC) pragma(inline, true);
909 
910         static if (hasHoleableKey) { debug assert(!isHoleKeyConstant(cast(K)adjustKey(keyOf(element)))); }
911         static if (borrowChecked) { debug assert(!isBorrowed, borrowedErrorMessage); }
912 
913         reserveExtra(1);
914         static if (__traits(isPOD, SomeElement))
915             immutable hitIndex = insertWithoutGrowthNoStatus(element);
916         else
917             immutable hitIndex = insertWithoutGrowthNoStatus(move(element));
918         return _store[hitIndex];
919     }
920 
921     /** Insert `elements`, all being either a key-value (map-case) or a just a key (set-case).
922      */
923     void insertN(R)(R elements) @trusted
924     if (isIterable!R &&
925         __traits(isCopyable, T))           // TODO: support uncopyable T?
926     {
927         static if (borrowChecked) { debug assert(!isBorrowed, borrowedErrorMessage); }
928         import std.range.primitives : hasLength;
929         static if (hasLength!R)
930             reserveExtra(elements.length); // might create unused space in `_store` store
931         foreach (ref element; elements)
932         {
933             static if (!hasLength!R)
934                 reserveExtra(1);
935             static if (hasIndirections!T)
936                 insertWithoutGrowthNoStatus(element);
937             else
938                 insertWithoutGrowthNoStatus(*cast(Unqual!T*)&element);
939         }
940     }
941 
942     /// Is `true` iff in-place rehashing during growth should be performed.
943     enum bool growInPlaceFlag = false; // TODO: warning `growInPlaceWithCapacity` is buggy
944 
945     /// Numerator for grow scale.
946     enum growScaleP = 3;
947     /// Denominator for grow scale.
948     enum growScaleQ = 2;
949 
950     /** Reserve rom for `extraCapacity` number of extra buckets. */
951     void reserveExtra(in size_t extraCapacity) // not template-lazy
952     {
953         version(LDC) pragma(inline, true);
954         static if (borrowChecked) { debug assert(!isBorrowed, borrowedErrorMessage); }
955         immutable newCapacity = (_count + extraCapacity)*growScaleP/growScaleQ;
956         if (newCapacity > _store.length)
957             growWithNewCapacity(newCapacity);
958     }
959 
960     /// Grow (rehash) to make for `newCapacity` number of elements.
961     private void growWithNewCapacity()(in size_t newCapacity) /* template-lazy */
962     {
963         version(unittest) assert(newCapacity > _store.length);
964         static if (__traits(hasMember, Allocator, "reallocate"))
965         {
966             static if (growInPlaceFlag)
967                 growInPlaceWithCapacity(newCapacity);
968             else
969                 growStandardWithNewCapacity(newCapacity);
970         }
971         else
972             growStandardWithNewCapacity(newCapacity);
973     }
974 
975     /// Grow (rehash) store to make room for `newCapacity` number of elements.
976     private void growStandardWithNewCapacity()(in size_t newCapacity) /* template-lazy */
977     {
978         version(unittest) assert(newCapacity > _store.length);
979         auto next = typeof(this).withCapacity(newCapacity);
980         foreach (immutable i, ref bin; _store)
981             if (isOccupiedAtIndex(i))
982             {
983                 next.insertMoveWithoutGrowth(bin); // value is zeroed but
984                 static if (!hasHoleableKey)
985                     keyOf(bin).nullify(); // keyC must zeroed
986             }
987         move(next, this);
988     }
989 
990 	/// Tag as lazily delete element at index `index`.`
991     private void tagAsLazilyDeletedElementAtIndex(in size_t index)
992     {
993         version(LDC) pragma(inline, true);
994 
995         // key
996         static if (useSmallLinearSearch)
997             if (_store.length * T.sizeof <= linearSearchMaxSize)
998             {
999                 keyOf(_store[index]).nullify();
1000                 goto done;
1001             }
1002 
1003         static if (hasHoleableKey)
1004             keyOf(_store[index]) = holeKeyConstant;
1005         else
1006         {
1007             keyOf(_store[index]).nullify();
1008             tagHoleAtIndex(index);
1009         }
1010 
1011     done:
1012 
1013         // value
1014         static if (hasValue)
1015         {
1016             static if (hasElaborateDestructor!V) // if we should clear all
1017                 .destroy(valueOf(_store[index]));
1018             static if (mustAddGCRange!V) // if we should clear all
1019                 valueOf(_store[index]) = V.init; // avoid GC mark-phase dereference
1020         }
1021     }
1022 
1023 	/// Insert `element` at `index`.
1024     private void insertElementAtIndex(SomeElement)(scope SomeElement element, in size_t index) @trusted /* template-lazy */
1025     {
1026         version(LDC) pragma(inline, true);
1027         static if (isSlice!SomeElement &&
1028                    !is(typeof(SomeElement.init[0]) == immutable))
1029         {
1030             /* key is an array of non-`immutable` elements which cannot safely
1031              * be stored because keys must be immutable for hashing to work
1032              * properly, therefore duplicate */
1033             keyOf(_store[index]) = element.idup;
1034         }
1035         else
1036         {
1037             static if (__traits(isPOD, SomeElement))
1038                 _store[index] = element;
1039             else
1040             {
1041                 static if (__traits(isPOD, K))
1042                     keyOf(_store[index]) = keyOf(element);
1043                 else
1044                     move(keyOf(element),
1045                          keyOf(_store[index]));
1046 
1047                 static if (hasValue)
1048                 {
1049                     import core.lifetime : moveEmplace;
1050                     moveEmplace(valueOf(element),
1051                                 valueOf(_store[index]));
1052                 }
1053             }
1054         }
1055     }
1056 
1057     /// Rehash elements in-place.
1058     private void rehashInPlace()() @trusted /* template-lazy */
1059     {
1060         import core.bitop : bts, bt;
1061         import nxt.array_help : makeZeroedBitArray, wordCountOfBitCount;
1062 
1063         size_t* dones = makeZeroedBitArray!Allocator(_store.length);
1064 
1065         foreach (immutable doneIndex; 0 .. _store.length)
1066         {
1067             if (bt(dones, doneIndex)) { continue; } // if _store[doneIndex] continue
1068             if (isOccupiedAtIndex(doneIndex))
1069             {
1070                 import core.lifetime : moveEmplace;
1071                 T currentElement = void;
1072 
1073                 // TODO: functionize:
1074                 moveEmplace(_store[doneIndex], currentElement);
1075                 static if (isInstanceOf!(Nullable, K))
1076                     keyOf(_store[doneIndex]).nullify(); // `moveEmplace` doesn't init source of type Nullable
1077 
1078                 while (true)
1079                 {
1080                     // TODO remove param `element`
1081                     static if (passElementByValue)
1082                         alias pred = (immutable scope index,
1083                                       const scope element) => (!isOccupiedAtIndex(index) || // free slot
1084                                                                !bt(dones, index)); // or a not yet replaced element
1085                     else
1086                         alias pred = (immutable scope index,
1087                                       const scope ref element) => (!isOccupiedAtIndex(index) || // free slot
1088                                                                    !bt(dones, index)); // or a not yet replaced element
1089                     static if (usePrimeCapacity)
1090                         immutable hitIndex = xxx;
1091                     else
1092                         immutable hitIndex = _store[].triangularProbeFromIndex!(pred, assumeNonFullHaystack)(keyToIndex(keyOf(currentElement)));
1093                     assert(hitIndex != _store.length, "no free slot");
1094 
1095                     bts(dones, hitIndex); // _store[hitIndex] will be at it's correct position
1096 
1097                     if (isOccupiedAtIndex(doneIndex))
1098                     {
1099                         T nextElement = void;
1100 
1101                         // TODO: functionize:
1102                         moveEmplace(_store[hitIndex], nextElement); // save non-free slot
1103                         static if (isInstanceOf!(Nullable, K))
1104                             keyOf(_store[hitIndex]).nullify(); // `moveEmplace` doesn't init source of type Nullable
1105 
1106                         moveEmplace(currentElement, _store[hitIndex]);
1107                         moveEmplace(nextElement, currentElement);
1108                     }
1109                     else // if no free slot
1110                     {
1111                         moveEmplace(currentElement, _store[hitIndex]);
1112                         break; // inner iteration is finished
1113                     }
1114                 }
1115             }
1116             bts(dones, doneIndex); // _store[doneIndex] is at it's correct position
1117         }
1118 
1119         allocator.deallocate(cast(void[])(dones[0 .. wordCountOfBitCount(_store.length)]));
1120 
1121         static if (!hasHoleableKey)
1122             clearHoles();
1123     }
1124 
1125     /** Grow (with rehash) store in-place making room for `minimumCapacity` number of elements. */
1126     private void growInPlaceWithCapacity()(in size_t minimumCapacity) @trusted /* template-lazy */
1127     {
1128         assert(minimumCapacity > _store.length);
1129 
1130         static if (usePrimeCapacity)
1131             immutable newCapacity = ceilingPrime(minimumCapacity, _primeIndex);
1132         else
1133             immutable newCapacity = nextPow2(minimumCapacity);
1134 
1135         immutable newByteCount = T.sizeof*newCapacity;
1136 
1137         const oldStorePtr = _store.ptr;
1138         immutable oldLength = _store.length;
1139 
1140         auto rawStore = cast(void[])_store;
1141         if (allocator.reallocate(rawStore, newByteCount))
1142         {
1143             _store = cast(T[])rawStore;
1144             static if (mustAddGCRange!T)
1145             {
1146                 if (oldStorePtr !is null)
1147                     gc_removeRange(oldStorePtr); // `gc_removeRange` fails for null input
1148                 gc_addRange(_store.ptr, newByteCount);
1149             }
1150 
1151             static if (!hasHoleableKey)
1152                 if (_holesPtr)
1153                     _holesPtr = makeReallocatedBitArrayZeroPadded!Allocator(_holesPtr,
1154                                                                             oldLength,
1155                                                                             _store.length);
1156 
1157             // TODO: make this an array operation `nullifyAll` or `nullifyN`
1158             foreach (ref bin; _store[oldLength .. newCapacity])
1159                 keyOf(bin).nullify(); // move this `init` to reallocate() above?
1160 
1161             rehashInPlace();
1162         }
1163         else
1164             assert(0, "couldn't reallocate bin");
1165     }
1166 
1167     /** Insert (without growth) `element` at `hitIndex`. */
1168     private InsertionStatus insertWithoutGrowth(SomeElement)(const scope SomeElement element, /* template-lazy */
1169                                                              out size_t hitIndex) @trusted
1170     {
1171         version(LDC) pragma(inline, true);
1172         version(unittest)
1173         {
1174             assert(!keyOf(element).isNull);
1175             static if (hasHoleableKey) { assert(!isHoleKeyConstant(adjustKey(keyOf(element)))); }
1176         }
1177 
1178         size_t holeIndex = size_t.max; // first hole index to written to if hole found
1179         immutable hitIndexPrel = indexOfKeyOrVacancyAndFirstHole(keyOf(element), holeIndex);
1180         if (hitIndexPrel == _store.length || // keys miss and holes may have filled all empty slots
1181             keyOf(_store[hitIndexPrel]).isNull) // just key miss but a hole may have been found on the way
1182         {
1183             immutable hasHole = holeIndex != size_t.max; // hole was found along the way
1184 
1185             if (hasHole)
1186                 hitIndex = holeIndex; // pick hole instead
1187             else
1188                 hitIndex = hitIndexPrel; // normal hit
1189 
1190             version(unittest) assert(hitIndex != _store.length, "no null or hole slot");
1191 
1192             static if (__traits(isPOD, SomeElement))
1193                 insertElementAtIndex(*cast(SomeElement*)&element, hitIndex);
1194             else
1195                 insertElementAtIndex(move(*cast(SomeElement*)&element), hitIndex);
1196 
1197             static if (!hasHoleableKey)
1198                 if (hasHole)
1199                     untagHoleAtIndex(hitIndex);
1200 
1201             _count = _count + 1;
1202             return InsertionStatus.added;
1203         }
1204         else
1205             hitIndex = hitIndexPrel;
1206 
1207         static if (hasValue)
1208         {
1209             static if (__traits(isStaticArray, V))
1210                 // identity comparison of static arrays implicitly coerces them
1211                 // to slices, which are compared by reference, so don't use !is here
1212                 immutable valueDiffers = (valueOf(element) !=
1213                                           valueOf(_store[hitIndexPrel])); // only value changed
1214             else
1215                 immutable valueDiffers = (valueOf(element) !is
1216                                           valueOf(_store[hitIndexPrel])); // only value changed
1217 
1218             if (valueDiffers) // only value changed
1219             {
1220                 move(valueOf(*cast(SomeElement*)&element),
1221                      valueOf(_store[hitIndexPrel])); // value is defined so overwrite it
1222                 return InsertionStatus.modified;
1223             }
1224         }
1225         return InsertionStatus.unmodified;
1226     }
1227 
1228     /** Insert (without growth) `element` and return index to bin where insertion happended. */
1229     private size_t insertWithoutGrowthNoStatus(SomeElement)(const scope SomeElement element) @trusted /* template-lazy */
1230     {
1231         version(LDC) pragma(inline, true);
1232         version(unittest)
1233         {
1234             assert(!keyOf(element).isNull);
1235             static if (hasHoleableKey) { assert(!isHoleKeyConstant(adjustKey(keyOf(element)))); }
1236         }
1237 
1238         size_t hitIndex = 0;
1239         size_t holeIndex = size_t.max; // first hole index to written to if hole found
1240         immutable hitIndexPrel = indexOfKeyOrVacancyAndFirstHole(adjustKey(keyOf(element)), holeIndex);
1241         if (hitIndexPrel == _store.length || // keys miss and holes may have filled all empty slots
1242             keyOf(_store[hitIndexPrel]).isNull) // just key miss but a hole may have been found on the way
1243         {
1244             immutable hasHole = holeIndex != size_t.max; // hole was found along the way
1245             if (hasHole)
1246                 hitIndex = holeIndex; // pick hole instead
1247             else
1248                 hitIndex = hitIndexPrel; // normal hit
1249 
1250             version(unittest) assert(hitIndex != _store.length, "no null or hole slot");
1251 
1252             static if (__traits(isPOD, SomeElement))
1253                 insertElementAtIndex(*cast(SomeElement*)&element, hitIndex);
1254             else
1255                 insertElementAtIndex(move(*cast(SomeElement*)&element), hitIndex);
1256 
1257             static if (!hasHoleableKey)
1258                 if (hasHole) { untagHoleAtIndex(hitIndex); }
1259 
1260             _count = _count + 1;
1261             return hitIndex;
1262         }
1263         else
1264             hitIndex = hitIndexPrel;
1265 
1266         static if (hasValue)
1267             // modify existing value
1268             move(valueOf(*cast(SomeElement*)&element),
1269                  valueOf(_store[hitIndexPrel])); // value is defined so overwrite it
1270 
1271         return hitIndex;
1272     }
1273 
1274     /** Insert `element`, being either a key-value (map-case) or a just a key (set-case).
1275      */
1276     private InsertionStatus insertMoveWithoutGrowth()(ref T element) /* template-lazy */
1277     {
1278         version(LDC) pragma(inline, true);
1279         size_t hitIndex = 0;
1280         return insertWithoutGrowth(move(element), hitIndex);
1281     }
1282 
1283     static if (hasValue)
1284     {
1285         /** Insert or replace `value` at `key`. */
1286         InsertionStatus insert()(K key, V value) /* template-lazy */
1287         {
1288             pragma(inline, true); // LDC must have this
1289             static if (__traits(isPOD, K))
1290             {
1291                 static if (__traits(isPOD, V))
1292                     return insert(T(key, value));
1293                 else
1294                     return insert(T(key, move(value)));
1295             }
1296             else
1297             {
1298                 static if (__traits(isPOD, V))
1299                     return insert(T(move(key), value));
1300                 else
1301                     return insert(T(move(key), move(value)));
1302             }
1303         }
1304     }
1305 
1306     static if (!hasValue)       // HashSet
1307     {
1308         scope const(K)* opBinaryRight(string op, SomeKey)(const scope SomeKey key) const return @trusted
1309         if (op == `in` &&
1310             isScopedKeyType!(typeof(key)))
1311         in(!key.isNull)
1312         {
1313             version(D_Coverage) {} else pragma(inline, true);
1314             static if (hasHoleableKey) { assert(!isHoleKeyConstant(cast(K)adjustKey(key))); }
1315             immutable hitIndex = indexOfKeyOrVacancySkippingHoles(adjustKey(key)); // cast scoped `key` is @trusted
1316 			immutable match = hitIndex != _store.length && isOccupiedAtIndex(hitIndex);
1317             if (match)
1318                 return &_store[hitIndex];
1319             else
1320                 return null;
1321         }
1322 
1323         ref typeof(this) opOpAssign(string op, SomeKey)(const scope SomeKey key) return @trusted
1324         if (op == `~` &&        // binary assignment operator `~=`
1325             isScopedKeyType!(typeof(key)))
1326         {
1327             version(LDC) pragma(inline, true);
1328             reserveExtra(1);
1329             immutable hitIndex = insertWithoutGrowthNoStatus(key);
1330             return this;
1331         }
1332 
1333         /** Try to retrieve `class`-element of type `Class` constructed with
1334          * parameters `params`.
1335          *
1336          * Typically used to implement (polymorphic) caching of class-types
1337          * without the need for GG-allocating a temporary instance of a
1338          * `class`-element potentially already stored in `this` set.
1339          *
1340          * Polymorphic caching can be realized by setting `hasher` to
1341          * `hash_functions.hashOfPolymorphic`.
1342          */
1343         scope const(Class) tryGetElementFromCtorParams(Class, Params...)(scope Params params) const return @trusted
1344         if (is(Class : K))
1345         {
1346             import core.lifetime : emplace;
1347             void[__traits(classInstanceSize, Class)] tempNode_ = void;
1348             scope Class temp = emplace!(Class)(tempNode_, params);
1349             Class* hit = cast(Class*)(temp in this);
1350 
1351             static if (__traits(hasMember, Class, "__dtor"))
1352                 temp.__dtor();
1353 
1354             if (hit)
1355             {
1356                 auto typedHit = cast(typeof(return))*hit;
1357                 assert(typedHit, "Expected class " ~ Class.stringof ~ " but got hit was of other type"); // TODO: give warning or throw
1358                 return typedHit;
1359             }
1360             return null;
1361         }
1362     }
1363 
1364     static if (hasValue)        // HashMap
1365     {
1366         scope inout(V)* opBinaryRight(string op, SomeKey)(const scope SomeKey key) inout return @trusted // `auto ref` here makes things slow
1367         if (op == `in` &&
1368             isScopedKeyType!(SomeKey))
1369         {
1370             version(LDC) pragma(inline, true);
1371             // pragma(msg, SomeKey, " => ", K);
1372             immutable hitIndex = indexOfKeyOrVacancySkippingHoles(cast(const(K))adjustKey(key)); // cast scoped `key` is @trusted
1373 			immutable match = hitIndex != _store.length && isOccupiedAtIndex(hitIndex);
1374             if (match)
1375                 return cast(typeof(return))&_store[hitIndex].value;
1376             else
1377                 return null;
1378         }
1379 
1380         /// Indexing.
1381         scope ref inout(V) opIndex(SomeKey)(const scope SomeKey key) inout return @trusted // `auto ref` here makes things slow.
1382         if (isScopedKeyType!(typeof(key)))
1383         {
1384             import core.exception : onRangeError;
1385             version(LDC) pragma(inline, true);
1386             immutable hitIndex = indexOfKeyOrVacancySkippingHoles(adjustKey(key)); // cast scoped `key` is @trusted
1387 			immutable match = hitIndex != _store.length && isOccupiedAtIndex(hitIndex);
1388             if (!match)
1389 				onRangeError();
1390             return _store[hitIndex].value;
1391         }
1392 
1393         /** Get value of `key` or `defaultValue` if `key` not present (and
1394          * therefore `nothrow`).
1395          *
1396          * Returns: value reference iff `defaultValue` is an l-value.
1397          *
1398          * TODO: make `defaultValue` `lazy` when that can be `nothrow`
1399          */
1400         auto ref inout(V) get()(const scope K key, /* template-lazy */
1401                                 auto ref inout(V) defaultValue) inout
1402         {
1403             version(LDC) pragma(inline, true);
1404             if (auto valuePtr = key in this)
1405                 return *valuePtr;
1406             else
1407                 return defaultValue;
1408         }
1409 
1410         /** Get reference to `key`-part of stored element at `key`, if present,
1411          * otherwise return `defaultKey`.
1412          *
1413          * Used to implement caching inside the key part of a map.
1414          */
1415         ref const(K) getKeyRef(SomeKey)(const scope SomeKey key, /* template-lazy */
1416                                         ref const(K) defaultKey) const return @trusted @nogc
1417         if (isScopedKeyType!(SomeKey))
1418         {
1419             version(LDC) pragma(inline, true);
1420             immutable hitIndex = indexOfKeyOrVacancySkippingHoles(adjustKey(key)); // cast scoped `key` is @trusted
1421 			immutable match = hitIndex != _store.length && isOccupiedAtIndex(hitIndex);
1422             if (match)
1423                 return _store[hitIndex].key;
1424             return defaultKey;
1425         }
1426 
1427         /** Supports the syntax `aa[key] = value;`.
1428          */
1429         ref V opIndexAssign()(V value, K key) /* template-lazy. TODO: return scope */
1430         in(!key.isNull)
1431         {
1432             version(LDC) pragma(inline, true);
1433 
1434             static if (hasHoleableKey) { debug assert(!isHoleKeyConstant(key)); }
1435             static if (borrowChecked) { debug assert(!isBorrowed, borrowedErrorMessage); }
1436 
1437             reserveExtra(1);
1438 
1439             static if (__traits(isPOD, K))
1440             {
1441                 static if (__traits(isPOD, V))
1442                     immutable hitIndex = insertWithoutGrowthNoStatus(T(key, value));
1443                 else
1444                     immutable hitIndex = insertWithoutGrowthNoStatus(T(key, move(value)));
1445             }
1446             else
1447             {
1448                 static if (__traits(isPOD, V))
1449                     immutable hitIndex = insertWithoutGrowthNoStatus(T(move(key), value));
1450                 else
1451                     immutable hitIndex = insertWithoutGrowthNoStatus(T(move(key), move(value)));
1452             }
1453 
1454             return _store[hitIndex].value;
1455         }
1456 
1457         ref V opIndexOpAssign(string op, Rhs)(Rhs rhs, K key) // TODO: return scope
1458         // if (true)               // TODO: pre-check that mixin will work
1459         in(!key.isNull)
1460         {
1461             // pragma(msg, "opIndexOpAssign: Key:", K, " Value:", V, " Rhs:", Rhs, " op:", op);
1462             static if (hasHoleableKey) { debug assert(!isHoleKeyConstant(key)); }
1463             static if (borrowChecked) { debug assert(!isBorrowed, borrowedErrorMessage); }
1464 
1465             reserveExtra(1);
1466 
1467             size_t holeIndex = size_t.max; // first hole index to written to if hole found
1468             immutable hitIndex = indexOfKeyOrVacancyAndFirstHole(key, holeIndex);
1469             if (hitIndex == _store.length || // keys miss and holes may have filled all empty slots
1470                 keyOf(_store[hitIndex]).isNull) // just key miss but a hole may have been found on the way
1471             {
1472                 immutable hasHole = holeIndex != size_t.max; // hole was found along the way
1473                 immutable index = (hasHole ?
1474                                holeIndex : // pick hole instead
1475                                hitIndex); // normal hit
1476                 version(unittest) assert(index != _store.length, "no null or hole slot");
1477                 static if (__traits(isCopyable, K))
1478                 {
1479                     static if (op == "~" ||
1480                                op == "+" ||
1481                                op == "*")
1482                     {
1483                         static if (is(V : Rhs[])) // isDynamicArray of `Rhs`
1484                             insertElementAtIndex(T(key, [rhs]), // TODO: if `V(rhs)` is not supported use `V.init` followed by `OP= rhs`
1485                                                  index);
1486                         else
1487                             // dbg("opIndexOpAssign-new: k:", key, " rhs:", rhs);
1488                             insertElementAtIndex(T(key, V(rhs)), // TODO: if `V(rhs)` is not supported use `V.init` followed by `OP= rhs`
1489                                                  index);
1490                     }
1491                     else
1492                         static assert(0, "Handel op " ~ op);
1493                 }
1494                 else
1495                     static assert(0, "Handle uncopyable key " ~ K.stringof);
1496                     // insertElementAtIndex(move(*cast(SomeElement*)&element), index);
1497 
1498                 static if (!hasHoleableKey)
1499                     if (hasHole) { untagHoleAtIndex(index); }
1500 
1501                 _count = _count + 1;
1502                 return _store[index].value;
1503             }
1504             else                // `key`-hit at index `hitIndex`
1505             {
1506                 // dbg("opIndexOpAssign-mod: k:", key, " rhs:", rhs);
1507                 mixin(`return _store[hitIndex].value ` ~ op ~ `= rhs;`); // modify existing value
1508             }
1509         }
1510     }
1511 
1512     /** Remove `element`.
1513      * Returns: `true` if element was removed, `false` otherwise.
1514      */
1515     bool remove(SomeKey)(const scope SomeKey key) scope /* template-lazy */
1516     if (isScopedKeyType!(typeof(key)))
1517     {
1518         version(LDC) pragma(inline, true);
1519         static if (borrowChecked) { debug assert(!isBorrowed, borrowedErrorMessage); }
1520 
1521         static if (useSmallLinearSearch)
1522             if (_store.length * T.sizeof <= linearSearchMaxSize)
1523             {
1524                 foreach (immutable i, const ref element; _store) // linear search is faster for small arrays
1525                     if (keyEqualPredFn(keyOf(element), key))
1526                     {
1527                         tagAsLazilyDeletedElementAtIndex(i);
1528                         _count = _count - 1;
1529                         return true;
1530                     }
1531                 return false;
1532             }
1533 
1534         immutable hitIndex = indexOfKeyOrVacancySkippingHoles(cast(const(K))adjustKey(key));
1535 		immutable match = hitIndex != _store.length && isOccupiedAtIndex(hitIndex);
1536         if (match)
1537         {
1538             tagAsLazilyDeletedElementAtIndex(hitIndex);
1539             _count = _count - 1;
1540             return true;
1541         }
1542         return false;
1543     }
1544 
1545     /** Remove all elements matching `keys` followed by a rehash.
1546      *
1547      * Returns: number of elements that were removed.
1548      */
1549     version(none)
1550     {
1551         import nxt.traits_ex : isRefIterable;
1552         import std.range.primitives : front;
1553 
1554         size_t rehashingRemoveN(Keys)(const scope Keys keys) /* template-lazy */
1555         if (isRefIterable!Keys &&
1556             is(typeof(Keys.front == K.init)))
1557         {
1558             static if (borrowChecked) { debug assert(!isBorrowed, borrowedErrorMessage); }
1559             rehash!("!a.isNull && keys.canFind(a)")(); // TODO: make this work
1560             return 0;
1561         }
1562     }
1563 
1564     /// Check if empty.
1565     bool empty() const @property => _count == 0;
1566 
1567     /// Get length (read-only).
1568     @property size_t length() const => _count;
1569 
1570     /// Get bin count.
1571     @property size_t binCount() const => _store.length;
1572 
1573     /** Returns: get total probe count for all elements stored. */
1574     size_t totalProbeCount()() const /* template-lazy */
1575     {
1576         static if (hasValue)
1577             auto range = byKeyValue(this);
1578         else
1579             auto range = byElement(this);
1580 
1581         typeof(return) totalCount = 0;
1582 
1583         foreach (const ref currentElement; range)
1584         {
1585             static if (__traits(isCopyable, T)) // TODO why does using `passElementByValue` fail as an expression for certain element types?
1586                 /* don't use `auto ref` for copyable `T`'s to prevent
1587                  * massive performance drop for small elements when compiled
1588                  * with LDC. TODO: remove when LDC is fixed. */
1589                 alias pred = (const scope element) => (keyEqualPredFn(keyOf(element),
1590                                                                       keyOf(currentElement)));
1591             else
1592                 alias pred = (const scope ref element) => (keyEqualPredFn(keyOf(element),
1593                                                                           keyOf(currentElement)));
1594 
1595             static if (usePrimeCapacity)
1596                 immutable probeCount = xxx;
1597             else
1598                 immutable probeCount = triangularProbeCountFromIndex!(pred)(_store[], keyToIndex(keyOf(currentElement)));
1599 
1600             totalCount += probeCount;
1601         }
1602         return totalCount;
1603     }
1604 
1605     /** Returns: average probe count for all elements stored. */
1606     double averageProbeCount()() const /* template-lazy */
1607 		=> (cast(typeof(return))totalProbeCount)/length;
1608 
1609     /** Unsafe access to raw store.
1610      *
1611      * Needed by wrapper containers such as `SSOHybridHashSet`.
1612      */
1613 	pragma(inline, true)
1614     inout(T)[] rawStore() inout @system pure nothrow @nogc => _store;
1615 
1616     static if (hasHoleableKey)
1617     {
1618         static bool isOccupiedBin(const ref T bin)
1619 			=> (keyOf(bin).isNull &&
1620 				!isHoleKeyConstant(keyOf(bin)));
1621     }
1622 
1623 private:
1624     import nxt.allocator_traits : AllocatorState;
1625     mixin AllocatorState!Allocator; // put first as emsi-containers do
1626 
1627     static if (hasFunctionAttributes!(allocator.allocate, "@nogc"))
1628     {
1629         import nxt.gc_traits : NoGc;
1630         @NoGc T[] _store;        // one element per bin
1631     }
1632     else
1633         T[] _store;              // one element per bin
1634 
1635     static if (usePrimeCapacity)
1636         PrimeIndex _primeIndex = PrimeIndex.init;
1637 
1638     static if (!hasHoleableKey)
1639     {
1640         static if (hasFunctionAttributes!(allocator.allocate, "@nogc"))
1641             @NoGc size_t* _holesPtr; // bit array describing which bin elements that has been removed (holes)
1642         else
1643             size_t* _holesPtr; // bit array describing which bin elements that has been removed (holes)
1644     }
1645 
1646     static if (borrowChecked)
1647     {
1648         debug // use Rust-style borrow checking at run-time
1649         {
1650             size_t _count;
1651             size_t _borrowCount;
1652 
1653             /// Number of bits needed to store number of read borrows.
1654             enum borrowCountBits = 8*borrowChecked.sizeof;
1655 
1656             /// Maximum value possible for `_borrowCount`.
1657             enum borrowCountMax = 2^^borrowCountBits - 1;
1658 
1659             version(none)
1660             {
1661                 /// Number of bits needed to store number of read borrows.
1662                 enum borrowCountBits = 24;
1663 
1664                 /// Maximum value possible for `_borrowCount`.
1665                 enum borrowCountMax = 2^^borrowCountBits - 1;
1666 
1667                 import std.bitmanip : bitfields;
1668                 mixin(bitfields!(size_t, "_count", 8*size_t.sizeof - borrowCountBits,
1669                                  uint, "_borrowCount", borrowCountBits,
1670                           ));
1671             }
1672 
1673             pragma(inline, true):
1674             @safe pure nothrow @nogc:
1675 
1676             @property
1677             {
1678                 /// Returns: `true` iff `this` is borrowed (either read or write).
1679                 bool isBorrowed() const => _borrowCount >= 1;
1680 
1681                 /// Returns: number of borrowers of `this` (both read and write).
1682                 auto borrowCount() const => _borrowCount;
1683             }
1684 
1685             /// Increase borrow count.
1686             void incBorrowCount()
1687 			in(_borrowCount + 1 != borrowCountMax)
1688             {
1689                 _borrowCount = _borrowCount + 1;
1690             }
1691 
1692             /// Decrease borrow count.
1693             void decBorrowCount()
1694 			in(_borrowCount != 0)
1695             {
1696                 _borrowCount = _borrowCount - 1;
1697             }
1698         }
1699         else
1700         {
1701             size_t _count;        // total number of non-null elements stored in `_store`
1702         }
1703     }
1704     else
1705     {
1706         size_t _count;        // total number of non-null elements stored in `_store`
1707     }
1708 
1709     /** Returns: bin index of `key`. */
1710     private size_t keyToIndex(SomeKey)(const scope SomeKey key) const @trusted
1711     {
1712         version(LDC) pragma(inline, true); // TODO: inline always
1713 
1714         /** Returns: current index mask from bin count `_store.length`.
1715 		 * TODO: Inline this and check for speed-up.
1716 		 */
1717         static size_t powerOf2Mask(in size_t length) @safe pure nothrow @nogc
1718         {
1719             version(unittest)
1720             {
1721 				// TODO: move to in contract:
1722                 debug import std.math : isPowerOf2;
1723                 debug assert(isPowerOf2(length));
1724             }
1725 			else
1726 			{
1727 				version(D_Coverage) {} else pragma(inline, true);
1728 			}
1729             return length - 1;
1730         }
1731 
1732         static if (is(typeof(hasher(key)) == hash_t)) // for instance when hasher being `hashOf`
1733             immutable hash = hasher(key);
1734         else static if (is(hasher == struct) || // such as `FNV`
1735                         is(hasher == class))
1736         {
1737             import nxt.digestion : hashOf2;
1738             immutable hash = hashOf2!(hasher)(key);
1739         }
1740         else
1741             static assert(false, "Unsupported hasher of type " ~ typeof(hasher).stringof);
1742 
1743         static if (usePrimeCapacity)
1744             return moduloPrimeIndex(hash, _primeIndex);
1745         else
1746             return hash & powerOf2Mask(_store.length);
1747     }
1748 
1749     /** Find index to `key` if it exists or to first empty slot found, skipping
1750      * (ignoring) lazily deleted slots.
1751      */
1752     private size_t indexOfKeyOrVacancySkippingHoles(const scope K key) const @trusted scope // `auto ref` here makes things slow
1753     // TODO: if (...)
1754     {
1755         version(LDC) pragma(inline, true);
1756         version(unittest)
1757         {
1758             assert(!key.isNull);
1759             static if (hasHoleableKey) { assert(!isHoleKeyConstant(key)); }
1760         }
1761 
1762         static if (useSmallLinearSearch)
1763             if (_store.length * T.sizeof <= linearSearchMaxSize)
1764             {
1765                 foreach (immutable i, const ref element; _store) // linear search is faster for small arrays
1766                     if ((keyOf(element).isNull ||
1767                          keyEqualPredFn(keyOf(element), key)))
1768                         return i;
1769                 return _store.length;
1770             }
1771 
1772         static if (passElementByValue) // Ref: https://github.com/dlang/dmd/pull/11000#issuecomment-671103778
1773         {
1774             static if (hasHoleableKey)
1775                 alias pred = (const scope element) => (keyOf(element).isNull ||
1776                                                        keyEqualPredFn(keyOf(element), key));
1777             else
1778                 alias pred = (immutable scope index,
1779                               const scope element) => (!hasHoleAtPtrIndex(_holesPtr, index) &&
1780                                                        (keyOf(element).isNull ||
1781                                                         keyEqualPredFn(keyOf(element), key)));
1782         }
1783         else
1784         {
1785             static if (hasHoleableKey)
1786                 alias pred = (const scope auto ref element) => (keyOf(element).isNull ||
1787                                                                 keyEqualPredFn(keyOf(element), key));
1788             else
1789                 alias pred = (immutable scope index,
1790                               const scope auto ref element) => (!hasHoleAtPtrIndex(_holesPtr, index) &&
1791                                                                 (keyOf(element).isNull ||
1792                                                                  keyEqualPredFn(keyOf(element), key)));
1793         }
1794 
1795         static if (usePrimeCapacity)
1796             return xxx;
1797         else
1798             return _store[].triangularProbeFromIndex!(pred, assumeNonFullHaystack)(keyToIndex(key));
1799     }
1800 
1801     private size_t indexOfKeyOrVacancyAndFirstHole(const scope K key, // `auto ref` here makes things slow
1802                                                    ref size_t holeIndex) const @trusted scope
1803     {
1804         version(LDC) pragma(inline, true);
1805         version(unittest)
1806         {
1807             assert(!key.isNull);
1808             static if (hasHoleableKey) { assert(!isHoleKeyConstant(key)); }
1809         }
1810 
1811         static if (useSmallLinearSearch)
1812             if (_store.length * T.sizeof <= linearSearchMaxSize)
1813             {
1814                 foreach (immutable i, const ref element; _store) // linear search is faster for small arrays
1815                     if ((keyOf(element).isNull ||
1816                          keyEqualPredFn(keyOf(element), key)))
1817                         return i;
1818                 return _store.length;
1819             }
1820 
1821         static if (passElementByValue) // Ref: https://github.com/dlang/dmd/pull/11000#issuecomment-671103778
1822         {
1823             static if (hasHoleableKey)
1824             {
1825                 alias hitPred = (const scope element) => (keyOf(element).isNull ||
1826                                                           keyEqualPredFn(keyOf(element), key));
1827                 alias holePred = (const scope element) => (isHoleKeyConstant(keyOf(element)));
1828             }
1829             else
1830             {
1831                 alias hitPred = (immutable scope index,
1832                                  const scope element) => (!hasHoleAtPtrIndex(_holesPtr, index) &&
1833                                                           (keyOf(element).isNull ||
1834                                                            keyEqualPredFn(keyOf(element), key)));
1835                 alias holePred = (immutable scope index, // TODO: use only index
1836                                   const scope element) => (hasHoleAtPtrIndex(_holesPtr, index));
1837             }
1838         }
1839         else
1840         {
1841             static if (hasHoleableKey)
1842             {
1843                 alias hitPred = (const scope auto ref element) => (keyOf(element).isNull ||
1844                                                                    keyEqualPredFn(keyOf(element), key));
1845                 alias holePred = (const scope auto ref element) => (isHoleKeyConstant(keyOf(element)));
1846             }
1847             else
1848             {
1849                 alias hitPred = (immutable scope index,
1850                                  const scope auto ref element) => (!hasHoleAtPtrIndex(_holesPtr, index) &&
1851                                                                    (keyOf(element).isNull ||
1852                                                                     keyEqualPredFn(keyOf(element), key)));
1853                 alias holePred = (immutable scope index, // TODO: use only index
1854                                   const scope auto ref element) => (hasHoleAtPtrIndex(_holesPtr, index));
1855             }
1856         }
1857 
1858         static if (usePrimeCapacity)
1859             return xxx;
1860         else
1861             return _store[].triangularProbeFromIndexIncludingHoles!(hitPred, holePred, assumeNonFullHaystack)(keyToIndex(key), holeIndex);
1862     }
1863 
1864     /// Returns: `true` iff `index` indexes a non-null element, `false` otherwise.
1865     private bool isOccupiedAtIndex(in size_t index) const
1866     {
1867         version(LDC) pragma(inline, true);
1868         version(unittest) assert(index < _store.length);
1869         if (keyOf(_store[index]).isNull) { return false; }
1870         static if (hasHoleableKey)
1871             return !isHoleKeyConstant(keyOf(_store[index]));
1872         else
1873             return !hasHoleAtPtrIndex(_holesPtr, index);
1874     }
1875 }
1876 
1877 /** Duplicate `src` into uninitialized `dst` ignoring prior destruction of `dst`.
1878  *
1879  * TODO: Move to a more generic place either in phobos-next or Phobos.
1880  */
1881 static private void duplicateEmplace(T)(const scope ref T src,
1882                                         scope ref T dst) @system
1883 {
1884     version(D_Coverage) {} else pragma(inline, true);
1885     import core.internal.traits : hasElaborateCopyConstructor;
1886     import std.traits : isBasicType, isInstanceOf;
1887     static if (!hasElaborateCopyConstructor!T)
1888     {
1889         import std.typecons : Nullable;
1890         static if (is(T == class) ||
1891                    is(T == string))
1892             dst = cast(T)src;
1893         else static if (isBasicType!T ||
1894                         isInstanceOf!(Nullable, T)) // `Nullable` types cannot be emplaced
1895             dst = src;
1896         else                    // TODO: can this case occur?
1897         {
1898             import core.internal.traits : Unqual;
1899             import core.lifetime : emplace;
1900             emplace(&dst, cast(Unqual!T)src);
1901         }
1902     }
1903     else static if (__traits(hasMember, T, "dup"))
1904     {
1905         import core.lifetime : emplace;
1906         // TODO: when `emplace` can handle src being an r-value of uncopyable types replace with: `emplace(&dst, src.dup);`
1907         emplace(&dst);
1908         dst = src.dup;
1909     }
1910     else
1911         debug static assert(0, "cannot duplicate a " ~ T.stringof);
1912 }
1913 
1914 /** L-value element reference (and in turn range iterator).
1915  */
1916 static private struct LvalueElementRef(SomeMap)
1917 {
1918     import std.traits : isMutable;
1919     debug static assert(isMutable!SomeMap, "SomeMap type must be mutable");
1920 
1921     private SomeMap* _table;      // scoped access
1922     private size_t _binIndex;   // index to bin inside `table`
1923     private size_t _hitCounter; // counter over number of elements popped (needed for length)
1924 
1925     this(SomeMap* table) @trusted
1926     {
1927         version(D_Coverage) {} else pragma(inline, true);
1928         this._table = table;
1929         static if (SomeMap.isBorrowChecked)
1930             debug { _table.incBorrowCount(); }
1931     }
1932 
1933     ~this() nothrow @nogc @trusted
1934     {
1935         version(D_Coverage) {} else pragma(inline, true);
1936         static if (SomeMap.isBorrowChecked)
1937             debug { _table.decBorrowCount(); }
1938     }
1939 
1940     this(this) @trusted
1941     {
1942         version(D_Coverage) {} else pragma(inline, true);
1943         static if (SomeMap.isBorrowChecked)
1944             debug
1945             {
1946                 assert(_table._borrowCount != 0);
1947                 _table.incBorrowCount();
1948             }
1949     }
1950 
1951     /// Check if empty.
1952     bool empty() const @property @safe pure nothrow @nogc
1953     {
1954         version(D_Coverage) {} else pragma(inline, true);
1955         return _binIndex == _table.binCount;
1956     }
1957 
1958     /// Get number of element left to pop.
1959     @property size_t length() const @safe pure nothrow @nogc
1960     {
1961         version(D_Coverage) {} else pragma(inline, true);
1962         return _table.length - _hitCounter;
1963     }
1964 
1965     @property typeof(this) save() // ForwardRange
1966     {
1967         version(D_Coverage) {} else pragma(inline, true);
1968         return this;
1969     }
1970 
1971     void popFront() in(!empty)
1972     {
1973         version(LDC) pragma(inline, true);
1974         _binIndex += 1;
1975         findNextNonEmptyBin();
1976         _hitCounter += 1;
1977     }
1978 
1979     private void findNextNonEmptyBin()
1980     {
1981         version(D_Coverage) {} else pragma(inline, true);
1982         while (_binIndex != (*_table).binCount &&
1983                !(*_table).isOccupiedAtIndex(_binIndex))
1984             _binIndex += 1;
1985     }
1986 }
1987 
1988 /** R-value element reference (and in turn range iterator).
1989  *
1990  * Does need to do borrow-checking.
1991  */
1992 static private struct RvalueElementRef(SomeMap)
1993 {
1994     debug import std.traits : isMutable;
1995     debug static assert(isMutable!SomeMap, "SomeMap type must be mutable");
1996 
1997     SomeMap _table;                // owned table
1998     size_t _binIndex;            // index to bin inside table
1999     size_t _hitCounter;    // counter over number of elements popped
2000 
2001     /// Check if empty.
2002     bool empty() const @property @safe pure nothrow @nogc
2003     {
2004         version(D_Coverage) {} else pragma(inline, true);
2005         return _binIndex == _table.binCount;
2006     }
2007 
2008     /// Get number of element left to pop.
2009     @property size_t length() const @safe pure nothrow @nogc
2010     {
2011         version(D_Coverage) {} else pragma(inline, true);
2012         return _table.length - _hitCounter;
2013     }
2014 
2015     void popFront() in(!empty)
2016     {
2017         version(LDC) pragma(inline, true);
2018         _binIndex += 1;
2019         findNextNonEmptyBin();
2020         _hitCounter += 1;
2021     }
2022 
2023     private void findNextNonEmptyBin()
2024     {
2025         version(D_Coverage) {} else pragma(inline, true);
2026         while (_binIndex != _table.binCount &&
2027                !_table.isOccupiedAtIndex(_binIndex))
2028             _binIndex += 1;
2029     }
2030 }
2031 
2032 /** Hash set with in-place open-addressing, storing keys (elements) of type `K`.
2033  *
2034  * Reuse `HybridHashMap` with its V-type set to `void`.
2035  *
2036  * See_Also: `HybridHashMap`.
2037  */
2038 alias HybridHashSet(K,
2039                   alias hasher = hashOf,
2040                   string keyEqualPred = defaultKeyEqualPredOf!K,
2041                   Allocator = Mallocator,
2042                   bool borrowChecked = false,
2043                   bool useSmallLinearSearch = true,
2044                   bool usePrimeCapacity = false) =
2045 HybridHashMap!(K, void, hasher, keyEqualPred, Allocator, borrowChecked, useSmallLinearSearch, usePrimeCapacity);
2046 
2047 import std.traits : isInstanceOf;
2048 import std.functional : unaryFun;
2049 
2050 /** Remove all elements in `x` matching `pred`.
2051  *
2052  * TODO: make this generic for all iterable containers and move to
2053  * container/common.d.
2054  */
2055 size_t removeAllMatching(alias pred, SomeMap)(auto ref SomeMap x) @trusted
2056 if (isInstanceOf!(HybridHashMap, SomeMap) && // TODO: generalize to `isSetOrMap`
2057     is(typeof((unaryFun!pred))))
2058 {
2059     import nxt.nullable_traits : nullify;
2060     size_t removalCount = 0;
2061     foreach (immutable i, ref bin; x._store)
2062     {
2063         // TODO:
2064         // move to SomeMap.removeRef(bin) // uses: `offset = &bin - _store.ptr`
2065         // move to SomeMap.inplaceRemove(bin) // uses: `offset = &bin - _store.ptr`
2066         // or   to SomeMap.removeAtIndex(i)
2067         if (x.isOccupiedAtIndex(i) &&
2068             unaryFun!pred(bin))
2069         {
2070             x.tagAsLazilyDeletedElementAtIndex(i);
2071             removalCount += 1;
2072         }
2073     }
2074     x._count = x._count - removalCount;
2075     return removalCount;        // TODO: remove this return value
2076 }
2077 
2078 /** Returns: `x` eagerly filtered on `pred`.
2079  *
2080  * TODO: move to container/common.d with more generic template restrictions
2081  */
2082 SomeMap filtered(alias pred, SomeMap)(SomeMap x)
2083 if (isInstanceOf!(HybridHashMap, SomeMap)) // TODO: generalize to `isSetOrMap`
2084 {
2085     import core.lifetime : move;
2086     import std.functional : not;
2087     x.removeAllMatching!(not!pred); // `x` is a singleton (r-value) so safe to mutate
2088     return move(x);             // functional
2089 }
2090 
2091 /** Returns: `x` eagerly intersected with `y`.
2092  *
2093  * TODO: move to container/common.d.
2094  * TODO: Check that `ElementType`'s of `C1` and `C2` match. Look at std.algorithm.intersection for hints.
2095  */
2096 auto intersectedWith(C1, C2)(C1 x, auto ref C2 y)
2097 if (isInstanceOf!(HybridHashMap, C1) && // TODO: generalize to `isSetOrMap`
2098     isInstanceOf!(HybridHashMap, C2))   // TODO: generalize to `isSetOrMap`
2099 {
2100     import core.lifetime : move;
2101     static if (__traits(isRef, y)) // y is l-value
2102         // @("complexity", "O(x.length)")
2103         return move(x).filtered!(_ => y.contains(_)); // only x can be reused
2104     else
2105     {
2106         /* both are r-values so reuse the shortest */
2107         // @("complexity", "O(min(x.length), min(y.length))")
2108         if (x.length < y.length)
2109             return move(x).filtered!(_ => y.contains(_)); // functional
2110         else
2111             return move(y).filtered!(_ => x.contains(_)); // functional
2112     }
2113 }
2114 
2115 /// exercise opEquals
2116 @safe pure nothrow @nogc unittest
2117 {
2118     import std.typecons : Nullable;
2119     import nxt.digestx.fnv : FNV;
2120 
2121     alias K = Nullable!(ulong, ulong.max);
2122     alias X = HybridHashSet!(K, FNV!(64, true));
2123 
2124     const n = 100;
2125 
2126     X a;
2127     foreach (const i_; 0 .. n)
2128     {
2129         const i = 1113*i_;           // insert in order
2130         assert(!a.contains(K(i)));
2131         assert(!a.containsUsingLinearSearch(K(i)));
2132         assert(a.insertAndReturnElement(K(i)) == K(i));
2133         assert(a.contains(K(i)));
2134         assert(a.containsUsingLinearSearch(K(i)));
2135     }
2136 
2137     X b;
2138     foreach (const i_; 0 .. n)
2139     {
2140         const i = 1113*(n - 1 - i_);   // insert in reverse
2141         assert(!b.contains(K(i)));
2142         assert(!b.containsUsingLinearSearch(K(i)));
2143         assert(b.insertAndReturnElement(K(i)) == K(i));
2144         assert(b.contains(K(i)));
2145         assert(b.containsUsingLinearSearch(K(i)));
2146     }
2147 
2148     assert(a == b);
2149 
2150     // bin storage must be deterministic
2151     () @trusted { assert(a._store != b._store); }();
2152 }
2153 
2154 @safe pure nothrow @nogc unittest
2155 {
2156     import nxt.digestx.fnv : FNV;
2157 
2158     enum Pot { noun, verb }
2159     struct ExprPot
2160     {
2161         string expr;
2162         Pot pot;
2163 
2164         alias nullifier = expr;
2165         static immutable nullValue = typeof(this).init;
2166     }
2167 
2168     alias X = HybridHashSet!(ExprPot, FNV!(64, true));
2169 
2170     X x;
2171 
2172     const aa = "aa";
2173 
2174     // two keys with same contents but different place in memory
2175     const key1 = ExprPot(aa[0 .. 1], Pot.noun);
2176     const key2 = ExprPot(aa[1 .. 2], Pot.noun);
2177 
2178     assert(key1 == key2);
2179     assert(key1 !is key2);
2180 
2181     assert(!x.contains(key1));
2182     assert(!x.contains(key2));
2183     x.insert(key1);
2184     assert(x.contains(key1));
2185     assert(x.containsUsingLinearSearch(key1));
2186     assert(x.contains(key2));
2187     /* assert(x.containsUsingLinearSearch(key2)); */
2188     assert(key1 in x);
2189     assert(key2 in x);
2190 }
2191 
2192 /// `string` as key
2193 @safe pure nothrow @nogc unittest
2194 {
2195     import nxt.container.traits : mustAddGCRange;
2196     import nxt.digestx.fnv : FNV;
2197 
2198     alias X = HybridHashSet!(string, FNV!(64, true));
2199     debug static assert(!mustAddGCRange!X);
2200     debug static assert(X.sizeof == 24); // dynamic arrays also `hasAddressLikeKey`
2201 
2202     auto x = X();
2203 
2204     auto testEscapeShouldFail()() @safe pure
2205     {
2206         X x;
2207         x.insert("a");
2208         return x.byElement;
2209     }
2210 
2211     auto testEscapeShouldFailFront()() @safe pure
2212     {
2213         X x;
2214         x.insert("a");
2215         return x.byElement.front;
2216     }
2217 
2218     assert(&"a"[0] is &"a"[0]); // string literals are store in common place
2219 
2220     const aa = "aa";
2221 
2222     // string slices are equal when elements are equal regardless of position
2223     // (.ptr) in memory
2224     assert(x.insertAndReturnElement(aa[0 .. 1]) !is "a");
2225     x.insert(aa[0 .. 1]);
2226     assert(x.insertAndReturnElement(aa[0 .. 1]) is aa[0 .. 1]);
2227     assert(x.contains(aa[1 .. 2]));
2228     assert(x.containsUsingLinearSearch(aa[1 .. 2]));
2229 
2230     const(char)[] aa_ = "aa";
2231     assert(x.contains(aa_[1 .. 2]));
2232     assert(x.containsUsingLinearSearch(aa_[1 .. 2]));
2233     assert(aa_[1 .. 2] in x);
2234 
2235     char[2] aa__; aa__ = "aa";
2236     assert(x.contains(aa__[1 .. 2]));
2237     assert(x.containsUsingLinearSearch(aa__[1 .. 2]));
2238     assert(aa__[1 .. 2] in x);
2239 
2240     const bb = "bb";
2241 
2242     assert(x.insertAndReturnElement(bb[0 .. 1]) is bb[0 .. 1]); // returns newly added ref
2243     assert(x.insertAndReturnElement(bb[0 .. 1]) !is "b");       // return other ref not equal new literal
2244     x.insert(bb[0 .. 1]);
2245     assert(x.contains(bb[1 .. 2]));
2246     assert(x.containsUsingLinearSearch(bb[1 .. 2]));
2247 
2248     x.remove(aa[0 .. 1]);
2249     assert(!x.contains(aa[1 .. 2]));
2250     assert(!x.containsUsingLinearSearch(aa[1 .. 2]));
2251     assert(x.contains(bb[1 .. 2]));
2252     assert(x.containsUsingLinearSearch(bb[1 .. 2]));
2253 
2254     x.remove(bb[0 .. 1]);
2255     assert(!x.contains(bb[1 .. 2]));
2256     assert(!x.containsUsingLinearSearch(bb[1 .. 2]));
2257 
2258     x.insert("a");
2259     x.insert("b");
2260     assert(x.contains("a"));
2261     assert(x.containsUsingLinearSearch("a"));
2262     assert(x.contains("b"));
2263     assert(x.containsUsingLinearSearch("b"));
2264 
2265     debug static assert(!__traits(compiles, { testEscapeShouldFail(); } ));
2266     // TODO: this should fail:
2267     // TODO: debug static assert(!__traits(compiles, { testEscapeShouldFailFront(); } ));
2268 }
2269 
2270 /// `string` as key
2271 @safe pure nothrow unittest
2272 {
2273     import nxt.digestx.fnv : FNV;
2274     alias X = HybridHashSet!(string, FNV!(64, true));
2275     auto x = X();
2276 
2277     char[2] cc = "cc";          // mutable chars
2278     assert(x.insertAndReturnElement(cc[]) !is cc[]); // will allocate new slice
2279 
2280     const cc_ = "cc";           // immutable chars
2281     assert(x.insertAndReturnElement(cc_[]) !is cc[]); // will not allocate
2282 }
2283 
2284 /// array container as value type
2285 @safe pure nothrow @nogc unittest
2286 {
2287     import std.meta : AliasSeq;
2288     import std.typecons : Nullable;
2289     import nxt.container.traits : mustAddGCRange;
2290     import nxt.digestx.fnv : FNV;
2291     import nxt.array_help : s;
2292 
2293     alias K = Nullable!(uint, uint.max);
2294 
2295     alias VE = Nullable!(uint, uint.max);
2296     alias V = HybridHashSet!(VE, FNV!(64, true));
2297 
2298     debug static assert(!mustAddGCRange!V);
2299 
2300     foreach (X; AliasSeq!(HybridHashMap!(K, V, FNV!(64, true))))
2301     {
2302         const VE n = 600;
2303 
2304         auto x = X();
2305 
2306         {                       // scoped range
2307             auto xkeys = x.byKey;
2308             assert(xkeys.length == 0);
2309             foreach (ref key; xkeys)
2310             {
2311                 debug static assert(is(typeof(key) == const(K)));
2312                 assert(0);
2313             }
2314             foreach (ref key; X().byKey)
2315             {
2316                 debug static assert(is(typeof(key) == const(K)));
2317                 assert(0);
2318             }
2319         }
2320 
2321         foreach (immutable i; 0 .. n)
2322         {
2323             assert(x.length == i);
2324 
2325             auto key = K(i);
2326             auto value = V.withElements([VE(i)].s);
2327 
2328             x[key] = value.dup;
2329             assert(x.length == i + 1);
2330             assert(x.contains(key));
2331             // TODO: assert(x.containsUsingLinearSearch(key));
2332             {
2333                 auto valuePtr = key in x;
2334                 assert(valuePtr && *valuePtr == value);
2335             }
2336 
2337             x.remove(key);
2338             assert(x.length == i);
2339             assert(!x.contains(key));
2340             assert(key !in x);
2341 
2342             x[key] = value.dup;
2343             assert(x.length == i + 1);
2344             assert(x.contains(key));
2345             {
2346                 auto valuePtr = key in x;
2347                 assert(valuePtr && *valuePtr == value);
2348             }
2349         }
2350 
2351         assert(x is x);
2352 
2353         x = x.dup;
2354 
2355         auto y = x.dup;
2356         assert(x !is y);
2357         assert(x.length == y.length);
2358 
2359         assert(y == x);
2360         assert(x == y);
2361 
2362         foreach (ref key; x.byKey)
2363         {
2364             assert(x.contains(key));
2365         }
2366 
2367         foreach (ref keyValue; x.byKeyValue)
2368         {
2369             assert(x.contains(keyValue.key));
2370             auto keyValuePtr = keyValue.key in x;
2371             assert(keyValuePtr &&
2372                    *keyValuePtr == keyValue.value);
2373         }
2374 
2375         foreach (immutable i; 0 .. n)
2376         {
2377             assert(x.length == n - i);
2378 
2379             auto key = K(i);
2380             auto value = V.withElements([VE(i)].s);
2381 
2382             assert(x.contains(key));
2383             {
2384                 auto valuePtr = key in x;
2385                 assert(valuePtr && *valuePtr == value);
2386             }
2387 
2388             x.remove(key);
2389             assert(!x.contains(key));
2390             assert(key !in x);
2391         }
2392 
2393         auto z = y.dup;
2394         assert(y == z);
2395 
2396         /* remove all elements in `y` using `removeAllMatching` and all elements
2397          * in `z` using `removeAllMatching` */
2398         foreach (immutable i; 0 .. n)
2399         {
2400             assert(y.length == n - i);
2401             assert(z.length == n - i);
2402 
2403             auto key = K(i);
2404             auto value = V.withElements([VE(i)].s);
2405 
2406             assert(y.contains(key));
2407             {
2408                 auto valuePtr = key in y;
2409                 assert(valuePtr && *valuePtr == value);
2410             }
2411             assert(z.contains(key));
2412             {
2413                 auto valuePtr = key in z;
2414                 assert(valuePtr && *valuePtr == value);
2415             }
2416 
2417             y.remove(key);
2418             assert(z.removeAllMatching!((const scope ref element) => element.key is key) == 1);
2419             assert(y == z);
2420 
2421             assert(!y.contains(key));
2422             assert(!z.contains(key));
2423 
2424             assert(key !in y);
2425             assert(key !in z);
2426         }
2427     }
2428 }
2429 
2430 /// r-value and l-value intersection
2431 @safe pure nothrow @nogc unittest
2432 {
2433     import core.lifetime : move;
2434     import std.typecons : Nullable;
2435     import nxt.digestx.fnv : FNV;
2436     import nxt.array_help : s;
2437 
2438     alias K = Nullable!(uint, uint.max);
2439     alias X = HybridHashSet!(K, FNV!(64, true));
2440 
2441     auto x = X();
2442 
2443     {                           // scoped range
2444         foreach (ref _; x.byElement) { assert(0); }
2445     }
2446 
2447     auto x0 = X.init;
2448     assert(x0.length == 0);
2449     assert(x0._store.length == 0);
2450     assert(!x0.contains(K(1)));
2451 
2452     auto x1 = X.withElements([K(12)].s);
2453     assert(x1.length == 1);
2454     assert(x1.contains(K(12)));
2455 
2456     auto x2 = X.withElements([K(10), K(12)].s);
2457     assert(x2.length == 2);
2458     assert(x2.contains(K(10)));
2459     assert(x2.contains(K(12)));
2460 
2461     auto x3 = X.withElements([K(12), K(13), K(14)].s);
2462     assert(x3.length == 3);
2463     assert(x3.contains(K(12)));
2464     assert(x3.contains(K(13)));
2465     assert(x3.contains(K(14)));
2466 
2467     auto z = X.withElements([K(10), K(12), K(13), K(15)].s);
2468     assert(z.length == 4);
2469     assert(z.contains(K(10)));
2470     assert(z.contains(K(12)));
2471     assert(z.contains(K(13)));
2472     assert(z.contains(K(15)));
2473 
2474     auto y = move(z).intersectedWith(x2);
2475     assert(y.length == 2);
2476     assert(y.contains(K(10)));
2477     assert(y.contains(K(12)));
2478     assert(y.containsUsingLinearSearch(K(10)));
2479     assert(y.containsUsingLinearSearch(K(12)));
2480 }
2481 
2482 /// r-value and r-value intersection
2483 @safe pure nothrow @nogc unittest
2484 {
2485     import std.typecons : Nullable;
2486     import nxt.digestx.fnv : FNV;
2487     import nxt.array_help : s;
2488 
2489     alias K = Nullable!(uint, uint.max);
2490     alias X = HybridHashSet!(K, FNV!(64, true));
2491 
2492     auto y = X.withElements([K(10), K(12), K(13), K(15)].s).intersectedWith(X.withElements([K(12), K(13)].s));
2493     assert(y.length == 2);
2494     assert(y.contains(K(12)));
2495     assert(y.contains(K(13)));
2496     assert(y.containsUsingLinearSearch(K(12)));
2497     assert(y.containsUsingLinearSearch(K(13)));
2498 }
2499 
2500 /** Returns: `x` eagerly intersected with `y`.
2501     TODO: move to container/common.d.
2502  */
2503 auto intersectWith(C1, C2)(ref C1 x, auto ref const(C2) y)
2504 if (isInstanceOf!(HybridHashMap, C1) &&
2505     isInstanceOf!(HybridHashMap, C2))
2506 {
2507     return x.removeAllMatching!(_ => !y.contains(_));
2508 }
2509 
2510 /// r-value and l-value intersection
2511 @safe pure nothrow @nogc unittest
2512 {
2513     import std.typecons : Nullable;
2514     import nxt.digestx.fnv : FNV;
2515     import nxt.array_help : s;
2516 
2517     alias K = Nullable!(uint, uint.max);
2518     alias X = HybridHashSet!(K, FNV!(64, true));
2519 
2520     auto x = X.withElements([K(12), K(13)].s);
2521     auto y = X.withElements([K(10), K(12), K(13), K(15)].s);
2522     y.intersectWith(x);
2523     assert(y.length == 2);
2524     assert(y.contains(K(12)));
2525     assert(y.containsUsingLinearSearch(K(12)));
2526     assert(y.contains(K(13)));
2527     assert(y.containsUsingLinearSearch(K(13)));
2528 }
2529 
2530 /// Range over elements of l-value instance of this.
2531 static struct ByLvalueElement(SomeMap) // public for now because this is needed in `knet.zing.Zing.EdgesOfRels`
2532 {
2533 pragma(inline, true):
2534     // TODO: functionize
2535     static if (isAddress!(SomeMap.ElementType)) // for reference types
2536     {
2537         /// Get reference to front element.
2538         @property scope SomeMap.ElementType front()() return @trusted
2539         {
2540             // cast to head-const for class key
2541             return (cast(typeof(return))_table._store[_binIndex]);
2542         }
2543     }
2544     else
2545     {
2546         /// Get reference to front element.
2547         @property scope auto ref front() return @trusted
2548         {
2549             return *(cast(const(SomeMap.ElementType)*)&_table._store[_binIndex]); // propagate constnes
2550         }
2551     }
2552     import core.internal.traits : Unqual;
2553     // unqual to reduce number of instantations of `LvalueElementRef`
2554     public LvalueElementRef!(Unqual!SomeMap) _elementRef;
2555     alias _elementRef this;
2556 }
2557 
2558 /// Range over elements of r-value instance of this.
2559 static private struct ByRvalueElement(SomeMap)
2560 {
2561     @disable this(this);
2562 pragma(inline, true):
2563     static if (isAddress!(SomeMap.ElementType)) // for reference types
2564     {
2565         /// Get reference to front element.
2566         @property scope SomeMap.ElementType front()() return @trusted
2567         {
2568             // cast to head-const for class key
2569             return cast(typeof(return))_table._store[_binIndex];
2570         }
2571     }
2572     else
2573     {
2574         /// Get reference to front element.
2575         @property scope auto ref front() return
2576         {
2577             return *(cast(const(SomeMap.ElementType)*)&_table._store[_binIndex]); // propagate constnes
2578         }
2579     }
2580     import core.internal.traits : Unqual;
2581     public RvalueElementRef!(Unqual!SomeMap) _elementRef;
2582     alias _elementRef this;
2583 }
2584 
2585 /** Returns: range that iterates through the elements of `c` in undefined order.
2586  */
2587 auto byElement(SomeMap)(auto ref return SomeMap c) @trusted
2588 if (isInstanceOf!(HybridHashMap, SomeMap) &&
2589     !SomeMap.hasValue)
2590 {
2591     import core.internal.traits : Unqual;
2592     alias M = Unqual!SomeMap;
2593     alias C = const(SomeMap);        // be const for now
2594     static if (__traits(isRef, c)) // `c` is an l-value and must be borrowed
2595         auto result = ByLvalueElement!C((LvalueElementRef!(M)(cast(M*)&c)));
2596     else                        // `c` was is an r-value and can be moved
2597     {
2598         import core.lifetime : move;
2599         auto result = ByRvalueElement!C((RvalueElementRef!(M)(move(*(cast(M*)&c))))); // reinterpret
2600     }
2601     result.findNextNonEmptyBin();
2602     return result;
2603 }
2604 alias range = byElement;        // EMSI-container naming
2605 
2606 static private struct ByKey_lvalue(SomeMap)
2607 if (isInstanceOf!(HybridHashMap, SomeMap) &&
2608     SomeMap.hasValue)
2609 {
2610     @property const scope auto ref front() return // key access must be const, TODO: auto ref => ref K
2611     {
2612         version(D_Coverage) {} else pragma(inline, true);
2613         return _table._store[_binIndex].key;
2614     }
2615     import core.internal.traits : Unqual;
2616     public LvalueElementRef!(Unqual!SomeMap) _elementRef;
2617     alias _elementRef this;
2618 }
2619 
2620 static private struct ByKey_rvalue(SomeMap)
2621 if (isInstanceOf!(HybridHashMap, SomeMap) &&
2622     SomeMap.hasValue)
2623 {
2624     @property const scope auto ref front() return // key access must be const, TODO: auto ref => ref K
2625     {
2626         version(D_Coverage) {} else pragma(inline, true);
2627         return _table._store[_binIndex].key;
2628     }
2629     import core.internal.traits : Unqual;
2630     public RvalueElementRef!(Unqual!SomeMap) _elementRef;
2631     alias _elementRef this;
2632 }
2633 
2634 /** Returns: range that iterates through the keys of `c` in undefined order.
2635  */
2636 auto byKey(SomeMap)(auto ref /*TODO: return*/ SomeMap c) @trusted
2637 if (isInstanceOf!(HybridHashMap, SomeMap) &&
2638     SomeMap.hasValue)
2639 {
2640     import core.internal.traits : Unqual;
2641     alias M = Unqual!SomeMap;
2642     alias C = const(SomeMap);        // be const
2643     static if (__traits(isRef, c)) // `c` is an l-value and must be borrowed
2644         auto result = ByKey_lvalue!C((LvalueElementRef!(M)(cast(M*)&c)));
2645     else                        // `c` was is an r-value and can be moved
2646     {
2647         import core.lifetime : move;
2648         auto result = ByKey_rvalue!C((RvalueElementRef!M(move(*(cast(M*)&c))))); // reinterpret
2649     }
2650     result.findNextNonEmptyBin();
2651     return result;
2652 }
2653 
2654 static private struct ByValue_lvalue(SomeMap)
2655 if (isInstanceOf!(HybridHashMap, SomeMap) &&
2656     SomeMap.hasValue)
2657 {
2658     @property scope auto ref front() return @trusted // TODO: auto ref => ref V
2659     {
2660         version(D_Coverage) {} else pragma(inline, true);
2661         // TODO: functionize
2662         import std.traits : isMutable;
2663         static if (isMutable!(SomeMap)) // TODO: can this be solved without this `static if`?
2664             alias E = SomeMap.ValueType;
2665         else
2666             alias E = const(SomeMap.ValueType);
2667         return *(cast(E*)&_table._store[_binIndex].value);
2668     }
2669     import core.internal.traits : Unqual;
2670     public LvalueElementRef!(Unqual!SomeMap) _elementRef;
2671     alias _elementRef this;
2672 }
2673 
2674 static private struct ByValue_rvalue(SomeMap)
2675 if (isInstanceOf!(HybridHashMap, SomeMap) &&
2676     SomeMap.hasValue)
2677 {
2678     @property scope auto ref front() return @trusted // TODO: auto ref => ref V
2679     {
2680         version(D_Coverage) {} else pragma(inline, true);
2681         // TODO: functionize
2682         import std.traits : isMutable;
2683         static if (isMutable!(SomeMap)) // TODO: can this be solved without this `static if`?
2684             alias E = SomeMap.ValueType;
2685         else
2686             alias E = const(SomeMap.ValueType);
2687         return *(cast(E*)&_table._store[_binIndex].value);
2688     }
2689     import core.internal.traits : Unqual;
2690     public RvalueElementRef!(Unqual!SomeMap) _elementRef;
2691     alias _elementRef this;
2692 }
2693 
2694 /** Returns: range that iterates through the values of `c` in undefined order.
2695  */
2696 auto byValue(SomeMap)(auto ref return SomeMap c) @trusted
2697 if (isInstanceOf!(HybridHashMap, SomeMap) &&
2698     SomeMap.hasValue)
2699 {
2700     import core.internal.traits : Unqual;
2701     import std.traits : isMutable;
2702     alias M = Unqual!SomeMap;
2703     alias C = const(SomeMap);
2704     static if (__traits(isRef, c)) // `c` is an l-value and must be borrowed
2705         auto result = ByValue_lvalue!SomeMap((LvalueElementRef!(M)(cast(M*)&c)));
2706     else                        // `c` was is an r-value and can be moved
2707     {
2708         import core.lifetime : move;
2709         auto result = ByValue_rvalue!C((RvalueElementRef!M(move(*(cast(M*)&c))))); // reinterpret
2710     }
2711     result.findNextNonEmptyBin();
2712     return result;
2713 }
2714 
2715 static private struct ByKeyValue_lvalue(SomeMap)
2716 if (isInstanceOf!(HybridHashMap, SomeMap) &&
2717     SomeMap.hasValue)
2718 {
2719     @property scope auto ref front() return @trusted // TODO: auto ref => ref T
2720     {
2721         version(D_Coverage) {} else pragma(inline, true);
2722         // TODO: functionize
2723         import std.traits : isMutable;
2724         static if (isMutable!(SomeMap))
2725             alias E = SomeMap.KeyValueType;
2726         else
2727             alias E = const(SomeMap.T);
2728         return *(cast(E*)&_table._store[_binIndex]);
2729     }
2730     import core.internal.traits : Unqual;
2731     public LvalueElementRef!(Unqual!SomeMap) _elementRef;
2732     alias _elementRef this;
2733 }
2734 
2735 /** Returns: range that iterates through the key-value-pairs of `c` in undefined order.
2736  */
2737 auto byKeyValue(SomeMap)(auto ref return SomeMap c) @trusted
2738 if (isInstanceOf!(HybridHashMap, SomeMap) &&
2739     SomeMap.hasValue)
2740 {
2741     import core.internal.traits : Unqual;
2742     alias M = Unqual!SomeMap;
2743     static if (__traits(isRef, c)) // `c` is an l-value and must be borrowed
2744         auto result = ByKeyValue_lvalue!SomeMap((LvalueElementRef!(M)(cast(M*)&c)));
2745     else                        // `c` was is an r-value and can be moved
2746     {
2747         import core.lifetime : move;
2748         auto result = ByKeyValue_rvalue!SomeMap((RvalueElementRef!M(move(*(cast(M*)&c))))); // reinterpret
2749     }
2750     result.findNextNonEmptyBin();
2751     return result;
2752 }
2753 
2754 /// make range from l-value and r-value. element access is always const
2755 @safe pure unittest
2756 {
2757     import core.exception : AssertError;
2758     import std.typecons : Nullable;
2759     import nxt.digestx.fnv : FNV;
2760     import nxt.array_help : s;
2761     debug import std.exception : assertThrown;
2762 
2763     import std.algorithm.searching : count;
2764     alias K = Nullable!(uint, uint.max);
2765     alias X = HybridHashSet!(K,
2766                            FNV!(64, true),
2767                            defaultKeyEqualPredOf!K,
2768                            Mallocator,
2769                            true);
2770 
2771     auto k11 = K(11);
2772     auto k22 = K(22);
2773     auto k33 = K(33);
2774     auto ks = [k11, k22, k33].s;
2775     auto k44 = K(44);
2776 
2777     // mutable
2778     auto x = X.withElements(ks);
2779     assert(!x.contains(k44));
2780     assert(!x.containsUsingLinearSearch(k44));
2781     assert(x.length == 3);
2782 
2783     assert(x.byElement.count == x.length);
2784     foreach (e; x.byElement)    // from l-value
2785     {
2786         debug static assert(is(typeof(e) == const(K))); // always const access
2787 
2788         // range invalidation forbidden:
2789         debug
2790         {
2791             assertThrown!AssertError(x.reserveExtra(1));  // range invalidation
2792             assertThrown!AssertError(x.clear());          // range invalidation
2793             assertThrown!AssertError(x.insert(k11));      // range invalidation
2794             assertThrown!AssertError(x.insertN([k11].s)); // range invalidation
2795             assertThrown!AssertError(x.remove(k11));      // range invalidation
2796         }
2797 
2798         // allowed
2799         assert(x.contains(e));
2800         assert(x.containsUsingLinearSearch(e));
2801 
2802         const eHit = e in x;
2803         assert(eHit);           // found
2804         assert(*eHit is e);     // and the value equals what we searched for
2805 
2806         const eDup = x.dup;     // duplication is `const` and allowed
2807         assert(eDup == x);
2808     }
2809 
2810     // const
2811     const y = X.withElements(ks);
2812     assert(!x.contains(k44));
2813     assert(!x.containsUsingLinearSearch(k44));
2814     foreach (e; y.byElement)    // from l-value
2815     {
2816         auto _ = y.byElement;   // ok to read-borrow again
2817         assert(y.contains(e));
2818         assert(y.containsUsingLinearSearch(e));
2819         debug static assert(is(typeof(e) == const(K)));
2820     }
2821 
2822     foreach (e; X.withElements([K(11)].s).byElement) // from r-value
2823     {
2824         assert(e == K(11));
2825         debug static assert(is(typeof(e) == const(K))); // always const access
2826     }
2827 }
2828 
2829 /// range checking
2830 @safe pure unittest
2831 {
2832     import core.exception : RangeError;
2833     import std.typecons : Nullable;
2834     import nxt.digestx.fnv : FNV;
2835     debug import std.exception : assertThrown, assertNotThrown;
2836     immutable n = 11;
2837 
2838     alias K = Nullable!(uint, uint.max);
2839     alias V = uint;
2840 
2841     alias X = HybridHashMap!(K, V, FNV!(64, true));
2842 
2843     auto s = X.withCapacity(n);
2844 
2845     void dummy(ref V value) {}
2846 
2847     debug assertThrown!RangeError(dummy(s[K(0)]));
2848 
2849     foreach (immutable i; 0 .. n)
2850     {
2851         const k = K(i);
2852         s[k] = V(i);
2853         debug assertNotThrown!RangeError(dummy(s[k]));
2854     }
2855 
2856     foreach (immutable i; 0 .. n)
2857     {
2858         const k = K(i);
2859         assert(s.remove(k));
2860         debug assertThrown!RangeError(dummy(s[k]));
2861     }
2862 
2863     s[K(0)] = V.init;
2864     auto vp = K(0) in s;
2865     debug static assert(is(typeof(vp) == V*));
2866     assert((*vp) == V.init);
2867 
2868     assert(s.remove(K(0)));
2869     assert(K(0) !in s);
2870 
2871     X t;
2872     t.reserveExtra(4096);
2873 
2874     t.clear();
2875 }
2876 
2877 /// class as value
2878 @safe pure unittest
2879 {
2880     import core.exception : RangeError;
2881     import std.typecons : Nullable;
2882     debug import std.exception : assertThrown, assertNotThrown;
2883     import nxt.digestx.fnv : FNV;
2884 
2885     immutable n = 11;
2886 
2887     alias K = Nullable!(uint, uint.max);
2888     class V
2889     {
2890         this(uint data) { this.data = data; }
2891         uint data;
2892     }
2893 
2894     alias X = HybridHashMap!(K, V, FNV!(64, true));
2895 
2896     auto s = X.withCapacity(n);
2897 
2898     void dummy(ref V value) {}
2899 
2900     debug assertThrown!RangeError(dummy(s[K(0)]));
2901 
2902     foreach (immutable i; 0 .. n)
2903     {
2904         const k = K(i);
2905         s[k] = new V(i);
2906         debug assertNotThrown!RangeError(dummy(s[k]));
2907     }
2908 
2909     // test range
2910     {
2911         auto sr = s.byKeyValue; // scoped range
2912         assert(sr.length == n);
2913         foreach (immutable i; 0 .. n)
2914         {
2915             sr.popFront();
2916             assert(sr.length == n - i - 1);
2917         }
2918     }
2919 
2920     foreach (immutable i; 0 .. n)
2921     {
2922         const k = K(i);
2923         assert(s.remove(k));
2924         debug assertThrown!RangeError(dummy(s[k]));
2925     }
2926 
2927     s[K(0)] = V.init;
2928     auto vp = K(0) in s;
2929     debug static assert(is(typeof(vp) == V*));
2930 
2931     assert(s.remove(K(0)));
2932     assert(K(0) !in s);
2933 
2934     X t;
2935     t.reserveExtra(4096);
2936 }
2937 
2938 /// constness inference of ranges
2939 pure nothrow unittest
2940 {
2941     import std.typecons : Nullable;
2942     import nxt.digestx.fnv : FNV;
2943 
2944     alias K = Nullable!(uint, uint.max);
2945     class V
2946     {
2947         this(uint data) { this.data = data; }
2948         uint data;
2949     }
2950 
2951     alias X = HybridHashMap!(K, V, FNV!(64, true));
2952     const x = X();
2953 
2954     foreach (const e; x.byKey)
2955     {
2956         debug static assert(is(typeof(e) == const(X.KeyType)));
2957     }
2958 
2959     foreach (const e; x.byValue)
2960     {
2961         debug static assert(is(typeof(e) == const(X.ValueType)));
2962     }
2963 
2964     foreach (const e; X.init.byValue)
2965     {
2966         debug static assert(is(typeof(e) == const(X.ValueType)));
2967     }
2968 
2969     foreach (const e; x.byKeyValue)
2970     {
2971         debug static assert(is(typeof(e.key) == const(X.KeyType)));
2972         debug static assert(is(typeof(e.value) == const(X.ValueType)));
2973         debug static assert(is(typeof(e) == const(X.ElementType)));
2974     }
2975 }
2976 
2977 /// range key constness and value mutability with `class` value
2978 pure nothrow unittest
2979 {
2980     import std.typecons : Nullable;
2981     import nxt.digestx.fnv : FNV;
2982 
2983     struct S
2984     {
2985         uint value;
2986     }
2987     alias K = Nullable!(S, S(uint.min)); // use uint.min to trigger use of faster `allocator.allocateZeroed`
2988 
2989     class V
2990     {
2991         this(uint data) { this.data = data; }
2992         uint data;
2993     }
2994 
2995     alias X = HybridHashMap!(K, V, FNV!(64, true));
2996     auto x = X();
2997 
2998     x[K(S(42))] = new V(43);
2999 
3000     assert(x.length == 1);
3001 
3002     foreach (e; x.byValue)      // `e` is auto ref
3003     {
3004         debug static assert(is(typeof(e) == X.ValueType)); // mutable access to value
3005         assert(e.data == 43);
3006 
3007         // value mutation side effects
3008         e.data += 1;
3009         assert(e.data == 44);
3010         e.data -= 1;
3011         assert(e.data == 43);
3012     }
3013 
3014     foreach (ref e; x.byKeyValue)   // `e` is auto ref
3015     {
3016         debug static assert(is(typeof(e.key) == const(X.KeyType))); // const access to key
3017         debug static assert(is(typeof(e.value) == X.ValueType)); // mutable access to value
3018 
3019         assert(e.key.value == 42);
3020         assert(e.value.data == 43);
3021 
3022         // key cannot be mutated
3023         debug static assert(!__traits(compiles, { e.key.value += 1; }));
3024 
3025         // value mutation side effects
3026         e.value.data += 1;
3027         assert(e.value.data == 44);
3028         e.value.data -= 1;
3029         assert(e.value.data == 43);
3030     }
3031 }
3032 
3033 /// range key constness and value mutability with `class` key and `class` value
3034 pure nothrow unittest
3035 {
3036     import nxt.digestx.fnv : FNV;
3037 
3038     class K
3039     {
3040         this(uint value)
3041         {
3042             this.value = value;
3043         }
3044 
3045         @property bool opEquals(const scope typeof(this) rhs) const
3046         {
3047             return value == rhs.value;
3048         }
3049 
3050         uint value;
3051     }
3052 
3053     class V
3054     {
3055         this(uint data) { this.data = data; }
3056         uint data;
3057     }
3058 
3059     alias X = HybridHashMap!(K, V, FNV!(64, true));
3060     auto x = X();
3061 
3062     x[new K(42)] = new V(43);
3063 
3064     assert(x.length == 1);
3065 
3066     foreach (e; x.byValue)      // `e` is auto ref
3067     {
3068         debug static assert(is(typeof(e) == X.ValueType)); // mutable access to value
3069         assert(e.data == 43);
3070 
3071         // value mutation side effects
3072         e.data += 1;
3073         assert(e.data == 44);
3074         e.data -= 1;
3075         assert(e.data == 43);
3076     }
3077 
3078     foreach (ref e; x.byKeyValue)   // `e` is auto ref
3079     {
3080         debug static assert(is(typeof(e.key) == X.KeyType)); // mutable access to class key
3081         debug static assert(is(typeof(e.value) == X.ValueType)); // mutable access to value
3082 
3083         assert(e.key.value == 42);
3084         assert(e.value.data == 43);
3085 
3086         // class key itself should not be mutable
3087         debug static assert(!__traits(compiles, { e.key = null; }));
3088 
3089         // members of key can be mutated
3090         debug static assert(__traits(compiles, { e.key.value += 1; }));
3091 
3092         // value mutation side effects
3093         e.value.data += 1;
3094         assert(e.value.data == 44);
3095         e.value.data -= 1;
3096         assert(e.value.data == 43);
3097     }
3098 }
3099 
3100 /// range key constness and value mutability with `class` key and `class` value
3101 pure nothrow unittest
3102 {
3103     import nxt.digestx.fnv : FNV;
3104     class K
3105     {
3106         this(uint value)
3107         {
3108             this.value = value;
3109         }
3110         uint value;
3111     }
3112 
3113     struct V
3114     {
3115         this(uint data) { this.data = data; }
3116         @disable this(this);
3117         uint data;
3118     }
3119 
3120     alias X = HybridHashMap!(K, V, FNV!(64, true));
3121     auto x = X();
3122 
3123     scope key42 = new K(42);
3124     x[key42] = V(43);
3125 
3126     assert(x.length == 1);
3127 
3128     foreach (ref e; x.byValue)  // `e` is auto ref
3129     {
3130         debug static assert(is(typeof(e) == X.ValueType)); // mutable access to value
3131         assert(e.data == 43);
3132 
3133         // value mutation side effects
3134         e.data += 1;
3135         assert(e.data == 44);
3136         e.data -= 1;
3137         assert(e.data == 43);
3138     }
3139 
3140     foreach (ref e; x.byKeyValue) // `e` is auto ref
3141     {
3142         debug static assert(is(typeof(e.key) == X.KeyType)); // mutable access to class key
3143         debug static assert(is(typeof(e.value) == X.ValueType)); // mutable access to value
3144 
3145         assert(e.key.value == 42);
3146         assert(e.value.data == 43);
3147 
3148         // value mutation side effects
3149         e.value.data += 1;
3150         assert(e.value.data == 44);
3151         e.value.data -= 1;
3152         assert(e.value.data == 43);
3153     }
3154 
3155     assert(x.length == 1);
3156 
3157     assert(x.remove(key42));
3158     assert(x.length == 0);
3159 
3160     x[key42] = V(43);
3161     assert(x.length == 1);
3162 }
3163 
3164 version(unittest)
3165 {
3166     T make(T)(ulong value)
3167     {
3168         static if (is(T == class))
3169             return new T(value);
3170         else
3171             return T(value);
3172     }
3173 }
3174 
3175 /// test various things
3176 @trusted unittest
3177 {
3178     import std.meta : AliasSeq;
3179     import std.typecons : Nullable;
3180     import std.algorithm.comparison : equal;
3181     import nxt.container.traits : mustAddGCRange;
3182     import nxt.digestx.fnv : FNV;
3183     import nxt.array_help : s;
3184 
3185     const n = 100;
3186 
3187     void testEmptyAll(K, V, X)(ref X x, size_t n,
3188                                scope K[] keys)
3189     {
3190         assert(x.length == n);
3191         foreach (key; keys)
3192         {
3193             static if (X.hasValue)
3194                 const element = X.ElementType(key, V.init);
3195             else
3196                 alias element = key;
3197 
3198             assert(x.length == n - key.get);
3199 
3200             const hitPtr = key in x;
3201             static if (X.hasValue)
3202                 assert(hitPtr && *hitPtr is element.value);
3203             else
3204                 assert(hitPtr && *hitPtr is element);
3205 
3206             assert(x.remove(key));
3207             assert(x.length == n - key.get - 1);
3208 
3209             static if (!X.hasValue)
3210             {
3211                 assert(!x.contains(key));
3212                 assert(!x.containsUsingLinearSearch(key));
3213             }
3214             assert(key !in x);
3215             assert(!x.remove(key));
3216             assert(x.length == n - key.get - 1);
3217         }
3218 
3219         assert(x.length == 0);
3220 
3221         x.clear();
3222         assert(x.length == 0);
3223     }
3224 
3225     X testDup(X)(scope ref X x, size_t n)
3226     {
3227         typeof(return) y = x.dup;
3228 
3229         assert(x._store.ptr !is y._store.ptr);
3230         assert(x.length == y.length);
3231         assert(y.length == n);
3232         // non-symmetric algorithm so both are needed
3233         assert(y == x);
3234         assert(x == y);
3235 
3236         static if (X.hasValue)
3237         {
3238             assert(equal(x.byKey,
3239                          y.byKey));
3240             assert(equal(x.byValue,
3241                          y.byValue));
3242             auto a = x.byKeyValue;
3243             auto b = y.byKeyValue;
3244             size_t i = 0;
3245             while (!a.empty &&
3246                    !b.empty)
3247             {
3248                 a.popFront();
3249                 b.popFront();
3250                 i++;
3251             }
3252 			auto xR = x.byKeyValue;
3253 			auto yR = y.byKeyValue;
3254 			assert(xR.length == yR.length);
3255 			size_t ix = 0;
3256 			while (!xR.empty &&
3257 				   !yR.empty)
3258 			{
3259 				auto xK = xR.front.key;
3260 				auto yK = yR.front.key;
3261 				auto xV = xR.front.value;
3262 				auto yV = yR.front.value;
3263 				// import std.stdio : writeln;
3264 				// writeln("ix:", ix, " xV:", xV, " yV:", yV);
3265 				assert(xK == yK);
3266 				assert(xV == yV);
3267 				assert(xR.front == yR.front);
3268 				xR.popFront();
3269 				yR.popFront();
3270 				ix++;
3271 			}
3272             assert(equal(x.byKeyValue,
3273                          y.byKeyValue));
3274         }
3275         else
3276             assert(equal(x.byElement,
3277                          y.byElement));
3278 
3279         debug static assert(!__traits(compiles, { const _ = x < y; })); // no ordering
3280 
3281         return y;
3282     }
3283 
3284     alias NullableUlong = Nullable!(ulong, ulong.max);
3285 
3286     static class SomeSimpleClass
3287     {
3288         @safe pure nothrow @nogc
3289         this(ulong value)
3290         {
3291             this._value = value;
3292         }
3293 
3294         @safe pure nothrow @nogc
3295         ulong get() const
3296         {
3297             return _value;
3298         }
3299 
3300         void toString(Sink)(ref scope Sink sink) const
3301         {
3302             import std.format : formattedWrite;
3303             sink.formattedWrite(typeof(this).stringof, "(%s)", _value);
3304         }
3305 
3306         @property bool opEquals(const scope typeof(this) rhs) const
3307         {
3308             return _value == rhs._value;
3309         }
3310 
3311         private ulong _value;
3312     }
3313 
3314     debug static assert(mustAddGCRange!string);
3315 
3316     foreach (K; AliasSeq!(SomeSimpleClass,
3317                           NullableUlong))
3318     {
3319         foreach (V; AliasSeq!(string, int, void))
3320         {
3321             alias X = HybridHashMap!(K, V, FNV!(64, true));
3322 
3323             static if (!X.hasValue)
3324             {
3325                 auto k11 = make!K(11);
3326                 auto k12 = make!K(12);
3327                 auto k13 = make!K(13);
3328 
3329                 auto x = X.withElements([k11, k12, k13].s);
3330 
3331                 import std.algorithm : count;
3332 
3333                 // ByLvalueElement
3334                 auto xr = x.byElement;
3335 
3336                 alias R = typeof(xr);
3337                 import std.range.primitives : isInputRange;
3338                 import std.traits : ReturnType;
3339                 debug static assert(is(typeof(R.init) == R));
3340                 debug static assert(is(ReturnType!((R xr) => xr.empty) == bool));
3341 
3342                 // TODO: Is this needed? debug static assert(!__traits(compiles, { xr.front == K.init; })); // always head-const
3343                 auto f = xr.front;
3344                 static if (is(K == class))
3345                 {
3346                     debug static assert(is(typeof(f) == K)); // tail-mutable
3347                 }
3348                 else
3349                 {
3350                     debug static assert(is(typeof(f) == const(K))); // tail-const
3351                 }
3352 
3353                 debug static assert(is(typeof((R xr) => xr.front)));
3354                 debug static assert(!is(ReturnType!((R xr) => xr.front) == void));
3355                 debug static assert(is(typeof((R xr) => xr.popFront)));
3356 
3357                 debug static assert(isInputRange!(typeof(xr)));
3358 
3359                 assert(x.byElement.count == 3);
3360 
3361                 X y;
3362                 size_t ix = 0;
3363                 foreach (ref e; x.byElement)
3364                 {
3365                     assert(x.contains(e));
3366                     assert(x.containsUsingLinearSearch(e));
3367                     assert(!y.contains(e));
3368                     assert(!y.containsUsingLinearSearch(e));
3369                     static if (is(K == class))
3370                         y.insert(cast(K)e); // ugly but ok in tests
3371                     else
3372                         y.insert(e);
3373                     assert(y.contains(e));
3374                     assert(y.containsUsingLinearSearch(e));
3375                     ix++;
3376                 }
3377 
3378                 assert(y.byElement.count == 3);
3379                 assert(x == y);
3380 
3381                 const z = X();
3382                 assert(z.byElement.count == 0);
3383 
3384                 immutable w = X();
3385                 assert(w.byElement.count == 0);
3386 
3387                 {
3388                     auto xc = X.withElements([k11, k12, k13].s);
3389                     assert(xc.length == 3);
3390                     assert(xc.contains(k11));
3391                     assert(xc.containsUsingLinearSearch(k11));
3392 
3393                     // TODO: http://forum.dlang.org/post/kvwrktmameivubnaifdx@forum.dlang.org
3394                     xc.removeAllMatching!(_ => _ == k11);
3395 
3396                     assert(xc.length == 2);
3397                     assert(!xc.contains(k11));
3398                     assert(!xc.containsUsingLinearSearch(k11));
3399 
3400                     xc.removeAllMatching!(_ => _ == k12);
3401                     assert(!xc.contains(k12));
3402                     assert(!xc.containsUsingLinearSearch(k12));
3403                     assert(xc.length == 1);
3404 
3405                     xc.removeAllMatching!(_ => _ == k13);
3406                     assert(!xc.contains(k13));
3407                     assert(!xc.containsUsingLinearSearch(k13));
3408                     assert(xc.length == 0);
3409 
3410                     // this is ok
3411                     foreach (_; xc.byElement) {}
3412                 }
3413 
3414                 {               // ByRvalueElement
3415                     auto k = X.withElements([k11, k12].s).filtered!(_ => _ != k11).byElement;
3416                     debug static assert(isInputRange!(typeof(k)));
3417                     assert(k.front == k12);
3418 
3419                     debug static assert(!__traits(compiles, { k.front = K.init; })); // head-const
3420                     static if (is(K == class))
3421                     {
3422                         debug static assert(is(typeof(k.front) == K)); // tail-mutable
3423                     }
3424                     else
3425                     {
3426                         debug static assert(is(typeof(k.front) == const(K))); // tail-const
3427                     }
3428 
3429                     k.popFront();
3430                     assert(k.empty);
3431                 }
3432 
3433                 {
3434                     X q;
3435                     auto qv = [make!K(11U), make!K(12U), make!K(13U), make!K(14U)].s;
3436                     q.insertN(qv[]);
3437                     foreach (e; qv[])
3438                     {
3439                         assert(q.contains(e));
3440                         assert(q.containsUsingLinearSearch(e));
3441                     }
3442                     q.clear();
3443                     assert(q.empty);
3444                 }
3445             }
3446 
3447             static if (is(V == string))
3448             {
3449                 debug static assert(mustAddGCRange!V);
3450                 debug static assert(mustAddGCRange!(V[1]));
3451                 debug static assert(mustAddGCRange!(X.T));
3452             }
3453 
3454             auto x1 = X();            // start empty
3455 
3456             // fill x1
3457 
3458             import std.array : Appender;
3459             Appender!(K[]) keys;
3460 
3461             foreach (immutable key_; 0 .. n)
3462             {
3463                 auto key = make!K(key_);
3464                 keys.put(key);
3465 
3466                 // create elements
3467                 static if (X.hasValue)
3468                 {
3469                     auto value = V.init;
3470                     auto element = X.ElementType(key, value);
3471                 }
3472                 else
3473                     // no assignment because Nullable.opAssign may leave rhs in null state
3474                     auto element = key;
3475 
3476                 assert(key !in x1);
3477 
3478                 assert(x1.length == key.get);
3479                 assert(x1.insert(element) == X.InsertionStatus.added);
3480                 assert(x1.length == key.get + 1);
3481 
3482                 static if (X.hasValue)
3483                 {
3484                     import std.conv : to;
3485                     auto e2 = X.ElementType(key, (42 + key_).to!V);
3486                     assert(x1.insert(e2) == X.InsertionStatus.modified);
3487                     assert(x1.contains(key));
3488                     assert(x1.get(key, V.init) == (42 + key_).to!V);
3489 
3490                     assert(x1.remove(key));
3491                     assert(!x1.contains(key));
3492 
3493                     x1[key] = value; // restore value
3494                     assert(x1.contains(key));
3495                 }
3496 
3497                 assert(x1.length == key.get + 1);
3498 
3499                 const hitPtr = key in x1;
3500                 static if (X.hasValue)
3501                     assert(hitPtr && *hitPtr == value);
3502                 else
3503                     assert(hitPtr && *hitPtr is key);
3504 
3505                 auto status = x1.insert(element);
3506                 assert(status == X.InsertionStatus.unmodified);
3507                 static if (X.hasValue)
3508                     assert(x1.insert(key, value) == X.InsertionStatus.unmodified);
3509                 assert(x1.length == key.get + 1);
3510 
3511                 assert(key in x1);
3512             }
3513 
3514             static if (X.hasValue)
3515             {
3516                 import nxt.container.dynamic_array : Array = DynamicArray;
3517                 Array!(X.ElementType) a1; // remember the keys
3518 
3519                 foreach (const ref key; x1.byKey)
3520                 {
3521                     auto keyPtr = key in x1;
3522                     assert(keyPtr);
3523                     a1 ~= X.ElementType(cast(K)key, (*keyPtr));
3524                 }
3525 
3526                 assert(x1.length == a1.length);
3527 
3528                 foreach (ae; a1[])
3529                 {
3530                     auto keyPtr = ae.key in x1;
3531                     assert(keyPtr);
3532                     assert((*keyPtr) is ae.value);
3533                 }
3534             }
3535 
3536             assert(x1.length == n);
3537 
3538             auto x2 = testDup(x1, n);
3539 
3540             testEmptyAll!(K, V)(x1, n, keys.data);
3541 
3542             testEmptyAll!(K, V)(x2, n, keys.data); // should be not affected by emptying of x1
3543         }
3544     }
3545 }
3546 
3547 ///
3548 @safe pure nothrow @nogc unittest
3549 {
3550     import std.typecons : Nullable;
3551     import nxt.digestx.fnv : FNV;
3552 
3553     alias X = HybridHashMap!(Nullable!(size_t, size_t.max), size_t, FNV!(64, true));
3554     X x;
3555     assert(x.empty);
3556     // import nxt.container.dynamic_array : Array = DynamicArray;
3557     // TODO: these segfault:
3558     // TODO: auto a = Array!(X.KeyType).withElementsOfRange_untested(x.byKey); // l-value byKey
3559     // TODO: auto b = Array!(X.KeyType).withElementsOfRange_untested(X().byKey); // r-value byKey
3560 }
3561 
3562 /// manual Nullable type
3563 @safe pure unittest
3564 {
3565     import nxt.digestx.fnv : FNV;
3566 
3567     static class Zing
3568     {
3569         @safe pure nothrow @nogc:
3570         this(ulong value) { this._value = value; }
3571         private ulong _value;
3572     }
3573     debug static assert(isNullable!Zing);
3574 
3575     enum Alt
3576     {
3577         unknown,
3578         a,
3579         b,
3580         c,
3581         d
3582     }
3583 
3584     struct ZingRelation
3585     {
3586         Zing zing;
3587         Alt alts;
3588 
3589         alias nullifier = zing;
3590         static immutable nullValue = typeof(this).init;
3591 
3592         bool opEquals(const scope typeof(this) that) const @safe pure nothrow @nogc
3593         {
3594             return (this.zing is that.zing &&
3595                     this.alts == that.alts);
3596         }
3597     }
3598     debug static assert(isNullable!ZingRelation);
3599 
3600     alias X = HybridHashSet!(ZingRelation, FNV!(64, true));
3601     debug static assert(X.sizeof == 32); // TODO: fix hole handling and change to 24
3602     X x;
3603 
3604     scope e = ZingRelation(new Zing(42), Alt.init);
3605 
3606     assert(!x.contains(e));
3607     assert(!x.containsUsingLinearSearch(e));
3608     assert(x.insert(e) == X.InsertionStatus.added);
3609     assert(x.contains(e));
3610     assert(x.containsUsingLinearSearch(e));
3611 }
3612 
3613 /// abstract class value type
3614 @safe unittest
3615 {
3616     static abstract class Zing
3617     {
3618         @safe pure nothrow @nogc:
3619     }
3620     static class Node : Zing
3621     {
3622         @safe pure nothrow @nogc:
3623     }
3624 
3625     alias X = HybridHashSet!(Zing);
3626     X x;
3627 
3628     const Zing cz = new Node();
3629     x.insert(cz);               // ok to insert const
3630 
3631     Zing z = new Node();
3632     x.insert(z); // ok to insert mutable because hashing is on address by default
3633 }
3634 
3635 /// class type with default hashing
3636 @safe unittest
3637 {
3638     static class Base
3639     {
3640         static size_t dtorCount = 0; // number of calls to this destructor
3641     @safe nothrow @nogc:
3642         ~this() nothrow @nogc { dtorCount += 1; }
3643     pure:
3644         this(ulong value) { this._value = value; }
3645         @property bool opEquals(const scope typeof(this) rhs) const
3646         {
3647             return _value == rhs._value;
3648         }
3649         override hash_t toHash() const
3650         {
3651             return hashOf(_value);
3652         }
3653         private ulong _value;
3654     }
3655 
3656     /** Node containing same data members but different type. */
3657     static class Node : Base
3658     {
3659         @safe pure nothrow @nogc:
3660         this(ulong value) { super(value);  }
3661     }
3662     debug static assert(is(Node : Base));
3663 
3664     import nxt.hash_functions : hashOfPolymorphic; // neede to separate hash of `Base(N)` from `Node(N)`
3665     alias X = HybridHashSet!(Base, hashOfPolymorphic, "a && b && (typeid(a) is typeid(b)) && a.opEquals(b)");
3666     debug static assert(X.sizeof == 24);
3667     X x;
3668 
3669     // top-class
3670     scope b42 = new Base(42);
3671     assert(!x.contains(b42));
3672     assert(!x.containsUsingLinearSearch(b42));
3673     assert(x.insert(b42) == X.InsertionStatus.added);
3674     assert(x.contains(b42));
3675     assert(x.containsUsingLinearSearch(b42));
3676     assert(x.tryGetElementFromCtorParams!Base(42) !is null);
3677     assert(Base.dtorCount == 1);
3678     assert(x.tryGetElementFromCtorParams!Base(42)._value == 42);
3679     assert(Base.dtorCount == 2);
3680     assert(x.tryGetElementFromCtorParams!Base(41) is null);
3681     assert(Base.dtorCount == 3);
3682 
3683     // top-class
3684     scope b43 = new Base(43);
3685     assert(!x.contains(b43));
3686     assert(!x.containsUsingLinearSearch(b43));
3687     assert(x.insert(b43) == X.InsertionStatus.added);
3688     assert(x.contains(b43));
3689     assert(x.containsUsingLinearSearch(b43));
3690     assert(x.tryGetElementFromCtorParams!Base(43) !is null);
3691     assert(Base.dtorCount == 4);
3692     assert(x.tryGetElementFromCtorParams!Base(43)._value == 43);
3693     assert(Base.dtorCount == 5);
3694 
3695     // sub-class
3696     assert(x.tryGetElementFromCtorParams!Node(42) is null);
3697     assert(Base.dtorCount == 6);
3698     immutable n42 = new Node(42);
3699     assert(!x.contains(n42));     // mustn't equal to `b42`
3700     assert(!x.containsUsingLinearSearch(n42)); // mustn't equal to `b42`
3701     assert(x.insert(n42) == X.InsertionStatus.added); // added as separate type
3702     assert(x.contains(n42));
3703     assert(x.containsUsingLinearSearch(n42));
3704     assert(x.tryGetElementFromCtorParams!Node(42) !is null);
3705     assert(Base.dtorCount == 7);
3706     assert(x.tryGetElementFromCtorParams!Node(42)._value == 42);
3707     assert(Base.dtorCount == 8);
3708 
3709     assert(hashOf(b42) == hashOf(n42));
3710 
3711     // sub-class
3712     assert(x.tryGetElementFromCtorParams!Node(43) is null);
3713     assert(Base.dtorCount == 9);
3714     auto n43 = new Node(43);
3715     assert(!x.contains(n43));     // mustn't equal to `b43`
3716     assert(!x.containsUsingLinearSearch(n43)); // mustn't equal to `b43`
3717     assert(x.insert(n43) == X.InsertionStatus.added); // added as separate type
3718     assert(x.contains(n43));
3719     assert(x.containsUsingLinearSearch(n43));
3720     assert(x.tryGetElementFromCtorParams!Node(43) !is null);
3721     assert(Base.dtorCount == 10);
3722     assert(x.tryGetElementFromCtorParams!Node(43)._value == 43);
3723     assert(Base.dtorCount == 11);
3724 
3725     assert(hashOf(b43) == hashOf(n43));
3726 }
3727 
3728 /// enumeration key
3729 @safe pure unittest
3730 {
3731     import nxt.digestx.fnv : FNV;
3732 
3733     enum Alt
3734     {
3735         nullValue,              // trait
3736         a, b, c, d
3737     }
3738     alias X = HybridHashSet!(Alt, FNV!(64, true));
3739     X x;
3740     assert(!x.contains(Alt.a));
3741 
3742     assert(x.insert(Alt.a) == X.InsertionStatus.added);
3743 
3744     assert(x.contains(Alt.a));
3745     assert(x.containsUsingLinearSearch(Alt.a));
3746     assert(!x.contains(Alt.b));
3747     assert(!x.contains(Alt.c));
3748     assert(!x.contains(Alt.d));
3749     assert(!x.containsUsingLinearSearch(Alt.b));
3750     assert(!x.containsUsingLinearSearch(Alt.c));
3751     assert(!x.containsUsingLinearSearch(Alt.d));
3752 
3753     assert(x.remove(Alt.a));
3754     assert(!x.contains(Alt.a));
3755     assert(!x.containsUsingLinearSearch(Alt.a));
3756 }
3757 
3758 ///
3759 @safe pure nothrow unittest
3760 {
3761     import nxt.digestx.fnv : FNV;
3762     static struct Rel
3763     {
3764         static immutable nullValue = typeof(this).init;
3765         string name;            // relation name. WARNING compiler crashes when qualified with `package`
3766     }
3767     alias X = HybridHashSet!(Rel, FNV!(64, true));
3768     X x;
3769     foreach (const i; 0 .. 100)
3770     {
3771         const char[1] ch = ['a' + i];
3772         assert(!x.contains(Rel(ch.idup)));
3773         assert(!x.containsUsingLinearSearch(Rel(ch.idup)));
3774         x.insert(Rel(ch.idup));
3775         assert(x.contains(Rel(ch.idup)));
3776         /* TODO: assert(x.containsUsingLinearSearch(Rel(ch.idup))); */
3777     }
3778 }
3779 
3780 /// `SSOString` as set key type
3781 @safe pure nothrow @nogc unittest
3782 {
3783     import nxt.sso_string : SSOString;
3784     import nxt.digestx.fnv : FNV;
3785 
3786     alias K = SSOString;
3787     static assert(isHoleable!K);
3788     alias X = HybridHashSet!(K, FNV!(64, true));
3789     const n = 100;
3790 
3791     X a;
3792     foreach (const i; 0 .. n)
3793     {
3794         const char[1] ch = ['a' + i];
3795         const k = K(ch);        // @nogc
3796 
3797         assert(!a.contains(k));
3798         assert(!a.containsUsingLinearSearch(k));
3799 
3800         assert(a.insert(K(ch)) == X.InsertionStatus.added);
3801         // TODO: assert(a.insertAndReturnElement(K(ch)) == k);
3802         assert(a.contains(k));
3803         assert(a.containsUsingLinearSearch(k));
3804 
3805         assert(a.remove(k));
3806         assert(!a.contains(k));
3807         assert(a.insert(K(ch)) == X.InsertionStatus.added);
3808 
3809         assert(a.remove(ch[]));
3810         assert(!a.contains(k));
3811         assert(a.insert(K(ch)) == X.InsertionStatus.added);
3812     }
3813 
3814     X b;
3815     foreach (const i; 0 .. n)
3816     {
3817         const char[1] ch = ['a' + (n - 1 - i)];
3818         const k = K(ch);        // @nogc
3819 
3820         assert(!b.contains(k));
3821         assert(!b.containsUsingLinearSearch(k));
3822 
3823         assert(b.insert(K(ch)) == X.InsertionStatus.added);
3824         // TODO: assert(b.insertAndReturnElement(K(ch)) == k);
3825 
3826         assert(b.contains(k));
3827         assert(b.containsUsingLinearSearch(k));
3828 
3829         assert(b.remove(k));
3830         assert(!b.contains(k));
3831 
3832         assert(b.insert(K(ch)) == X.InsertionStatus.added);
3833     }
3834 
3835     assert(a == b);
3836 
3837     const us = K("_");
3838     assert(!a.contains(us));
3839     a ~= us;
3840     assert(a.contains(us));
3841 }
3842 
3843 /// test `opIndexOpAssign`
3844 @safe pure nothrow unittest
3845 {
3846     import nxt.sso_string : SSOString;
3847     import nxt.digestx.fnv : FNV;
3848 
3849     alias K = SSOString;
3850     alias V = long;
3851     alias X = HybridHashMap!(K, V, FNV!(64, true));
3852 
3853     X x;
3854 
3855     const a = K("a");
3856     const b = K("b");
3857 
3858     x[a] = 17;
3859     assert(x[a] == 17);
3860 
3861     x[a] += 10;                 // opIndexOpAssign!("+=") with existing key
3862     assert(x[a] == 27);
3863 
3864     x[b] += 10;                 // opIndexOpAssign!("+=") with non-existing key
3865     assert(x[b] == 10);
3866 
3867     x[b] *= 10;                 // opIndexOpAssign!("*=") with non-existing key
3868     assert(x[b] == 100);
3869 
3870     assert(x.length == 2);
3871 
3872     assert(x.contains(a));
3873     assert(x.contains(a[]));
3874     assert(a in x);
3875     assert(a[] in x);
3876 
3877     assert(x.contains(b));
3878     assert(x.contains(b[]));
3879     assert(b in x);
3880     assert(b[] in x);
3881 
3882     const c = K("c");
3883     assert(!x.contains(c));
3884     assert(!x.contains(c[]));
3885     assert(c !in x);
3886     assert(c[] !in x);
3887 }
3888 
3889 /// use prime numbers as capacity
3890 @safe pure unittest
3891 {
3892     import nxt.address : AlignedAddress;
3893     alias K = AlignedAddress!1;
3894     alias V = size_t;
3895     enum bool usePrimeCapacity = false; // TODO: enable
3896     alias M = HybridHashMap!(K, V, hashOf, defaultKeyEqualPredOf!K, Mallocator, false, true, usePrimeCapacity);
3897     M x;
3898     assert(x.empty);
3899 }
3900 
3901 /// `SSOString` as map key type
3902 @safe pure nothrow @nogc unittest
3903 {
3904     import nxt.sso_string : SSOString;
3905     import nxt.digestx.fnv : FNV;
3906     alias K = SSOString;
3907     alias V = long;
3908     alias X = HybridHashMap!(K, V, FNV!(64, true));
3909     const n = 100;
3910 
3911     immutable default_k = K("miss");
3912 
3913     X a;
3914 
3915     // insert all
3916     foreach (const i; 0 .. n)
3917     {
3918         const char[1] ch = ['a' + i];
3919         const k = K(ch);        // @nogc
3920         assert(k[] == ch[]);
3921 
3922         assert(!a.contains(k));
3923         assert(!a.contains(ch[]));                          // @nogc
3924         assert(a.getKeyRef(k, default_k)[] is default_k[]); // on miss use `default_k`
3925         // TODO: assert(a.getKeyRef(ch, default_k)[] is default_k[]); // on miss use `default_k`
3926 
3927         a[k] = V.init;
3928 
3929         assert(a.contains(k));
3930         assert(a.contains(ch[]));                    // @nogc
3931         assert(a.getKeyRef(k, default_k)[] !is k[]); // on hit doesn't use `default_k`
3932         assert(a.getKeyRef(k, default_k)[] == ch);
3933         // TODO: assert(a.getKeyRef(ch, default_k)[] !is k[]); // on hit doesn't use `default_k`
3934         // assert(a.getKeyRef(ch, default_k)[] == ch);
3935     }
3936     assert(a.length == n);
3937 
3938     // remove all
3939     foreach (const i; 0 .. n)
3940     {
3941         const char[1] ch = ['a' + i];
3942         const k = K(ch);        // @nogc
3943         assert(a.contains(k));
3944         assert(a.remove(k));
3945         assert(!a.contains(k));
3946     }
3947     assert(a.length == 0);
3948 
3949     // insert all again
3950     foreach (const i; 0 .. n)
3951     {
3952         const char[1] ch = ['a' + i];
3953         const k = K(ch);        // @nogc
3954         assert(k[] == ch[]);
3955 
3956         assert(!a.contains(k));
3957         assert(!a.contains(ch[]));                          // @nogc
3958         assert(a.getKeyRef(k, default_k)[] is default_k[]); // on miss use `default_k`
3959         // TODO: assert(a.getKeyRef(ch, default_k)[] is default_k[]); // on miss use `default_k`
3960 
3961         a[k] = V.init;
3962     }
3963     assert(a.length == n);
3964 
3965     X b;
3966     foreach (const i; 0 .. n)
3967     {
3968         const char[1] ch = ['a' + (n - 1 - i)];
3969         const k = K(ch);        // @nogc
3970 
3971         assert(!b.contains(k));
3972 
3973         b[k] = V.init;
3974 
3975         assert(b.contains(k));
3976     }
3977 
3978     assert(a == b);
3979 }
3980 
3981 ///
3982 @safe pure nothrow @nogc unittest
3983 {
3984     import nxt.address : AlignedAddress;
3985 	alias A = AlignedAddress!1;
3986     HybridHashMap!(A, A) m;
3987     static assert(m.sizeof == 3*size_t.sizeof); // assure that hole bitmap is not used
3988     foreach (const address; 1 .. 0x1000)
3989     {
3990         const key = address;
3991         const value = 2*address;
3992         assert(A(key) !in m);
3993         m[A(key)] = A(value);
3994 		const eq = m[A(key)] == A(value);
3995         assert(eq);
3996         assert(A(key) in m);
3997     }
3998 }
3999 
4000 /** Is `true` iff `T` is a memory address (either a `class` or a pointer). */
4001 enum bool isAddress(T) = (is(T == class) ||
4002                           (is(T == U*, U) &&
4003                            // exclude alias this:
4004                            !(is(T == struct) ||
4005                              is(T == union) ||
4006                              is(T == interface))));
4007 
4008 /** Is `true` iff `T` has a specific value dedicated to representing holes
4009  * (removed/erase) values.
4010  */
4011 enum isHoleable(T) = (// __traits(hasMember, T, "isHole") &&
4012                       // __traits(hasMember, T, "holeify") &&
4013     __traits(hasMember, T, "holeValue"));
4014 
4015 /** Default key equality/equivalence predicate for the type `T`.
4016  */
4017 template defaultKeyEqualPredOf(T)
4018 {
4019     static if (is(T == class))
4020         // static assert(__traits(hasMember, T, "opEquals"),
4021         //               "Type" ~ T.stringof ~ " doesn't have local opEquals() defined");
4022         // enum defaultKeyEqualPredOf = "a && b && a.opEquals(b)";
4023         enum defaultKeyEqualPredOf = "a is b";
4024         // (const T a, const T b) => ((a !is null) && (b !is null) && a.opEquals(b));
4025     else
4026         enum defaultKeyEqualPredOf = "a == b";
4027 }
4028 
4029 ///
4030 @safe pure nothrow unittest
4031 {
4032     class C
4033     {
4034         @safe pure nothrow @nogc:
4035         this(int x)
4036         {
4037             this.x = x;
4038         }
4039         @property bool opEquals(const scope typeof(this) rhs) const
4040         {
4041             return x == rhs.x;
4042         }
4043         @property override bool opEquals(const scope Object rhs) const @trusted
4044         {
4045             C rhs_ = cast(C)rhs;
4046             return rhs_ && x == rhs_.x;
4047         }
4048         int x;
4049     }
4050     static assert(defaultKeyEqualPredOf!(C) == "a is b");
4051 }