1 /**
2 	Internal hash map implementation.
3 
4 	Copyright: © 2013 RejectedSoftware e.K.
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 vutil.hashmap;
9 
10 import vutil.memory;
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 (is(Key == class)) return a is b;
21 		else return a == b;
22 	}
23 	static size_t hashOf(in ref Key k)
24 	{
25 		static if (is(Key == class) && &Key.toHash == &Object.toHash)
26 			return cast(size_t)cast(void*)k;
27 		else static if (__traits(compiles, Key.init.toHash()))
28 			return k.toHash();
29 		else static if (__traits(compiles, Key.init.toHashShared()))
30 			return k.toHashShared();
31 		else {
32 			// evil casts to be able to get the most basic operations of
33 			// HashMap nothrow and @nogc
34 			static size_t hashWrapper(in ref Key k) {
35 				static typeinfo = typeid(Key);
36 				return typeinfo.getHash(&k);
37 			}
38 			static @nogc nothrow size_t properlyTypedWrapper(in ref Key k) { return 0; }
39 			return (cast(typeof(&properlyTypedWrapper))&hashWrapper)(k);
40 		}
41 	}
42 }
43 
44 struct HashMap(TKey, TValue, Traits = DefaultHashMapTraits!TKey)
45 {
46 	import vmeta.traits : isOpApplyDg;
47 
48 	alias Key = TKey;
49 	alias Value = TValue;
50 
51 	struct TableEntry {
52 		UnConst!Key key = Traits.clearValue;
53 		Value value;
54 
55 		this(Key key, Value value) { this.key = cast(UnConst!Key)key; this.value = value; }
56 	}
57 	private {
58 		TableEntry[] m_table; // NOTE: capacity is always POT
59 		size_t m_length;
60 		Allocator m_allocator;
61 		bool m_resizing;
62 	}
63 
64 	this(Allocator allocator)
65 	{
66 		m_allocator = allocator;
67 	}
68 
69 	~this()
70 	{
71 		if (m_table) freeArray(m_allocator, m_table);
72 	}
73 
74 	@disable this(this);
75 
76 	@property size_t length() const { return m_length; }
77 
78 	void remove(Key key)
79 	{
80 		auto idx = findIndex(key);
81 		assert (idx != size_t.max, "Removing non-existent element.");
82 		auto i = idx;
83 		while (true) {
84 			m_table[i].key = Traits.clearValue;
85 			m_table[i].value = Value.init;
86 
87 			size_t j = i, r;
88 			do {
89 				if (++i >= m_table.length) i -= m_table.length;
90 				if (Traits.equals(m_table[i].key, Traits.clearValue)) {
91 					m_length--;
92 					return;
93 				}
94 				r = Traits.hashOf(m_table[i].key) & (m_table.length-1);
95 			} while ((j<r && r<=i) || (i<j && j<r) || (r<=i && i<j));
96 			m_table[j] = m_table[i];
97 		}
98 	}
99 
100 	Value get(Key key, lazy Value default_value = Value.init)
101 	{
102 		auto idx = findIndex(key);
103 		if (idx == size_t.max) return default_value;
104 		return m_table[idx].value;
105 	}
106 
107 	/// Workaround #12647
108 	package Value getNothrow(Key key, Value default_value = Value.init)
109 	{
110 		auto idx = findIndex(key);
111 		if (idx == size_t.max) return default_value;
112 		return m_table[idx].value;
113 	}
114 
115 	static if (!is(typeof({ Value v; const(Value) vc; v = vc; }))) {
116 		const(Value) get(Key key, lazy const(Value) default_value = Value.init)
117 		{
118 			auto idx = findIndex(key);
119 			if (idx == size_t.max) return default_value;
120 			return m_table[idx].value;
121 		}
122 	}
123 
124 	void clear()
125 	{
126 		foreach (i; 0 .. m_table.length)
127 			if (!Traits.equals(m_table[i].key, Traits.clearValue)) {
128 				m_table[i].key = Traits.clearValue;
129 				m_table[i].value = Value.init;
130 			}
131 		m_length = 0;
132 	}
133 
134 	void opIndexAssign(Value value, Key key)
135 	{
136 		assert(!Traits.equals(key, Traits.clearValue), "Inserting clear value into hash map.");
137 		grow(1);
138 		auto i = findInsertIndex(key);
139 		if (!Traits.equals(m_table[i].key, key)) m_length++;
140 		m_table[i] = TableEntry(key, value);
141 	}
142 
143 	ref inout(Value) opIndex(Key key)
144 	inout {
145 		auto idx = findIndex(key);
146 		assert (idx != size_t.max, "Accessing non-existent key.");
147 		return m_table[idx].value;
148 	}
149 
150 	inout(Value)* opBinaryRight(string op)(Key key)
151 	inout if (op == "in") {
152 		auto idx = findIndex(key);
153 		if (idx == size_t.max) return null;
154 		return &m_table[idx].value;
155 	}
156 
157 	int opApply(DG)(scope DG del) if (isOpApplyDg!(DG, Key, Value))
158 	{
159 		import std.traits : arity;
160 		foreach (i; 0 .. m_table.length)
161 			if (!Traits.equals(m_table[i].key, Traits.clearValue)) {
162 				static assert(arity!del >= 1 && arity!del <= 2,
163 					      "isOpApplyDg should have prevented this");
164 				static if (arity!del == 1) {
165 					if (int ret = del(m_table[i].value))
166 						return ret;
167 				} else
168 					if (int ret = del(m_table[i].key, m_table[i].value))
169 						return ret;
170 			}
171 		return 0;
172 	}
173 
174 	private size_t findIndex(Key key)
175 	const {
176 		if (m_length == 0) return size_t.max;
177 		size_t start = Traits.hashOf(key) & (m_table.length-1);
178 		auto i = start;
179 		while (!Traits.equals(m_table[i].key, key)) {
180 			if (Traits.equals(m_table[i].key, Traits.clearValue)) return size_t.max;
181 			if (++i >= m_table.length) i -= m_table.length;
182 			if (i == start) return size_t.max;
183 		}
184 		return i;
185 	}
186 
187 	private size_t findInsertIndex(Key key)
188 	const {
189 		auto hash = Traits.hashOf(key);
190 		size_t target = hash & (m_table.length-1);
191 		auto i = target;
192 		while (!Traits.equals(m_table[i].key, Traits.clearValue) && !Traits.equals(m_table[i].key, key)) {
193 			if (++i >= m_table.length) i -= m_table.length;
194 			assert (i != target, "No free bucket found, HashMap full!?");
195 		}
196 		return i;
197 	}
198 
199 	private void grow(size_t amount)
200 	{
201 		auto newsize = m_length + amount;
202 		if (newsize < (m_table.length*2)/3) return;
203 		auto newcap = m_table.length ? m_table.length : 16;
204 		while (newsize >= (newcap*2)/3) newcap *= 2;
205 		resize(newcap);
206 	}
207 
208 	private void resize(size_t new_size)
209 	@trusted {
210 		assert(!m_resizing);
211 		m_resizing = true;
212 		scope(exit) m_resizing = false;
213 
214 		if (!m_allocator) m_allocator = defaultAllocator();
215 
216 		uint pot = 0;
217 		while (new_size > 1) pot++, new_size /= 2;
218 		new_size = 1 << pot;
219 
220 		auto oldtable = m_table;
221 
222 		// allocate the new array, automatically initializes with empty entries (Traits.clearValue)
223 		m_table = allocArray!TableEntry(m_allocator, new_size);
224 
225 		// perform a move operation of all non-empty elements from the old array to the new one
226 		foreach (ref el; oldtable)
227 			if (!Traits.equals(el.key, Traits.clearValue)) {
228 				auto idx = findInsertIndex(el.key);
229 				(cast(ubyte[])(&m_table[idx])[0 .. 1])[] = (cast(ubyte[])(&el)[0 .. 1])[];
230 			}
231 
232 		// all elements have been moved to the new array, so free the old one without calling destructors
233 		if (oldtable) freeArray(m_allocator, oldtable, false);
234 	}
235 }
236 
237 unittest {
238 	import std.conv;
239 
240 	HashMap!(string, string) map;
241 
242 	foreach (i; 0 .. 100) {
243 		map[to!string(i)] = to!string(i) ~ "+";
244 		assert(map.length == i+1);
245 	}
246 
247 	foreach (i; 0 .. 100) {
248 		auto str = to!string(i);
249 		auto pe = str in map;
250 		assert(pe !is null && *pe == str ~ "+");
251 		assert(map[str] == str ~ "+");
252 	}
253 
254 	foreach (i; 0 .. 50) {
255 		map.remove(to!string(i));
256 		assert(map.length == 100-i-1);
257 	}
258 
259 	foreach (i; 50 .. 100) {
260 		auto str = to!string(i);
261 		auto pe = str in map;
262 		assert(pe !is null && *pe == str ~ "+");
263 		assert(map[str] == str ~ "+");
264 	}
265 }
266 
267 // test for nothrow/@nogc compliance
268 static if (__VERSION__ >= 2066)
269 nothrow unittest {
270 	HashMap!(int, int) map1;
271 	HashMap!(string, string) map2;
272 	map1[1] = 2;
273 	map2["1"] = "2";
274 
275 	@nogc nothrow void performNoGCOps()
276 	{
277 		foreach (int v; map1) {}
278 		foreach (int k, int v; map1) {}
279 		assert(1 in map1);
280 		assert(map1.length == 1);
281 		assert(map1[1] == 2);
282 		assert(map1.getNothrow(1, -1) == 2);
283 
284 		foreach (string v; map2) {}
285 		foreach (string k, string v; map2) {}
286 		assert("1" in map2);
287 		assert(map2.length == 1);
288 		assert(map2["1"] == "2");
289 		assert(map2.getNothrow("1", "") == "2");
290 	}
291 
292 	performNoGCOps();
293 }
294 
295 unittest { // test for proper use of constructor/post-blit/destructor
296 	static struct Test {
297 		static size_t constructedCounter = 0;
298 		bool constructed = false;
299 		this(int) { constructed = true; constructedCounter++; }
300 		this(this) { if (constructed) constructedCounter++; }
301 		~this() { if (constructed) constructedCounter--; }
302 	}
303 
304 	assert(Test.constructedCounter == 0);
305 
306 	{ // sanity check
307 		Test t;
308 		assert(Test.constructedCounter == 0);
309 		t = Test(1);
310 		assert(Test.constructedCounter == 1);
311 		auto u = t;
312 		assert(Test.constructedCounter == 2);
313 		t = Test.init;
314 		assert(Test.constructedCounter == 1);
315 	}
316 	assert(Test.constructedCounter == 0);
317 	
318 	{ // basic insertion and hash map resizing
319 		HashMap!(int, Test) map;
320 		foreach (i; 1 .. 67) {
321 			map[i] = Test(1);
322 			assert(Test.constructedCounter == i);
323 		}
324 	}
325 
326 	assert(Test.constructedCounter == 0);
327 
328 	{ // test clear() and overwriting existing entries
329 		HashMap!(int, Test) map;
330 		foreach (i; 1 .. 67) {
331 			map[i] = Test(1);
332 			assert(Test.constructedCounter == i);
333 		}
334 		map.clear();
335 		foreach (i; 1 .. 67) {
336 			map[i] = Test(1);
337 			assert(Test.constructedCounter == i);
338 		}
339 		foreach (i; 1 .. 67) {
340 			map[i] = Test(1);
341 			assert(Test.constructedCounter == 66);
342 		}
343 	}
344 
345 	assert(Test.constructedCounter == 0);
346 
347 	{ // test removing entries and adding entries after remove
348 		HashMap!(int, Test) map;
349 		foreach (i; 1 .. 67) {
350 			map[i] = Test(1);
351 			assert(Test.constructedCounter == i);
352 		}
353 		foreach (i; 1 .. 33) {
354 			map.remove(i);
355 			assert(Test.constructedCounter == 66 - i);
356 		}
357 		foreach (i; 67 .. 130) {
358 			map[i] = Test(1);
359 			assert(Test.constructedCounter == i - 32);
360 		}
361 	}
362 
363 	assert(Test.constructedCounter == 0);
364 }
365 
366 private template UnConst(T) {
367 	static if (is(T U == const(U))) {
368 		alias UnConst = U;
369 	} else static if (is(T V == immutable(V))) {
370 		alias UnConst = V;
371 	} else alias UnConst = T;
372 }
373 
374 static if (__VERSION__ < 2066) private static bool nogc() { return false; }