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