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