1 /**
2 	Internal hash map implementation.
3 
4 	Copyright: © 2013 Sönke Ludwig
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module vibe.utils.hashmap;
9 
10 import vibe.internal.utilallocator;
11 
12 import std.conv : emplace;
13 import std.traits;
14 
15 
16 struct DefaultHashMapTraits(Key) {
17 	enum clearValue = Key.init;
18 	static bool equals(in Key a, in Key b)
19 	{
20 		static if (__traits(isFinalClass, Key) && &Unqual!Key.init.opEquals is &Object.init.opEquals)
21 			return a is b;
22 		else static if (is(Key == class))
23 			// BUG: improperly casting away const
24 			return () @trusted { return a is b ? true : (a !is null && (cast(Object) a).opEquals(cast(Object) b)); }();
25 		else return a == b;
26 	}
27 
28 	static size_t hashOf(const scope ref Key k)
29 	@safe {
30 		static if (__traits(isFinalClass, Key) && &Unqual!Key.init.toHash is &Object.init.toHash)
31 			return () @trusted { return cast(size_t)cast(void*)k; } ();
32 		else static if (__traits(compiles, Key.init.toHash()))
33 			return () @trusted { return (cast(Key)k).toHash(); } ();
34 		else static if (__traits(compiles, Key.init.toHashShared()))
35 			return k.toHashShared();
36 		else static if ((__traits(isScalar, Key) ||
37 				(isArray!Key && is(Key : E[], E) && __traits(isScalar, E))) &&
38 				is(typeof((in Key x) @nogc nothrow pure @safe => .object.hashOf(x))))
39 			return .object.hashOf(k);
40 		else {
41 			// evil casts to be able to get the most basic operations of
42 			// HashMap nothrow and @nogc
43 			static size_t hashWrapper(const scope ref Key k) {
44 				static typeinfo = typeid(Key);
45 				return typeinfo.getHash(&k);
46 			}
47 			static @nogc nothrow size_t properlyTypedWrapper(const scope ref Key k) { return 0; }
48 			return () @trusted { return (cast(typeof(&properlyTypedWrapper))&hashWrapper)(k); } ();
49 		}
50 	}
51 }
52 
53 unittest
54 {
55 	final class Integer : Object {
56 		public const int value;
57 
58 		this(int x) @nogc nothrow pure @safe { value = x; }
59 
60 		override bool opEquals(Object rhs) const @nogc nothrow pure @safe {
61 			if (auto r = cast(Integer) rhs)
62 				return value == r.value;
63 			return false;
64 		}
65 
66 		override size_t toHash() const @nogc nothrow pure @safe {
67 			return value;
68 		}
69 	}
70 
71 	auto hashMap = HashMap!(Object, int)(vibeThreadAllocator());
72 	foreach (x; [2, 4, 8, 16])
73 		hashMap[new Integer(x)] = x;
74 	foreach (x; [2, 4, 8, 16])
75 		assert(hashMap[new Integer(x)] == x);
76 }
77 
78 struct HashMap(TKey, TValue, Traits = DefaultHashMapTraits!TKey, Allocator = IAllocator)
79 	if (is(typeof(Traits.clearValue) : TKey))
80 {
81 	import core.memory : GC;
82 	import vibe.internal.meta.traits : isOpApplyDg;
83 	import std.algorithm.iteration : filter, map;
84 
85 	alias Key = TKey;
86 	alias Value = TValue;
87 
88 	Allocator AW(Allocator a) { return a; }
89 	alias AllocatorType = AffixAllocator!(Allocator, int);
90 	static if (is(typeof(AllocatorType.instance)))
91 		alias AllocatorInstanceType = typeof(AllocatorType.instance);
92 	else alias AllocatorInstanceType = AllocatorType;
93 
94 	struct TableEntry {
95 		UnConst!Key key = Traits.clearValue;
96 		Value value;
97 
98 		this(ref Key key, ref Value value)
99 		{
100 			import std.algorithm.mutation : move;
101 			this.key = cast(UnConst!Key)key;
102 			static if (is(typeof(value.move)))
103 				this.value = value.move;
104 			else this.value = value;
105 		}
106 	}
107 	private {
108 		TableEntry[] m_table; // NOTE: capacity is always POT
109 		size_t m_length;
110 		static if (!is(typeof(Allocator.instance)))
111 			AllocatorInstanceType m_allocator;
112 		bool m_resizing;
113 	}
114 
115 	static if (!is(typeof(Allocator.instance))) {
116 		this(Allocator allocator)
117 		{
118 			m_allocator = typeof(m_allocator)(AW(allocator));
119 		}
120 	}
121 
122 	~this()
123 	{
124 		int rc;
125 		try rc = m_table is null ? 1 : () @trusted { return --allocator.prefix(m_table); } ();
126 		catch (Exception e) assert(false, e.msg);
127 
128 		if (rc == 0) {
129 			clear();
130 			if (m_table.ptr !is null) () @trusted {
131 				static if (hasIndirections!TableEntry) GC.removeRange(m_table.ptr);
132 				try allocator.dispose(m_table);
133 				catch (Exception e) assert(false, e.msg);
134 			} ();
135 		}
136 	}
137 
138 	this(this)
139 	@trusted {
140 		if (m_table.ptr) {
141 			try allocator.prefix(m_table)++;
142 			catch (Exception e) assert(false, e.msg);
143 		}
144 	}
145 
146 	@property size_t length() const { return m_length; }
147 
148 	void remove(Key key)
149 	{
150 		import std.algorithm.mutation : move;
151 
152 		auto idx = findIndex(key);
153 		assert (idx != size_t.max, "Removing non-existent element.");
154 		auto i = idx;
155 		while (true) {
156 			m_table[i].key = Traits.clearValue;
157 			m_table[i].value = Value.init;
158 
159 			size_t j = i, r;
160 			do {
161 				if (++i >= m_table.length) i -= m_table.length;
162 				if (Traits.equals(m_table[i].key, Traits.clearValue)) {
163 					m_length--;
164 					return;
165 				}
166 				r = Traits.hashOf(m_table[i].key) & (m_table.length-1);
167 			} while ((j<r && r<=i) || (i<j && j<r) || (r<=i && i<j));
168 			static if (is(typeof(m_table[i].move)))
169 				m_table[j] = m_table[i].move;
170 			else m_table[j] = m_table[i];
171 		}
172 	}
173 
174 	Value get(Key key, lazy Value default_value = Value.init)
175 	{
176 		auto idx = findIndex(key);
177 		if (idx == size_t.max) return default_value;
178 		return m_table[idx].value;
179 	}
180 
181 	/// Workaround #12647
182 	package(vibe) Value getNothrow(Key key, Value default_value = Value.init)
183 	{
184 		auto idx = findIndex(key);
185 		if (idx == size_t.max) return default_value;
186 		return m_table[idx].value;
187 	}
188 
189 	static if (!is(typeof({ Value v; const(Value) vc; v = vc; }))) {
190 		const(Value) get(Key key, lazy const(Value) default_value = Value.init)
191 		{
192 			auto idx = findIndex(key);
193 			if (idx == size_t.max) return default_value;
194 			return m_table[idx].value;
195 		}
196 	}
197 
198 	void clear()
199 	{
200 		foreach (i; 0 .. m_table.length)
201 			if (!Traits.equals(m_table[i].key, Traits.clearValue)) {
202 				m_table[i].key = Traits.clearValue;
203 				m_table[i].value = Value.init;
204 			}
205 		m_length = 0;
206 	}
207 
208 	void opIndexAssign(T)(T value, Key key)
209 	{
210 		import std.algorithm.mutation : move;
211 
212 		assert(!Traits.equals(key, Traits.clearValue), "Inserting clear value into hash map.");
213 		grow(1);
214 		auto i = findInsertIndex(key);
215 		if (!Traits.equals(m_table[i].key, key)) m_length++;
216 		m_table[i].key = () @trusted { return cast(UnConst!Key)key; } ();
217 		m_table[i].value = value;
218 	}
219 
220 	ref inout(Value) opIndex(Key key)
221 	inout {
222 		auto idx = findIndex(key);
223 		assert (idx != size_t.max, "Accessing non-existent key.");
224 		return m_table[idx].value;
225 	}
226 
227 	inout(Value)* opBinaryRight(string op)(Key key)
228 	inout if (op == "in") {
229 		auto idx = findIndex(key);
230 		if (idx == size_t.max) return null;
231 		return &m_table[idx].value;
232 	}
233 
234 	int opApply(DG)(scope DG del) if (isOpApplyDg!(DG, Key, Value))
235 	{
236 		import std.traits : arity;
237 		foreach (i; 0 .. m_table.length)
238 			if (!Traits.equals(m_table[i].key, Traits.clearValue)) {
239 				static assert(arity!del >= 1 && arity!del <= 2,
240 						  "isOpApplyDg should have prevented this");
241 				static if (arity!del == 1) {
242 					if (int ret = del(m_table[i].value))
243 						return ret;
244 				} else
245 					if (int ret = del(m_table[i].key, m_table[i].value))
246 						return ret;
247 			}
248 		return 0;
249 	}
250 
251 	auto byKey() { return bySlot.map!(e => e.key); }
252 	auto byKey() const { return bySlot.map!(e => e.key); }
253 	auto byValue() { return bySlot.map!(e => e.value); }
254 	auto byValue() const { return bySlot.map!(e => e.value); }
255 	auto byKeyValue() { import std.typecons : Tuple; return bySlot.map!(e => Tuple!(Key, "key", Value, "value")(e.key, e.value)); }
256 	auto byKeyValue() const { import std.typecons : Tuple; return bySlot.map!(e => Tuple!(const(Key), "key", const(Value), "value")(e.key, e.value)); }
257 
258 	private auto bySlot() { return m_table[].filter!(e => !Traits.equals(e.key, Traits.clearValue)); }
259 	private auto bySlot() const { return m_table[].filter!(e => !Traits.equals(e.key, Traits.clearValue)); }
260 
261 	private @property AllocatorInstanceType allocator()
262 	{
263 		static if (is(typeof(Allocator.instance)))
264 			return AllocatorType.instance;
265 		else {
266 			if (!m_allocator._parent) {
267 				static if (is(Allocator == IAllocator)) {
268 					try m_allocator = typeof(m_allocator)(AW(vibeThreadAllocator()));
269 					catch (Exception e) assert(false, e.msg);
270 				} else assert(false, "Allocator not initialized.");
271 			}
272 			return m_allocator;
273 		}
274 	}
275 
276 	private size_t findIndex(Key key)
277 	const {
278 		if (m_length == 0) return size_t.max;
279 		size_t start = Traits.hashOf(key) & (m_table.length-1);
280 		auto i = start;
281 		while (!Traits.equals(m_table[i].key, key)) {
282 			if (Traits.equals(m_table[i].key, Traits.clearValue)) return size_t.max;
283 			if (++i >= m_table.length) i -= m_table.length;
284 			if (i == start) return size_t.max;
285 		}
286 		return i;
287 	}
288 
289 	private size_t findInsertIndex(Key key)
290 	const {
291 		auto hash = Traits.hashOf(key);
292 		size_t target = hash & (m_table.length-1);
293 		auto i = target;
294 		while (!Traits.equals(m_table[i].key, Traits.clearValue) && !Traits.equals(m_table[i].key, key)) {
295 			if (++i >= m_table.length) i -= m_table.length;
296 			assert (i != target, "No free bucket found, HashMap full!?");
297 		}
298 		return i;
299 	}
300 
301 	private void grow(size_t amount)
302 	@trusted {
303 		auto newsize = m_length + amount;
304 		if (newsize < (m_table.length*2)/3) {
305 			int rc;
306 			try rc = allocator.prefix(m_table);
307 			catch (Exception e) assert(false, e.msg);
308 			if (rc > 1) {
309 				// enforce copy-on-write
310 				auto oldtable = m_table;
311 				try {
312 					m_table = allocator.makeArray!TableEntry(m_table.length);
313 					m_table[] = oldtable;
314 					allocator.prefix(oldtable)--;
315 					assert(allocator.prefix(oldtable) > 0);
316 					allocator.prefix(m_table) = 1;
317 				} catch (Exception e) {
318 					assert(false, e.msg);
319 				}
320 			}
321 			return;
322 		}
323 		auto newcap = m_table.length ? m_table.length : 16;
324 		while (newsize >= (newcap*2)/3) newcap *= 2;
325 		resize(newcap);
326 	}
327 
328 	private void resize(size_t new_size)
329 	@trusted {
330 		assert(!m_resizing);
331 		m_resizing = true;
332 		scope(exit) m_resizing = false;
333 
334 		uint pot = 0;
335 		while (new_size > 1) {
336 			pot++;
337 			new_size /= 2;
338 		}
339 		new_size = 1 << pot;
340 
341 		auto oldtable = m_table;
342 
343 		// allocate the new array, automatically initializes with empty entries (Traits.clearValue)
344 		try {
345 			m_table = allocator.makeArray!TableEntry(new_size);
346 			allocator.prefix(m_table) = 1;
347 		} catch (Exception e) assert(false, e.msg);
348 		static if (hasIndirections!TableEntry) GC.addRange(m_table.ptr, m_table.length * TableEntry.sizeof);
349 		// perform a move operation of all non-empty elements from the old array to the new one
350 		foreach (ref el; oldtable)
351 			if (!Traits.equals(el.key, Traits.clearValue)) {
352 				auto idx = findInsertIndex(el.key);
353 				(cast(ubyte[])(&m_table[idx])[0 .. 1])[] = (cast(ubyte[])(&el)[0 .. 1])[];
354 			}
355 
356 		// all elements have been moved to the new array, so free the old one without calling destructors
357 		int rc;
358 		try rc = oldtable is null ? 1 : --allocator.prefix(oldtable);
359 		catch (Exception e) assert(false, e.msg);
360 		if (rc == 0) {
361 			static if (hasIndirections!TableEntry) GC.removeRange(oldtable.ptr);
362 			try allocator.deallocate(oldtable);
363 			catch (Exception e) assert(false, e.msg);
364 		}
365 	}
366 }
367 
368 nothrow unittest {
369 	import std.conv;
370 
371 	HashMap!(string, string) map;
372 
373 	foreach (i; 0 .. 100) {
374 		map[to!string(i)] = to!string(i) ~ "+";
375 		assert(map.length == i+1);
376 	}
377 
378 	foreach (i; 0 .. 100) {
379 		auto str = to!string(i);
380 		auto pe = str in map;
381 		assert(pe !is null && *pe == str ~ "+");
382 		assert(map[str] == str ~ "+");
383 	}
384 
385 	foreach (i; 0 .. 50) {
386 		map.remove(to!string(i));
387 		assert(map.length == 100-i-1);
388 	}
389 
390 	foreach (i; 50 .. 100) {
391 		auto str = to!string(i);
392 		auto pe = str in map;
393 		assert(pe !is null && *pe == str ~ "+");
394 		assert(map[str] == str ~ "+");
395 	}
396 }
397 
398 // test for nothrow/@nogc compliance
399 nothrow unittest {
400 	HashMap!(int, int) map1;
401 	HashMap!(string, string) map2;
402 	map1[1] = 2;
403 	map2["1"] = "2";
404 
405 	@nogc nothrow void performNoGCOps()
406 	{
407 		foreach (int v; map1) {}
408 		foreach (int k, int v; map1) {}
409 		assert(1 in map1);
410 		assert(map1.length == 1);
411 		assert(map1[1] == 2);
412 		assert(map1.getNothrow(1, -1) == 2);
413 
414 		foreach (string v; map2) {}
415 		foreach (string k, string v; map2) {}
416 		assert("1" in map2);
417 		assert(map2.length == 1);
418 		assert(map2["1"] == "2");
419 		assert(map2.getNothrow("1", "") == "2");
420 	}
421 
422 	performNoGCOps();
423 }
424 
425 unittest { // test for proper use of constructor/post-blit/destructor
426 	static struct Test {
427 		static size_t constructedCounter = 0;
428 		bool constructed = false;
429 		this(int) { constructed = true; constructedCounter++; }
430 		this(this) nothrow { if (constructed) constructedCounter++; }
431 		~this() nothrow { if (constructed) constructedCounter--; }
432 	}
433 
434 	assert(Test.constructedCounter == 0);
435 
436 	{ // sanity check
437 		Test t;
438 		assert(Test.constructedCounter == 0);
439 		t = Test(1);
440 		assert(Test.constructedCounter == 1);
441 		auto u = t;
442 		assert(Test.constructedCounter == 2);
443 		t = Test.init;
444 		assert(Test.constructedCounter == 1);
445 	}
446 	assert(Test.constructedCounter == 0);
447 
448 	{ // basic insertion and hash map resizing
449 		HashMap!(int, Test) map;
450 		foreach (i; 1 .. 67) {
451 			map[i] = Test(1);
452 			assert(Test.constructedCounter == i);
453 		}
454 	}
455 
456 	assert(Test.constructedCounter == 0);
457 
458 	{ // test clear() and overwriting existing entries
459 		HashMap!(int, Test) map;
460 		foreach (i; 1 .. 67) {
461 			map[i] = Test(1);
462 			assert(Test.constructedCounter == i);
463 		}
464 		map.clear();
465 		foreach (i; 1 .. 67) {
466 			map[i] = Test(1);
467 			assert(Test.constructedCounter == i);
468 		}
469 		foreach (i; 1 .. 67) {
470 			map[i] = Test(1);
471 			assert(Test.constructedCounter == 66);
472 		}
473 	}
474 
475 	assert(Test.constructedCounter == 0);
476 
477 	{ // test removing entries and adding entries after remove
478 		HashMap!(int, Test) map;
479 		foreach (i; 1 .. 67) {
480 			map[i] = Test(1);
481 			assert(Test.constructedCounter == i);
482 		}
483 		foreach (i; 1 .. 33) {
484 			map.remove(i);
485 			assert(Test.constructedCounter == 66 - i);
486 		}
487 		foreach (i; 67 .. 130) {
488 			map[i] = Test(1);
489 			assert(Test.constructedCounter == i - 32);
490 		}
491 	}
492 
493 	assert(Test.constructedCounter == 0);
494 }
495 
496 private template UnConst(T) {
497 	static if (is(T U == const(U))) {
498 		alias UnConst = U;
499 	} else static if (is(T V == immutable(V))) {
500 		alias UnConst = V;
501 	} else alias UnConst = T;
502 }