1 module nxt.sso_open_hashset;
2 
3 import nxt.container.hybrid_hashmap;
4 
5 import std.traits : isInstanceOf;
6 import nxt.nullable_traits : isNullable, isAddress;
7 import nxt.my_mallocator : Mallocator;
8 
9 /** Small-Size-Optimized (SSO) `HybridHashSet`.
10  *
11  * TODO: search for `nullify`, `isNull`, `nullValue` and support deleted keys (`isDull`)
12  *
13  * TODO: use opPostMove to update `gc_addRange` and `gc_removeRange` when
14  * implemented. See: https://github.com/dlang/DIPs/pull/109
15  *
16  * See_Also: https://forum.dlang.org/post/pb87rn$2icb$1@digitalmars.com
17  */
18 struct SSOHybridHashSet(K, alias hasher = hashOf, alias Allocator = Mallocator.instance)
19 if (isNullable!K)
20 {
21     import nxt.qcmeman : gc_addRange, gc_removeRange;
22 
23     import core.lifetime : emplace, move, moveEmplace;
24     import std.traits : isDynamicArray;
25     import core.internal.traits : hasElaborateDestructor;
26     import nxt.nullable_traits : defaultNullKeyConstantOf, isNull, nullify;
27     import nxt.bit_traits : isAllZeroBits;
28 
29     alias InsertionStatus = Large.InsertionStatus;
30 
31     @safe:
32 
33     static if (!large.hasAddressLikeKey &&
34                (__traits(hasMember, K, `nullValue`) && // if key has a null value
35                 __traits(compiles, { enum _ = isAllZeroBits!(K, K.nullValue); }) && // prevent strange error given when `K` is `knet.data.Data`
36                 !isAllZeroBits!(K, K.nullValue)))
37     {
38         pragma(msg, "TODO: warning key type ", K, " has non-zero-bit init value, default construction should be disabled or Small._store should be set to init value");
39     }
40     // TODO: @disable this();
41 
42     static typeof(this) withCapacity()(size_t minimumCapacity) @trusted /* template-lazy */
43     {
44         typeof(return) result;                   // TODO: `result = void` for nullify case
45         if (minimumCapacity > Small.maxCapacity) // will be large
46             result.large = Large.withCapacity(minimumCapacity);
47         else                    // small
48         {
49             static if (Large.hasAddressLikeKey ||
50                        (__traits(hasMember, K, `nullValue`) && // if key has a null value
51                         __traits(compiles, { enum _ = isAllZeroBits!(K, K.nullValue); }) && // prevent strange error given when `K` is `knet.data.Data`
52                         isAllZeroBits!(K, K.nullValue))) // check that it's zero bits only
53             {
54                 // nothing needed
55             }
56             else                // needs explicit null
57                 static foreach (immutable index; 0 .. small.maxCapacity)
58                     result.small._store[index].nullify();
59 
60             result.small._capacityDummy = 2; // tag as small
61         }
62         return result;
63     }
64 
65     ~this() nothrow @trusted @nogc
66     {
67         if (isLarge)
68         {
69             static if (hasElaborateDestructor!Large)
70                 .destroy(large);
71         }
72         else
73         {
74             static if (hasElaborateDestructor!K)
75                 static assert(0, "Destroy non-null elements");
76         }
77     }
78 
79     @disable this(this);
80 
81     @property size_t capacity() const pure nothrow @trusted @nogc
82     {
83         version(D_Coverage) {} else pragma(inline, true);
84         return small._capacityDummy;
85     }
86 
87     @property size_t length() const pure nothrow @trusted @nogc
88     {
89         version(LDC) pragma(inline, true);
90         if (isLarge)
91             return large.length;
92 		import std.algorithm.searching : count;
93 		return small._store[].count!(bin => Large.isOccupiedBin(bin));
94     }
95 
96     InsertionStatus insert(K key) @trusted
97     {
98         if (isLarge)
99             return large.insert(key);
100         else
101         {
102             assert(!key.isNull);
103 
104             // try inserting into small
105             foreach (immutable index; 0 .. small.maxCapacity) // TODO: benchmark with `static foreach`
106             {
107                 if (small._store[index].isNull) // free slot
108                 {
109                     move(key, small._store[index]);
110                     return InsertionStatus.added;
111                 }
112             }
113 
114             // not hit
115             expandWithExtraCapacity(1);
116             assert(isLarge);
117             return large.insert(key);
118         }
119     }
120 
121     bool remove()(const scope K key) @trusted /* template-lazy */
122     {
123         assert(!key.isNull);
124         if (isLarge)
125         {
126             const hit = large.remove(key);
127             if (large.length <= small.maxCapacity)
128                 shrinkLargeToSmall();
129             return hit;
130         }
131         else
132         {
133             foreach (immutable index; 0 .. small.maxCapacity) // TODO: benchmark with `static foreach`
134             {
135                 if (small._store[index] is key)
136                 {
137                     small._store[index].nullify(); // don't need holes for small array
138                     return true;
139                 }
140             }
141             return false;
142         }
143     }
144 
145     private void shrinkLargeToSmall()() @trusted /* template-lazy */
146     {
147         Large largeCopy = void;
148         moveEmplace(large, largeCopy); // TODO: no need to reset `large`
149 
150         size_t count = 0;
151         foreach (ref e; largeCopy.rawStore)
152         {
153             if (e.isNull) { continue; }
154             static if (Large.hasAddressLikeKey)
155                 if (Large.isHoleKeyConstant(e))
156 					continue;
157             moveEmplace(e, small._store[count]);
158             count += 1;
159         }
160 
161         foreach (immutable index; count .. small.maxCapacity)
162         {
163             static if (hasElaborateDestructor!K)
164                 emplace(&small._store[index]); // reset
165             small._store[index].nullify();
166         }
167     }
168 
169     /** Check if `element` is stored.
170         Returns: `true` if element is present, `false` otherwise.
171     */
172     bool contains(const scope K key) const @trusted /* template-lazy */ // `auto ref` here makes things slow
173     in(!key.isNull)
174     {
175         if (isLarge)
176             return large.contains(key);
177         static if (Large.hasAddressLikeKey)
178 			assert(!Large.isHoleKeyConstant(key));
179         // TODO: is static foreach faster here?
180         import std.algorithm.searching : canFind;
181         alias pred = (a, b) => a is b;            // TODO: add to template
182         return small._store[].canFind!(pred)(key);
183     }
184 
185     private void expandWithExtraCapacity(size_t extraCapacity) @trusted
186     {
187         Small.Bins binsCopy = small._store; // TODO: moveEmplace
188         // TODO: merge these lines?
189         emplace!Large(&large);
190         large.reserveExtra(Small.maxCapacity + extraCapacity);
191         large.insertN(binsCopy);
192     }
193 
194     /// Get bin count.
195     @property size_t binCount() const @trusted
196     {
197         pragma(inline, true)
198         if (isLarge)
199             return large.binCount;
200         return small.maxCapacity;
201     }
202 
203     @property private scope inout(K)[] bins() inout @trusted return
204     {
205         version(D_Coverage) {} else pragma(inline, true);
206         if (isLarge)
207             return large.rawStore;
208         return small._store[];
209     }
210 
211 private:
212     bool isLarge() const pure nothrow @trusted @nogc
213     {
214         version(D_Coverage) {} else pragma(inline, true);
215         return small._capacityDummy > Small.maxCapacity;
216     }
217 
218     /// Returns: `true` if `this` currently uses small (packed) array storage.
219     bool isSmall() const pure nothrow @trusted @nogc { return !isLarge; }
220 
221     union
222     {
223         alias Large = HybridHashSet!(K, hasher);
224         Large large;
225         static struct Small
226         {
227             /* discriminator between large and small storage; must be placed at
228              * exactly here (maps to position of `large._store.length`) and
229              * always contain `maxCapacity` when this is small */
230             size_t _capacityDummy;
231             enum maxCapacity = (large.sizeof - _capacityDummy.sizeof)/K.sizeof;
232             static assert(maxCapacity, "Cannot fit a single element in a Small");
233             alias Bins = K[maxCapacity];
234             Bins _store;
235         }
236         Small small;
237     };
238 }
239 
240 /** L-value element reference (and in turn range iterator).
241  */
242 static private struct LvalueElementRef(Table)
243 {
244     import std.traits : isMutable;
245     debug static assert(isMutable!Table, "Table type should always be mutable");
246 
247     private Table* _table;      // scoped access
248     private size_t _binIndex;   // index to bin inside `table`
249     private size_t _hitCounter; // counter over number of elements popped (needed for length)
250 
251     this(Table* table) @trusted
252     {
253         version(D_Coverage) {} else pragma(inline, true);
254         this._table = table;
255         // static if (Table.isBorrowChecked)
256         // {
257         //     debug
258         //     {
259         //         _table.incBorrowCount();
260         //     }
261         // }
262     }
263 
264     ~this() nothrow @trusted @nogc
265     {
266         version(D_Coverage) {} else pragma(inline, true);
267         // static if (Table.isBorrowChecked)
268         // {
269         //     debug
270         //     {
271         //         _table.decBorrowCount();
272         //     }
273         // }
274     }
275 
276     this(this) @trusted
277     {
278         version(D_Coverage) {} else pragma(inline, true);
279         // static if (Table.isBorrowChecked)
280         // {
281         //     debug
282         //     {
283         //         assert(_table._borrowCount != 0);
284         //         _table.incBorrowCount();
285         //     }
286         // }
287     }
288 
289     /// Check if empty.
290     bool empty() const @property @safe pure nothrow @nogc
291     {
292         version(D_Coverage) {} else pragma(inline, true);
293         return _binIndex == _table.binCount;
294     }
295 
296     /// Get number of element left to pop.
297     @property size_t length() const @safe pure nothrow @nogc
298     {
299         version(D_Coverage) {} else pragma(inline, true);
300         return _table.length - _hitCounter;
301     }
302 
303     @property typeof(this) save() // ForwardRange
304     {
305         version(D_Coverage) {} else pragma(inline, true);
306         return this;
307     }
308 
309     void popFront()
310     {
311         version(LDC) pragma(inline, true);
312         assert(!empty);
313         _binIndex += 1;
314         findNextNonEmptyBin();
315         _hitCounter += 1;
316     }
317 
318     private void findNextNonEmptyBin()
319     {
320         version(D_Coverage) {} else pragma(inline, true);
321         while (_binIndex != (*_table).binCount &&
322                !Table.Large.isOccupiedBin(_table.bins[_binIndex]))
323             _binIndex += 1;
324     }
325 }
326 
327 /// Range over elements of l-value instance of this.
328 static private struct ByLvalueElement(Table)
329 {
330 pragma(inline, true):
331     import std.traits : isMutable;
332     static if (isAddress!(Table.Large.ElementType)) // for reference types
333     {
334         /// Get reference to front element.
335         @property scope Table.Large.ElementType front()() return @trusted
336         {
337             // cast to head-const for class key
338             return (cast(typeof(return))_table.bins[_binIndex]);
339         }
340     }
341     else
342     {
343         /// Get reference to front element.
344         @property scope auto ref front() return @trusted
345         {
346             return *(cast(const(Table.ElementType)*)&_table._store[_binIndex]); // propagate constnes
347         }
348     }
349     import core.internal.traits : Unqual;
350     // unqual to reduce number of instantations of `LvalueElementRef`
351     public LvalueElementRef!(Unqual!Table) _elementRef;
352     alias _elementRef this;
353 }
354 
355 /** Returns: range that iterates through the elements of `c` in undefined order.
356  */
357 auto byElement(Table)(auto ref return Table c) @trusted
358     if (isInstanceOf!(SSOHybridHashSet, Table))
359 {
360     import core.internal.traits : Unqual;
361     alias M = Unqual!Table;
362     alias C = const(Table);        // be const for now
363     static if (__traits(isRef, c)) // `c` is an l-value and must be borrowed
364         auto result = ByLvalueElement!C((LvalueElementRef!(M)(cast(M*)&c)));
365     else                        // `c` was is an r-value and can be moved
366         static assert(0, "R-value parameter not supported");
367     result.findNextNonEmptyBin();
368     return result;
369 }
370 alias range = byElement;        // EMSI-container naming
371 
372 /// start small and expand to large
373 @safe pure nothrow unittest
374 {
375     import std.algorithm.comparison : equal;
376     import nxt.digestx.fnv : FNV;
377     import nxt.array_help : s;
378 
379     // construct small
380     alias X = SSOHybridHashSet!(K, FNV!(64, true));
381     static assert(X.sizeof == 24);
382     auto x = X.withCapacity(X.small.maxCapacity);
383     assert(x.isSmall);
384     assert(x.capacity == X.small.maxCapacity);
385     assert(x.length == 0);
386 
387     auto k42 = new K(42);
388     auto k43 = new K(43);
389     auto k44 = new K(44);
390 
391     // insert first into small
392 
393     assert(!x.contains(k42));
394 
395     assert(x.insert(k42) == x.InsertionStatus.added);
396     assert(x.contains(k42));
397 
398     assert(x.remove(k42));
399     assert(!x.contains(k42));
400 
401     assert(x.insert(k42) == x.InsertionStatus.added);
402     assert(x.contains(k42));
403 
404     assert(x.byElement.equal!((a, b) => a is b)([k42].s[]));
405     assert(x.isSmall);
406     assert(x.length == 1);
407 
408     // insert second into small
409 
410     assert(!x.contains(k43));
411 
412     assert(x.insert(k43) == x.InsertionStatus.added);
413     assert(x.contains(k42));
414     assert(x.contains(k43));
415 
416     assert(x.remove(k43));
417     assert(x.remove(k42));
418     assert(!x.contains(k43));
419     assert(!x.contains(k42));
420 
421     assert(x.insert(k43) == x.InsertionStatus.added);
422     assert(x.insert(k42) == x.InsertionStatus.added);
423 
424     assert(x.byElement.equal!((a, b) => a is b)([k43, k42].s[]));
425     assert(x.isSmall);
426     assert(x.length == 2);
427 
428     // expanding insert third into large
429 
430     assert(!x.contains(k44));
431     assert(x.insert(k44) == x.InsertionStatus.added);
432     // unordered store so equal doesn't work anymore
433     assert(x.contains(k42));
434     assert(x.contains(k43));
435     assert(x.contains(k44));
436     foreach (ref e; x.byElement)
437     {
438         static assert(is(typeof(e) == K));
439         assert(x.contains(e));
440     }
441     assert(x.isLarge);
442     assert(x.length == 3);
443 
444     // shrinking remove third into small
445     assert(x.remove(k44));
446     assert(!x.contains(k44));
447     assert(x.isSmall);
448     assert(x.length == 2);
449     assert(x.byElement.equal!((a, b) => a is b)([k43, k42].s[]));
450     // auto xr = x.byElement;
451     // assert(xr.front.value == 43);
452     // assert(xr.front is k43);
453     // xr.popFront();
454     // assert(xr.front.value == 42);
455     // assert(xr.front is k42);
456     // xr.popFront();
457     // assert(xr.empty);
458 
459     const cx = X.withCapacity(X.small.maxCapacity);
460     foreach (ref e; cx.byElement)
461         static assert(is(typeof(e) == K)); // head-const because of `is`-equality
462 }
463 
464 /// start large
465 @safe pure nothrow unittest
466 {
467     import nxt.digestx.fnv : FNV;
468     alias X = SSOHybridHashSet!(K, FNV!(64, true));
469     import nxt.container.traits : mustAddGCRange;
470     static assert(mustAddGCRange!K);
471     static assert(mustAddGCRange!X);
472     auto x = X.withCapacity(3);
473     assert(x.isLarge);
474     assert(x.capacity == 4);    // nextPow2(3)
475     assert(x.length == 0);
476     assert(x.insert(new K(42)) == x.InsertionStatus.added);
477     assert(x.length == 1);
478     assert(x.insert(new K(43)) == x.InsertionStatus.added);
479     assert(x.length == 2);
480 }
481 
482 version(unittest)
483 {
484     class K
485     {
486         this(uint value) @safe pure nothrow @nogc
487         {
488             this.value = value;
489         }
490         uint value;
491     }
492 }