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         static if (__VERSION__ >= 2098) import std.math.algebraic : nextPow2; else 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                 insert(element);
457             else
458                 insert(*cast(Unqual!T*)&element);
459         }
460     }
461 
462     /** Reserve rom for `extraCapacity` number of extra buckets. */
463     pragma(inline, true)
464     void reserveExtra()(size_t extraCapacity)
465     {
466         if ((capacityScaleNumerator *
467              (_length + extraCapacity) /
468              capacityScaleDenominator) >
469             _bins.length * smallBinCapacity)
470         {
471             growWithExtraCapacity(extraCapacity);
472         }
473     }
474 
475     static if (hasValue)
476     {
477         /** Insert or replace `value` at `key`. */
478         pragma(inline, true)    // LDC must have this
479         InsertionStatus insert(K key, V value)
480         {
481             return insert(T(move(key),
482                             move(value)));
483         }
484     }
485 
486     static private size_t offsetOfKey(in T[] elements,
487                                       in ref K key)
488     {
489         version(LDC) pragma(inline, true);
490         size_t elementOffset = 0;
491         foreach (const ref e; elements)
492         {
493             if (keyOf(e) is key) { break; }
494             elementOffset += 1;
495         }
496         return elementOffset;
497     }
498 
499     pragma(inline, true)
500     static private bool hasKey(in T[] elements,
501                                in ref K key)
502     {
503         return offsetOfKey(elements, key) != elements.length;
504     }
505 
506     /** Insert `element` like with `insert()` without automatic growth of number
507      * of bins.
508      */
509     InsertionStatus insertMoveWithoutBinCountGrowth(ref T element) @trusted // ref simplifies move
510     {
511         immutable binIx = keyToBinIx(keyRefOf(element));
512         T[] elements = binElementsAt(binIx);
513         immutable elementOffset = offsetOfKey(elements, keyOf(element));
514         immutable elementFound = elementOffset != elements.length;
515 
516         if (elementFound)
517         {
518             static if (hasValue)
519             {
520                 /* TODO: Rust does the same in its `insert()` at
521                  * https://doc.rust-lang.org/std/collections/struct.HashMap.html
522                  */
523                 if (elements[elementOffset].value !is valueOf(element)) // if different value (or identity for classes)
524                 {
525                     // replace value
526                     static if (needsMove!V)
527                     {
528                         move(valueOf(element),
529                              elements[elementOffset].value);
530                     }
531                     else
532                     {
533                         elements[elementOffset].value = valueOf(element);
534                     }
535                     return typeof(return).modified;
536                 }
537             }
538             return typeof(return).unmodified;
539         }
540         else                    // no elementFound
541         {
542             if (_bstates[binIx].isLarge) // stay large
543             {
544                 _bins[binIx].large.insertBackMove(element);
545             }
546             else
547             {
548                 if (_bstates[binIx].isFullSmall) // expand small to large
549                 {
550                     static if (needsMove!T)
551                     {
552                         // TODO: functionize to concatenation:moveConcatenate()
553                         T[smallBinCapacity + 1] smallCopy = void;
554                         moveEmplaceAllNoReset(_bins[binIx].small[],
555                                               smallCopy[0 .. smallBinCapacity]);
556                         moveEmplace(element,
557                                     smallCopy[smallBinCapacity]);
558 
559                         LargeBin.emplaceWithMovedElements(&_bins[binIx].large,
560                                                           smallCopy[]);
561                     }
562                     else
563                     {
564                         import nxt.concatenation : concatenate;
565                         auto smallCopy = concatenate(_bins[binIx].small, element);
566                         emplace!(LargeBin)(&_bins[binIx].large, smallCopy);
567                     }
568                     _bstates[binIx].makeLarge();
569                 }
570                 else            // stay small
571                 {
572                     static if (needsMove!T)
573                     {
574                         moveEmplace(element,
575                                     _bins[binIx].small[_bstates[binIx].smallCount]);
576                     }
577                     else
578                     {
579                         _bins[binIx].small[_bstates[binIx].smallCount] = element;
580                     }
581                     _bstates[binIx].incSmallCount();
582                 }
583             }
584             _length += 1;
585             return typeof(return).added;
586         }
587     }
588 
589     /** L-value element reference (and in turn range iterator).
590      */
591     static private struct LvalueElementRef(SomeHashMapOrSet)
592     {
593         SomeHashMapOrSet* table;
594         size_t binIx;           // index to bin inside table
595         size_t elementOffset;   // offset to element inside bin
596         size_t elementCounter;  // counter over number of elements popped
597 
598         pragma(inline, true):
599 
600         /// Check if empty.
601         @property bool empty() const @safe pure nothrow @nogc
602         {
603             return binIx == table.binCount;
604         }
605 
606         /// Get number of element left to pop.
607         @property size_t length() const @safe pure nothrow @nogc
608         {
609             return table.length - elementCounter;
610         }
611 
612         void initFirstNonEmptyBin()
613         {
614             while (binIx < table.binCount &&
615                    table.binElementCountAt(binIx) == 0)
616             {
617                 binIx += 1;
618             }
619         }
620 
621         pragma(inline)
622         void popFront()
623         {
624             assert(!empty);
625             elementOffset += 1; // next element
626             // if current bin was emptied
627             while (elementOffset >= table.binElementsAt(binIx).length)
628             {
629                 // next bin
630                 binIx += 1;
631                 elementOffset = 0;
632                 if (empty) { break; }
633             }
634             elementCounter += 1;
635         }
636 
637         @property typeof(this) save() // ForwardRange
638         {
639             return this;
640         }
641     }
642 
643     /** R-value element reference (and in turn range iterator).
644      */
645     static private struct RvalueElementRef(SomeHashMapOrSet)
646     {
647         SomeHashMapOrSet table; // owned
648         size_t binIx;           // index to bin inside table
649         size_t elementOffset;   // offset to element inside bin
650         size_t elementCounter;  // counter over number of elements popped
651 
652         pragma(inline, true):
653 
654         /// Check if empty.
655         @property bool empty() const @safe pure nothrow @nogc
656         {
657             return binIx == table.binCount;
658         }
659 
660         /// Get number of element left to pop.
661         @property size_t length() const @safe pure nothrow @nogc
662         {
663             return table.length - elementCounter;
664         }
665 
666         void initFirstNonEmptyBin()
667         {
668             while (binIx < table.binCount &&
669                    table.binElementCountAt(binIx) == 0)
670             {
671                 binIx += 1;
672             }
673         }
674 
675         pragma(inline)
676         void popFront()
677         {
678             assert(!empty);
679             elementOffset += 1; // next element
680             // if current bin was emptied
681             while (elementOffset >= table.binElementsAt(binIx).length)
682             {
683                 // next bin
684                 binIx += 1;
685                 elementOffset = 0;
686                 if (empty) { break; }
687             }
688             elementCounter += 1;
689         }
690     }
691 
692     static if (!hasValue)       // HashSet
693     {
694         pragma(inline, true)
695         bool opBinaryRight(string op)(in K key) inout @trusted
696             if (op == "in")
697         {
698             return contains(key);
699         }
700 
701         /// Range over elements of l-value instance of this.
702         static private struct ByLvalueElement(SomeHashMapOrSet)
703         {
704         pragma(inline, true):
705             static if (is(ElementType == class))
706             {
707                 /// Get reference to front element (key and value).
708                 @property scope auto front()() return @trusted
709                 {
710                     /* cast away const from `SomeHashMapOrSet` for classes
711                      * because class elements are currently hashed and compared
712                      * compared using their identity (pointer value) `is`
713                      */
714                     return cast(ElementType)table.binElementsAt(binIx)[elementOffset];
715                 }
716             }
717             else
718             {
719                 /// Get reference to front element (key and value).
720                 @property scope auto front()()
721                 {
722                     return table.binElementsAt(binIx)[elementOffset];
723                 }
724             }
725             public LvalueElementRef!SomeHashMapOrSet _elementRef;
726             alias _elementRef this;
727         }
728 
729         /// Range over elements of r-value instance of this.
730         static private struct ByRvalueElement(SomeHashMapOrSet)
731         {
732         pragma(inline, true):
733             static if (is(ElementType == class))
734             {
735                 /// Get reference to front element (key and value).
736                 @property scope auto front()() return @trusted
737                 {
738                     /* cast away const from `SomeHashMapOrSet` for classes
739                      * because class elements are currently hashed and compared
740                      * compared using their identity (pointer value) `is`
741                      */
742                     return cast(ElementType)table.binElementsAt(binIx)[elementOffset];
743                 }
744             }
745             else
746             {
747                 /// Get reference to front element (key and value).
748                 @property scope auto front()()
749                 {
750                     return table.binElementsAt(binIx)[elementOffset];
751                 }
752             }
753             public RvalueElementRef!SomeHashMapOrSet _elementRef;
754             alias _elementRef this;
755         }
756 
757         /// ditto
758         version(none)           // cannot be combined
759         {
760             pragma(inline, true)
761             scope auto opSlice()() inout return // template-lazy
762             {
763                 return byElement();
764             }
765         }
766     }
767 
768     static if (hasValue)        // HashMap
769     {
770         scope inout(V)* opBinaryRight(string op)(in K key) inout @trusted return
771             if (op == "in")
772         {
773             if (empty)
774             {
775                 // prevent range error in `binElementsAt` when `this` is empty
776                 return typeof(return).init;
777             }
778             immutable binIx = keyToBinIx(key);
779             const elements = binElementsAt(binIx);
780             immutable elementOffset = offsetOfKey(elements, key);
781             immutable elementFound = elementOffset != elements.length;
782             if (elementFound)
783             {
784                 return cast(typeof(return))&elements[elementOffset].value;
785                 // return typeof(return)(&this, binIx, elementOffset);
786             }
787             else                    // miss
788             {
789                 return typeof(return).init;
790             }
791         }
792 
793         static private struct ByKey(SomeHashMapOrSet)
794         {
795             pragma(inline, true):
796             /// Get reference to key of front element.
797             @property const scope auto ref front()() return // key access must be const
798             {
799                 return table.binElementsAt(binIx)[elementOffset].key;
800             }
801             public LvalueElementRef!SomeHashMapOrSet _elementRef;
802             alias _elementRef this;
803         }
804 
805         /// Returns forward range that iterates through the keys of `this`.
806         @property scope auto byKey()() inout return @trusted // template-lazy property
807         {
808             alias This = ConstThis;
809             auto result = ByKey!This((LvalueElementRef!This(cast(This*)&this)));
810             result.initFirstNonEmptyBin();
811             return result;
812         }
813 
814         static private struct ByValue(SomeHashMapOrSet)
815         {
816             pragma(inline, true):
817             /// Get reference to value of front element.
818             @property scope auto ref front()() return @trusted // template-lazy property. TODO: remove @trusted
819             {
820                 return *(cast(ValueType*)(&table.binElementsAt(binIx)[elementOffset].value)); // TODO: remove reinterpret cast
821             }
822             public LvalueElementRef!SomeHashMapOrSet _elementRef;
823             alias _elementRef this;
824         }
825 
826         /// Returns forward range that iterates through the values of `this`.
827         @property scope auto byValue()() inout return @trusted // template-lazy property
828         {
829             alias This = ConstThis;
830             auto result = ByValue!This((LvalueElementRef!This(cast(This*)&this)));
831             result.initFirstNonEmptyBin();
832             return result;
833         }
834 
835         static private struct ByKeyValue(SomeHashMapOrSet)
836         {
837             pragma(inline, true):
838             /// Get reference to front element (key and value).
839             @property scope auto ref front()() return @trusted
840             {
841                 static if (isMutable!(SomeHashMapOrSet))
842                 {
843                     alias E = CT;
844                 }
845                 else
846                 {
847                     alias E = const(T);
848                 }
849                 return *(cast(E*)&table.binElementsAt(binIx)[elementOffset]); // TODO: remove cast
850             }
851             public LvalueElementRef!SomeHashMapOrSet _elementRef;
852             alias _elementRef this;
853         }
854 
855         /// Returns forward range that iterates through the keys and values of `this`.
856         @property scope auto byKeyValue()() return @trusted // template-lazy property
857         {
858             alias This = MutableThis;
859             auto result = ByKeyValue!This((LvalueElementRef!This(cast(This*)&this)));
860             result.initFirstNonEmptyBin();
861             return result;
862         }
863         /// ditto
864         @property scope auto byKeyValue()() const return @trusted // template-lazy property
865         {
866             alias This = ConstThis;
867             auto result = ByKeyValue!This((LvalueElementRef!This(cast(This*)&this)));
868             result.initFirstNonEmptyBin();
869             return result;
870         }
871 
872         /// ditto
873         pragma(inline, true)
874         scope auto opSlice()() return  // template-lazy
875         {
876             return byKeyValue();
877         }
878 
879         /// Indexing.
880         scope ref inout(V) opIndex()(in K key) inout return // auto ref here makes things slow
881         {
882             version(LDC) pragma(inline, true);
883             immutable binIx = keyToBinIx(key);
884             auto elements = binElementsAt(binIx);
885 
886             immutable elementOffset = offsetOfKey(elements, key);
887             immutable elementFound = elementOffset != elements.length;
888             if (elementFound)
889             {
890                 return binElementsAt(binIx)[elementOffset].value;
891             }
892             version(assert)
893             {
894                 import core.exception : RangeError;
895                 throw new RangeError("Key not found");
896             }
897         }
898 
899         /** Get value of `key` or `defaultValue` if `key` not present (and
900          * therefore `nothrow`).
901          *
902          * Returns: value reference iff `defaultValue` is an l-value.
903          *
904          * TODO: make `defaultValue` `lazy` when that can be `nothrow`
905          */
906         auto ref V get()(in K key, in V defaultValue) @trusted // auto ref here makes things slow
907         {
908             import std.algorithm.searching : countUntil;
909             immutable binIx = keyToBinIx(key);
910             immutable ptrdiff_t elementOffset = binElementsAt(binIx).countUntil!(_ => _.key is key); // TODO: functionize
911             if (elementOffset != -1) // elementFound
912             {
913                 return binElementsAt(binIx)[elementOffset].value;
914             }
915             else                    // miss
916             {
917                 return defaultValue;
918             }
919         }
920 
921 	/** Supports $(B aa[key] = value;) syntax.
922 	 */
923         void opIndexAssign()(V value, K key) // template-lazy
924 	{
925             version(LDC) pragma(inline, true);
926             insert(T(move(key),
927                      move(value)));
928             // TODO: return reference to value
929 	}
930 
931         static if (__traits(compiles, { V _; _ += 1; })) // if we can increase the key
932         {
933             /** Increase value at `key`, or set value to 1 if `key` hasn't yet
934              * been added.
935              */
936             pragma(inline, true)
937             void autoinitIncAt()(in K key) // template-lazy
938             {
939                 auto elementFound = key in this;
940                 if (elementFound)
941                 {
942                     (*elementFound) += 1;
943                 }
944                 else
945                 {
946                     insert(key, V.init + 1);
947                 }
948             }
949         }
950 
951     }
952 
953     /** Remove `element` and, when possible, shrink its large bin to small.
954 
955         Returns: `true` if element was removed, `false` otherwise.
956     */
957     bool remove()(in K key)     // template-lazy
958         @trusted
959     {
960         immutable binIx = keyToBinIx(key);
961         import nxt.container_algorithm : popFirstMaybe;
962         if (_bstates[binIx].isLarge)
963         {
964             immutable elementFound = _bins[binIx].large.popFirstMaybe!keyEqualPred(key);
965             _length -= elementFound ? 1 : 0;
966             if (elementFound)
967             {
968                 tryShrinkLargeBinAt(binIx);
969             }
970             return elementFound;
971         }
972         else
973         {
974             const elements = smallBinElementsAt(binIx);
975             immutable elementIx = offsetOfKey(elements, key);
976             immutable elementFound = elementIx != elements.length;
977             if (elementFound)
978             {
979                 removeSmallElementAt(binIx, elementIx);
980             }
981             _length -= elementFound ? 1 : 0;
982             return elementFound;
983         }
984     }
985 
986     /** Remove small element at `elementIx` in bin `binIx`. */
987     private void removeSmallElementAt()(size_t binIx, // template-lazy
988                                         size_t elementIx)
989     {
990         assert(!_bstates[binIx].isLarge);
991         import nxt.container_algorithm : shiftToFrontAt;
992         smallBinElementsAt(binIx).shiftToFrontAt(elementIx);
993         _bstates[binIx].decSmallCount();
994         static if (hasElaborateDestructor!T)
995         {
996             .destroy(_bins[binIx].small[_bstates[binIx].smallCount]); // TODO: this is incorrect
997         }
998     }
999 
1000     /** Shrink large bin at `binIx` possible posbiel. */
1001     private void tryShrinkLargeBinAt()(size_t binIx) // template-lazy
1002     {
1003         assert(_bstates[binIx].isLarge);
1004         immutable count = _bins[binIx].large.length;
1005         if (count <= smallBinCapacity) // large fits in small
1006         {
1007             SmallBin smallCopy = void;
1008             moveEmplaceAllNoReset(_bins[binIx].large[0 .. count],
1009                                   smallCopy[0 .. count]);
1010             static if (hasElaborateDestructor!LargeBin)
1011             {
1012                 .destroy(_bins[binIx].large);
1013             }
1014             moveEmplaceAllNoReset(smallCopy[0 .. count],
1015                                   _bins[binIx].small[0 .. count]);
1016             _bstates[binIx].smallCount = count;
1017         }
1018     }
1019 
1020     /** Rehash.
1021      *
1022      * Reorganize `this` in place so that lookups are more efficient.
1023      */
1024     ref typeof(this) rehash()() @trusted // template-lazy
1025     {
1026         static assert(0, "TODO: remove template parens of this functions and implement");
1027         // return this;
1028     }
1029 
1030     /// Check if empty.
1031     pragma(inline, true)
1032     @property bool empty() const { return _length == 0; }
1033 
1034     /// Get length (read-only).
1035     pragma(inline, true)
1036     @property size_t length() const { return _length; }
1037 
1038     /// Get bin count.
1039     pragma(inline, true)
1040     @property size_t binCount() const { return _bins.length; }
1041 
1042     /// Bin count statistics.
1043     struct BinCounts
1044     {
1045         size_t smallCount;      // number of hybrid bins being small
1046         size_t largeCount;      // number of hybrid bins being large
1047     }
1048 
1049     /// Returns: bin count statistics for small and large bins.
1050     pragma(inline, false)
1051     BinCounts binCounts()() const // template-lazy
1052     {
1053         import std.algorithm : count;
1054         immutable largeCount = _bstates[].count!(_ => _.isLarge);
1055         immutable smallCount = _bstates.length - largeCount;
1056         auto result = typeof(return)(smallCount,
1057                                      largeCount);
1058         assert(result.largeCount + result.smallCount == _bstates.length);
1059         return result;
1060     }
1061 
1062     /** Returns: elements in bin at `binIx`. */
1063     pragma(inline, true)
1064     private scope inout(T)[] binElementsAt(size_t binIx) inout return @trusted
1065     {
1066         if (_bstates[binIx].isLarge)
1067         {
1068             return _bins[binIx].large[];
1069         }
1070         else
1071         {
1072             return smallBinElementsAt(binIx);
1073         }
1074     }
1075 
1076     pragma(inline, true)
1077     private scope inout(T)[] smallBinElementsAt(size_t binIx) inout return
1078     {
1079         return _bins[binIx].small[0 .. _bstates[binIx].smallCount];
1080     }
1081 
1082     /** Returns: number of elements in bin at `binIx`. */
1083     pragma(inline, true)
1084     private size_t binElementCountAt(size_t binIx) const @trusted
1085     {
1086         if (_bstates[binIx].isLarge)
1087         {
1088             return _bins[binIx].large.length;
1089         }
1090         else
1091         {
1092             return _bstates[binIx].smallCount;
1093         }
1094     }
1095 
1096     /** Maximum number of elements that fits in a small bin
1097      * (`SmallBin`).
1098      */
1099     enum smallBinCapacity = max(smallBinMinCapacity,
1100                                    LargeBin.sizeof / T.sizeof);
1101 
1102 private:
1103     import nxt.dynamic_array : Array = DynamicArray;
1104 
1105     /** 32-bit capacity and length for LargeBinLnegth on 64-bit platforms
1106      * saves one word and makes insert() and contains() significantly faster */
1107     alias LargeBinCapacity = uint;
1108     alias LargeBin = Array!(T, Allocator, LargeBinCapacity);
1109 
1110     alias SmallBin = T[smallBinCapacity];
1111 
1112     /// no space for `SmallBin`s should be wasted
1113     static assert(SmallBin.sizeof >= LargeBin.sizeof);
1114 
1115     /** Small-size-optimized bin.
1116      */
1117     union HybridBin
1118     {
1119         SmallBin small;
1120         LargeBin large;
1121     }
1122 
1123     static if (mustAddGCRange!LargeBin)
1124     {
1125         static assert(mustAddGCRange!HybridBin, "HybridBin mustAddGCRange when LargeBin is " ~ LargeBin.stringof);
1126     }
1127     static if (mustAddGCRange!SmallBin)
1128     {
1129         static assert(mustAddGCRange!HybridBin, "HybridBin mustAddGCRange when SmallBin is " ~ SmallBin.stringof);
1130     }
1131 
1132     /** Count and large status of bin. */
1133     struct Bstate
1134     {
1135         alias Count = ubyte;
1136 
1137         pragma(inline, true):
1138 
1139         @property Count smallCount() const
1140         {
1141             assert(_count <= smallBinCapacity);
1142             return _count;
1143         }
1144 
1145         @property void smallCount(size_t count)
1146         {
1147             assert(count <= smallBinCapacity);
1148             _count = cast(Count)count;
1149         }
1150 
1151         void decSmallCount()
1152         {
1153             assert(_count >= 1);
1154             _count -= 1;
1155         }
1156 
1157         void incSmallCount()
1158         {
1159             assert(_count + 1 <= smallBinCapacity);
1160             _count += 1;
1161         }
1162 
1163         /** Returns: `true` iff `this` is a large bin. */
1164         @property bool isLarge() const
1165         {
1166             return _count == Count.max;
1167         }
1168 
1169         void makeLarge()
1170         {
1171             _count = Count.max;
1172         }
1173 
1174         /** Returns: `true` iff `this` is a full small bin. */
1175         @property bool isFullSmall() const
1176         {
1177             return _count == smallBinCapacity;
1178         }
1179 
1180         Count _count;
1181     }
1182 
1183     /** Small-size-optimized bin array.
1184         Size-state (including small or large) for each element is determined by
1185         corresponding element in `Bstates`.
1186      */
1187     alias Bins = Array!(HybridBin, Allocator);
1188 
1189     /// Bin states.
1190     alias Bstates = Array!(Bstate, Allocator);
1191 
1192     // TODO: merge these into an instance of soa.d and remove invariant
1193     Bins _bins;                 // bin elements
1194     Bstates _bstates;           // bin states
1195     invariant
1196     {
1197         assert(_bins.length ==
1198                _bstates.length);
1199     }
1200 
1201     size_t _length;             // total number of elements stored
1202 
1203     /** Returns: bin index of `hash`. */
1204     pragma(inline, true)
1205     size_t hashToIndex(hash_t hash) const
1206     {
1207         return hash & powerOf2Mask;
1208     }
1209 
1210     /** Returns: bin index of `key`. */
1211     size_t keyToBinIx()(in auto ref K key) const
1212     {
1213         version(LDC) pragma(inline, true);
1214         import nxt.digestion : hashOf2;
1215         return hashToIndex(hashOf2!(hasher)(key));
1216     }
1217 
1218     /** Returns: current index mask from bin count. */
1219     pragma(inline, true)
1220     private size_t powerOf2Mask() const
1221     {
1222         immutable typeof(return) mask = _bins.length - 1;
1223         assert((~mask ^ mask) == size_t.max); // isPowerOf2(_bins.length)
1224         return mask;
1225     }
1226 }
1227 
1228 /** Hash map storing keys of type `K` and values of type `V`.
1229  */
1230 alias SSOHashMap(K, V,
1231                  alias Allocator = null,
1232                  alias hasher = hashOf,
1233                  uint smallBinMinCapacity = 1) = SSOHashMapOrSet!(K, V,
1234                                                                   Allocator,
1235                                                                   hasher,
1236                                                                   smallBinMinCapacity);
1237 
1238 /** Hash map storing keys of type `K`.
1239  */
1240 alias SSOHashSet(K,
1241                  alias Allocator = null,
1242                  alias hasher = hashOf,
1243                  uint smallBinMinCapacity = 1) = SSOHashMapOrSet!(K, void,
1244                                                                   Allocator,
1245                                                                   hasher,
1246                                                                   smallBinMinCapacity);
1247 
1248 import std.traits : isInstanceOf;
1249 import std.functional : unaryFun;
1250 
1251 /** Remove all elements in `x` matching `predicate`.
1252     TODO: move to container_algorithm.d.
1253 */
1254 void removeAllMatching(alias predicate, SomeHashMapOrSet)(auto ref SomeHashMapOrSet x)
1255     @trusted
1256     if (isInstanceOf!(SSOHashMapOrSet,
1257                       SomeHashMapOrSet))
1258 {
1259     import core.lifetime : moveEmplace;
1260     foreach (immutable binIx; 0 .. x._bins.length)
1261     {
1262         if (x._bstates[binIx].isLarge)
1263         {
1264             import nxt.dynamic_array : remove;
1265             immutable removeCount = x._bins[binIx].large.remove!predicate();
1266             x._length -= removeCount;
1267             x.tryShrinkLargeBinAt(binIx);
1268         }
1269         else
1270         {
1271             SomeHashMapOrSet.SmallBin tmpSmall;
1272             SomeHashMapOrSet.Bstate tmpBstate;
1273             foreach (ref element; x.smallBinElementsAt(binIx))
1274             {
1275                 if (unaryFun!predicate(element))
1276                 {
1277                     import core.internal.traits : hasElaborateDestructor;
1278                     static if (hasElaborateDestructor!(SomeHashMapOrSet.T))
1279                     {
1280                         destroy(element);
1281                     }
1282                     x._length -= 1;
1283                 }
1284                 else
1285                 {
1286                     moveEmplace(element, tmpSmall[tmpBstate.smallCount()]);
1287                     tmpBstate.incSmallCount();
1288                 }
1289             }
1290             assert(!tmpBstate.isLarge); // should stay small
1291             moveEmplace(tmpSmall, x._bins[binIx].small);
1292             moveEmplace(tmpBstate, x._bstates[binIx]);
1293         }
1294     }
1295 }
1296 
1297 /** Returns: `x` eagerly filtered on `predicate`.
1298     TODO: move to container_algorithm.d.
1299 */
1300 SomeHashMapOrSet filtered(alias predicate, SomeHashMapOrSet)(SomeHashMapOrSet x)
1301     @trusted
1302     if (isInstanceOf!(SSOHashMapOrSet,
1303                       SomeHashMapOrSet))
1304 {
1305     import std.functional : not;
1306     removeAllMatching!(not!predicate)(x);
1307     import std.algorithm.mutation : move;
1308     return move(x);
1309 }
1310 
1311 /** Returns: `x` eagerly intersected with `y`.
1312     TODO: move to container_algorithm.d.
1313  */
1314 auto intersectedWith(C1, C2)(C1 x, auto ref C2 y)
1315     if (isInstanceOf!(SSOHashMapOrSet, C1) &&
1316         isInstanceOf!(SSOHashMapOrSet, C2))
1317 {
1318     import std.algorithm.mutation : move;
1319     static if (__traits(isRef, y)) // y is l-value
1320     {
1321         // @("complexity", "O(x.length)")
1322         return move(x).filtered!(_ => y.contains(_)); // only x can be reused
1323     }
1324     else
1325     {
1326         /* both are r-values so reuse the shortest */
1327         // @("complexity", "O(min(x.length), min(y.length))")
1328         if (x.length <
1329             y.length)
1330         {
1331             return move(x).filtered!(_ => y.contains(_));
1332         }
1333         else
1334         {
1335             return move(y).filtered!(_ => x.contains(_));
1336         }
1337     }
1338 }
1339 
1340 /// r-value and l-value intersection
1341 @safe pure nothrow @nogc unittest
1342 {
1343     alias K = uint;
1344     alias X = SSOHashMapOrSet!(K, void, null, FNV!(64, true));
1345 
1346     auto x = X.withElements([12, 13].s);
1347     auto y = X.withElements([10, 12, 13, 15].s).intersectedWith(x);
1348     assert(y.length == 2);
1349     assert(y.contains(12));
1350     assert(y.contains(13));
1351 }
1352 
1353 /// r-value and r-value intersection
1354 @safe pure nothrow @nogc unittest
1355 {
1356     alias K = uint;
1357     alias X = SSOHashMapOrSet!(K, void, null, FNV!(64, true));
1358 
1359     auto y = X.withElements([10, 12, 13, 15].s).intersectedWith(X.withElements([12, 13].s));
1360     assert(y.length == 2);
1361     assert(y.contains(12));
1362     assert(y.contains(13));
1363 }
1364 
1365 /** Returns: `x` eagerly intersected with `y`.
1366     TODO: move to container_algorithm.d.
1367  */
1368 auto intersectWith(C1, C2)(ref C1 x,
1369                            auto ref const(C2) y)
1370     if (isInstanceOf!(SSOHashMapOrSet, C1) &&
1371         isInstanceOf!(SSOHashMapOrSet, C2))
1372 {
1373     return x.removeAllMatching!(_ => !y.contains(_));
1374 }
1375 
1376 /// r-value and l-value intersection
1377 @safe pure nothrow @nogc unittest
1378 {
1379     alias K = uint;
1380     alias X = SSOHashMapOrSet!(K, void, null, FNV!(64, true));
1381 
1382     auto x = X.withElements([12, 13].s);
1383     auto y = X.withElements([10, 12, 13, 15].s);
1384     y.intersectWith(x);
1385     assert(y.length == 2);
1386     assert(y.contains(12));
1387     assert(y.contains(13));
1388 }
1389 
1390 /// Returns forward range that iterates through the elements of `c`.
1391 auto byElement(SomeHashMapOrSet)(auto ref inout(SomeHashMapOrSet) c)
1392     @trusted
1393     if (isInstanceOf!(SSOHashMapOrSet,
1394                       SomeHashMapOrSet))
1395 {
1396     alias C = const(SomeHashMapOrSet);
1397     static if (__traits(isRef, c))
1398     {
1399         auto result = C.ByLvalueElement!C((C.LvalueElementRef!C(cast(C*)&c)));
1400         result.initFirstNonEmptyBin();
1401         return result;
1402     }
1403     else
1404     {
1405         import std.algorithm.mutation : move;
1406         auto result = C.ByRvalueElement!C((C.RvalueElementRef!C(move(*(cast(SomeHashMapOrSet*)&c))))); // reinterpret
1407         result.initFirstNonEmptyBin();
1408         return move(result);
1409     }
1410 }
1411 alias range = byElement;        // EMSI-container naming
1412 
1413 @safe:
1414 
1415 /// make range from l-value and r-value. element access is always const
1416 pure nothrow @nogc unittest
1417 {
1418     alias K = uint;
1419     alias X = SSOHashMapOrSet!(K, void, null, FNV!(64, true));
1420 
1421     immutable a = [11, 22].s;
1422 
1423     // mutable
1424     auto x = X.withElements(a);
1425     foreach (e; x.byElement)    // from l-value
1426     {
1427         static assert(is(typeof(e) == const(K))); // always const access
1428     }
1429 
1430     // const
1431     const y = X.withElements(a);
1432     foreach (e; y.byElement)    // from l-value
1433     {
1434         static assert(is(typeof(e) == const(K)));
1435     }
1436 
1437     foreach (e; X.withElements([11].s).byElement) // from r-value
1438     {
1439         static assert(is(typeof(e) == const(K))); // always const access
1440     }
1441 }
1442 
1443 /// test various things
1444 pure nothrow @nogc unittest
1445 {
1446     immutable n = 600;
1447 
1448     alias K = uint;
1449 
1450     import std.meta : AliasSeq;
1451     foreach (V; AliasSeq!(void, string))
1452     {
1453         alias X = SSOHashMapOrSet!(K, V, null, FNV!(64, true));
1454 
1455         static if (!X.hasValue)
1456         {
1457             auto x = X.withElements([11, 12, 13].s);
1458 
1459             import std.algorithm : count;
1460             auto xr = x.byElement;
1461 
1462             alias R = typeof(xr);
1463             import std.range.primitives : isInputRange;
1464             import std.traits : ReturnType;
1465             static assert(is(typeof(R.init) == R));
1466             static assert(is(ReturnType!((R xr) => xr.empty) == bool));
1467             auto f = xr.front;
1468             static assert(is(typeof((R xr) => xr.front)));
1469             static assert(!is(ReturnType!((R xr) => xr.front) == void));
1470             static assert(is(typeof((R xr) => xr.popFront)));
1471 
1472             static assert(isInputRange!(typeof(xr)));
1473 
1474             assert(x.byElement.count == 3);
1475 
1476             X y;
1477             foreach (const ref e; x.byElement)
1478             {
1479                 y.insert(e);
1480             }
1481 
1482             assert(y.byElement.count == 3);
1483             assert(x == y);
1484 
1485             const z = X();
1486             assert(z.byElement.count == 0);
1487 
1488             immutable w = X();
1489             assert(w.byElement.count == 0);
1490 
1491             {
1492                 auto xc = X.withElements([11, 12, 13].s);
1493                 assert(xc.length == 3);
1494                 assert(xc.contains(11));
1495 
1496                 // TODO: http://forum.dlang.org/post/kvwrktmameivubnaifdx@forum.dlang.org
1497                 xc.removeAllMatching!(_ => _ == 11);
1498 
1499                 assert(xc.length == 2);
1500                 assert(!xc.contains(11));
1501 
1502                 xc.removeAllMatching!(_ => _ == 12);
1503                 assert(!xc.contains(12));
1504                 assert(xc.length == 1);
1505 
1506                 xc.removeAllMatching!(_ => _ == 13);
1507                 assert(!xc.contains(13));
1508                 assert(xc.length == 0);
1509 
1510                 // this is ok
1511                 foreach (e; xc.byElement) {}
1512 
1513             }
1514 
1515             {
1516                 auto k = X.withElements([11, 12].s).filtered!(_ => _ != 11).byElement;
1517                 static assert(isInputRange!(typeof(k)));
1518                 assert(k.front == 12);
1519                 k.popFront();
1520                 assert(k.empty);
1521             }
1522 
1523             {
1524                 X q;
1525                 auto qv = [11U, 12U, 13U, 14U].s;
1526                 q.insertN(qv);
1527                 foreach (e; qv[])
1528                 {
1529                     assert(q.contains(e));
1530                 }
1531                 q.clear();
1532                 assert(q.empty);
1533             }
1534         }
1535 
1536         import nxt.container_traits : mustAddGCRange;
1537         static if (X.hasValue &&
1538                    is(V == string))
1539         {
1540             static assert(mustAddGCRange!V);
1541             static assert(mustAddGCRange!(V[1]));
1542             static assert(mustAddGCRange!(X.T));
1543             static assert(mustAddGCRange!(X.SmallBin));
1544             static assert(!mustAddGCRange!(X.LargeBin));
1545         }
1546         else
1547         {
1548             static assert(!mustAddGCRange!(X.T));
1549             static assert(!mustAddGCRange!(X.SmallBin), "Fails for X being " ~ X.SmallBin.stringof);
1550         }
1551 
1552         auto x1 = X();            // start empty
1553 
1554         // all bins start small
1555         assert(x1.binCounts.largeCount == 0);
1556 
1557         // fill x1
1558 
1559         foreach (immutable key; 0 .. n)
1560         {
1561             static if (X.hasValue)
1562             {
1563                 const value = V.init;
1564                 const element = X.ElementType(key, value);
1565             }
1566             else
1567             {
1568                 const element = key;
1569             }
1570 
1571             assert(key !in x1);
1572 
1573             assert(x1.length == key);
1574             assert(x1.insert(element) == X.InsertionStatus.added);
1575 
1576             static if (X.hasValue)
1577             {
1578                 const e2 = X.ElementType(key, "a");
1579                 assert(x1.insert(e2) == X.InsertionStatus.modified);
1580                 assert(x1.contains(key));
1581                 assert(x1.get(key, null) == "a");
1582                 x1.remove(key);
1583                 x1[key] = value;
1584             }
1585 
1586             assert(x1.length == key + 1);
1587 
1588             assert(key in x1);
1589             static if (X.hasValue)
1590             {
1591                 auto elementFound = key in x1;
1592                 assert(elementFound);
1593                 assert(*elementFound != "_");
1594             }
1595 
1596             assert(x1.insert(element) == X.InsertionStatus.unmodified);
1597             static if (X.hasValue)
1598             {
1599                 assert(x1.insert(key, value) == X.InsertionStatus.unmodified);
1600             }
1601             assert(x1.length == key + 1);
1602 
1603             assert(key in x1);
1604         }
1605 
1606         static if (X.hasValue)
1607         {
1608             import nxt.dynamic_array : Array = DynamicArray;
1609             Array!(X.ElementType) a1;
1610 
1611             foreach (const ref key; x1.byKey)
1612             {
1613                 auto keyPtr = key in x1;
1614                 assert(keyPtr);
1615                 a1 ~= X.ElementType(key, (*keyPtr));
1616             }
1617 
1618             assert(x1.length == a1.length);
1619 
1620             foreach (aElement; a1[])
1621             {
1622                 auto keyPtr = aElement.key in x1;
1623                 assert(keyPtr);
1624                 assert((*keyPtr) is aElement.value);
1625             }
1626         }
1627 
1628         assert(x1.length == n);
1629 
1630         // duplicate x1
1631 
1632         auto x2 = x1.dup;
1633 
1634         // non-symmetric algorithm so both are needed
1635         assert(x2 == x1);
1636         assert(x1 == x2);
1637 
1638         static if (X.hasValue)
1639         {
1640             assert(equal(x1.byKey, x2.byKey));
1641             assert(equal(x1.byValue, x2.byValue));
1642             assert(equal(x1.byKeyValue, x2.byKeyValue));
1643             assert(equal(x1[], x2[]));
1644         }
1645 
1646         assert(x1.binCounts.largeCount ==
1647                x2.binCounts.largeCount);
1648 
1649         static assert(!__traits(compiles, { const _ = x1 < x2; })); // no ordering
1650 
1651         assert(x2.length == n);
1652 
1653         // empty x1
1654 
1655         foreach (immutable key; 0 .. n)
1656         {
1657             static if (X.hasValue)
1658             {
1659                 const element = X.ElementType(key, V.init);
1660             }
1661             else
1662             {
1663                 const element = key;
1664             }
1665 
1666             assert(x1.length == n - key);
1667 
1668             auto elementFound = key in x1;
1669             assert(elementFound);
1670             static if (X.hasValue)
1671             {
1672                 assert(*elementFound is element.value);
1673             }
1674 
1675             assert(x1.remove(key));
1676             assert(x1.length == n - key - 1);
1677 
1678             static if (!X.hasValue)
1679             {
1680                 assert(!x1.contains(key));
1681             }
1682             assert(key !in x1);
1683             assert(!x1.remove(key));
1684             assert(x1.length == n - key - 1);
1685         }
1686 
1687         assert(x1.binCounts.largeCount == 0);
1688 
1689         assert(x1.length == 0);
1690 
1691         x1.clear();
1692         assert(x1.length == 0);
1693 
1694         // empty x2
1695 
1696         assert(x2.length == n); // should be not affected by emptying of x1
1697 
1698         foreach (immutable key; 0 .. n)
1699         {
1700             static if (X.hasValue)
1701             {
1702                 const element = X.ElementType(key, V.init);
1703             }
1704             else
1705             {
1706                 const element = key;
1707             }
1708 
1709             assert(x2.length == n - key);
1710 
1711             assert(key in x2);
1712 
1713             assert(x2.remove(key));
1714             assert(x2.length == n - key - 1);
1715 
1716             assert(key !in x2);
1717             assert(!x2.remove(key));
1718             assert(x2.length == n - key - 1);
1719         }
1720 
1721         assert(x2.binCounts.largeCount == 0);
1722 
1723         assert(x2.length == 0);
1724 
1725         x2.clear();
1726         assert(x2.length == 0);
1727     }
1728 }
1729 
1730 /// range checking
1731 @trusted pure unittest
1732 {
1733     import std.exception : assertThrown, assertNotThrown;
1734     import core.exception : RangeError;
1735     import nxt.uncopyable_sample : V = SomeUncopyable;
1736 
1737     immutable n = 11;
1738 
1739     alias K = uint;
1740 
1741     alias X = SSOHashMapOrSet!(K, V, null, FNV!(64, true));
1742 
1743     auto s = X.withCapacity(n);
1744 
1745     void dummy(ref V value) {}
1746 
1747     assertThrown!RangeError(dummy(s[K.init]));
1748 
1749     foreach (immutable uint i; 0 .. n)
1750     {
1751         s[i] = V(i);
1752         assertNotThrown!RangeError(dummy(s[i]));
1753     }
1754 
1755     foreach (immutable uint i; 0 .. n)
1756     {
1757         s.remove(i);
1758         assertThrown!RangeError(dummy(s[i]));
1759     }
1760 
1761     s[K.init] = V.init;
1762     auto vp = K.init in s;
1763     static assert(is(typeof(vp) == V*));
1764     assert((*vp) == V.init);
1765 
1766     s.remove(K.init);
1767     assert(K.init !in s);
1768 
1769     X t;
1770     t.reserveExtra(4096);
1771     assert(t.binCount == 8192);
1772 
1773     t.clear();
1774 }
1775 
1776 /// class as value
1777 @trusted pure unittest
1778 {
1779     import std.exception : assertThrown, assertNotThrown;
1780     import core.exception : RangeError;
1781 
1782     immutable n = 11;
1783 
1784     alias K = uint;
1785     class V
1786     {
1787         this(uint data) { this.data = data; }
1788         uint data;
1789     }
1790 
1791     alias X = SSOHashMapOrSet!(K, V, null, FNV!(64, true));
1792 
1793     auto s = X.withCapacity(n);
1794 
1795     void dummy(ref V value) {}
1796 
1797     assertThrown!RangeError(dummy(s[K.init]));
1798 
1799     foreach (immutable uint i; 0 .. n)
1800     {
1801         s[i] = new V(i);
1802         assertNotThrown!RangeError(dummy(s[i]));
1803     }
1804 
1805     // test range
1806     auto sr = s.byKeyValue;
1807     assert(sr.length == n);
1808     foreach (immutable uint i; 0 .. n)
1809     {
1810         sr.popFront();
1811         assert(sr.length == n - i - 1);
1812     }
1813 
1814     foreach (immutable uint i; 0 .. n)
1815     {
1816         s.remove(i);
1817         assertThrown!RangeError(dummy(s[i]));
1818     }
1819 
1820     s[K.init] = V.init;
1821     auto vp = K.init in s;
1822     static assert(is(typeof(vp) == V*));
1823 
1824     s.remove(K.init);
1825     assert(K.init !in s);
1826 
1827     X t;
1828     t.reserveExtra(4096);
1829     assert(t.binCount == 8192);
1830 }
1831 
1832 /// constness inference of ranges
1833 pure nothrow unittest
1834 {
1835     alias K = uint;
1836     class V
1837     {
1838         this(uint data) { this.data = data; }
1839         uint data;
1840     }
1841 
1842     alias X = SSOHashMapOrSet!(K, V, null, FNV!(64, true));
1843     const x = X();
1844 
1845     foreach (e; x.byKey)
1846     {
1847         static assert(is(typeof(e) == const(X.KeyType)));
1848     }
1849 
1850     foreach (e; x.byValue)
1851     {
1852         static assert(is(typeof(e) == X.ValueType)); // TODO: should be const(X.ValueType)
1853     }
1854 
1855     foreach (e; x.byKeyValue)
1856     {
1857         static assert(is(typeof(e.key) == const(X.KeyType)));
1858         static assert(is(typeof(e.value) == const(X.ValueType)));
1859         static assert(is(typeof(e) == const(X.ElementType)));
1860     }
1861 }
1862 
1863 /// range key constness and value mutability with `class` value
1864 pure nothrow unittest
1865 {
1866     struct K
1867     {
1868         @disable this(this);
1869         uint value;
1870     }
1871 
1872     class V
1873     {
1874         this(uint data) { this.data = data; }
1875         uint data;
1876     }
1877 
1878     alias X = SSOHashMapOrSet!(K, V, null, FNV!(64, true));
1879     auto x = X();
1880 
1881     x[K(42)] = new V(43);
1882 
1883     assert(x.length == 1);
1884 
1885     foreach (e; x.byValue)      // `e` is auto ref
1886     {
1887         static assert(is(typeof(e) == X.ValueType)); // mutable access to value
1888         assert(e.data == 43);
1889 
1890         // value mutation side effects
1891         e.data += 1;
1892         assert(e.data == 44);
1893         e.data -= 1;
1894         assert(e.data == 43);
1895     }
1896 
1897     foreach (ref e; x.byKeyValue)   // `e` is auto ref
1898     {
1899         static assert(is(typeof(e.key) == const(X.KeyType))); // const access to key
1900 
1901         static assert(is(typeof(e.value) == X.ValueType)); // mutable access to value
1902 
1903         assert(e.value.data == 43);
1904 
1905         // value mutation side effects
1906         e.value.data += 1;
1907         assert(e.value.data == 44);
1908         e.value.data -= 1;
1909         assert(e.value.data == 43);
1910     }
1911 }
1912 
1913 version(unittest)
1914 {
1915     import std.algorithm.comparison : equal;
1916     import nxt.digestx.fnv : FNV;
1917     import nxt.array_help : s;
1918 }