1 ///
2 module stdx.collections.hashtable;
3 
4 import stdx.collections.common;
5 
6 debug(CollectionHashtable) import std.stdio;
7 
8 version(unittest)
9 {
10     import std.experimental.allocator.mallocator;
11     import std.experimental.allocator.building_blocks.stats_collector;
12     import std.experimental.allocator : allocatorObject,
13            RCIAllocator, RCISharedAllocator;
14 
15     private alias SCAlloc = StatsCollector!(Mallocator, Options.bytesUsed);
16 }
17 
18 ///
19 struct Hashtable(K, V)
20 {
21     import std.experimental.allocator : RCIAllocator, RCISharedAllocator;
22     import std.traits : isImplicitlyConvertible;
23     import std.typecons : Tuple, Nullable;
24     import std.algorithm.mutation : move;
25     import stdx.collections.slist : SList;
26     import stdx.collections.array : Array;
27 
28 private:
29     alias KVPair = Tuple!(K, "key", V, "value");
30     Array!(SList!(KVPair)) _buckets;
31     Array!size_t _numElems; // This needs to be ref counted
32     static enum double loadFactor = 0.75;
33     AllocatorHandler _allocator;
34 
35     // Allocators internals
36 
37     /// Constructs the ouroboros allocator from allocator if the ouroboros
38     // allocator wasn't previously set
39     /*@nogc*/ nothrow pure @safe
40     bool setAllocator(A)(ref A allocator)
41     if (is(A == RCIAllocator) || is(A == RCISharedAllocator))
42     {
43         if (_allocator.isNull)
44         {
45             auto a = typeof(_allocator)(allocator);
46             move(a, _allocator);
47 
48             _buckets = Array!(SList!(KVPair))(allocator, SList!(KVPair)(allocator));
49             //_buckets = Array!(SList!(KVPair))(allocator);
50             _numElems = Array!size_t(allocator, 0UL);
51 
52             return true;
53         }
54         return false;
55     }
56 
57 public:
58     /**
59      * Constructs a qualified hashtable that will use the provided
60      * allocator object. For `immutable` objects, a `RCISharedAllocator` must
61      * be supplied.
62      *
63      * Params:
64      *      allocator = a $(REF RCIAllocator, std,experimental,allocator) or
65      *                  $(REF RCISharedAllocator, std,experimental,allocator)
66      *                  allocator object
67      *
68      * Complexity: $(BIGOH 1)
69      */
70     this(A, this Q)(A allocator)
71     if (!is(Q == shared)
72         && (is(A == RCISharedAllocator) || !is(Q == immutable))
73         && (is(A == RCIAllocator) || is(A == RCISharedAllocator)))
74     {
75         debug(CollectionHashtable)
76         {
77             writefln("Array.ctor: begin");
78             scope(exit) writefln("Array.ctor: end");
79         }
80         static if (is(Q == immutable) || is(Q == const))
81         {
82             V[K] empty;
83             this(allocator, empty);
84         }
85         else
86         {
87             setAllocator(allocator);
88         }
89     }
90 
91     ///
92     static if (is(K == int))
93     @safe unittest
94     {
95         import std.experimental.allocator : theAllocator, processAllocator;
96 
97         auto h = Hashtable!(int, int)(theAllocator);
98         auto ch = const Hashtable!(int, int)(processAllocator);
99         auto ih = immutable Hashtable!(int, int)(processAllocator);
100     }
101 
102     /**
103      * Constructs a qualified hashtable out of an associative array.
104      * Because no allocator was provided, the hashtable will use the
105      * $(REF, GCAllocator, std,experimental,allocator,gc_allocator).
106      *
107      * Params:
108      *      assocArr = an associative array
109      *
110      * Complexity: $(BIGOH m), where `m` is the number of (key, value) pairs
111      *             in the associative array.
112      */
113     this(U, this Q)(U assocArr)
114     if (is(U == Value[Key], Value : V, Key : K))
115     {
116         this(defaultAllocator!(typeof(this)), assocArr);
117     }
118 
119     ///
120     static if (is(K == int))
121     @safe unittest
122     {
123         import std.algorithm.comparison : equal;
124 
125         auto h = Hashtable!(int, int)([1 : 10]);
126         assert(equal(h.keys(), [1]));
127         assert(equal(h.values(), [10]));
128     }
129 
130     /**
131      * Constructs a qualified hashtable out of an associative array
132      * that will use the provided allocator object.
133      * For `immutable` objects, a `RCISharedAllocator` must be supplied.
134      *
135      * Params:
136      *      allocator = a $(REF RCIAllocator, std,experimental,allocator) or
137      *                  $(REF RCISharedAllocator, std,experimental,allocator)
138      *                  allocator object
139      *      assocArr = an associative array
140      *
141      * Complexity: $(BIGOH m), where `m` is the number of (key, value) pairs
142      *             in the associative array.
143      */
144     this(A, U, this Q)(A allocator, U assocArr)
145     if (!is(Q == shared)
146         && (is(A == RCISharedAllocator) || !is(Q == immutable))
147         && (is(A == RCIAllocator) || is(A == RCISharedAllocator))
148         && is(U == Value[Key], Value : V, Key : K))
149     {
150         debug(CollectionHashtable)
151         {
152             writefln("Hashtable.ctor: begin");
153             scope(exit) writefln("Hashtable.ctor: end");
154         }
155         static if (is(Q == immutable) || is(Q == const))
156         {
157             // Build a mutable hashtable on the stack and pass ownership to this
158 
159             // TODO: is this ok?
160             auto tmp = Hashtable!(K, V)(assocArr);
161             //_buckets = cast(typeof(_buckets))(tmp._buckets);
162             _buckets = typeof(_buckets)(allocator, tmp._buckets);
163             //_numElems = cast(typeof(_numElems))(tmp._numElems);
164             _numElems = typeof(_numElems)(allocator, tmp._numElems);
165 
166             _allocator = immutable AllocatorHandler(allocator);
167         }
168         else
169         {
170             setAllocator(allocator);
171             insert(assocArr);
172         }
173     }
174 
175     ///
176     static if (is(K == int))
177     @safe unittest
178     {
179         import std.algorithm.comparison : equal;
180         import stdx.collections.array : Array;
181 
182         {
183             auto h = Hashtable!(int, int)([1 : 10]);
184             assert(equal(h.keys(), [1]));
185             assert(equal(h.values(), [10]));
186         }
187 
188         {
189             auto h = immutable Hashtable!(int, int)([1 : 10]);
190             assert(equal(h.values(), Array!int([10])));
191         }
192     }
193 
194     static if (is(K == int))
195     /*nothrow*/ pure @safe unittest
196     {
197         auto h = Hashtable!(int, int)([1 : 42]);
198 
199         // Infer safety
200         static assert(!__traits(compiles, () @safe { Hashtable!(Unsafe, int)([Unsafe(1) : 1]); }));
201         static assert(!__traits(compiles, () @safe { auto s = const Hashtable!(Unsafe, int)([Unsafe(1) : 1]); }));
202         static assert(!__traits(compiles, () @safe { auto s = immutable Hashtable!(Unsafe, int)([Unsafe(1) : 1]); }));
203 
204         static assert(!__traits(compiles, () @safe { Hashtable!(UnsafeDtor, int)([UnsafeDtor(1) : 1]); }));
205         static assert(!__traits(compiles, () @safe { auto s = const Hashtable!(UnsafeDtor, int)([UnsafeDtor(1) : 1]); }));
206         static assert(!__traits(compiles, () @safe { auto s = immutable Hashtable!(UnsafeDtor, int)([UnsafeDtor(1) : 1]); }));
207 
208         // Infer purity
209         static assert(!__traits(compiles, () @safe { Hashtable!(Impure, int)([Impure(1) : 1]); }));
210         static assert(!__traits(compiles, () @safe { auto s = const Hashtable!(Impure, int)([Impure(1) : 1]); }));
211         static assert(!__traits(compiles, () @safe { auto s = immutable Hashtable!(Impure, int)([Impure(1) : 1]); }));
212 
213         static assert(!__traits(compiles, () @safe { Hashtable!(ImpureDtor, int)([ImpureDtor(1) : 1]); }));
214         static assert(!__traits(compiles, () @safe { auto s = const Hashtable!(ImpureDtor, int)([ImpureDtor(1) : 1]); }));
215         static assert(!__traits(compiles, () @safe { auto s = immutable Hashtable!(ImpureDtor, int)([ImpureDtor(1) : 1]); }));
216     }
217 
218     /**
219      * Insert the (key, value) pairs of an associative array into the
220      * hashtable.
221      *
222      * If no allocator was provided when the list was created, the
223      * $(REF, GCAllocator, std,experimental,allocator,gc_allocator) will be used.
224      *
225      * Params:
226      *      assocArr = an associative array
227      *
228      * Complexity: $(BIGOH m), where `m` is the number of (key, value) pairs
229      *             in the associative array.
230      */
231     size_t insert(U)(U assocArr)
232     if (is(U == Value[Key], Value : V, Key : K))
233     {
234         debug(CollectionHashtable)
235         {
236             writefln("Hashtable.insert: begin");
237             scope(exit) writefln("Hashtable.insert: end");
238         }
239 
240         // Will be optimized away, but the type system infers T's safety
241         if (0) { K t = K.init; V v = V.init; }
242 
243         // Ensure we have an allocator. If it was already set, this will do nothing
244         auto a = threadAllocatorObject();
245         setAllocator(a);
246 
247         if (_buckets.empty)
248         {
249             auto reqCap = requiredCapacity(assocArr.length);
250             _buckets.reserve(reqCap);
251             _buckets.forceLength(reqCap);
252             _numElems.reserve(1);
253             _numElems.forceLength(1);
254         }
255         foreach(k, v; assocArr)
256         {
257             size_t pos = k.hashOf & (_buckets.length - 1);
258             if (_buckets[pos].empty)
259             {
260                 _buckets[pos] = SList!(KVPair)(_allocator.getLocalAlloc, KVPair(k, v));
261             }
262             else
263             {
264                 _buckets[pos].insert(0, KVPair(k, v));
265             }
266         }
267         _numElems[0] += assocArr.length;
268         return assocArr.length;
269     }
270 
271     ///
272     static if (is(K == int))
273     @safe unittest
274     {
275         import std.algorithm.comparison : equal;
276 
277         auto h = Hashtable!(int, int)();
278         assert(h.length == 0);
279         h.insert([1 : 10]);
280         assert(equal(h.keys(), [1]));
281         assert(equal(h.values(), [10]));
282     }
283 
284     private @nogc nothrow pure @safe
285     size_t requiredCapacity(size_t numElems)
286     {
287         static enum size_t maxPow2 = cast(size_t)(1) << (size_t.sizeof * 8 - 1);
288         while (numElems & (numElems - 1))
289         {
290             numElems &= (numElems - 1);
291         }
292         return numElems < maxPow2 ? 2 * numElems : maxPow2;
293     }
294 
295     /**
296      * Returns number of key-value pairs.
297      *
298      * Returns:
299      *      a positive integer.
300      *
301      * Complexity: $(BIGOH 1).
302      */
303     @nogc nothrow pure @safe
304     size_t length() const
305     {
306         return _numElems.empty ? 0 : _numElems[0];
307     }
308 
309     ///
310     static if (is(K == int))
311     @safe unittest
312     {
313         import std.algorithm.comparison : equal;
314 
315         auto h = Hashtable!(int, int)();
316         assert(h.length == 0);
317         h.insert([1 : 10]);
318         assert(h.length == 1);
319     }
320 
321     /**
322      * Returns number of buckets.
323      *
324      * Returns:
325      *      a positive integer.
326      *
327      * Complexity: $(BIGOH 1).
328      */
329     @nogc nothrow pure @safe
330     size_t size() const
331     {
332         return _buckets.length;
333     }
334 
335     /**
336      * Check if the hashtable is empty.
337      *
338      * Returns:
339      *      `true` if there are no elements in the hashtable; `false` otherwise.
340      *
341      * Complexity: $(BIGOH 1).
342      */
343     @nogc nothrow pure @safe
344     bool empty() const
345     {
346         return _buckets.empty || _numElems.empty || _numElems[0] == 0;
347     }
348 
349     /**
350      * Provide access to the first value in the hashtable. The user must check
351      * that the hashtable isn't `empty`, prior to calling this function.
352      * There is no guarantee of the order of the values, as they are placed in
353      * the hashtable based on the result of the hash function.
354      *
355      * Returns:
356      *      a reference to the first found value.
357      *
358      * Complexity: $(BIGOH length).
359      */
360     ref auto front(this _)()
361     {
362         debug(CollectionHashtable)
363         {
364             writefln("Hashtable.front: begin");
365             scope(exit) writefln("Hashtable.front: end");
366         }
367         auto tmpBuckets = _buckets;
368         while ((!tmpBuckets.empty) && tmpBuckets.front.empty)
369         {
370             tmpBuckets.popFront;
371         }
372         assert(!tmpBuckets.empty, "Hashtable.front: Hashtable is empty");
373         return tmpBuckets.front.front;
374     }
375 
376     void popFront()
377     {
378         debug(CollectionHashtable)
379         {
380             writefln("Hashtable.popFront: begin");
381             scope(exit) writefln("Hashtable.popFront: end");
382         }
383         if (!_buckets.isUnique)
384         {
385             _buckets = _buckets.dup();
386             _numElems = _numElems.dup();
387         }
388         while ((!_buckets.empty) && _buckets.front.empty)
389         {
390             _buckets.popFront;
391         }
392         assert(!_buckets.empty, "Hashtable.front: Hashtable is empty");
393         _buckets.front.popFront;
394         --_numElems[0];
395         // Find the next non-empty bucketList
396         if (_buckets.front.empty)
397         {
398             while ((!_buckets.empty) && _buckets.front.empty)
399             {
400                 _buckets.popFront;
401             }
402         }
403     }
404 
405     private const
406     void getKeyValues(string kv, BuckQual, ListQual, ArrQual)(BuckQual b, ListQual l, ref ArrQual values)
407     {
408         if (b.empty)
409         {
410             return;
411         }
412         else if (l.empty)
413         {
414             auto bTail = b.tail;
415             if (!bTail.empty)
416             {
417                 getKeyValues!kv(bTail, bTail.front, values);
418             }
419         }
420         else
421         {
422             static if (kv.length)
423             {
424                 mixin("values ~= l.front." ~ kv ~ ";");
425             }
426             else
427             {
428                 mixin("values ~= l.front;");
429             }
430             getKeyValues!kv(b, l.tail, values);
431         }
432     }
433 
434     /**
435      * Get an array with the existing keys in the hashtable.
436      *
437      * Returns:
438      *      an `Array!K` array of keys
439      *
440      * Complexity: $(BIGOH size).
441      */
442     Array!K keys(this _)()
443     {
444         debug(CollectionHashtable)
445         {
446             writefln("Hashtable.keys: begin");
447             scope(exit) writefln("Hashtable.keys: end");
448         }
449 
450         Array!K keys;
451         auto tmp = _buckets;
452         if (!_buckets.empty)
453         //if (!tmp.empty)
454         {
455             //getKeyValues!("key")(tmp, tmp.front, keys);
456             getKeyValues!("key")(_buckets, _buckets.front, keys);
457         }
458         return keys;
459     }
460 
461     /**
462      * Get an array with the existing values in the hashtable.
463      *
464      * Returns:
465      *      an `Array!V` array of values
466      *
467      * Complexity: $(BIGOH length).
468      */
469     Array!V values(this _)()
470     {
471         debug(CollectionHashtable)
472         {
473             writefln("Hashtable.values: begin");
474             scope(exit) writefln("Hashtable.values: end");
475         }
476 
477         Array!V values;
478         if (!_buckets.empty)
479         {
480             getKeyValues!("value")(_buckets, _buckets.front, values);
481         }
482 
483         return values;
484     }
485 
486     /**
487      * Get an array with the existing key-value pairs in the hashtable.
488      *
489      * Returns:
490      *      an `Array!(Tuple!(K, "key", V, "value"))` array of key-value pairs
491      *
492      * Complexity: $(BIGOH length).
493      */
494     Array!(Tuple!(K, "key", V, "value")) keyValuePairs(this _)()
495     {
496         debug(CollectionHashtable)
497         {
498             writefln("Hashtable.keyValuePairs: begin");
499             scope(exit) writefln("Hashtable.keyValuePairs: end");
500         }
501 
502         Array!KVPair pairs;
503         if (!_buckets.empty)
504         {
505             getKeyValues!("")(_buckets, _buckets.front, pairs);
506         }
507 
508         return pairs;
509     }
510 
511     private const
512     Nullable!V getValue(ListQual)(ListQual list, ref K key)
513     {
514         if (list.empty)
515         {
516             return Nullable!V.init;
517         }
518         else if (list.front.key == key)
519         {
520             return Nullable!V(list.front.value);
521         }
522         else
523         {
524             return getValue(list.tail, key);
525         }
526     }
527 
528     /**
529      * Get the value corresponding to the given `key`; if it doesn't exist
530      * return `nullValue`.
531      *
532      * Params:
533      *      key = the key corresponding to a value
534      *      nullValue = a value that will be returned if there is no such
535      *                  key-value pair
536      *
537      * Returns:
538      *      the corresponding key value, or `nullValue`.
539      *
540      * Complexity: $(BIGOH n), where n is the number of elements in the
541      *             corresponding key bucket.
542      */
543     V get(this _)(K key, V nullValue)
544     {
545         debug(CollectionHashtable)
546         {
547             writefln("Hashtable.get: begin");
548             scope(exit) writefln("Hashtable.get: end");
549         }
550 
551         size_t pos = key.hashOf & (_buckets.length - 1);
552         auto result = getValue(_buckets[pos], key);
553         if (!result.isNull)
554         {
555             return result.get;
556         }
557         return nullValue;
558     }
559 
560     /**
561      * Get a `Nullable!V` corresponding to the given `key` index.
562      *
563      * Params:
564      *      key = the key corresponding to a value
565      *
566      * Returns:
567      *      a nullable value
568      *
569      * Complexity: $(BIGOH n), where n is the number of elements in the
570      *             corresponding key bucket.
571      */
572     Nullable!V opIndex(this _)(K key)
573     {
574         debug(CollectionHashtable)
575         {
576             writefln("Hashtable.opIndex: begin");
577             scope(exit) writefln("Hashtable.opIndex: end");
578         }
579 
580         size_t pos = key.hashOf & (_buckets.length - 1);
581         return getValue(_buckets[pos], key);
582     }
583 
584     /**
585      * Apply a unary operation to the value corresponding to `key`.
586      * If there isn't such a value, return a `V.init` wrapped inside a
587      * `Nullable`.
588      *
589      * Params:
590      *      key = the key corresponding to a value
591      *
592      * Returns:
593      *      a nullable value corresponding to the result or `V.init`.
594      *
595      * Complexity: $(BIGOH n), where n is the number of elements in the
596      *             corresponding key bucket.
597      */
598     Nullable!V opIndexUnary(string op)(K key)
599     {
600         debug(CollectionHashtable)
601         {
602             writefln("Hashtable.opIndexUnary!" ~ op ~ ": begin");
603             scope(exit) writefln("Hashtable.opIndexUnary!" ~ op ~ ": end");
604         }
605 
606         size_t pos = key.hashOf & (_buckets.length - 1);
607         foreach(ref pair; _buckets[pos])
608         {
609             if (pair.key == key)
610             {
611                 return Nullable!V(mixin(op ~ "pair.value"));
612             }
613         }
614         return Nullable!V.init;
615     }
616 
617     /**
618      * Assign `val` to the element corresponding to `key`.
619      * If there isn't such a value, return a `V.init` wrapped inside a
620      * `Nullable`.
621      *
622      * Params:
623      *      val = the value to be set
624      *      key = the key corresponding to a value
625      *
626      * Returns:
627      *      a nullable value corresponding to the result or `V.init`.
628      *
629      * Complexity: $(BIGOH n), where n is the number of elements in the
630      *             corresponding key bucket.
631      */
632     Nullable!V opIndexAssign(U)(U val, K key)
633     if (isImplicitlyConvertible!(U, V))
634     {
635         debug(CollectionHashtable)
636         {
637             writefln("Hashtable.opIndexAssign: begin");
638             scope(exit) writefln("Hashtable.opIndexAssign: end");
639         }
640 
641         size_t pos = key.hashOf & (_buckets.length - 1);
642         foreach(ref pair; _buckets[pos])
643         {
644             if (pair.key == key)
645             {
646                 return Nullable!V(pair.value);
647             }
648         }
649         return Nullable!V.init;
650     }
651 
652     /**
653      * Assign to the element corresponding to `key` the result of
654      * applying `op` to the current value.
655      * If there isn't such a value, return a `V.init` wrapped inside a
656      * `Nullable`.
657      *
658      * Params:
659      *      val = the value to be used with `op`
660      *      key = the key corresponding to a value
661      *
662      * Returns:
663      *      a nullable value corresponding to the result or `V.init`.
664      *
665      * Complexity: $(BIGOH n), where n is the number of elements in the
666      *             corresponding key bucket.
667      */
668     Nullable!V opIndexOpAssign(string op, U)(U val, K key)
669     if (isImplicitlyConvertible!(U, V))
670     {
671         debug(CollectionHashtable)
672         {
673             writefln("Hashtable.opIndexOpAssign: begin");
674             scope(exit) writefln("Hashtable.opIndexOpAssign: end");
675         }
676 
677         size_t pos = key.hashOf & (_buckets.length - 1);
678         foreach(ref pair; _buckets[pos])
679         {
680             if (pair.key == key)
681             {
682                 return Nullable!V(mixin("pair.value" ~ op ~ "= val"));
683             }
684         }
685         return Nullable!V.init;
686     }
687 
688     auto ref opBinary(string op, U)(auto ref U rhs)
689         if (op == "~" &&
690             (is (U == typeof(this))
691              || is (U : T)
692              || (isInputRange!U && isImplicitlyConvertible!(ElementType!U, T))
693             ));
694 
695     /**
696      * Assign `rhs` to this hashtable. The current hashtable will now become
697      * another reference to `rhs`, unless `rhs` is `null`, in which case the
698      * current hashtable will become empty. If `rhs` refers to the current
699      * hashtable nothing will happen.
700      *
701      * If there are no more references to the previous hashtable the previous
702      * hashtable will be destroyed; this leads to a $(BIGOH length) complexity.
703      *
704      * Params:
705      *      rhs = a reference to a hashtable
706      *
707      * Returns:
708      *      a reference to this hashtable
709      *
710      * Complexity: $(BIGOH length).
711      */
712     auto ref opAssign()(auto ref typeof(this) rhs)
713     {
714         debug(CollectionHashtable)
715         {
716             writefln("Hashtable.opAssign: begin");
717             scope(exit) writefln("Hashtable.opAssign: end");
718         }
719 
720         _buckets = rhs._buckets;
721         _numElems = rhs._numElems;
722         _ouroborosAllocator = rhs._ouroborosAllocator;
723         return this;
724     }
725 
726     void remove(K key);
727 
728     /**
729      * Removes all the elements in the current hashtable.
730      *
731      * Complexity: $(BIGOH length).
732      */
733     void clear()
734     {
735         debug(CollectionHashtable)
736         {
737             writefln("Hashtable.clear: begin");
738             scope(exit) writefln("Hashtable.clear: end");
739         }
740         auto alloc = _allocator.getLocalAlloc;
741         _buckets = Array!(SList!(KVPair))(alloc, SList!(KVPair)(alloc));
742         _numElems = Array!size_t(alloc, 0UL);
743     }
744 
745     void rehash();
746 }
747 
748 version(unittest) private /*nothrow*/ pure @safe
749 void testSimple(RCIAllocator allocator)
750 {
751     import std.algorithm.comparison : equal;
752 
753     auto h = Hashtable!(int, int)(allocator, [1 : 10]);
754 
755     assert(equal(h.keys(), [1]));
756     assert(equal(h.values(), [10]));
757     assert(h.get(1, -1) == 10);
758     assert(h.get(200, -1) == -1);
759     assert(--h[1] == 9);
760     assert(h.get(1, -1) == 9);
761 }
762 
763 @safe unittest
764 {
765     import std.conv;
766     SCAlloc statsCollectorAlloc;
767     {
768         auto _allocator = (() @trusted => allocatorObject(&statsCollectorAlloc))();
769         () /*nothrow pure*/ @safe {
770             testSimple(_allocator);
771         }();
772     }
773     auto bytesUsed = statsCollectorAlloc.bytesUsed;
774     assert(bytesUsed == 0, "SList ref count leaks memory; leaked "
775             ~ to!string(bytesUsed) ~ " bytes");
776 }
777 
778 version(unittest) private /*nothrow*/ pure @safe
779 void testIndex(RCIAllocator allocator)
780 {
781     import std.algorithm.comparison : equal;
782     import std.typecons : Tuple;
783 
784     auto h = Hashtable!(int, int)();
785     assert(h.length == 0);
786     h.insert([1 : 10]);
787     assert(h.length == 1);
788     assert(h._buckets[0].front == Tuple!(int, int)(1, 10));
789     h.clear();
790     assert(h.length == 0);
791     assert(h.empty);
792 
793     h.insert([1 : 10]);
794     assert(equal(h.keys(), [1]));
795 }
796 
797 @safe unittest
798 {
799     import std.conv;
800     SCAlloc statsCollectorAlloc;
801     {
802         auto _allocator = (() @trusted => allocatorObject(&statsCollectorAlloc))();
803         () /*nothrow pure*/ @safe {
804             testIndex(_allocator);
805         }();
806     }
807     auto bytesUsed = statsCollectorAlloc.bytesUsed;
808     assert(bytesUsed == 0, "SList ref count leaks memory; leaked "
809             ~ to!string(bytesUsed) ~ " bytes");
810 }
811 
812 version(unittest) private /*nothrow*/ pure @safe
813 void testSimpleImmutable(RCIAllocator allocator)
814 {
815     import std.algorithm.comparison : equal;
816     import std.typecons : Tuple;
817     import stdx.collections.array : Array;
818 
819     auto h = immutable Hashtable!(int, int)([1 : 10]);
820     assert(h._buckets[0].front == Tuple!(int, int)(1, 10));
821     assert(h.get(1, -1) == 10);
822     assert(equal(h.values(), Array!int([10])));
823     assert(equal(h.keyValuePairs(), Array!(Tuple!(int, int))(Tuple!(int, int)(1, 10))));
824 }
825 
826 @safe unittest
827 {
828     import std.conv;
829     SCAlloc statsCollectorAlloc;
830     {
831         auto _allocator = (() @trusted => allocatorObject(&statsCollectorAlloc))();
832         () /*nothrow pure*/ @safe {
833             testSimpleImmutable(_allocator);
834         }();
835     }
836     auto bytesUsed = statsCollectorAlloc.bytesUsed;
837     assert(bytesUsed == 0, "SList ref count leaks memory; leaked "
838             ~ to!string(bytesUsed) ~ " bytes");
839 }