1 module nxt.sso_hashmap_or_hashset;
2 
3 import nxt.container_traits;
4 
5 /** Set insertion status.
6  */
7 enum SetInsertionStatus
8 {
9     added,                      // element was added
10     unmodified                  // element was left unchanged
11 }
12 
13 /** Map insertion status.
14  */
15 enum MapInsertionStatus
16 {
17     added,                      // element was added
18     modified,                   // value of element was changed (map only). TODO only for Map-case
19     unmodified                  // element was left unchanged
20 }
21 
22 /** Hash set (or map) storing (key) elements of type `K` and values of type `V`.
23  *
24  * Uses small-size-optimized (SSO) arrays as bins, having a more stable
25  * worst-case performance than open-addressing.
26  *
27  * Params:
28  *      K = key type.
29  *      V = value type.
30  *      Allocator = memory allocator for bin array and large bins (`LargeBin`)
31  *      hasher = hash function or std.digest Hash.
32  *      smallBinMinCapacity = minimum capacity of small bin
33  *
34  * See_Also: https://probablydance.com/2017/02/26/i-wrote-the-fastest-hashtable/
35  *
36  * TODO support HashSet-in operator: assert(*("a" in s) == "a");
37  *
38  * TODO in non-sso optimized store add flag similar to Nullable that reserves a
39  * specific value for key that indicates that slot is unused. Use this when
40  * storing indexes in knet.storage
41  *
42  * TODO add extractElement that moves it out similar to
43  * http://en.cppreference.com/w/cpp/container/unordered_set/extract
44  *
45  * TODO add merge or union algorithm here or into container_algorithm.d. See
46  * also: http://en.cppreference.com/w/cpp/container/unordered_set/merge. this
47  * algorithm moves elements from source if they are not already in `this`
48  *
49  * TODO adjust rehashing to occur when relative number of LargeBuckets is
50  * larger than, say, 1/10. Experiment with different ratios.
51  *
52  * TODO add template parameter `alias nullKeyValue` that avoids having to store
53  * `bstates` when smallBinCapacity == 1, similar to:
54  *     std.typecons.nullable(alias nullValue, T)( T t )
55  *
56  * TODO add flag for use of growth factor smaller than powers of two. use prime_modulo.d
57  *
58  * TODO use core.bitop : bsr, bsl to find first empty element in bin. if as fast
59  * as current find use it to optimize remove()
60  *
61  * TODO Avoid extra length and capacity in _statuses (length or large) by making
62  * it allocate in sync with bins (using soa.d).
63  *
64  * TODO also try allocating values in a separate array using soa.d and see if
65  * benchmarks become better
66  *
67  * TODO growWithExtraCapacity(): if allocator has realloc we can do rehashing in-place similar to
68  * reordering in in-place radix (integer_sorting.d), otherwise rehash into new
69  * copy of bins and free old bins when done. If bin element count is >
70  * 1 this is more complicated since each bin contains a set of elements to
71  * swap out and must be put in a queue.
72  *
73  * TODO benchmark against https://github.com/greg7mdp/sparsepp
74  */
75 struct SSOHashMapOrSet(K, V = void,
76                        alias Allocator = null,
77                        alias hasher = hashOf,
78                        uint smallBinMinCapacity = 1,
79                        uint capacityScaleNumerator = 2,
80                        uint capacityScaleDenominator = 1)
81     if (// !hasAliasing!K &&
82         smallBinMinCapacity >= 1) // no use having empty small bins
83 {
84     import core.lifetime : emplace, move, moveEmplace;
85     import core.internal.traits : hasElaborateCopyConstructor, hasElaborateDestructor, Unqual;
86     import nxt.emplace_all : moveEmplaceAllNoReset;
87     import std.traits : isMutable, hasIndirections;
88     import std.algorithm.comparison : max;
89     // TODO activate and use import nxt.prime_modulo;
90 
91     /** In the hash map case, `V` is non-void, and a value is stored alongside
92      * the key of type `K`.
93      */
94     enum hasValue = !is(V == void);
95 
96     alias MutableThis = Unqual!(typeof(this));
97     alias ConstThis = const(MutableThis);
98 
99     pragma(inline):
100 
101     /// Element type.
102     static if (hasValue)
103     {
104         alias InsertionStatus = MapInsertionStatus;
105 
106         /// Constant element reference with both constant key and value.
107         struct T
108         {
109             K key;
110             V value;
111         }
112 
113         /// Mutable element reference with constant key and mutable value.
114         struct CT
115         {
116             const K key;
117             V value;
118         }
119 
120         /// Get key part of element.
121         pragma(inline, true)
122         static auto ref inout(K) keyOf()(auto ref return inout(T) element)
123         {
124             return element.key;
125         }
126 
127         /// Get reference to key part of `element`.
128         pragma(inline, true)
129         static ref inout(K) keyRefOf()(ref return inout(T) element) // template-lazy
130         {
131             return element.key;
132         }
133 
134         /// Get value part of element.
135         pragma(inline, true)
136         static auto ref inout(V) valueOf()(auto ref return inout(T) element)
137         {
138             return element.value;
139         }
140 
141         /** Type of key stored. */
142         alias KeyType = K;
143 
144         /** Type of value stored. */
145         alias ValueType = V;
146 
147         enum keyEqualPred = "a.key is b";
148     }
149     else                        // HashSet
150     {
151         alias InsertionStatus = SetInsertionStatus;
152 
153         alias T = K;
154 
155         /// Get key part of element.
156         pragma(inline, true)
157         static auto ref inout(K) keyOf()(auto ref return inout(T) element)
158         {
159             return element;
160         }
161 
162         /// Get reference to key part of `element`.
163         pragma(inline, true)
164         static ref inout(K) keyRefOf()(ref return inout(T) element) // template-lazy
165         {
166             return element;
167         }
168 
169         enum keyEqualPred = "a is b";
170     }
171 
172     alias ElementType = T;
173 
174     /** Make with room for storing at least `capacity` number of elements.
175      */
176     static typeof(this) withCapacity()(size_t capacity) // template-lazy
177     {
178         version(LDC) pragma(inline, true);
179         return typeof(return)(capacity);
180     }
181 
182     import std.traits : isIterable;
183 
184     /** Make with `elements`. */
185     static typeof(this) withElements(R)(R elements)
186         if (isIterable!R)
187     {
188         import std.range.primitives : hasLength;
189         static if (hasLength!R)
190         {
191             typeof(this) that = withCapacity(elements.length);
192         }
193         else
194         {
195             typeof(this) that;  // TODO if `isForwardRange` count elements
196         }
197         foreach (ref element; elements)
198         {
199             that.insert(element);
200         }
201         return that;
202     }
203 
204     private static typeof(this) withBinCount()(size_t binCount) // template-lazy
205     {
206         version(LDC) pragma(inline, true);
207         typeof(return) that;    // TODO return direct call to store constructor
208         that._bins = Bins.withLength(binCount);
209         that._bstates = Bstates.withLength(binCount);
210         that._length = 0;
211         return that;
212     }
213 
214     /** Construct with room for storing at least `capacity` number of elements.
215      */
216     private this()(size_t capacity) // template-lazy
217     {
218         immutable binCount = binCountOfCapacity(capacity);
219         _bins = Bins.withLength(binCount);
220         _bstates = Bstates.withLength(binCount);
221         _length = 0;
222     }
223 
224     /** Lookup bin count from capacity `capacity`.
225      */
226     static private size_t binCountOfCapacity()(size_t capacity) // template-lazy
227     {
228         immutable minimumBinCount = ((capacityScaleNumerator *
229                                       capacity) /
230                                      (smallBinCapacity *
231                                       capacityScaleDenominator));
232         import std.math : nextPow2;
233         return nextPow2(minimumBinCount == 0 ?
234                         0 :
235                         minimumBinCount - 1);
236     }
237 
238     /// Destruct.
239     ~this() @nogc
240     {
241         version(D_Coverage) {} else pragma(inline, true);
242         release();
243     }
244 
245     /// No copying.
246     @disable this(this);
247 
248     /// Duplicate.
249     static if (__traits(isCopyable, T))
250     {
251         typeof(this) dup()() const @trusted // template-lazy
252         {
253             typeof(return) that;
254 
255             that._bins.reserve(_bins.length);
256             // TODO merge these
257             that._bins.length = _bins.length; // TODO this zero-initializes before initialization below, use unsafe setLengthOnlyUNSAFE
258 
259             that._bstates = _bstates.dup;
260             assert(that._bstates[] == _bstates[]);
261 
262             that._length = _length;
263 
264             foreach (immutable binIx; 0 .. _bins.length)
265             {
266                 if (_bstates[binIx].isLarge)
267                 {
268                     LargeBin.emplaceWithCopiedElements(&that._bins[binIx].large,
269                                                        _bins[binIx].large[]);
270                 }
271                 else
272                 {
273                     auto elements = smallBinElementsAt(binIx);
274                     /** TODO functionize to `emplaceAll` in emplace_all.d. See_Also:
275                      * http://forum.dlang.org/post/xxigbqqflzwfgycrclyq@forum.dlang.org
276                      */
277                     static if (hasElaborateCopyConstructor!T)
278                     {
279                         foreach (immutable elementIx, immutable ref element; elements)
280                         {
281                             emplace(&that._bins[binIx].small[elementIx],
282                                     element);
283                         }
284                     }
285                     else
286                     {
287                         enum useMemcpy = true;
288                         static if (useMemcpy)
289                         {
290                             // currently faster than slice assignment on else branch
291                             import core.stdc.string : memcpy;
292                             memcpy(that._bins[binIx].small.ptr, // cannot overlap
293                                    elements.ptr,
294                                    elements.length * T.sizeof);
295                         }
296                         else
297                         {
298                             that._bins[binIx].small.ptr[0 .. _bstates[binIx].smallCount] = elements;
299                         }
300                     }
301                 }
302             }
303 
304             return that;
305         }
306     }
307 
308     /// Grow by duplicating number of bins.
309     private void growWithExtraCapacity(size_t extraCapacity) @trusted // not template-lazy
310     {
311         size_t newBinCount = 0;
312         if (extraCapacity == 1)
313         {
314             newBinCount = binCount ? 2 * binCount : 1; // 0 => 1, 1 => 2, 2 => 4, ...
315         }
316         else
317         {
318             newBinCount = binCountOfCapacity(_length + extraCapacity);
319         }
320         auto copy = withBinCount(newBinCount);
321 
322         // move elements to copy
323         foreach (immutable binIx; 0 .. _bins.length)
324         {
325             foreach (ref element; binElementsAt(binIx))
326             {
327                 copy.insertMoveWithoutBinCountGrowth(element);
328             }
329         }
330         assert(copy._length == _length); // length shouldn't change
331 
332         moveEmplace(copy._bstates, _bstates); // `_bstates` doesn't need destroying
333         move(copy._bins, _bins);
334 
335         assert(!_bins.empty);
336         assert(!_bstates.empty);
337     }
338 
339     /// Equality.
340     bool opEquals()(in auto ref typeof(this) rhs) const @trusted
341     {
342         if (_length != rhs._length) { return false; }
343 
344         foreach (immutable binIx; 0 .. _bins.length)
345         {
346             foreach (const ref element; binElementsAt(binIx))
347             {
348                 static if (hasValue)
349                 {
350                     auto elementFound = element.key in rhs;
351                     if (!elementFound) { return false; }
352                     if ((*elementFound) !is element.value) { return false; }
353                 }
354                 else
355                 {
356                     if (!rhs.contains(element)) { return false; }
357                 }
358             }
359         }
360 
361         return true;
362     }
363 
364     /// Empty.
365     void clear()()              // template-lazy
366     {
367         version(LDC) pragma(inline, true);
368         release();
369         resetInternalData();
370     }
371 
372     /// Release internal allocations.
373     private void release() @trusted
374     {
375         foreach (immutable binIx; 0 .. _bins.length)
376         {
377             if (_bstates[binIx].isLarge)
378             {
379                 static if (hasElaborateDestructor!LargeBin)
380                 {
381                     .destroy(_bins[binIx].large);
382                 }
383             }
384             else
385             {
386                 static if (hasElaborateDestructor!T)
387                 {
388                     // TODO use emplace_all : destroyAll(smallBinElementsAt(binIx))
389                     foreach (ref element; smallBinElementsAt(binIx))
390                     {
391                         .destroy(element);
392                     }
393                 }
394             }
395         }
396     }
397 
398     /// Reset internal data.
399     pragma(inline, true)
400     private void resetInternalData()
401     {
402         _bins.clear();
403         _bstates.clear();
404         _length = 0;
405     }
406 
407     /** Check if `element` is stored.
408         Returns: `true` if element was already present, `false` otherwise.
409      */
410     bool contains()(in K key) const // template-lazy. TODO make `auto ref K` work
411     {
412         version(LDC) pragma(inline, true);
413         if (empty)              // TODO can this check be avoided?
414         {
415             return false; // prevent `RangeError` in `binElementsAt` when empty
416         }
417         immutable binIx = keyToBinIx(key);
418         return hasKey(binElementsAt(binIx), key);
419     }
420     /// ditto
421     bool contains()(in ref K key) const // template-lazy
422     {
423         version(LDC) pragma(inline, true);
424         if (empty)              // TODO can this check be avoided?
425         {
426             return false; // prevent `RangeError` in `binElementsAt` when empty
427         }
428         immutable binIx = keyToBinIx(key);
429         return hasKey(binElementsAt(binIx), key);
430     }
431 
432     /** Insert `element`, being either a key-value (map-case) or a just a key (set-case).
433      */
434     InsertionStatus insert(T element)
435     {
436         version(LDC) pragma(inline, true);
437         reserveExtra(1);
438         return insertMoveWithoutBinCountGrowth(element);
439     }
440 
441     /** Insert `elements`, all being either a key-value (map-case) or a just a key (set-case).
442      */
443     void insertN(R)(R elements) @trusted
444         if (isIterable!R &&
445             __traits(isCopyable, T))
446     {
447         import std.range.primitives : hasLength;
448         static if (hasLength!R)
449         {
450             // reserveExtra(elements.length); // TODO this fails when starting knet
451         }
452         foreach (element; elements)
453         {
454             // TODO use `insertMoveWithoutBinCountGrowth` when call to `reserveExtra` works
455             static if (hasIndirections!T)
456             {
457                 insert(element);
458             }
459             else
460             {
461                 insert(*cast(Unqual!T*)&element);
462             }
463         }
464     }
465 
466     /** Reserve rom for `extraCapacity` number of extra buckets. */
467     pragma(inline, true)
468     void reserveExtra()(size_t extraCapacity)
469     {
470         if ((capacityScaleNumerator *
471              (_length + extraCapacity) /
472              capacityScaleDenominator) >
473             _bins.length * smallBinCapacity)
474         {
475             growWithExtraCapacity(extraCapacity);
476         }
477     }
478 
479     static if (hasValue)
480     {
481         /** Insert or replace `value` at `key`. */
482         pragma(inline, true)    // LDC must have this
483         InsertionStatus insert(K key, V value)
484         {
485             return insert(T(move(key),
486                             move(value)));
487         }
488     }
489 
490     static private size_t offsetOfKey(in T[] elements,
491                                       in ref K key)
492     {
493         version(LDC) pragma(inline, true);
494         size_t elementOffset = 0;
495         foreach (const ref e; elements)
496         {
497             if (keyOf(e) is key) { break; }
498             elementOffset += 1;
499         }
500         return elementOffset;
501     }
502 
503     pragma(inline, true)
504     static private bool hasKey(in T[] elements,
505                                in ref K key)
506     {
507         return offsetOfKey(elements, key) != elements.length;
508     }
509 
510     /** Insert `element` like with `insert()` without automatic growth of number
511      * of bins.
512      */
513     InsertionStatus insertMoveWithoutBinCountGrowth(ref T element) @trusted // ref simplifies move
514     {
515         immutable binIx = keyToBinIx(keyRefOf(element));
516         T[] elements = binElementsAt(binIx);
517         immutable elementOffset = offsetOfKey(elements, keyOf(element));
518         immutable elementFound = elementOffset != elements.length;
519 
520         if (elementFound)
521         {
522             static if (hasValue)
523             {
524                 /* TODO Rust does the same in its `insert()` at
525                  * https://doc.rust-lang.org/std/collections/struct.HashMap.html
526                  */
527                 if (elements[elementOffset].value !is valueOf(element)) // if different value (or identity for classes)
528                 {
529                     // replace value
530                     static if (needsMove!V)
531                     {
532                         move(valueOf(element),
533                              elements[elementOffset].value);
534                     }
535                     else
536                     {
537                         elements[elementOffset].value = valueOf(element);
538                     }
539                     return typeof(return).modified;
540                 }
541             }
542             return typeof(return).unmodified;
543         }
544         else                    // no elementFound
545         {
546             if (_bstates[binIx].isLarge) // stay large
547             {
548                 _bins[binIx].large.insertBackMove(element);
549             }
550             else
551             {
552                 if (_bstates[binIx].isFullSmall) // expand small to large
553                 {
554                     static if (needsMove!T)
555                     {
556                         // TODO functionize to concatenation:moveConcatenate()
557                         T[smallBinCapacity + 1] smallCopy = void;
558                         moveEmplaceAllNoReset(_bins[binIx].small[],
559                                               smallCopy[0 .. smallBinCapacity]);
560                         moveEmplace(element,
561                                     smallCopy[smallBinCapacity]);
562 
563                         LargeBin.emplaceWithMovedElements(&_bins[binIx].large,
564                                                           smallCopy[]);
565                     }
566                     else
567                     {
568                         import nxt.concatenation : concatenate;
569                         auto smallCopy = concatenate(_bins[binIx].small, element);
570                         emplace!(LargeBin)(&_bins[binIx].large, smallCopy);
571                     }
572                     _bstates[binIx].makeLarge();
573                 }
574                 else            // stay small
575                 {
576                     static if (needsMove!T)
577                     {
578                         moveEmplace(element,
579                                     _bins[binIx].small[_bstates[binIx].smallCount]);
580                     }
581                     else
582                     {
583                         _bins[binIx].small[_bstates[binIx].smallCount] = element;
584                     }
585                     _bstates[binIx].incSmallCount();
586                 }
587             }
588             _length += 1;
589             return typeof(return).added;
590         }
591     }
592 
593     /** L-value element reference (and in turn range iterator).
594      */
595     static private struct LvalueElementRef(SomeHashMapOrSet)
596     {
597         SomeHashMapOrSet* table;
598         size_t binIx;           // index to bin inside table
599         size_t elementOffset;   // offset to element inside bin
600         size_t elementCounter;  // counter over number of elements popped
601 
602         pragma(inline, true):
603 
604         /// Check if empty.
605         @property bool empty() const @safe pure nothrow @nogc
606         {
607             return binIx == table.binCount;
608         }
609 
610         /// Get number of element left to pop.
611         @property size_t length() const @safe pure nothrow @nogc
612         {
613             return table.length - elementCounter;
614         }
615 
616         void initFirstNonEmptyBin()
617         {
618             while (binIx < table.binCount &&
619                    table.binElementCountAt(binIx) == 0)
620             {
621                 binIx += 1;
622             }
623         }
624 
625         pragma(inline)
626         void popFront()
627         {
628             assert(!empty);
629             elementOffset += 1; // next element
630             // if current bin was emptied
631             while (elementOffset >= table.binElementsAt(binIx).length)
632             {
633                 // next bin
634                 binIx += 1;
635                 elementOffset = 0;
636                 if (empty) { break; }
637             }
638             elementCounter += 1;
639         }
640 
641         @property typeof(this) save() // ForwardRange
642         {
643             return this;
644         }
645     }
646 
647     /** R-value element reference (and in turn range iterator).
648      */
649     static private struct RvalueElementRef(SomeHashMapOrSet)
650     {
651         SomeHashMapOrSet table; // owned
652         size_t binIx;           // index to bin inside table
653         size_t elementOffset;   // offset to element inside bin
654         size_t elementCounter;  // counter over number of elements popped
655 
656         pragma(inline, true):
657 
658         /// Check if empty.
659         @property bool empty() const @safe pure nothrow @nogc
660         {
661             return binIx == table.binCount;
662         }
663 
664         /// Get number of element left to pop.
665         @property size_t length() const @safe pure nothrow @nogc
666         {
667             return table.length - elementCounter;
668         }
669 
670         void initFirstNonEmptyBin()
671         {
672             while (binIx < table.binCount &&
673                    table.binElementCountAt(binIx) == 0)
674             {
675                 binIx += 1;
676             }
677         }
678 
679         pragma(inline)
680         void popFront()
681         {
682             assert(!empty);
683             elementOffset += 1; // next element
684             // if current bin was emptied
685             while (elementOffset >= table.binElementsAt(binIx).length)
686             {
687                 // next bin
688                 binIx += 1;
689                 elementOffset = 0;
690                 if (empty) { break; }
691             }
692             elementCounter += 1;
693         }
694     }
695 
696     static if (!hasValue)       // HashSet
697     {
698         pragma(inline, true)
699         bool opBinaryRight(string op)(in K key) inout @trusted
700             if (op == "in")
701         {
702             return contains(key);
703         }
704 
705         /// Range over elements of l-value instance of this.
706         static private struct ByLvalueElement(SomeHashMapOrSet)
707         {
708         pragma(inline, true):
709             static if (is(ElementType == class))
710             {
711                 /// Get reference to front element (key and value).
712                 @property scope auto front()() return @trusted
713                 {
714                     /* cast away const from `SomeHashMapOrSet` for classes
715                      * because class elements are currently hashed and compared
716                      * compared using their identity (pointer value) `is`
717                      */
718                     return cast(ElementType)table.binElementsAt(binIx)[elementOffset];
719                 }
720             }
721             else
722             {
723                 /// Get reference to front element (key and value).
724                 @property scope auto front()()
725                 {
726                     return table.binElementsAt(binIx)[elementOffset];
727                 }
728             }
729             public LvalueElementRef!SomeHashMapOrSet _elementRef;
730             alias _elementRef this;
731         }
732 
733         /// Range over elements of r-value instance of this.
734         static private struct ByRvalueElement(SomeHashMapOrSet)
735         {
736         pragma(inline, true):
737             static if (is(ElementType == class))
738             {
739                 /// Get reference to front element (key and value).
740                 @property scope auto front()() return @trusted
741                 {
742                     /* cast away const from `SomeHashMapOrSet` for classes
743                      * because class elements are currently hashed and compared
744                      * compared using their identity (pointer value) `is`
745                      */
746                     return cast(ElementType)table.binElementsAt(binIx)[elementOffset];
747                 }
748             }
749             else
750             {
751                 /// Get reference to front element (key and value).
752                 @property scope auto front()()
753                 {
754                     return table.binElementsAt(binIx)[elementOffset];
755                 }
756             }
757             public RvalueElementRef!SomeHashMapOrSet _elementRef;
758             alias _elementRef this;
759         }
760 
761         /// ditto
762         version(none)           // cannot be combined
763         {
764             pragma(inline, true)
765             scope auto opSlice()() inout return // template-lazy
766             {
767                 return byElement();
768             }
769         }
770     }
771 
772     static if (hasValue)        // HashMap
773     {
774         scope inout(V)* opBinaryRight(string op)(in K key) inout @trusted return
775             if (op == "in")
776         {
777             if (empty)
778             {
779                 // prevent range error in `binElementsAt` when `this` is empty
780                 return typeof(return).init;
781             }
782             immutable binIx = keyToBinIx(key);
783             const elements = binElementsAt(binIx);
784             immutable elementOffset = offsetOfKey(elements, key);
785             immutable elementFound = elementOffset != elements.length;
786             if (elementFound)
787             {
788                 return cast(typeof(return))&elements[elementOffset].value;
789                 // return typeof(return)(&this, binIx, elementOffset);
790             }
791             else                    // miss
792             {
793                 return typeof(return).init;
794             }
795         }
796 
797         static private struct ByKey(SomeHashMapOrSet)
798         {
799             pragma(inline, true):
800             /// Get reference to key of front element.
801             @property const scope auto ref front()() return // key access must be const
802             {
803                 return table.binElementsAt(binIx)[elementOffset].key;
804             }
805             public LvalueElementRef!SomeHashMapOrSet _elementRef;
806             alias _elementRef this;
807         }
808 
809         /// Returns forward range that iterates through the keys of `this`.
810         @property scope auto byKey()() inout return @trusted // template-lazy property
811         {
812             alias This = ConstThis;
813             auto result = ByKey!This((LvalueElementRef!This(cast(This*)&this)));
814             result.initFirstNonEmptyBin();
815             return result;
816         }
817 
818         static private struct ByValue(SomeHashMapOrSet)
819         {
820             pragma(inline, true):
821             /// Get reference to value of front element.
822             @property scope auto ref front()() return @trusted // template-lazy property. TODO remove @trusted
823             {
824                 return *(cast(ValueType*)(&table.binElementsAt(binIx)[elementOffset].value)); // TODO remove reinterpret cast
825             }
826             public LvalueElementRef!SomeHashMapOrSet _elementRef;
827             alias _elementRef this;
828         }
829 
830         /// Returns forward range that iterates through the values of `this`.
831         @property scope auto byValue()() inout return @trusted // template-lazy property
832         {
833             alias This = ConstThis;
834             auto result = ByValue!This((LvalueElementRef!This(cast(This*)&this)));
835             result.initFirstNonEmptyBin();
836             return result;
837         }
838 
839         static private struct ByKeyValue(SomeHashMapOrSet)
840         {
841             pragma(inline, true):
842             /// Get reference to front element (key and value).
843             @property scope auto ref front()() return @trusted
844             {
845                 static if (isMutable!(SomeHashMapOrSet))
846                 {
847                     alias E = CT;
848                 }
849                 else
850                 {
851                     alias E = const(T);
852                 }
853                 return *(cast(E*)&table.binElementsAt(binIx)[elementOffset]); // TODO remove cast
854             }
855             public LvalueElementRef!SomeHashMapOrSet _elementRef;
856             alias _elementRef this;
857         }
858 
859         /// Returns forward range that iterates through the keys and values of `this`.
860         @property scope auto byKeyValue()() return @trusted // template-lazy property
861         {
862             alias This = MutableThis;
863             auto result = ByKeyValue!This((LvalueElementRef!This(cast(This*)&this)));
864             result.initFirstNonEmptyBin();
865             return result;
866         }
867         /// ditto
868         @property scope auto byKeyValue()() const return @trusted // template-lazy property
869         {
870             alias This = ConstThis;
871             auto result = ByKeyValue!This((LvalueElementRef!This(cast(This*)&this)));
872             result.initFirstNonEmptyBin();
873             return result;
874         }
875 
876         /// ditto
877         pragma(inline, true)
878         scope auto opSlice()() return  // template-lazy
879         {
880             return byKeyValue();
881         }
882 
883         /// Indexing.
884         scope ref inout(V) opIndex()(in K key) inout return // auto ref here makes things slow
885         {
886             version(LDC) pragma(inline, true);
887             immutable binIx = keyToBinIx(key);
888             auto elements = binElementsAt(binIx);
889 
890             immutable elementOffset = offsetOfKey(elements, key);
891             immutable elementFound = elementOffset != elements.length;
892             if (elementFound)
893             {
894                 return binElementsAt(binIx)[elementOffset].value;
895             }
896             version(assert)
897             {
898                 import core.exception : RangeError;
899                 throw new RangeError("Key not found");
900             }
901         }
902 
903         /** Get value of `key` or `defaultValue` if `key` not present (and
904          * therefore `nothrow`).
905          *
906          * Returns: value reference iff `defaultValue` is an l-value.
907          *
908          * TODO make `defaultValue` `lazy` when that can be `nothrow`
909          */
910         auto ref V get()(in K key, in V defaultValue) @trusted // auto ref here makes things slow
911         {
912             import std.algorithm.searching : countUntil;
913             immutable binIx = keyToBinIx(key);
914             immutable ptrdiff_t elementOffset = binElementsAt(binIx).countUntil!(_ => _.key is key); // TODO functionize
915             if (elementOffset != -1) // elementFound
916             {
917                 return binElementsAt(binIx)[elementOffset].value;
918             }
919             else                    // miss
920             {
921                 return defaultValue;
922             }
923         }
924 
925 	/** Supports $(B aa[key] = value;) syntax.
926 	 */
927         void opIndexAssign()(V value, K key) // template-lazy
928 	{
929             version(LDC) pragma(inline, true);
930             insert(T(move(key),
931                      move(value)));
932             // TODO return reference to value
933 	}
934 
935         static if (__traits(compiles, { V _; _ += 1; })) // if we can increase the key
936         {
937             /** Increase value at `key`, or set value to 1 if `key` hasn't yet
938              * been added.
939              */
940             pragma(inline, true)
941             void autoinitIncAt()(in K key) // template-lazy
942             {
943                 auto elementFound = key in this;
944                 if (elementFound)
945                 {
946                     (*elementFound) += 1;
947                 }
948                 else
949                 {
950                     insert(key, V.init + 1);
951                 }
952             }
953         }
954 
955     }
956 
957     /** Remove `element` and, when possible, shrink its large bin to small.
958 
959         Returns: `true` if element was removed, `false` otherwise.
960     */
961     bool remove()(in K key)     // template-lazy
962         @trusted
963     {
964         immutable binIx = keyToBinIx(key);
965         import nxt.container_algorithm : popFirstMaybe;
966         if (_bstates[binIx].isLarge)
967         {
968             immutable elementFound = _bins[binIx].large.popFirstMaybe!keyEqualPred(key);
969             _length -= elementFound ? 1 : 0;
970             if (elementFound)
971             {
972                 tryShrinkLargeBinAt(binIx);
973             }
974             return elementFound;
975         }
976         else
977         {
978             const elements = smallBinElementsAt(binIx);
979             immutable elementIx = offsetOfKey(elements, key);
980             immutable elementFound = elementIx != elements.length;
981             if (elementFound)
982             {
983                 removeSmallElementAt(binIx, elementIx);
984             }
985             _length -= elementFound ? 1 : 0;
986             return elementFound;
987         }
988     }
989 
990     /** Remove small element at `elementIx` in bin `binIx`. */
991     private void removeSmallElementAt()(size_t binIx, // template-lazy
992                                         size_t elementIx)
993     {
994         assert(!_bstates[binIx].isLarge);
995         import nxt.container_algorithm : shiftToFrontAt;
996         smallBinElementsAt(binIx).shiftToFrontAt(elementIx);
997         _bstates[binIx].decSmallCount();
998         static if (hasElaborateDestructor!T)
999         {
1000             .destroy(_bins[binIx].small[_bstates[binIx].smallCount]); // TODO this is incorrect
1001         }
1002     }
1003 
1004     /** Shrink large bin at `binIx` possible posbiel. */
1005     private void tryShrinkLargeBinAt()(size_t binIx) // template-lazy
1006     {
1007         assert(_bstates[binIx].isLarge);
1008         immutable count = _bins[binIx].large.length;
1009         if (count <= smallBinCapacity) // large fits in small
1010         {
1011             SmallBin smallCopy = void;
1012             moveEmplaceAllNoReset(_bins[binIx].large[0 .. count],
1013                                   smallCopy[0 .. count]);
1014             static if (hasElaborateDestructor!LargeBin)
1015             {
1016                 .destroy(_bins[binIx].large);
1017             }
1018             moveEmplaceAllNoReset(smallCopy[0 .. count],
1019                                   _bins[binIx].small[0 .. count]);
1020             _bstates[binIx].smallCount = count;
1021         }
1022     }
1023 
1024     /** Rehash.
1025      *
1026      * Reorganize `this` in place so that lookups are more efficient.
1027      */
1028     ref typeof(this) rehash()() @trusted // template-lazy
1029     {
1030         static assert(0, "TODO remove template parens of this functions and implement");
1031         // return this;
1032     }
1033 
1034     /// Check if empty.
1035     pragma(inline, true)
1036     @property bool empty() const { return _length == 0; }
1037 
1038     /// Get length (read-only).
1039     pragma(inline, true)
1040     @property size_t length() const { return _length; }
1041 
1042     /// Get bin count.
1043     pragma(inline, true)
1044     @property size_t binCount() const { return _bins.length; }
1045 
1046     /// Bin count statistics.
1047     struct BinCounts
1048     {
1049         size_t smallCount;      // number of hybrid bins being small
1050         size_t largeCount;      // number of hybrid bins being large
1051     }
1052 
1053     /// Returns: bin count statistics for small and large bins.
1054     pragma(inline, false)
1055     BinCounts binCounts()() const // template-lazy
1056     {
1057         import std.algorithm : count;
1058         immutable largeCount = _bstates[].count!(_ => _.isLarge);
1059         immutable smallCount = _bstates.length - largeCount;
1060         auto result = typeof(return)(smallCount,
1061                                      largeCount);
1062         assert(result.largeCount + result.smallCount == _bstates.length);
1063         return result;
1064     }
1065 
1066     /** Returns: elements in bin at `binIx`. */
1067     pragma(inline, true)
1068     private scope inout(T)[] binElementsAt(size_t binIx) inout return @trusted
1069     {
1070         if (_bstates[binIx].isLarge)
1071         {
1072             return _bins[binIx].large[];
1073         }
1074         else
1075         {
1076             return smallBinElementsAt(binIx);
1077         }
1078     }
1079 
1080     pragma(inline, true)
1081     private scope inout(T)[] smallBinElementsAt(size_t binIx) inout return
1082     {
1083         return _bins[binIx].small[0 .. _bstates[binIx].smallCount];
1084     }
1085 
1086     /** Returns: number of elements in bin at `binIx`. */
1087     pragma(inline, true)
1088     private size_t binElementCountAt(size_t binIx) const @trusted
1089     {
1090         if (_bstates[binIx].isLarge)
1091         {
1092             return _bins[binIx].large.length;
1093         }
1094         else
1095         {
1096             return _bstates[binIx].smallCount;
1097         }
1098     }
1099 
1100     /** Maximum number of elements that fits in a small bin
1101      * (`SmallBin`).
1102      */
1103     enum smallBinCapacity = max(smallBinMinCapacity,
1104                                    LargeBin.sizeof / T.sizeof);
1105 
1106 private:
1107     import nxt.dynamic_array : Array = DynamicArray;
1108 
1109     /** 32-bit capacity and length for LargeBinLnegth on 64-bit platforms
1110      * saves one word and makes insert() and contains() significantly faster */
1111     alias LargeBinCapacityType = uint;
1112     alias LargeBin = Array!(T, Allocator, LargeBinCapacityType);
1113 
1114     alias SmallBin = T[smallBinCapacity];
1115 
1116     /// no space for `SmallBin`s should be wasted
1117     static assert(SmallBin.sizeof >= LargeBin.sizeof);
1118 
1119     /** Small-size-optimized bin.
1120      */
1121     union HybridBin
1122     {
1123         SmallBin small;
1124         LargeBin large;
1125     }
1126 
1127     static if (mustAddGCRange!LargeBin)
1128     {
1129         static assert(mustAddGCRange!HybridBin, "HybridBin mustAddGCRange when LargeBin is " ~ LargeBin.stringof);
1130     }
1131     static if (mustAddGCRange!SmallBin)
1132     {
1133         static assert(mustAddGCRange!HybridBin, "HybridBin mustAddGCRange when SmallBin is " ~ SmallBin.stringof);
1134     }
1135 
1136     /** Count and large status of bin. */
1137     struct Bstate
1138     {
1139         alias Count = ubyte;
1140 
1141         pragma(inline, true):
1142 
1143         @property Count smallCount() const
1144         {
1145             assert(_count <= smallBinCapacity);
1146             return _count;
1147         }
1148 
1149         @property void smallCount(size_t count)
1150         {
1151             assert(count <= smallBinCapacity);
1152             _count = cast(Count)count;
1153         }
1154 
1155         void decSmallCount()
1156         {
1157             assert(_count >= 1);
1158             _count -= 1;
1159         }
1160 
1161         void incSmallCount()
1162         {
1163             assert(_count + 1 <= smallBinCapacity);
1164             _count += 1;
1165         }
1166 
1167         /** Returns: `true` iff `this` is a large bin. */
1168         @property bool isLarge() const
1169         {
1170             return _count == Count.max;
1171         }
1172 
1173         void makeLarge()
1174         {
1175             _count = Count.max;
1176         }
1177 
1178         /** Returns: `true` iff `this` is a full small bin. */
1179         @property bool isFullSmall() const
1180         {
1181             return _count == smallBinCapacity;
1182         }
1183 
1184         Count _count;
1185     }
1186 
1187     /** Small-size-optimized bin array.
1188         Size-state (including small or large) for each element is determined by
1189         corresponding element in `Bstates`.
1190      */
1191     alias Bins = Array!(HybridBin, Allocator);
1192 
1193     /// Bin states.
1194     alias Bstates = Array!(Bstate, Allocator);
1195 
1196     // TODO merge these into an instance of soa.d and remove invariant
1197     Bins _bins;                 // bin elements
1198     Bstates _bstates;           // bin states
1199     invariant
1200     {
1201         assert(_bins.length ==
1202                _bstates.length);
1203     }
1204 
1205     size_t _length;             // total number of elements stored
1206 
1207     /** Returns: bin index of `hash`. */
1208     pragma(inline, true)
1209     size_t hashToIndex(hash_t hash) const
1210     {
1211         return hash & powerOf2Mask;
1212     }
1213 
1214     /** Returns: bin index of `key`. */
1215     size_t keyToBinIx()(in auto ref K key) const
1216     {
1217         version(LDC) pragma(inline, true);
1218         import nxt.digestion : hashOf2;
1219         return hashToIndex(hashOf2!(hasher)(key));
1220     }
1221 
1222     /** Returns: current index mask from bin count. */
1223     pragma(inline, true)
1224     private size_t powerOf2Mask() const
1225     {
1226         immutable typeof(return) mask = _bins.length - 1;
1227         assert((~mask ^ mask) == size_t.max); // isPowerOf2(_bins.length)
1228         return mask;
1229     }
1230 }
1231 
1232 /** Hash map storing keys of type `K` and values of type `V`.
1233  */
1234 alias SSOHashMap(K, V,
1235                  alias Allocator = null,
1236                  alias hasher = hashOf,
1237                  uint smallBinMinCapacity = 1) = SSOHashMapOrSet!(K, V,
1238                                                                   Allocator,
1239                                                                   hasher,
1240                                                                   smallBinMinCapacity);
1241 
1242 /** Hash map storing keys of type `K`.
1243  */
1244 alias SSOHashSet(K,
1245                  alias Allocator = null,
1246                  alias hasher = hashOf,
1247                  uint smallBinMinCapacity = 1) = SSOHashMapOrSet!(K, void,
1248                                                                   Allocator,
1249                                                                   hasher,
1250                                                                   smallBinMinCapacity);
1251 
1252 import std.traits : isInstanceOf;
1253 import std.functional : unaryFun;
1254 
1255 /** Remove all elements in `x` matching `predicate`.
1256     TODO move to container_algorithm.d.
1257 */
1258 void removeAllMatching(alias predicate, SomeHashMapOrSet)(auto ref SomeHashMapOrSet x)
1259     @trusted
1260     if (isInstanceOf!(SSOHashMapOrSet,
1261                       SomeHashMapOrSet))
1262 {
1263     import core.lifetime : moveEmplace;
1264     foreach (immutable binIx; 0 .. x._bins.length)
1265     {
1266         if (x._bstates[binIx].isLarge)
1267         {
1268             import nxt.dynamic_array : remove;
1269             immutable removeCount = x._bins[binIx].large.remove!predicate();
1270             x._length -= removeCount;
1271             x.tryShrinkLargeBinAt(binIx);
1272         }
1273         else
1274         {
1275             SomeHashMapOrSet.SmallBin tmpSmall;
1276             SomeHashMapOrSet.Bstate tmpBstate;
1277             foreach (ref element; x.smallBinElementsAt(binIx))
1278             {
1279                 if (unaryFun!predicate(element))
1280                 {
1281                     import core.internal.traits : hasElaborateDestructor;
1282                     static if (hasElaborateDestructor!(SomeHashMapOrSet.T))
1283                     {
1284                         destroy(element);
1285                     }
1286                     x._length -= 1;
1287                 }
1288                 else
1289                 {
1290                     moveEmplace(element, tmpSmall[tmpBstate.smallCount()]);
1291                     tmpBstate.incSmallCount();
1292                 }
1293             }
1294             assert(!tmpBstate.isLarge); // should stay small
1295             moveEmplace(tmpSmall, x._bins[binIx].small);
1296             moveEmplace(tmpBstate, x._bstates[binIx]);
1297         }
1298     }
1299 }
1300 
1301 /** Returns: `x` eagerly filtered on `predicate`.
1302     TODO move to container_algorithm.d.
1303 */
1304 SomeHashMapOrSet filtered(alias predicate, SomeHashMapOrSet)(SomeHashMapOrSet x)
1305     @trusted
1306     if (isInstanceOf!(SSOHashMapOrSet,
1307                       SomeHashMapOrSet))
1308 {
1309     import std.functional : not;
1310     removeAllMatching!(not!predicate)(x);
1311     import std.algorithm.mutation : move;
1312     return move(x);
1313 }
1314 
1315 /** Returns: `x` eagerly intersected with `y`.
1316     TODO move to container_algorithm.d.
1317  */
1318 auto intersectedWith(C1, C2)(C1 x, auto ref C2 y)
1319     if (isInstanceOf!(SSOHashMapOrSet, C1) &&
1320         isInstanceOf!(SSOHashMapOrSet, C2))
1321 {
1322     import std.algorithm.mutation : move;
1323     static if (__traits(isRef, y)) // y is l-value
1324     {
1325         // @("complexity", "O(x.length)")
1326         return move(x).filtered!(_ => y.contains(_)); // only x can be reused
1327     }
1328     else
1329     {
1330         /* both are r-values so reuse the shortest */
1331         // @("complexity", "O(min(x.length), min(y.length))")
1332         if (x.length <
1333             y.length)
1334         {
1335             return move(x).filtered!(_ => y.contains(_));
1336         }
1337         else
1338         {
1339             return move(y).filtered!(_ => x.contains(_));
1340         }
1341     }
1342 }
1343 
1344 /// r-value and l-value intersection
1345 @safe pure nothrow @nogc unittest
1346 {
1347     alias K = uint;
1348     alias X = SSOHashMapOrSet!(K, void, null, FNV!(64, true));
1349 
1350     auto x = X.withElements([12, 13].s);
1351     auto y = X.withElements([10, 12, 13, 15].s).intersectedWith(x);
1352     assert(y.length == 2);
1353     assert(y.contains(12));
1354     assert(y.contains(13));
1355 }
1356 
1357 /// r-value and r-value intersection
1358 @safe pure nothrow @nogc unittest
1359 {
1360     alias K = uint;
1361     alias X = SSOHashMapOrSet!(K, void, null, FNV!(64, true));
1362 
1363     auto y = X.withElements([10, 12, 13, 15].s).intersectedWith(X.withElements([12, 13].s));
1364     assert(y.length == 2);
1365     assert(y.contains(12));
1366     assert(y.contains(13));
1367 }
1368 
1369 /** Returns: `x` eagerly intersected with `y`.
1370     TODO move to container_algorithm.d.
1371  */
1372 auto intersectWith(C1, C2)(ref C1 x,
1373                            auto ref const(C2) y)
1374     if (isInstanceOf!(SSOHashMapOrSet, C1) &&
1375         isInstanceOf!(SSOHashMapOrSet, C2))
1376 {
1377     return x.removeAllMatching!(_ => !y.contains(_));
1378 }
1379 
1380 /// r-value and l-value intersection
1381 @safe pure nothrow @nogc unittest
1382 {
1383     alias K = uint;
1384     alias X = SSOHashMapOrSet!(K, void, null, FNV!(64, true));
1385 
1386     auto x = X.withElements([12, 13].s);
1387     auto y = X.withElements([10, 12, 13, 15].s);
1388     y.intersectWith(x);
1389     assert(y.length == 2);
1390     assert(y.contains(12));
1391     assert(y.contains(13));
1392 }
1393 
1394 /// Returns forward range that iterates through the elements of `c`.
1395 auto byElement(SomeHashMapOrSet)(auto ref inout(SomeHashMapOrSet) c)
1396     @trusted
1397     if (isInstanceOf!(SSOHashMapOrSet,
1398                       SomeHashMapOrSet))
1399 {
1400     alias C = const(SomeHashMapOrSet);
1401     static if (__traits(isRef, c))
1402     {
1403         auto result = C.ByLvalueElement!C((C.LvalueElementRef!C(cast(C*)&c)));
1404         result.initFirstNonEmptyBin();
1405         return result;
1406     }
1407     else
1408     {
1409         import std.algorithm.mutation : move;
1410         auto result = C.ByRvalueElement!C((C.RvalueElementRef!C(move(*(cast(SomeHashMapOrSet*)&c))))); // reinterpret
1411         result.initFirstNonEmptyBin();
1412         return move(result);
1413     }
1414 }
1415 alias range = byElement;        // EMSI-container naming
1416 
1417 @safe:
1418 
1419 /// make range from l-value and r-value. element access is always const
1420 pure nothrow @nogc unittest
1421 {
1422     alias K = uint;
1423     alias X = SSOHashMapOrSet!(K, void, null, FNV!(64, true));
1424 
1425     immutable a = [11, 22].s;
1426 
1427     // mutable
1428     auto x = X.withElements(a);
1429     foreach (e; x.byElement)    // from l-value
1430     {
1431         static assert(is(typeof(e) == const(K))); // always const access
1432     }
1433 
1434     // const
1435     const y = X.withElements(a);
1436     foreach (e; y.byElement)    // from l-value
1437     {
1438         static assert(is(typeof(e) == const(K)));
1439     }
1440 
1441     foreach (e; X.withElements([11].s).byElement) // from r-value
1442     {
1443         static assert(is(typeof(e) == const(K))); // always const access
1444     }
1445 }
1446 
1447 /// test various things
1448 pure nothrow @nogc unittest
1449 {
1450     immutable n = 600;
1451 
1452     alias K = uint;
1453 
1454     import std.meta : AliasSeq;
1455     foreach (V; AliasSeq!(void, string))
1456     {
1457         alias X = SSOHashMapOrSet!(K, V, null, FNV!(64, true));
1458 
1459         static if (!X.hasValue)
1460         {
1461             auto x = X.withElements([11, 12, 13].s);
1462 
1463             import std.algorithm : count;
1464             auto xr = x.byElement;
1465 
1466             alias R = typeof(xr);
1467             import std.range.primitives : isInputRange;
1468             import std.traits : ReturnType;
1469             static assert(is(typeof(R.init) == R));
1470             static assert(is(ReturnType!((R xr) => xr.empty) == bool));
1471             auto f = xr.front;
1472             static assert(is(typeof((R xr) => xr.front)));
1473             static assert(!is(ReturnType!((R xr) => xr.front) == void));
1474             static assert(is(typeof((R xr) => xr.popFront)));
1475 
1476             static assert(isInputRange!(typeof(xr)));
1477 
1478             assert(x.byElement.count == 3);
1479 
1480             X y;
1481             foreach (const ref e; x.byElement)
1482             {
1483                 y.insert(e);
1484             }
1485 
1486             assert(y.byElement.count == 3);
1487             assert(x == y);
1488 
1489             const z = X();
1490             assert(z.byElement.count == 0);
1491 
1492             immutable w = X();
1493             assert(w.byElement.count == 0);
1494 
1495             {
1496                 auto xc = X.withElements([11, 12, 13].s);
1497                 assert(xc.length == 3);
1498                 assert(xc.contains(11));
1499 
1500                 // TODO http://forum.dlang.org/post/kvwrktmameivubnaifdx@forum.dlang.org
1501                 xc.removeAllMatching!(_ => _ == 11);
1502 
1503                 assert(xc.length == 2);
1504                 assert(!xc.contains(11));
1505 
1506                 xc.removeAllMatching!(_ => _ == 12);
1507                 assert(!xc.contains(12));
1508                 assert(xc.length == 1);
1509 
1510                 xc.removeAllMatching!(_ => _ == 13);
1511                 assert(!xc.contains(13));
1512                 assert(xc.length == 0);
1513 
1514                 // this is ok
1515                 foreach (e; xc.byElement) {}
1516 
1517             }
1518 
1519             {
1520                 auto k = X.withElements([11, 12].s).filtered!(_ => _ != 11).byElement;
1521                 static assert(isInputRange!(typeof(k)));
1522                 assert(k.front == 12);
1523                 k.popFront();
1524                 assert(k.empty);
1525             }
1526 
1527             {
1528                 X q;
1529                 auto qv = [11U, 12U, 13U, 14U].s;
1530                 q.insertN(qv);
1531                 foreach (e; qv[])
1532                 {
1533                     assert(q.contains(e));
1534                 }
1535                 q.clear();
1536                 assert(q.empty);
1537             }
1538         }
1539 
1540         import nxt.container_traits : mustAddGCRange;
1541         static if (X.hasValue &&
1542                    is(V == string))
1543         {
1544             static assert(mustAddGCRange!V);
1545             static assert(mustAddGCRange!(V[1]));
1546             static assert(mustAddGCRange!(X.T));
1547             static assert(mustAddGCRange!(X.SmallBin));
1548             static assert(!mustAddGCRange!(X.LargeBin));
1549         }
1550         else
1551         {
1552             static assert(!mustAddGCRange!(X.T));
1553             static assert(!mustAddGCRange!(X.SmallBin), "Fails for X being " ~ X.SmallBin.stringof);
1554         }
1555 
1556         auto x1 = X();            // start empty
1557 
1558         // all bins start small
1559         assert(x1.binCounts.largeCount == 0);
1560 
1561         // fill x1
1562 
1563         foreach (immutable key; 0 .. n)
1564         {
1565             static if (X.hasValue)
1566             {
1567                 const value = V.init;
1568                 const element = X.ElementType(key, value);
1569             }
1570             else
1571             {
1572                 const element = key;
1573             }
1574 
1575             assert(key !in x1);
1576 
1577             assert(x1.length == key);
1578             assert(x1.insert(element) == X.InsertionStatus.added);
1579 
1580             static if (X.hasValue)
1581             {
1582                 const e2 = X.ElementType(key, "a");
1583                 assert(x1.insert(e2) == X.InsertionStatus.modified);
1584                 assert(x1.contains(key));
1585                 assert(x1.get(key, null) == "a");
1586                 x1.remove(key);
1587                 x1[key] = value;
1588             }
1589 
1590             assert(x1.length == key + 1);
1591 
1592             assert(key in x1);
1593             static if (X.hasValue)
1594             {
1595                 auto elementFound = key in x1;
1596                 assert(elementFound);
1597                 assert(*elementFound != "_");
1598             }
1599 
1600             assert(x1.insert(element) == X.InsertionStatus.unmodified);
1601             static if (X.hasValue)
1602             {
1603                 assert(x1.insert(key, value) == X.InsertionStatus.unmodified);
1604             }
1605             assert(x1.length == key + 1);
1606 
1607             assert(key in x1);
1608         }
1609 
1610         static if (X.hasValue)
1611         {
1612             import nxt.dynamic_array : Array = DynamicArray;
1613             Array!(X.ElementType) a1;
1614 
1615             foreach (const ref key; x1.byKey)
1616             {
1617                 auto keyPtr = key in x1;
1618                 assert(keyPtr);
1619                 a1 ~= X.ElementType(key, (*keyPtr));
1620             }
1621 
1622             assert(x1.length == a1.length);
1623 
1624             foreach (aElement; a1[])
1625             {
1626                 auto keyPtr = aElement.key in x1;
1627                 assert(keyPtr);
1628                 assert((*keyPtr) is aElement.value);
1629             }
1630         }
1631 
1632         assert(x1.length == n);
1633 
1634         // duplicate x1
1635 
1636         auto x2 = x1.dup;
1637 
1638         // non-symmetric algorithm so both are needed
1639         assert(x2 == x1);
1640         assert(x1 == x2);
1641 
1642         static if (X.hasValue)
1643         {
1644             assert(equal(x1.byKey, x2.byKey));
1645             assert(equal(x1.byValue, x2.byValue));
1646             assert(equal(x1.byKeyValue, x2.byKeyValue));
1647             assert(equal(x1[], x2[]));
1648         }
1649 
1650         assert(x1.binCounts.largeCount ==
1651                x2.binCounts.largeCount);
1652 
1653         static assert(!__traits(compiles, { const _ = x1 < x2; })); // no ordering
1654 
1655         assert(x2.length == n);
1656 
1657         // empty x1
1658 
1659         foreach (immutable key; 0 .. n)
1660         {
1661             static if (X.hasValue)
1662             {
1663                 const element = X.ElementType(key, V.init);
1664             }
1665             else
1666             {
1667                 const element = key;
1668             }
1669 
1670             assert(x1.length == n - key);
1671 
1672             auto elementFound = key in x1;
1673             assert(elementFound);
1674             static if (X.hasValue)
1675             {
1676                 assert(*elementFound is element.value);
1677             }
1678 
1679             assert(x1.remove(key));
1680             assert(x1.length == n - key - 1);
1681 
1682             static if (!X.hasValue)
1683             {
1684                 assert(!x1.contains(key));
1685             }
1686             assert(key !in x1);
1687             assert(!x1.remove(key));
1688             assert(x1.length == n - key - 1);
1689         }
1690 
1691         assert(x1.binCounts.largeCount == 0);
1692 
1693         assert(x1.length == 0);
1694 
1695         x1.clear();
1696         assert(x1.length == 0);
1697 
1698         // empty x2
1699 
1700         assert(x2.length == n); // should be not affected by emptying of x1
1701 
1702         foreach (immutable key; 0 .. n)
1703         {
1704             static if (X.hasValue)
1705             {
1706                 const element = X.ElementType(key, V.init);
1707             }
1708             else
1709             {
1710                 const element = key;
1711             }
1712 
1713             assert(x2.length == n - key);
1714 
1715             assert(key in x2);
1716 
1717             assert(x2.remove(key));
1718             assert(x2.length == n - key - 1);
1719 
1720             assert(key !in x2);
1721             assert(!x2.remove(key));
1722             assert(x2.length == n - key - 1);
1723         }
1724 
1725         assert(x2.binCounts.largeCount == 0);
1726 
1727         assert(x2.length == 0);
1728 
1729         x2.clear();
1730         assert(x2.length == 0);
1731     }
1732 }
1733 
1734 /// range checking
1735 @trusted pure unittest
1736 {
1737     import std.exception : assertThrown, assertNotThrown;
1738     import core.exception : RangeError;
1739     import nxt.uncopyable_sample : V = SomeUncopyable;
1740 
1741     immutable n = 11;
1742 
1743     alias K = uint;
1744 
1745     alias X = SSOHashMapOrSet!(K, V, null, FNV!(64, true));
1746 
1747     auto s = X.withCapacity(n);
1748 
1749     void dummy(ref V value) {}
1750 
1751     assertThrown!RangeError(dummy(s[K.init]));
1752 
1753     foreach (immutable uint i; 0 .. n)
1754     {
1755         s[i] = V(i);
1756         assertNotThrown!RangeError(dummy(s[i]));
1757     }
1758 
1759     foreach (immutable uint i; 0 .. n)
1760     {
1761         s.remove(i);
1762         assertThrown!RangeError(dummy(s[i]));
1763     }
1764 
1765     s[K.init] = V.init;
1766     auto vp = K.init in s;
1767     static assert(is(typeof(vp) == V*));
1768     assert((*vp) == V.init);
1769 
1770     s.remove(K.init);
1771     assert(K.init !in s);
1772 
1773     X t;
1774     t.reserveExtra(4096);
1775     assert(t.binCount == 8192);
1776 
1777     t.clear();
1778 }
1779 
1780 /// class as value
1781 @trusted pure unittest
1782 {
1783     import std.exception : assertThrown, assertNotThrown;
1784     import core.exception : RangeError;
1785 
1786     immutable n = 11;
1787 
1788     alias K = uint;
1789     class V
1790     {
1791         this(uint data) { this.data = data; }
1792         uint data;
1793     }
1794 
1795     alias X = SSOHashMapOrSet!(K, V, null, FNV!(64, true));
1796 
1797     auto s = X.withCapacity(n);
1798 
1799     void dummy(ref V value) {}
1800 
1801     assertThrown!RangeError(dummy(s[K.init]));
1802 
1803     foreach (immutable uint i; 0 .. n)
1804     {
1805         s[i] = new V(i);
1806         assertNotThrown!RangeError(dummy(s[i]));
1807     }
1808 
1809     // test range
1810     auto sr = s.byKeyValue;
1811     assert(sr.length == n);
1812     foreach (immutable uint i; 0 .. n)
1813     {
1814         sr.popFront();
1815         assert(sr.length == n - i - 1);
1816     }
1817 
1818     foreach (immutable uint i; 0 .. n)
1819     {
1820         s.remove(i);
1821         assertThrown!RangeError(dummy(s[i]));
1822     }
1823 
1824     s[K.init] = V.init;
1825     auto vp = K.init in s;
1826     static assert(is(typeof(vp) == V*));
1827 
1828     s.remove(K.init);
1829     assert(K.init !in s);
1830 
1831     X t;
1832     t.reserveExtra(4096);
1833     assert(t.binCount == 8192);
1834 }
1835 
1836 /// constness inference of ranges
1837 pure nothrow unittest
1838 {
1839     alias K = uint;
1840     class V
1841     {
1842         this(uint data) { this.data = data; }
1843         uint data;
1844     }
1845 
1846     alias X = SSOHashMapOrSet!(K, V, null, FNV!(64, true));
1847     const x = X();
1848 
1849     foreach (e; x.byKey)
1850     {
1851         static assert(is(typeof(e) == const(X.KeyType)));
1852     }
1853 
1854     foreach (e; x.byValue)
1855     {
1856         static assert(is(typeof(e) == X.ValueType)); // TODO should be const(X.ValueType)
1857     }
1858 
1859     foreach (e; x.byKeyValue)
1860     {
1861         static assert(is(typeof(e.key) == const(X.KeyType)));
1862         static assert(is(typeof(e.value) == const(X.ValueType)));
1863         static assert(is(typeof(e) == const(X.ElementType)));
1864     }
1865 }
1866 
1867 /// range key constness and value mutability with `class` value
1868 pure nothrow unittest
1869 {
1870     struct K
1871     {
1872         @disable this(this);
1873         uint value;
1874     }
1875 
1876     class V
1877     {
1878         this(uint data) { this.data = data; }
1879         uint data;
1880     }
1881 
1882     alias X = SSOHashMapOrSet!(K, V, null, FNV!(64, true));
1883     auto x = X();
1884 
1885     x[K(42)] = new V(43);
1886 
1887     assert(x.length == 1);
1888 
1889     foreach (e; x.byValue)      // `e` is auto ref
1890     {
1891         static assert(is(typeof(e) == X.ValueType)); // mutable access to value
1892         assert(e.data == 43);
1893 
1894         // value mutation side effects
1895         e.data += 1;
1896         assert(e.data == 44);
1897         e.data -= 1;
1898         assert(e.data == 43);
1899     }
1900 
1901     foreach (ref e; x.byKeyValue)   // `e` is auto ref
1902     {
1903         static assert(is(typeof(e.key) == const(X.KeyType))); // const access to key
1904 
1905         static assert(is(typeof(e.value) == X.ValueType)); // mutable access to value
1906 
1907         assert(e.value.data == 43);
1908 
1909         // value mutation side effects
1910         e.value.data += 1;
1911         assert(e.value.data == 44);
1912         e.value.data -= 1;
1913         assert(e.value.data == 43);
1914     }
1915 }
1916 
1917 version(unittest)
1918 {
1919     import std.algorithm.comparison : equal;
1920     import nxt.digestx.fnv : FNV;
1921     import nxt.array_help : s;
1922 }