1 /**
2  * A specialized associative array with string keys stored in a variable length structure.
3  *
4  * Copyright: Copyright (C) 1999-2021 by The D Language Foundation, All Rights Reserved
5  * Authors:   Walter Bright, http://www.digitalmars.com
6  * License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7  * Source:    $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/root/stringtable.d, root/_stringtable.d)
8  * Documentation:  https://dlang.org/phobos/dmd_root_stringtable.html
9  * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/src/dmd/root/stringtable.d
10  */
11 
12 module dmd.root.stringtable;
13 
14 import core.stdc.string;
15 import dmd.root.rmem, dmd.root.hash;
16 
17 private enum POOL_BITS = 12;
18 private enum POOL_SIZE = (1U << POOL_BITS);
19 
20 /*
21 Returns the smallest integer power of 2 larger than val.
22 if val > 2^^63 on 64-bit targets or val > 2^^31 on 32-bit targets it enters an
23 endless loop because of overflow.
24 */
25 private size_t nextpow2(size_t val) @nogc nothrow pure @safe
26 {
27     size_t res = 1;
28     while (res < val)
29         res <<= 1;
30     return res;
31 }
32 
33 unittest
34 {
35     assert(nextpow2(0) == 1);
36     assert(nextpow2(0xFFFF) == (1 << 16));
37     assert(nextpow2(size_t.max / 2) == size_t.max / 2 + 1);
38     // note: nextpow2((1UL << 63) + 1) results in an endless loop
39 }
40 
41 private enum loadFactorNumerator = 8;
42 private enum loadFactorDenominator = 10;        // for a load factor of 0.8
43 
44 private struct StringEntry
45 {
46     uint hash;
47     uint vptr;
48 }
49 
50 // StringValue is a variable-length structure. It has neither proper c'tors nor a
51 // factory method because the only thing which should be creating these is StringTable.
52 struct StringValue(T)
53 {
54     T value; //T is/should typically be a pointer or a slice
55     private size_t length;
56 
57     char* lstring() @nogc nothrow pure return
58     {
59         return cast(char*)(&this + 1);
60     }
61 
62     size_t len() const @nogc nothrow pure @safe
63     {
64         return length;
65     }
66 
67     const(char)* toDchars() const @nogc nothrow pure return
68     {
69         return cast(const(char)*)(&this + 1);
70     }
71 
72     /// Returns: The content of this entry as a D slice
73     inout(char)[] toString() inout @nogc nothrow pure
74     {
75         return (cast(inout(char)*)(&this + 1))[0 .. length];
76     }
77 }
78 
79 struct StringTable(T)
80 {
81 private:
82     StringEntry[] table;
83     ubyte*[] pools;
84     size_t nfill;
85     size_t count;
86     size_t countTrigger;   // amount which will trigger growing the table
87 
88 public:
89     void _init(size_t size = 0) nothrow pure
90     {
91         size = nextpow2((size * loadFactorDenominator) / loadFactorNumerator);
92         if (size < 32)
93             size = 32;
94         table = (cast(StringEntry*)mem.xcalloc(size, (table[0]).sizeof))[0 .. size];
95         countTrigger = (table.length * loadFactorNumerator) / loadFactorDenominator;
96         pools = null;
97         nfill = 0;
98         count = 0;
99     }
100 
101     void reset(size_t size = 0) nothrow pure
102     {
103         freeMem();
104         _init(size);
105     }
106 
107     ~this() nothrow pure
108     {
109         freeMem();
110     }
111 
112     /**
113     Looks up the given string in the string table and returns its associated
114     value.
115 
116     Params:
117      s = the string to look up
118      length = the length of $(D_PARAM s)
119      str = the string to look up
120 
121     Returns: the string's associated value, or `null` if the string doesn't
122      exist in the string table
123     */
124     inout(StringValue!T)* lookup(const(char)[] str) inout @nogc nothrow pure
125     {
126         const(size_t) hash = calcHash(str);
127         const(size_t) i = findSlot(hash, str);
128         // printf("lookup %.*s %p\n", cast(int)str.length, str.ptr, table[i].value ?: null);
129         return getValue(table[i].vptr);
130     }
131 
132     /// ditto
133     inout(StringValue!T)* lookup(const(char)* s, size_t length) inout @nogc nothrow pure
134     {
135         return lookup(s[0 .. length]);
136     }
137 
138     /**
139     Inserts the given string and the given associated value into the string
140     table.
141 
142     Params:
143      s = the string to insert
144      length = the length of $(D_PARAM s)
145      ptrvalue = the value to associate with the inserted string
146      str = the string to insert
147      value = the value to associate with the inserted string
148 
149     Returns: the newly inserted value, or `null` if the string table already
150      contains the string
151     */
152     StringValue!(T)* insert(const(char)[] str, T value) nothrow pure
153     {
154         const(size_t) hash = calcHash(str);
155         size_t i = findSlot(hash, str);
156         if (table[i].vptr)
157             return null; // already in table
158         if (++count > countTrigger)
159         {
160             grow();
161             i = findSlot(hash, str);
162         }
163         table[i].hash = hash;
164         table[i].vptr = allocValue(str, value);
165         // printf("insert %.*s %p\n", cast(int)str.length, str.ptr, table[i].value ?: NULL);
166         return getValue(table[i].vptr);
167     }
168 
169     /// ditto
170     StringValue!(T)* insert(const(char)* s, size_t length, T value) nothrow pure
171     {
172         return insert(s[0 .. length], value);
173     }
174 
175     StringValue!(T)* update(const(char)[] str) nothrow pure
176     {
177         const(size_t) hash = calcHash(str);
178         size_t i = findSlot(hash, str);
179         if (!table[i].vptr)
180         {
181             if (++count > countTrigger)
182             {
183                 grow();
184                 i = findSlot(hash, str);
185             }
186             table[i].hash = hash;
187             table[i].vptr = allocValue(str, T.init);
188         }
189         // printf("update %.*s %p\n", cast(int)str.length, str.ptr, table[i].value ?: NULL);
190         return getValue(table[i].vptr);
191     }
192 
193     StringValue!(T)* update(const(char)* s, size_t length) nothrow pure
194     {
195         return update(s[0 .. length]);
196     }
197 
198     /********************************
199      * Walk the contents of the string table,
200      * calling fp for each entry.
201      * Params:
202      *      fp = function to call. Returns !=0 to stop
203      * Returns:
204      *      last return value of fp call
205      */
206     int apply(int function(const(StringValue!T)*) nothrow fp) nothrow
207     {
208         foreach (const se; table)
209         {
210             if (!se.vptr)
211                 continue;
212             const sv = getValue(se.vptr);
213             int result = (*fp)(sv);
214             if (result)
215                 return result;
216         }
217         return 0;
218     }
219 
220     /// ditto
221     extern(D) int opApply(scope int delegate(const(StringValue!T)*) nothrow dg) nothrow
222     {
223         foreach (const se; table)
224         {
225             if (!se.vptr)
226                 continue;
227             const sv = getValue(se.vptr);
228             int result = dg(sv);
229             if (result)
230                 return result;
231         }
232         return 0;
233     }
234 
235 private:
236     /// Free all memory in use by this StringTable
237     void freeMem() nothrow pure
238     {
239         foreach (pool; pools)
240             mem.xfree(pool);
241         mem.xfree(table.ptr);
242         mem.xfree(pools.ptr);
243         table = null;
244         pools = null;
245     }
246 
247     uint allocValue(const(char)[] str, T value) nothrow pure
248     {
249         const(size_t) nbytes = (StringValue!T).sizeof + str.length + 1;
250         if (!pools.length || nfill + nbytes > POOL_SIZE)
251         {
252             pools = (cast(ubyte**) mem.xrealloc(pools.ptr, (pools.length + 1) * (pools[0]).sizeof))[0 .. pools.length + 1];
253             pools[$-1] = cast(ubyte*) mem.xmalloc(nbytes > POOL_SIZE ? nbytes : POOL_SIZE);
254             if (mem.isGCEnabled)
255                 memset(pools[$ - 1], 0xff, POOL_SIZE); // 0xff less likely to produce GC pointer
256             nfill = 0;
257         }
258         StringValue!(T)* sv = cast(StringValue!(T)*)&pools[$ - 1][nfill];
259         sv.value = value;
260         sv.length = str.length;
261         .memcpy(sv.lstring(), str.ptr, str.length);
262         sv.lstring()[str.length] = 0;
263         const(uint) vptr = cast(uint)(pools.length << POOL_BITS | nfill);
264         nfill += nbytes + (-nbytes & 7); // align to 8 bytes
265         return vptr;
266     }
267 
268     inout(StringValue!T)* getValue(uint vptr) inout @nogc nothrow pure
269     {
270         if (!vptr)
271             return null;
272         const(size_t) idx = (vptr >> POOL_BITS) - 1;
273         const(size_t) off = vptr & POOL_SIZE - 1;
274         return cast(inout(StringValue!T)*)&pools[idx][off];
275     }
276 
277     size_t findSlot(hash_t hash, const(char)[] str) const @nogc nothrow pure
278     {
279         // quadratic probing using triangular numbers
280         // http://stackoverflow.com/questions/2348187/moving-from-linear-probing-to-quadratic-probing-hash-collisons/2349774#2349774
281         for (size_t i = hash & (table.length - 1), j = 1;; ++j)
282         {
283             const(StringValue!T)* sv;
284             auto vptr = table[i].vptr;
285             if (!vptr || table[i].hash == hash && (sv = getValue(vptr)).length == str.length && .memcmp(str.ptr, sv.toDchars(), str.length) == 0)
286                 return i;
287             i = (i + j) & (table.length - 1);
288         }
289     }
290 
291     void grow() nothrow pure
292     {
293         const odim = table.length;
294         auto otab = table;
295         const ndim = table.length * 2;
296         countTrigger = (ndim * loadFactorNumerator) / loadFactorDenominator;
297         table = (cast(StringEntry*)mem.xcalloc_noscan(ndim, (table[0]).sizeof))[0 .. ndim];
298         foreach (const se; otab[0 .. odim])
299         {
300             if (!se.vptr)
301                 continue;
302             const sv = getValue(se.vptr);
303             table[findSlot(se.hash, sv.toString())] = se;
304         }
305         mem.xfree(otab.ptr);
306     }
307 }
308 
309 nothrow unittest
310 {
311     StringTable!(const(char)*) tab;
312     tab._init(10);
313 
314     // construct two strings with the same text, but a different pointer
315     const(char)[6] fooBuffer = "foofoo";
316     const(char)[] foo = fooBuffer[0 .. 3];
317     const(char)[] fooAltPtr = fooBuffer[3 .. 6];
318 
319     assert(foo.ptr != fooAltPtr.ptr);
320 
321     // first insertion returns value
322     assert(tab.insert(foo, foo.ptr).value == foo.ptr);
323 
324     // subsequent insertion of same string return null
325     assert(tab.insert(foo.ptr, foo.length, foo.ptr) == null);
326     assert(tab.insert(fooAltPtr, foo.ptr) == null);
327 
328     const lookup = tab.lookup("foo");
329     assert(lookup.value == foo.ptr);
330     assert(lookup.len == 3);
331     assert(lookup.toString() == "foo");
332 
333     assert(tab.lookup("bar") == null);
334     tab.update("bar".ptr, "bar".length);
335     assert(tab.lookup("bar").value == null);
336 
337     tab.reset(0);
338     assert(tab.lookup("foo".ptr, "foo".length) == null);
339     //tab.insert("bar");
340 }
341 
342 nothrow unittest
343 {
344     StringTable!(void*) tab;
345     tab._init(100);
346 
347     enum testCount = 2000;
348 
349     char[2 * testCount] buf;
350 
351     foreach(i; 0 .. testCount)
352     {
353         buf[i * 2 + 0] = cast(char) (i % 256);
354         buf[i * 2 + 1] = cast(char) (i / 256);
355         auto toInsert = cast(const(char)[]) buf[i * 2 .. i * 2 + 2];
356         tab.insert(toInsert, cast(void*) i);
357     }
358 
359     foreach(i; 0 .. testCount)
360     {
361         auto toLookup = cast(const(char)[]) buf[i * 2 .. i * 2 + 2];
362         assert(tab.lookup(toLookup).value == cast(void*) i);
363     }
364 }
365 
366 nothrow unittest
367 {
368     StringTable!(int) tab;
369     tab._init(10);
370     tab.insert("foo",  4);
371     tab.insert("bar",  6);
372 
373     static int resultFp = 0;
374     int resultDg = 0;
375     static bool returnImmediately = false;
376 
377     int function(const(StringValue!int)*) nothrow applyFunc = (const(StringValue!int)* s)
378     {
379         resultFp += s.value;
380         return returnImmediately;
381     };
382 
383     scope int delegate(const(StringValue!int)*) nothrow applyDeleg = (const(StringValue!int)* s)
384     {
385         resultDg += s.value;
386         return returnImmediately;
387     };
388 
389     tab.apply(applyFunc);
390     tab.opApply(applyDeleg);
391 
392     assert(resultDg == 10);
393     assert(resultFp == 10);
394 
395     returnImmediately = true;
396 
397     tab.apply(applyFunc);
398     tab.opApply(applyDeleg);
399 
400     // Order of string table iteration is not specified, either foo or bar could
401     // have been visited first.
402     assert(resultDg == 14 || resultDg == 16);
403     assert(resultFp == 14 || resultFp == 16);
404 }