aboutsummaryrefslogtreecommitdiff
path: root/background/storage.js
blob: a4e626acc15fe557095f1abf5d1b5b5f063e0746 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
/**
 * This file is part of Haketilo.
 *
 * Function: Storage manager.
 *
 * Copyright (C) 2021 Wojtek Kosior
 * Redistribution terms are gathered in the `copyright' file.
 */

/*
 * IMPORTS_START
 * IMPORT raw_storage
 * IMPORT TYPE_NAME
 * IMPORT list_prefixes
 * IMPORT make_lock
 * IMPORT lock
 * IMPORT unlock
 * IMPORT make_once
 * IMPORT browser
 * IMPORT observables
 * IMPORTS_END
 */

var exports = {};

/* A special case of persisted variable is one that contains list of items. */

async function get_list_var(name)
{
    let list = await raw_storage.get_var(name);

    return list === undefined ? [] : list;
}

/* We maintain in-memory copies of some stored lists. */

async function list(prefix)
{
    let name = TYPE_NAME[prefix] + "s"; /* Make plural. */
    let map = new Map();

    for (let item of await get_list_var(name))
	map.set(item, await raw_storage.get(prefix + item));

    return {map, prefix, name, observable: observables.make(),
	    lock: make_lock()};
}

var list_by_prefix = {};

async function init()
{
    for (let prefix of list_prefixes)
	list_by_prefix[prefix] = await list(prefix);

    return exports;
}

/*
 * Facilitate listening to changes
 */

exports.add_change_listener = function (cb, prefixes=list_prefixes)
{
    if (typeof(prefixes) === "string")
	prefixes = [prefixes];

    for (let prefix of prefixes)
	observables.subscribe(list_by_prefix[prefix].observable, cb);
}

exports.remove_change_listener = function (cb, prefixes=list_prefixes)
{
    if (typeof(prefixes) === "string")
	prefixes = [prefixes];

    for (let prefix of prefixes)
	observables.unsubscribe(list_by_prefix[prefix].observable, cb);
}

/* Prepare some hepler functions to get elements of a list */

function list_items_it(list, with_values=false)
{
    return with_values ? list.map.entries() : list.map.keys();
}

function list_entries_it(list)
{
    return list_items_it(list, true);
}

function list_items(list, with_values=false)
{
    let array = [];

    for (let item of list_items_it(list, with_values))
	array.push(item);

    return array;
}

function list_entries(list)
{
    return list_items(list, true);
}

/*
 * Below we make additional effort to update map of given kind of items
 * every time an item is added/removed to keep everything coherent.
 */
async function set_item(item, value, list)
{
    await lock(list.lock);
    let result = await _set_item(...arguments);
    unlock(list.lock)
    return result;
}
async function _set_item(item, value, list)
{
    const key = list.prefix + item;
    const old_val = list.map.get(item);
    const set_obj = {[key]: value};
    if (old_val === undefined) {
	const items = list_items(list);
	items.push(item);
	set_obj["_" + list.name] = items;
    }

    await raw_storage.set(set_obj);
    list.map.set(item, value);

    const change = {
	prefix : list.prefix,
	item,
	old_val,
	new_val : value
    };

    observables.broadcast(list.observable, change);

    return old_val;
}

// TODO: The actual idea to set value to undefined is good - this way we can
//       also set a new list of items in the same API call. But such key
//       is still stored in the storage. We need to somehow remove it later.
//       For that, we're going to have to store 1 more list of each kind.
async function remove_item(item, list)
{
    await lock(list.lock);
    let result = await _remove_item(...arguments);
    unlock(list.lock)
    return result;
}
async function _remove_item(item, list)
{
    const old_val = list.map.get(item);
    if (old_val === undefined)
	return;

    const items = list_items(list);
    const index = items.indexOf(item);
    items.splice(index, 1);

    await raw_storage.set({
	[list.prefix + item]: undefined,
	["_" + list.name]: items
    });
    list.map.delete(item);

    const change = {
	prefix : list.prefix,
	item,
	old_val,
	new_val : undefined
    };

    observables.broadcast(list.observable, change);

    return old_val;
}

// TODO: same as above applies here
async function replace_item(old_item, new_item, list, new_val=undefined)
{
    await lock(list.lock);
    let result = await _replace_item(...arguments);
    unlock(list.lock)
    return result;
}
async function _replace_item(old_item, new_item, list, new_val=undefined)
{
    const old_val = list.map.get(old_item);
    if (new_val === undefined) {
	if (old_val === undefined)
	    return;
	new_val = old_val;
    } else if (new_val === old_val && new_item === old_item) {
	return old_val;
    }

    if (old_item === new_item || old_val === undefined) {
	await _set_item(new_item, new_val, list);
	return old_val;
    }

    const items = list_items(list);
    const index = items.indexOf(old_item);
    items[index] = new_item;

    await raw_storage.set({
	[list.prefix + old_item]: undefined,
	[list.prefix + new_item]: new_val,
	["_" + list.name]: items
    });
    list.map.delete(old_item);

    const change = {
	prefix : list.prefix,
	item : old_item,
	old_val,
	new_val : undefined
    };

    observables.broadcast(list.observable, change);

    list.map.set(new_item, new_val);

    change.item = new_item;
    change.old_val = undefined;
    change.new_val = new_val;

    observables.broadcast(list.observable, change);

    return old_val;
}

/*
 * For scripts, item name is chosen by user, data should be
 * an object containing:
 * - script's url and hash or
 * - script's text or
 * - all three
 */

/*
 * For bags, item name is chosen by user, data is an array of 2-element
 * arrays with type prefix and script/bag names.
 */

/*
 * For pages data argument is an object with properties `allow'
 * and `components'. Item name is url.
 */

exports.set = async function (prefix, item, data)
{
    return set_item(item, data, list_by_prefix[prefix]);
}

exports.get = function (prefix, item)
{
    return list_by_prefix[prefix].map.get(item);
}

exports.remove = async function (prefix, item)
{
    return remove_item(item, list_by_prefix[prefix]);
}

exports.replace = async function (prefix, old_item, new_item,
				  new_data=undefined)
{
    return replace_item(old_item, new_item, list_by_prefix[prefix],
			new_data);
}

exports.get_all_names = function (prefix)
{
    return list_items(list_by_prefix[prefix]);
}

exports.get_all_names_it = function (prefix)
{
    return list_items_it(list_by_prefix[prefix]);
}

exports.get_all = function (prefix)
{
    return list_entries(list_by_prefix[prefix]);
}

exports.get_all_it = function (prefix)
{
    return list_entries_it(list_by_prefix[prefix]);
}

/* Finally, a quick way to wipe all the data. */
// TODO: maybe delete items in such order that none of them ever references
// an already-deleted one?
exports.clear = async function ()
{
    let lists = list_prefixes.map((p) => list_by_prefix[p]);

    for (let list of lists)
	await lock(list.lock);

    for (let list of lists) {

	let change = {
	    prefix : list.prefix,
	    new_val : undefined
	};

	for (let [item, val] of list_entries_it(list)) {
	    change.item = item;
	    change.old_val = val;
	    observables.broadcast(list.observable, change);
	}

	list.map = new Map();
    }

    await browser.storage.local.clear();

    for (let list of lists)
	unlock(list.lock);
}

const get_storage = make_once(init);

/*
 * EXPORTS_START
 * EXPORT get_storage
 * EXPORTS_END
 */
the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright (C) 2016-present the asyncpg authors and contributors
+ <see AUTHORS file>
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/immutables/__init__.py b/immutables/__init__.py
new file mode 100644
index 0000000..fa060ed
--- /dev/null
+++ b/immutables/__init__.py
@@ -0,0 +1,4 @@
+from ._map import Map
+
+
+__all__ = 'Map',
diff --git a/immutables/_map.c b/immutables/_map.c
new file mode 100644
index 0000000..4cf29b1
--- /dev/null
+++ b/immutables/_map.c
@@ -0,0 +1,2995 @@
+#include <stddef.h> /* For offsetof */
+#include "_map.h"
+
+
+/*
+This file provides an implemention of an immutable mapping using the
+Hash Array Mapped Trie (or HAMT) datastructure.
+
+This design allows to have:
+
+1. Efficient copy: immutable mappings can be copied by reference,
+ making it an O(1) operation.
+
+2. Efficient mutations: due to structural sharing, only a portion of
+ the trie needs to be copied when the collection is mutated. The
+ cost of set/delete operations is O(log N).
+
+3. Efficient lookups: O(log N).
+
+(where N is number of key/value items in the immutable mapping.)
+
+
+HAMT
+====
+
+The core idea of HAMT is that the shape of the trie is encoded into the
+hashes of keys.
+
+Say we want to store a K/V pair in our mapping. First, we calculate the
+hash of K, let's say it's 19830128, or in binary:
+
+ 0b1001011101001010101110000 = 19830128
+
+Now let's partition this bit representation of the hash into blocks of
+5 bits each:
+
+ 0b00_00000_10010_11101_00101_01011_10000 = 19830128
+ (6) (5) (4) (3) (2) (1)
+
+Each block of 5 bits represents a number betwen 0 and 31. So if we have
+a tree that consists of nodes, each of which is an array of 32 pointers,
+those 5-bit blocks will encode a position on a single tree level.
+
+For example, storing the key K with hash 19830128, results in the following
+tree structure:
+
+ (array of 32 pointers)
+ +---+ -- +----+----+----+ -- +----+
+ root node | 0 | .. | 15 | 16 | 17 | .. | 31 | 0b10000 = 16 (1)
+ (level 1) +---+ -- +----+----+----+ -- +----+
+ |
+ +---+ -- +----+----+----+ -- +----+
+ a 2nd level node | 0 | .. | 10 | 11 | 12 | .. | 31 | 0b01011 = 11 (2)
+ +---+ -- +----+----+----+ -- +----+
+ |
+ +---+ -- +----+----+----+ -- +----+
+ a 3rd level node | 0 | .. | 04 | 05 | 06 | .. | 31 | 0b00101 = 5 (3)
+ +---+ -- +----+----+----+ -- +----+
+ |
+ +---+ -- +----+----+----+----+
+ a 4th level node | 0 | .. | 04 | 29 | 30 | 31 | 0b11101 = 29 (4)
+ +---+ -- +----+----+----+----+
+ |
+ +---+ -- +----+----+----+ -- +----+
+ a 5th level node | 0 | .. | 17 | 18 | 19 | .. | 31 | 0b10010 = 18 (5)
+ +---+ -- +----+----+----+ -- +----+
+ |
+ +--------------+
+ |
+ +---+ -- +----+----+----+ -- +----+
+ a 6th level node | 0 | .. | 15 | 16 | 17 | .. | 31 | 0b00000 = 0 (6)
+ +---+ -- +----+----+----+ -- +----+
+ |
+ V -- our value (or collision)
+
+To rehash: for a K/V pair, the hash of K encodes where in the tree V will
+be stored.
+
+To optimize memory footprint and handle hash collisions, our implementation
+uses three different types of nodes:
+
+ * A Bitmap node;
+ * An Array node;
+ * A Collision node.
+
+Because we implement an immutable dictionary, our nodes are also
+immutable. Therefore, when we need to modify a node, we copy it, and
+do that modification to the copy.
+
+
+Array Nodes
+-----------
+
+These nodes are very simple. Essentially they are arrays of 32 pointers
+we used to illustrate the high-level idea in the previous section.
+
+We use Array nodes only when we need to store more than 16 pointers
+in a single node.
+
+Array nodes do not store key objects or value objects. They are used
+only as an indirection level - their pointers point to other nodes in
+the tree.
+
+
+Bitmap Node
+-----------
+
+Allocating a new 32-pointers array for every node of our tree would be
+very expensive. Unless we store millions of keys, most of tree nodes would
+be very sparse.
+
+When we have less than 16 elements in a node, we don't want to use the
+Array node, that would mean that we waste a lot of memory. Instead,
+we can use bitmap compression and can have just as many pointers
+as we need!
+
+Bitmap nodes consist of two fields:
+
+1. An array of pointers. If a Bitmap node holds N elements, the
+ array will be of N pointers.
+
+2. A 32bit integer -- a bitmap field. If an N-th bit is set in the
+ bitmap, it means that the node has an N-th element.
+
+For example, say we need to store a 3 elements sparse array:
+
+ +---+ -- +---+ -- +----+ -- +----+
+ | 0 | .. | 4 | .. | 11 | .. | 17 |
+ +---+ -- +---+ -- +----+ -- +----+
+ | | |
+ o1 o2 o3
+
+We allocate a three-pointer Bitmap node. Its bitmap field will be
+then set to:
+
+ 0b_00100_00010_00000_10000 == (1 << 17) | (1 << 11) | (1 << 4)
+
+To check if our Bitmap node has an I-th element we can do:
+
+ bitmap & (1 << I)
+
+
+And here's a formula to calculate a position in our pointer array
+which would correspond to an I-th element:
+
+ popcount(bitmap & ((1 << I) - 1))
+
+
+Let's break it down:
+
+ * `popcount` is a function that returns a number of bits set to 1;
+
+ * `((1 << I) - 1)` is a mask to filter the bitmask to contain bits
+ set to the *right* of our bit.
+
+
+So for our 17, 11, and 4 indexes:
+
+ * bitmap & ((1 << 17) - 1) == 0b100000010000 => 2 bits are set => index is 2.
+
+ * bitmap & ((1 << 11) - 1) == 0b10000 => 1 bit is set => index is 1.
+
+ * bitmap & ((1 << 4) - 1) == 0b0 => 0 bits are set => index is 0.
+
+
+To conclude: Bitmap nodes are just like Array nodes -- they can store
+a number of pointers, but use bitmap compression to eliminate unused
+pointers.
+
+
+Bitmap nodes have two pointers for each item:
+
+ +----+----+----+----+ -- +----+----+
+ | k1 | v1 | k2 | v2 | .. | kN | vN |
+ +----+----+----+----+ -- +----+----+
+
+When kI == NULL, vI points to another tree level.
+
+When kI != NULL, the actual key object is stored in kI, and its
+value is stored in vI.
+
+
+Collision Nodes
+---------------
+
+Collision nodes are simple arrays of pointers -- two pointers per
+key/value. When there's a hash collision, say for k1/v1 and k2/v2
+we have `hash(k1)==hash(k2)`. Then our collision node will be:
+
+ +----+----+----+----+
+ | k1 | v1 | k2 | v2 |
+ +----+----+----+----+
+
+
+Tree Structure
+--------------
+
+All nodes are PyObjects.
+
+The `MapObject` object has a pointer to the root node (h_root),
+and has a length field (h_count).
+
+High-level functions accept a MapObject object and dispatch to
+lower-level functions depending on what kind of node h_root points to.
+
+
+Operations
+==========
+
+There are three fundamental operations on an immutable dictionary:
+
+1. "o.assoc(k, v)" will return a new immutable dictionary, that will be
+ a copy of "o", but with the "k/v" item set.
+
+ Functions in this file:
+
+ map_node_assoc, map_node_bitmap_assoc,
+ map_node_array_assoc, map_node_collision_assoc
+
+ `map_node_assoc` function accepts a node object, and calls
+ other functions depending on its actual type.
+
+2. "o.find(k)" will lookup key "k" in "o".
+
+ Functions:
+
+ map_node_find, map_node_bitmap_find,
+ map_node_array_find, map_node_collision_find
+
+3. "o.without(k)" will return a new immutable dictionary, that will be
+ a copy of "o", buth without the "k" key.
+
+ Functions:
+
+ map_node_without, map_node_bitmap_without,
+ map_node_array_without, map_node_collision_without
+
+
+Further Reading
+===============
+
+1. http://blog.higher-order.net/2009/09/08/understanding-clojures-persistenthashmap-deftwice.html
+
+2. http://blog.higher-order.net/2010/08/16/assoc-and-clojures-persistenthashmap-part-ii.html
+
+3. Clojure's PersistentHashMap implementation:
+ https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/PersistentHashMap.java
+*/
+
+
+#define IS_ARRAY_NODE(node) (Py_TYPE(node) == &_Map_ArrayNode_Type)
+#define IS_BITMAP_NODE(node) (Py_TYPE(node) == &_Map_BitmapNode_Type)
+#define IS_COLLISION_NODE(node) (Py_TYPE(node) == &_Map_CollisionNode_Type)
+
+
+/* Return type for 'find' (lookup a key) functions.
+
+ * F_ERROR - an error occurred;
+ * F_NOT_FOUND - the key was not found;
+ * F_FOUND - the key was found.
+*/
+typedef enum {F_ERROR, F_NOT_FOUND, F_FOUND} map_find_t;
+
+
+/* Return type for 'without' (delete a key) functions.
+
+ * W_ERROR - an error occurred;
+ * W_NOT_FOUND - the key was not found: there's nothing to delete;
+ * W_EMPTY - the key was found: the node/tree would be empty
+ if the key is deleted;
+ * W_NEWNODE - the key was found: a new node/tree is returned
+ without that key.
+*/
+typedef enum {W_ERROR, W_NOT_FOUND, W_EMPTY, W_NEWNODE} map_without_t;
+
+
+/* Low-level iterator protocol type.
+
+ * I_ITEM - a new item has been yielded;
+ * I_END - the whole tree was visited (similar to StopIteration).
+*/
+typedef enum {I_ITEM, I_END} map_iter_t;
+
+
+#define HAMT_ARRAY_NODE_SIZE 32
+
+
+typedef struct {
+ PyObject_HEAD
+ MapNode *a_array[HAMT_ARRAY_NODE_SIZE];
+ Py_ssize_t a_count;
+} MapNode_Array;
+
+
+typedef struct {
+ PyObject_VAR_HEAD
+ uint32_t b_bitmap;
+ PyObject *b_array[1];
+} MapNode_Bitmap;
+
+
+typedef struct {
+ PyObject_VAR_HEAD
+ int32_t c_hash;
+ PyObject *c_array[1];
+} MapNode_Collision;
+
+
+static MapNode_Bitmap *_empty_bitmap_node;
+static MapObject *_empty_map;
+
+
+/* Create a new HAMT immutable mapping. */
+static MapObject *
+map_new(void);
+
+/* Return a new collection based on "o", but with an additional
+ key/val pair. */
+static MapObject *
+map_assoc(MapObject *o, PyObject *key, PyObject *val);
+
+/* Return a new collection based on "o", but without "key". */
+static MapObject *
+map_without(MapObject *o, PyObject *key);
+
+/* Check if "v" is equal to "w".
+
+ Return:
+ - 0: v != w
+ - 1: v == w
+ - -1: An error occurred.
+*/
+static int
+map_eq(MapObject *v, MapObject *w);
+
+static map_find_t
+map_find(MapObject *o, PyObject *key, PyObject **val);
+
+/* Return the size of "o"; equivalent of "len(o)". */
+static Py_ssize_t
+map_len(MapObject *o);
+
+
+static MapObject *
+map_alloc(void);
+
+static MapNode *
+map_node_assoc(MapNode *node,
+ uint32_t shift, int32_t hash,
+ PyObject *key, PyObject *val, int* added_leaf);
+
+static map_without_t
+map_node_without(MapNode *node,
+ uint32_t shift, int32_t hash,
+ PyObject *key,
+ MapNode **new_node);
+
+static map_find_t
+map_node_find(MapNode *node,
+ uint32_t shift, int32_t hash,
+ PyObject *key, PyObject **val);
+
+static int
+map_node_dump(MapNode *node,
+ _PyUnicodeWriter *writer, int level);
+
+static MapNode *
+map_node_array_new(Py_ssize_t);
+
+static MapNode *
+map_node_collision_new(int32_t hash, Py_ssize_t size);
+
+static inline Py_ssize_t
+map_node_collision_count(MapNode_Collision *node);
+
+
+#ifdef NDEBUG
+static void
+_map_node_array_validate(void *o)
+{
+ assert(IS_ARRAY_NODE(o));
+ MapNode_Array *node = (MapNode_Array*)(o);
+ Py_ssize_t i = 0, count = 0;
+ for (; i < HAMT_ARRAY_NODE_SIZE; i++) {
+ if (node->a_array[i] != NULL) {
+ count++;
+ }
+ }
+ assert(count == node->a_count);
+}
+
+#define VALIDATE_ARRAY_NODE(NODE) \
+ do { _map_node_array_validate(NODE); } while (0);
+#else
+#define VALIDATE_ARRAY_NODE(NODE)
+#endif
+
+
+/* Returns -1 on error */
+static inline int32_t
+map_hash(PyObject *o)
+{
+ Py_hash_t hash = PyObject_Hash(o);
+
+#if SIZEOF_PY_HASH_T <= 4
+ return hash;
+#else
+ if (hash == -1) {
+ /* exception */
+ return -1;
+ }
+
+ /* While it's suboptimal to reduce Python's 64 bit hash to
+ 32 bits via XOR, it seems that the resulting hash function
+ is good enough (this is also how Long type is hashed in Java.)
+ Storing 10, 100, 1000 Python strings results in a relatively
+ shallow and uniform tree structure.
+
+ Please don't change this hashing algorithm, as there are many
+ tests that test some exact tree shape to cover all code paths.
+ */
+ int32_t xored = (int32_t)(hash & 0xffffffffl) ^ (int32_t)(hash >> 32);
+ return xored == -1 ? -2 : xored;
+#endif
+}
+
+static inline uint32_t
+map_mask(int32_t hash, uint32_t shift)
+{
+ return (((uint32_t)hash >> shift) & 0x01f);
+}
+
+static inline uint32_t
+map_bitpos(int32_t hash, uint32_t shift)
+{
+ return (uint32_t)1 << map_mask(hash, shift);
+}
+
+static inline uint32_t
+map_bitcount(uint32_t i)
+{
+ /* We could use native popcount instruction but that would
+ require to either add configure flags to enable SSE4.2
+ support or to detect it dynamically. Otherwise, we have
+ a risk of CPython not working properly on older hardware.
+
+ In practice, there's no observable difference in
+ performance between using a popcount instruction or the
+ following fallback code.
+
+ The algorithm is copied from:
+ https://graphics.stanford.edu/~seander/bithacks.html
+ */
+ i = i - ((i >> 1) & 0x55555555);
+ i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
+ return (((i + (i >> 4)) & 0xF0F0F0F) * 0x1010101) >> 24;
+}
+
+static inline uint32_t
+map_bitindex(uint32_t bitmap, uint32_t bit)
+{
+ return map_bitcount(bitmap & (bit - 1));
+}
+
+
+/////////////////////////////////// Dump Helpers
+
+static int
+_map_dump_ident(_PyUnicodeWriter *writer, int level)
+{
+ /* Write `' ' * level` to the `writer` */
+ PyObject *str = NULL;
+ PyObject *num = NULL;
+ PyObject *res = NULL;
+ int ret = -1;
+
+ str = PyUnicode_FromString(" ");
+ if (str == NULL) {
+ goto error;
+ }
+
+ num = PyLong_FromLong((long)level);
+ if (num == NULL) {
+ goto error;
+ }
+
+ res = PyNumber_Multiply(str, num);
+ if (res == NULL) {
+ goto error;
+ }
+
+ ret = _PyUnicodeWriter_WriteStr(writer, res);
+
+error:
+ Py_XDECREF(res);
+ Py_XDECREF(str);
+ Py_XDECREF(num);
+ return ret;
+}
+
+static int
+_map_dump_format(_PyUnicodeWriter *writer, const char *format, ...)
+{
+ /* A convenient helper combining _PyUnicodeWriter_WriteStr and
+ PyUnicode_FromFormatV.
+ */
+ PyObject* msg;
+ int ret;
+
+ va_list vargs;
+#ifdef HAVE_STDARG_PROTOTYPES
+ va_start(vargs, format);
+#else
+ va_start(vargs);
+#endif
+ msg = PyUnicode_FromFormatV(format, vargs);
+ va_end(vargs);
+
+ if (msg == NULL) {
+ return -1;
+ }
+
+ ret = _PyUnicodeWriter_WriteStr(writer, msg);
+ Py_DECREF(msg);
+ return ret;
+}
+
+/////////////////////////////////// Bitmap Node
+
+
+static MapNode *
+map_node_bitmap_new(Py_ssize_t size)
+{
+ /* Create a new bitmap node of size 'size' */
+
+ MapNode_Bitmap *node;
+ Py_ssize_t i;
+
+ assert(size >= 0);
+ assert(size % 2 == 0);
+
+ if (size == 0 && _empty_bitmap_node != NULL) {
+ Py_INCREF(_empty_bitmap_node);
+ return (MapNode *)_empty_bitmap_node;
+ }
+
+ /* No freelist; allocate a new bitmap node */
+ node = PyObject_GC_NewVar(
+ MapNode_Bitmap, &_Map_BitmapNode_Type, size);
+ if (node == NULL) {
+ return NULL;
+ }
+
+ Py_SIZE(node) = size;
+
+ for (i = 0; i < size; i++) {
+ node->b_array[i] = NULL;
+ }
+
+ node->b_bitmap = 0;
+
+ PyObject_GC_Track(node);
+
+ if (size == 0 && _empty_bitmap_node == NULL) {
+ /* Since bitmap nodes are immutable, we can cache the instance
+ for size=0 and reuse it whenever we need an empty bitmap node.
+ */
+ _empty_bitmap_node = node;
+ Py_INCREF(_empty_bitmap_node);
+ }
+
+ return (MapNode *)node;
+}
+
+static inline Py_ssize_t
+map_node_bitmap_count(MapNode_Bitmap *node)
+{
+ return Py_SIZE(node) / 2;
+}
+
+static MapNode_Bitmap *
+map_node_bitmap_clone(MapNode_Bitmap *node)
+{
+ /* Clone a bitmap node; return a new one with the same child notes. */
+
+ MapNode_Bitmap *clone;
+ Py_ssize_t i;
+
+ clone = (MapNode_Bitmap *)map_node_bitmap_new(Py_SIZE(node));
+ if (clone == NULL) {
+ return NULL;
+ }
+
+ for (i = 0; i < Py_SIZE(node); i++) {
+ Py_XINCREF(node->b_array[i]);
+ clone->b_array[i] = node->b_array[i];
+ }
+
+ clone->b_bitmap = node->b_bitmap;
+ return clone;
+}
+
+static MapNode_Bitmap *
+map_node_bitmap_clone_without(MapNode_Bitmap *o, uint32_t bit)
+{
+ assert(bit & o->b_bitmap);
+ assert(map_node_bitmap_count(o) > 1);
+
+ MapNode_Bitmap *new = (MapNode_Bitmap *)map_node_bitmap_new(
+ Py_SIZE(o) - 2);
+ if (new == NULL) {
+ return NULL;
+ }
+
+ uint32_t idx = map_bitindex(o->b_bitmap, bit);
+ uint32_t key_idx = 2 * idx;
+ uint32_t val_idx = key_idx + 1;
+ uint32_t i;
+
+ for (i = 0; i < key_idx; i++) {
+ Py_XINCREF(o->b_array[i]);
+ new->b_array[i] = o->b_array[i];
+ }
+
+ assert(Py_SIZE(o) >= 0 && Py_SIZE(o) <= 32);
+ for (i = val_idx + 1; i < (uint32_t)Py_SIZE(o); i++) {
+ Py_XINCREF(o->b_array[i]);
+ new->b_array[i - 2] = o->b_array[i];
+ }
+
+ new->b_bitmap = o->b_bitmap & ~bit;
+ return new;
+}
+
+static MapNode *
+map_node_new_bitmap_or_collision(uint32_t shift,
+ PyObject *key1, PyObject *val1,
+ int32_t key2_hash,
+ PyObject *key2, PyObject *val2)
+{
+ /* Helper method. Creates a new node for key1/val and key2/val2
+ pairs.
+
+ If key1 hash is equal to the hash of key2, a Collision node
+ will be created. If they are not equal, a Bitmap node is
+ created.
+ */
+
+ int32_t key1_hash = map_hash(key1);
+ if (key1_hash == -1) {
+ return NULL;
+ }
+
+ if (key1_hash == key2_hash) {
+ MapNode_Collision *n;
+ n = (MapNode_Collision *)map_node_collision_new(key1_hash, 4);
+ if (n == NULL) {
+ return NULL;
+ }
+
+ Py_INCREF(key1);
+ n->c_array[0] = key1;
+ Py_INCREF(val1);
+ n->c_array[1] = val1;
+
+ Py_INCREF(key2);
+ n->c_array[2] = key2;
+ Py_INCREF(val2);
+ n->c_array[3] = val2;
+
+ return (MapNode *)n;
+ }
+ else {
+ int added_leaf = 0;
+ MapNode *n = map_node_bitmap_new(0);
+ if (n == NULL) {
+ return NULL;
+ }
+
+ MapNode *n2 = map_node_assoc(
+ n, shift, key1_hash, key1, val1, &added_leaf);
+ Py_DECREF(n);
+ if (n2 == NULL) {
+ return NULL;
+ }
+
+ n = map_node_assoc(n2, shift, key2_hash, key2, val2, &added_leaf);
+ Py_DECREF(n2);
+ if (n == NULL) {
+ return NULL;
+ }
+
+ return n;
+ }
+}
+
+static MapNode *
+map_node_bitmap_assoc(MapNode_Bitmap *self,
+ uint32_t shift, int32_t hash,
+ PyObject *key, PyObject *val, int* added_leaf)
+{
+ /* assoc operation for bitmap nodes.
+
+ Return: a new node, or self if key/val already is in the
+ collection.
+
+ 'added_leaf' is later used in 'map_assoc' to determine if
+ `map.set(key, val)` increased the size of the collection.
+ */
+
+ uint32_t bit = map_bitpos(hash, shift);
+ uint32_t idx = map_bitindex(self->b_bitmap, bit);
+
+ /* Bitmap node layout:
+
+ +------+------+------+------+ --- +------+------+
+ | key1 | val1 | key2 | val2 | ... | keyN | valN |
+ +------+------+------+------+ --- +------+------+
+ where `N < Py_SIZE(node)`.
+
+ The `node->b_bitmap` field is a bitmap. For a given
+ `(shift, hash)` pair we can determine:
+
+ - If this node has the corresponding key/val slots.
+ - The index of key/val slots.
+ */
+
+ if (self->b_bitmap & bit) {
+ /* The key is set in this node */
+
+ uint32_t key_idx = 2 * idx;
+ uint32_t val_idx = key_idx + 1;
+
+ assert(val_idx < (size_t)Py_SIZE(self));
+
+ PyObject *key_or_null = self->b_array[key_idx];
+ PyObject *val_or_node = self->b_array[val_idx];
+
+ if (key_or_null == NULL) {
+ /* key is NULL. This means that we have a few keys
+ that have the same (hash, shift) pair. */
+
+ assert(val_or_node != NULL);
+
+ MapNode *sub_node = map_node_assoc(
+ (MapNode *)val_or_node,
+ shift + 5, hash, key, val, added_leaf);
+ if (sub_node == NULL) {
+ return NULL;
+ }
+
+ if (val_or_node == (PyObject *)sub_node) {
+ Py_DECREF(sub_node);
+ Py_INCREF(self);
+ return (MapNode *)self;
+ }
+
+ MapNode_Bitmap *ret = map_node_bitmap_clone(self);
+ if (ret == NULL) {
+ return NULL;
+ }
+ Py_SETREF(ret->b_array[val_idx], (PyObject*)sub_node);
+ return (MapNode *)ret;
+ }
+
+ assert(key != NULL);
+ /* key is not NULL. This means that we have only one other
+ key in this collection that matches our hash for this shift. */
+
+ int comp_err = PyObject_RichCompareBool(key, key_or_null, Py_EQ);
+ if (comp_err < 0) { /* exception in __eq__ */
+ return NULL;
+ }
+ if (comp_err == 1) { /* key == key_or_null */
+ if (val == val_or_node) {
+ /* we already have the same key/val pair; return self. */
+ Py_INCREF(self);
+ return (MapNode *)self;
+ }
+
+ /* We're setting a new value for the key we had before.
+ Make a new bitmap node with a replaced value, and return it. */
+ MapNode_Bitmap *ret = map_node_bitmap_clone(self);
+ if (ret == NULL) {
+ return NULL;
+ }
+ Py_INCREF(val);
+ Py_SETREF(ret->b_array[val_idx], val);
+ return (MapNode *)ret;
+ }
+
+ /* It's a new key, and it has the same index as *one* another key.
+ We have a collision. We need to create a new node which will
+ combine the existing key and the key we're adding.
+
+ `map_node_new_bitmap_or_collision` will either create a new
+ Collision node if the keys have identical hashes, or
+ a new Bitmap node.
+ */
+ MapNode *sub_node = map_node_new_bitmap_or_collision(
+ shift + 5,
+ key_or_null, val_or_node, /* existing key/val */
+ hash,
+ key, val /* new key/val */
+ );
+ if (sub_node == NULL) {
+ return NULL;
+ }
+
+ MapNode_Bitmap *ret = map_node_bitmap_clone(self);
+ if (ret == NULL) {
+ Py_DECREF(sub_node);
+ return NULL;
+ }
+ Py_SETREF(ret->b_array[key_idx], NULL);
+ Py_SETREF(ret->b_array[val_idx], (PyObject *)sub_node);
+
+ *added_leaf = 1;
+ return (MapNode *)ret;
+ }
+ else {
+ /* There was no key before with the same (shift,hash). */
+
+ uint32_t n = map_bitcount(self->b_bitmap);
+
+ if (n >= 16) {
+ /* When we have a situation where we want to store more
+ than 16 nodes at one level of the tree, we no longer
+ want to use the Bitmap node with bitmap encoding.
+
+ Instead we start using an Array node, which has
+ simpler (faster) implementation at the expense of
+ having prealocated 32 pointers for its keys/values
+ pairs.
+
+ Small map objects (<30 keys) usually don't have any
+ Array nodes at all. Betwen ~30 and ~400 keys map
+ objects usually have one Array node, and usually it's
+ a root node.
+ */
+
+ uint32_t jdx = map_mask(hash, shift);
+ /* 'jdx' is the index of where the new key should be added
+ in the new Array node we're about to create. */
+
+ MapNode *empty = NULL;
+ MapNode_Array *new_node = NULL;
+ MapNode *res = NULL;
+
+ /* Create a new Array node. */
+ new_node = (MapNode_Array *)map_node_array_new(n + 1);
+ if (new_node == NULL) {
+ goto fin;
+ }
+
+ /* Create an empty bitmap node for the next
+ map_node_assoc call. */
+ empty = map_node_bitmap_new(0);
+ if (empty == NULL) {
+ goto fin;
+ }
+
+ /* Make a new bitmap node for the key/val we're adding.
+ Set that bitmap node to new-array-node[jdx]. */
+ new_node->a_array[jdx] = map_node_assoc(
+ empty, shift + 5, hash, key, val, added_leaf);
+ if (new_node->a_array[jdx] == NULL) {
+ goto fin;
+ }
+
+ /* Copy existing key/value pairs from the current Bitmap
+ node to the new Array node we've just created. */
+ Py_ssize_t i, j;
+ for (i = 0, j = 0; i < HAMT_ARRAY_NODE_SIZE; i++) {
+ if (((self->b_bitmap >> i) & 1) != 0) {
+ /* Ensure we don't accidentally override `jdx` element
+ we set few lines above.
+ */
+ assert(new_node->a_array[i] == NULL);
+
+ if (self->b_array[j] == NULL) {
+ new_node->a_array[i] =
+ (MapNode *)self->b_array[j + 1];
+ Py_INCREF(new_node->a_array[i]);
+ }
+ else {
+ int32_t rehash = map_hash(self->b_array[j]);
+ if (rehash == -1) {
+ goto fin;
+ }
+
+ new_node->a_array[i] = map_node_assoc(
+ empty, shift + 5,
+ rehash,
+ self->b_array[j],
+ self->b_array[j + 1],
+ added_leaf);
+
+ if (new_node->a_array[i] == NULL) {
+ goto fin;
+ }
+ }
+ j += 2;
+ }
+ }
+
+ VALIDATE_ARRAY_NODE(new_node)
+
+ /* That's it! */
+ res = (MapNode *)new_node;
+
+ fin:
+ Py_XDECREF(empty);
+ if (res == NULL) {
+ Py_XDECREF(new_node);
+ }
+ return res;
+ }
+ else {
+ /* We have less than 16 keys at this level; let's just
+ create a new bitmap node out of this node with the
+ new key/val pair added. */
+
+ uint32_t key_idx = 2 * idx;
+ uint32_t val_idx = key_idx + 1;
+ uint32_t i;
+
+ *added_leaf = 1;
+
+ /* Allocate new Bitmap node which can have one more key/val
+ pair in addition to what we have already. */
+ MapNode_Bitmap *new_node =
+ (MapNode_Bitmap *)map_node_bitmap_new(2 * (n + 1));
+ if (new_node == NULL) {
+ return NULL;
+ }
+
+ /* Copy all keys/values that will be before the new key/value
+ we are adding. */
+ for (i = 0; i < key_idx; i++) {
+ Py_XINCREF(self->b_array[i]);
+ new_node->b_array[i] = self->b_array[i];
+ }
+
+ /* Set the new key/value to the new Bitmap node. */
+ Py_INCREF(key);
+ new_node->b_array[key_idx] = key;
+ Py_INCREF(val);
+ new_node->b_array[val_idx] = val;
+
+ /* Copy all keys/values that will be after the new key/value
+ we are adding. */
+ assert(Py_SIZE(self) >= 0 && Py_SIZE(self) <= 32);
+ for (i = key_idx; i < (uint32_t)Py_SIZE(self); i++) {
+ Py_XINCREF(self->b_array[i]);
+ new_node->b_array[i + 2] = self->b_array[i];
+ }
+
+ new_node->b_bitmap = self->b_bitmap | bit;
+ return (MapNode *)new_node;
+ }
+ }
+}
+
+static map_without_t
+map_node_bitmap_without(MapNode_Bitmap *self,
+ uint32_t shift, int32_t hash,
+ PyObject *key,
+ MapNode **new_node)
+{
+ uint32_t bit = map_bitpos(hash, shift);
+ if ((self->b_bitmap & bit) == 0) {
+ return W_NOT_FOUND;
+ }
+
+ uint32_t idx = map_bitindex(self->b_bitmap, bit);
+
+ uint32_t key_idx = 2 * idx;
+ uint32_t val_idx = key_idx + 1;
+
+ PyObject *key_or_null = self->b_array[key_idx];
+ PyObject *val_or_node = self->b_array[val_idx];
+
+ if (key_or_null == NULL) {
+ /* key == NULL means that 'value' is another tree node. */
+
+ MapNode *sub_node = NULL;
+
+ map_without_t res = map_node_without(
+ (MapNode *)val_or_node,
+ shift + 5, hash, key, &sub_node);
+
+ switch (res) {
+ case W_EMPTY:
+ /* It's impossible for us to receive a W_EMPTY here:
+
+ - Array nodes are converted to Bitmap nodes when
+ we delete 16th item from them;
+
+ - Collision nodes are converted to Bitmap when
+ there is one item in them;
+
+ - Bitmap node's without() inlines single-item
+ sub-nodes.
+
+ So in no situation we can have a single-item
+ Bitmap child of another Bitmap node.
+ */
+ abort();
+
+ case W_NEWNODE: {
+ assert(sub_node != NULL);
+
+ if (IS_BITMAP_NODE(sub_node)) {
+ MapNode_Bitmap *sub_tree = (MapNode_Bitmap *)sub_node;
+ if (map_node_bitmap_count(sub_tree) == 1 &&
+ sub_tree->b_array[0] != NULL)
+ {
+ /* A bitmap node with one key/value pair. Just
+ merge it into this node.
+
+ Note that we don't inline Bitmap nodes that
+ have a NULL key -- those nodes point to another
+ tree level, and we cannot simply move tree levels
+ up or down.
+ */
+
+ MapNode_Bitmap *clone = map_node_bitmap_clone(self);
+ if (clone == NULL) {
+ Py_DECREF(sub_node);
+ return W_ERROR;
+ }
+
+ PyObject *key = sub_tree->b_array[0];
+ PyObject *val = sub_tree->b_array[1];
+
+ Py_INCREF(key);
+ Py_XSETREF(clone->b_array[key_idx], key);
+ Py_INCREF(val);
+ Py_SETREF(clone->b_array[val_idx], val);
+
+ Py_DECREF(sub_tree);
+
+ *new_node = (MapNode *)clone;
+ return W_NEWNODE;
+ }
+ }
+
+#ifdef NDEBUG
+ /* Ensure that Collision.without implementation
+ converts to Bitmap nodes itself.
+ */
+ if (IS_COLLISION_NODE(sub_node)) {
+ assert(map_node_collision_count(
+ (MapNode_Collision*)sub_node) > 1);
+ }
+#endif
+
+ MapNode_Bitmap *clone = map_node_bitmap_clone(self);
+ if (clone == NULL) {
+ return W_ERROR;
+ }
+
+ Py_SETREF(clone->b_array[val_idx],
+ (PyObject *)sub_node); /* borrow */
+
+ *new_node = (MapNode *)clone;
+ return W_NEWNODE;
+ }
+
+ case W_ERROR:
+ case W_NOT_FOUND:
+ assert(sub_node == NULL);
+ return res;
+
+ default:
+ abort();
+ }
+ }
+ else {
+ /* We have a regular key/value pair */
+
+ int cmp = PyObject_RichCompareBool(key_or_null, key, Py_EQ);
+ if (cmp < 0) {
+ return W_ERROR;
+ }
+ if (cmp == 0) {
+ return W_NOT_FOUND;
+ }
+
+ if (map_node_bitmap_count(self) == 1) {
+ return W_EMPTY;
+ }
+
+ *new_node = (MapNode *)
+ map_node_bitmap_clone_without(self, bit);
+ if (*new_node == NULL) {
+ return W_ERROR;
+ }
+
+ return W_NEWNODE;
+ }
+}
+
+static map_find_t
+map_node_bitmap_find(MapNode_Bitmap *self,
+ uint32_t shift, int32_t hash,
+ PyObject *key, PyObject **val)
+{
+ /* Lookup a key in a Bitmap node. */
+
+ uint32_t bit = map_bitpos(hash, shift);
+ uint32_t idx;
+ uint32_t key_idx;
+ uint32_t val_idx;
+ PyObject *key_or_null;
+ PyObject *val_or_node;
+ int comp_err;
+
+ if ((self->b_bitmap & bit) == 0) {
+ return F_NOT_FOUND;
+ }
+
+ idx = map_bitindex(self->b_bitmap, bit);
+ key_idx = idx * 2;
+ val_idx = key_idx + 1;
+
+ assert(val_idx < (size_t)Py_SIZE(self));
+
+ key_or_null = self->b_array[key_idx];
+ val_or_node = self->b_array[val_idx];
+
+ if (key_or_null == NULL) {
+ /* There are a few keys that have the same hash at the current shift
+ that match our key. Dispatch the lookup further down the tree. */
+ assert(val_or_node != NULL);
+ return map_node_find((MapNode *)val_or_node,
+ shift + 5, hash, key, val);
+ }
+
+ /* We have only one key -- a potential match. Let's compare if the
+ key we are looking at is equal to the key we are looking for. */
+ assert(key != NULL);
+ comp_err = PyObject_RichCompareBool(key, key_or_null, Py_EQ);
+ if (comp_err < 0) { /* exception in __eq__ */
+ return F_ERROR;
+ }
+ if (comp_err == 1) { /* key == key_or_null */
+ *val = val_or_node;
+ return F_FOUND;
+ }
+
+ return F_NOT_FOUND;
+}
+
+static int
+map_node_bitmap_traverse(MapNode_Bitmap *self, visitproc visit, void *arg)
+{
+ /* Bitmap's tp_traverse */
+
+ Py_ssize_t i;
+
+ for (i = Py_SIZE(self); --i >= 0; ) {
+ Py_VISIT(self->b_array[i]);
+ }
+
+ return 0;
+}
+
+static void
+map_node_bitmap_dealloc(MapNode_Bitmap *self)
+{
+ /* Bitmap's tp_dealloc */
+
+ Py_ssize_t len = Py_SIZE(self);
+ Py_ssize_t i;
+
+ PyObject_GC_UnTrack(self);
+ Py_TRASHCAN_SAFE_BEGIN(self)
+
+ if (len > 0) {
+ i = len;
+ while (--i >= 0) {
+ Py_XDECREF(self->b_array[i]);
+ }
+ }
+
+ Py_TYPE(self)->tp_free((PyObject *)self);
+ Py_TRASHCAN_SAFE_END(self)
+}
+
+static int
+map_node_bitmap_dump(MapNode_Bitmap *node,
+ _PyUnicodeWriter *writer, int level)
+{
+ /* Debug build: __dump__() method implementation for Bitmap nodes. */
+
+ Py_ssize_t i;
+ PyObject *tmp1;
+ PyObject *tmp2;
+
+ if (_map_dump_ident(writer, level + 1)) {
+ goto error;
+ }
+
+ if (_map_dump_format(writer, "BitmapNode(size=%zd count=%zd ",
+ Py_SIZE(node), Py_SIZE(node) / 2))
+ {
+ goto error;
+ }
+
+ tmp1 = PyLong_FromUnsignedLong(node->b_bitmap);
+ if (tmp1 == NULL) {
+ goto error;
+ }
+ tmp2 = _PyLong_Format(tmp1, 2);
+ Py_DECREF(tmp1);
+ if (tmp2 == NULL) {
+ goto error;
+ }
+ if (_map_dump_format(writer, "bitmap=%S id=%p):\n", tmp2, node)) {
+ Py_DECREF(tmp2);
+ goto error;
+ }
+ Py_DECREF(tmp2);
+
+ for (i = 0; i < Py_SIZE(node); i += 2) {
+ PyObject *key_or_null = node->b_array[i];
+ PyObject *val_or_node = node->b_array[i + 1];
+
+ if (_map_dump_ident(writer, level + 2)) {
+ goto error;
+ }
+
+ if (key_or_null == NULL) {
+ if (_map_dump_format(writer, "NULL:\n")) {
+ goto error;
+ }
+
+ if (map_node_dump((MapNode *)val_or_node,
+ writer, level + 2))
+ {
+ goto error;
+ }
+ }
+ else {
+ if (_map_dump_format(writer, "%R: %R", key_or_null,
+ val_or_node))
+ {
+ goto error;
+ }
+ }
+
+ if (_map_dump_format(writer, "\n")) {
+ goto error;
+ }
+ }
+
+ return 0;
+error:
+ return -1;
+}
+
+
+/////////////////////////////////// Collision Node
+
+
+static MapNode *
+map_node_collision_new(int32_t hash, Py_ssize_t size)
+{
+ /* Create a new Collision node. */
+
+ MapNode_Collision *node;
+ Py_ssize_t i;
+
+ assert(size >= 4);
+ assert(size % 2 == 0);
+
+ node = PyObject_GC_NewVar(
+ MapNode_Collision, &_Map_CollisionNode_Type, size);
+ if (node == NULL) {
+ return NULL;
+ }
+
+ for (i = 0; i < size; i++) {
+ node->c_array[i] = NULL;
+ }
+
+ Py_SIZE(node) = size;
+ node->c_hash = hash;
+
+ PyObject_GC_Track(node);
+
+ return (MapNode *)node;
+}
+
+static map_find_t
+map_node_collision_find_index(MapNode_Collision *self, PyObject *key,
+ Py_ssize_t *idx)
+{
+ /* Lookup `key` in the Collision node `self`. Set the index of the
+ found key to 'idx'. */
+
+ Py_ssize_t i;
+ PyObject *el;
+
+ for (i = 0; i < Py_SIZE(self); i += 2) {
+ el = self->c_array[i];
+
+ assert(el != NULL);
+ int cmp = PyObject_RichCompareBool(key, el, Py_EQ);
+ if (cmp < 0) {
+ return F_ERROR;
+ }
+ if (cmp == 1) {
+ *idx = i;
+ return F_FOUND;
+ }
+ }
+
+ return F_NOT_FOUND;
+}
+
+static MapNode *
+map_node_collision_assoc(MapNode_Collision *self,
+ uint32_t shift, int32_t hash,
+ PyObject *key, PyObject *val, int* added_leaf)
+{
+ /* Set a new key to this level (currently a Collision node)
+ of the tree. */
+
+ if (hash == self->c_hash) {
+ /* The hash of the 'key' we are adding matches the hash of
+ other keys in this Collision node. */
+
+ Py_ssize_t key_idx = -1;
+ map_find_t found;
+ MapNode_Collision *new_node;
+ Py_ssize_t i;
+
+ /* Let's try to lookup the new 'key', maybe we already have it. */
+ found = map_node_collision_find_index(self, key, &key_idx);
+ switch (found) {
+ case F_ERROR:
+ /* Exception. */
+ return NULL;
+
+ case F_NOT_FOUND:
+ /* This is a totally new key. Clone the current node,
+ add a new key/value to the cloned node. */
+
+ new_node = (MapNode_Collision *)map_node_collision_new(
+ self->c_hash, Py_SIZE(self) + 2);
+ if (new_node == NULL) {
+ return NULL;
+ }
+
+ for (i = 0; i < Py_SIZE(self); i++) {
+ Py_INCREF(self->c_array[i]);
+ new_node->c_array[i] = self->c_array[i];
+ }
+
+ Py_INCREF(key);
+ new_node->c_array[i] = key;
+ Py_INCREF(val);
+ new_node->c_array[i + 1] = val;
+
+ *added_leaf = 1;
+ return (MapNode *)new_node;
+
+ case F_FOUND:
+ /* There's a key which is equal to the key we are adding. */
+
+ assert(key_idx >= 0);
+ assert(key_idx < Py_SIZE(self));
+ Py_ssize_t val_idx = key_idx + 1;
+
+ if (self->c_array[val_idx] == val) {
+ /* We're setting a key/value pair that's already set. */
+ Py_INCREF(self);
+ return (MapNode *)self;
+ }
+
+ /* We need to replace old value for the key
+ with a new value. Create a new Collision node.*/
+ new_node = (MapNode_Collision *)map_node_collision_new(
+ self->c_hash, Py_SIZE(self));
+ if (new_node == NULL) {
+ return NULL;
+ }
+
+ /* Copy all elements of the old node to the new one. */
+ for (i = 0; i < Py_SIZE(self); i++) {
+ Py_INCREF(self->c_array[i]);
+ new_node->c_array[i] = self->c_array[i];
+ }
+
+ /* Replace the old value with the new value for the our key. */
+ Py_DECREF(new_node->c_array[val_idx]);
+ Py_INCREF(val);
+ new_node->c_array[val_idx] = val;
+
+ return (MapNode *)new_node;
+
+ default:
+ abort();
+ }
+ }
+ else {
+ /* The hash of the new key is different from the hash that
+ all keys of this Collision node have.
+
+ Create a Bitmap node inplace with two children:
+ key/value pair that we're adding, and the Collision node
+ we're replacing on this tree level.
+ */
+
+ MapNode_Bitmap *new_node;
+ MapNode *assoc_res;
+
+ new_node = (MapNode_Bitmap *)map_node_bitmap_new(2);
+ if (new_node == NULL) {
+ return NULL;
+ }
+ new_node->b_bitmap = map_bitpos(self->c_hash, shift);
+ Py_INCREF(self);
+ new_node->b_array[1] = (PyObject*) self;
+
+ assoc_res = map_node_bitmap_assoc(
+ new_node, shift, hash, key, val, added_leaf);
+ Py_DECREF(new_node);
+ return assoc_res;
+ }
+}
+
+static inline Py_ssize_t
+map_node_collision_count(MapNode_Collision *node)
+{
+ return Py_SIZE(node) / 2;
+}
+
+static map_without_t
+map_node_collision_without(MapNode_Collision *self,
+ uint32_t shift, int32_t hash,
+ PyObject *key,
+ MapNode **new_node)
+{
+ if (hash != self->c_hash) {
+ return W_NOT_FOUND;
+ }
+
+ Py_ssize_t key_idx = -1;
+ map_find_t found = map_node_collision_find_index(self, key, &key_idx);
+
+ switch (found) {
+ case F_ERROR:
+ return W_ERROR;
+
+ case F_NOT_FOUND:
+ return W_NOT_FOUND;
+
+ case F_FOUND:
+ assert(key_idx >= 0);
+ assert(key_idx < Py_SIZE(self));
+
+ Py_ssize_t new_count = map_node_collision_count(self) - 1;
+
+ if (new_count == 0) {
+ /* The node has only one key/value pair and it's for the
+ key we're trying to delete. So a new node will be empty
+ after the removal.
+ */
+ return W_EMPTY;
+ }
+
+ if (new_count == 1) {
+ /* The node has two keys, and after deletion the
+ new Collision node would have one. Collision nodes
+ with one key shouldn't exist, so convert it to a
+ Bitmap node.
+ */
+ MapNode_Bitmap *node = (MapNode_Bitmap *)
+ map_node_bitmap_new(2);
+ if (node == NULL) {
+ return W_ERROR;
+ }
+
+ if (key_idx == 0) {
+ Py_INCREF(self->c_array[2]);
+ node->b_array[0] = self->c_array[2];
+ Py_INCREF(self->c_array[3]);
+ node->b_array[1] = self->c_array[3];
+ }
+ else {
+ assert(key_idx == 2);
+ Py_INCREF(self->c_array[0]);
+ node->b_array[0] = self->c_array[0];
+ Py_INCREF(self->c_array[1]);
+ node->b_array[1] = self->c_array[1];
+ }
+
+ node->b_bitmap = map_bitpos(hash, shift);
+
+ *new_node = (MapNode *)node;
+ return W_NEWNODE;
+ }
+
+ /* Allocate a new Collision node with capacity for one
+ less key/value pair */
+ MapNode_Collision *new = (MapNode_Collision *)
+ map_node_collision_new(
+ self->c_hash, Py_SIZE(self) - 2);
+ if (new == NULL) {
+ return W_ERROR;
+ }
+
+ /* Copy all other keys from `self` to `new` */
+ Py_ssize_t i;
+ for (i = 0; i < key_idx; i++) {
+ Py_INCREF(self->c_array[i]);
+ new->c_array[i] = self->c_array[i];
+ }
+ for (i = key_idx + 2; i < Py_SIZE(self); i++) {
+ Py_INCREF(self->c_array[i]);
+ new->c_array[i - 2] = self->c_array[i];
+ }
+
+ *new_node = (MapNode*)new;
+ return W_NEWNODE;
+
+ default:
+ abort();
+ }
+}
+
+static map_find_t
+map_node_collision_find(MapNode_Collision *self,
+ uint32_t shift, int32_t hash,
+ PyObject *key, PyObject **val)
+{
+ /* Lookup `key` in the Collision node `self`. Set the value
+ for the found key to 'val'. */
+
+ Py_ssize_t idx = -1;
+ map_find_t res;
+
+ res = map_node_collision_find_index(self, key, &idx);
+ if (res == F_ERROR || res == F_NOT_FOUND) {
+ return res;
+ }
+
+ assert(idx >= 0);
+ assert(idx + 1 < Py_SIZE(self));
+
+ *val = self->c_array[idx + 1];
+ assert(*val != NULL);
+
+ return F_FOUND;
+}
+
+
+static int
+map_node_collision_traverse(MapNode_Collision *self,
+ visitproc visit, void *arg)
+{
+ /* Collision's tp_traverse */
+
+ Py_ssize_t i;
+
+ for (i = Py_SIZE(self); --i >= 0; ) {
+ Py_VISIT(self->c_array[i]);
+ }
+
+ return 0;
+}
+
+static void
+map_node_collision_dealloc(MapNode_Collision *self)
+{
+ /* Collision's tp_dealloc */
+
+ Py_ssize_t len = Py_SIZE(self);
+
+ PyObject_GC_UnTrack(self);
+ Py_TRASHCAN_SAFE_BEGIN(self)
+
+ if (len > 0) {
+
+ while (--len >= 0) {
+ Py_XDECREF(self->c_array[len]);
+ }
+ }
+
+ Py_TYPE(self)->tp_free((PyObject *)self);
+ Py_TRASHCAN_SAFE_END(self)
+}
+
+static int
+map_node_collision_dump(MapNode_Collision *node,
+ _PyUnicodeWriter *writer, int level)
+{
+ /* Debug build: __dump__() method implementation for Collision nodes. */
+
+ Py_ssize_t i;
+
+ if (_map_dump_ident(writer, level + 1)) {
+ goto error;
+ }
+
+ if (_map_dump_format(writer, "CollisionNode(size=%zd id=%p):\n",
+ Py_SIZE(node), node))
+ {
+ goto error;
+ }
+
+ for (i = 0; i < Py_SIZE(node); i += 2) {
+ PyObject *key = node->c_array[i];
+ PyObject *val = node->c_array[i + 1];
+
+ if (_map_dump_ident(writer, level + 2)) {
+ goto error;
+ }
+
+ if (_map_dump_format(writer, "%R: %R\n", key, val)) {
+ goto error;
+ }
+ }
+
+ return 0;
+error:
+ return -1;
+}
+
+
+/////////////////////////////////// Array Node
+
+
+static MapNode *
+map_node_array_new(Py_ssize_t count)
+{
+ Py_ssize_t i;
+
+ MapNode_Array *node = PyObject_GC_New(
+ MapNode_Array, &_Map_ArrayNode_Type);
+ if (node == NULL) {
+ return NULL;
+ }
+
+ for (i = 0; i < HAMT_ARRAY_NODE_SIZE; i++) {
+ node->a_array[i] = NULL;
+ }
+
+ node->a_count = count;
+
+ PyObject_GC_Track(node);
+ return (MapNode *)node;
+}
+
+static MapNode_Array *
+map_node_array_clone(MapNode_Array *node)
+{
+ MapNode_Array *clone;
+ Py_ssize_t i;
+
+ VALIDATE_ARRAY_NODE(node)
+
+ /* Create a new Array node. */
+ clone = (MapNode_Array *)map_node_array_new(node->a_count);
+ if (clone == NULL) {
+ return NULL;
+ }
+
+ /* Copy all elements from the current Array node to the new one. */
+ for (i = 0; i < HAMT_ARRAY_NODE_SIZE; i++) {
+ Py_XINCREF(node->a_array[i]);
+ clone->a_array[i] = node->a_array[i];
+ }
+
+ VALIDATE_ARRAY_NODE(clone)
+ return clone;
+}
+
+static MapNode *
+map_node_array_assoc(MapNode_Array *self,
+ uint32_t shift, int32_t hash,
+ PyObject *key, PyObject *val, int* added_leaf)
+{
+ /* Set a new key to this level (currently a Collision node)
+ of the tree.
+
+ Array nodes don't store values, they can only point to
+ other nodes. They are simple arrays of 32 BaseNode pointers/
+ */
+
+ uint32_t idx = map_mask(hash, shift);
+ MapNode *node = self->a_array[idx];
+ MapNode *child_node;
+ MapNode_Array *new_node;
+ Py_ssize_t i;
+
+ if (node == NULL) {
+ /* There's no child node for the given hash. Create a new
+ Bitmap node for this key. */
+
+ MapNode_Bitmap *empty = NULL;
+
+ /* Get an empty Bitmap node to work with. */
+ empty = (MapNode_Bitmap *)map_node_bitmap_new(0);
+ if (empty == NULL) {
+ return NULL;
+ }
+
+ /* Set key/val to the newly created empty Bitmap, thus
+ creating a new Bitmap node with our key/value pair. */
+ child_node = map_node_bitmap_assoc(
+ empty,
+ shift + 5, hash, key, val, added_leaf);
+ Py_DECREF(empty);
+ if (child_node == NULL) {
+ return NULL;
+ }
+
+ /* Create a new Array node. */
+ new_node = (MapNode_Array *)map_node_array_new(self->a_count + 1);
+ if (new_node == NULL) {
+ Py_DECREF(child_node);
+ return NULL;
+ }
+
+ /* Copy all elements from the current Array node to the
+ new one. */
+ for (i = 0; i < HAMT_ARRAY_NODE_SIZE; i++) {
+ Py_XINCREF(self->a_array[i]);
+ new_node->a_array[i] = self->a_array[i];
+ }
+
+ assert(new_node->a_array[idx] == NULL);
+ new_node->a_array[idx] = child_node; /* borrow */
+ VALIDATE_ARRAY_NODE(new_node)
+ }
+ else {
+ /* There's a child node for the given hash.
+ Set the key to it./ */
+ child_node = map_node_assoc(
+ node, shift + 5, hash, key, val, added_leaf);
+ if (child_node == NULL) {
+ return NULL;
+ }
+ else if (child_node == (MapNode *)self) {
+ Py_DECREF(child_node);
+ return (MapNode *)self;
+ }
+
+ new_node = map_node_array_clone(self);
+ if (new_node == NULL) {
+ Py_DECREF(child_node);
+ return NULL;
+ }
+
+ Py_SETREF(new_node->a_array[idx], child_node); /* borrow */
+ VALIDATE_ARRAY_NODE(new_node)
+ }
+
+ return (MapNode *)new_node;
+}
+
+static map_without_t
+map_node_array_without(MapNode_Array *self,
+ uint32_t shift, int32_t hash,
+ PyObject *key,
+ MapNode **new_node)
+{
+ uint32_t idx = map_mask(hash, shift);
+ MapNode *node = self->a_array[idx];
+
+ if (node == NULL) {
+ return W_NOT_FOUND;
+ }
+
+ MapNode *sub_node = NULL;
+ map_without_t res = map_node_without(
+ (MapNode *)node,
+ shift + 5, hash, key, &sub_node);
+
+ switch (res) {
+ case W_NOT_FOUND:
+ case W_ERROR:
+ assert(sub_node == NULL);
+ return res;
+
+ case W_NEWNODE: {
+ /* We need to replace a node at the `idx` index.
+ Clone this node and replace.
+ */
+ assert(sub_node != NULL);
+
+ MapNode_Array *clone = map_node_array_clone(self);
+ if (clone == NULL) {
+ Py_DECREF(sub_node);
+ return W_ERROR;
+ }
+
+ Py_SETREF(clone->a_array[idx], sub_node); /* borrow */
+ *new_node = (MapNode*)clone; /* borrow */
+ return W_NEWNODE;
+ }
+
+ case W_EMPTY: {
+ assert(sub_node == NULL);
+ /* We need to remove a node at the `idx` index.
+ Calculate the size of the replacement Array node.
+ */
+ Py_ssize_t new_count = self->a_count - 1;
+
+ if (new_count == 0) {
+ return W_EMPTY;
+ }
+
+ if (new_count >= 16) {
+ /* We convert Bitmap nodes to Array nodes, when a
+ Bitmap node needs to store more than 15 key/value
+ pairs. So we will create a new Array node if we
+ the number of key/values after deletion is still
+ greater than 15.
+ */
+
+ MapNode_Array *new = map_node_array_clone(self);
+ if (new == NULL) {
+ return W_ERROR;
+ }
+ new->a_count = new_count;
+ Py_CLEAR(new->a_array[idx]);
+
+ *new_node = (MapNode*)new; /* borrow */
+ return W_NEWNODE;
+ }
+
+ /* New Array node would have less than 16 key/value
+ pairs. We need to create a replacement Bitmap node. */
+
+ Py_ssize_t bitmap_size = new_count * 2;
+ uint32_t bitmap = 0;
+
+ MapNode_Bitmap *new = (MapNode_Bitmap *)
+ map_node_bitmap_new(bitmap_size);
+ if (new == NULL) {
+ return W_ERROR;
+ }
+
+ Py_ssize_t new_i = 0;
+ for (uint32_t i = 0; i < HAMT_ARRAY_NODE_SIZE; i++) {
+ if (i == idx) {
+ /* Skip the node we are deleting. */
+ continue;
+ }
+
+ MapNode *node = self->a_array[i];
+ if (node == NULL) {
+ /* Skip any missing nodes. */
+ continue;
+ }
+
+ bitmap |= 1 << i;
+
+ if (IS_BITMAP_NODE(node)) {
+ MapNode_Bitmap *child = (MapNode_Bitmap *)node;
+
+ if (map_node_bitmap_count(child) == 1 &&
+ child->b_array[0] != NULL)
+ {
+ /* node is a Bitmap with one key/value pair, just
+ merge it into the new Bitmap node we're building.
+
+ Note that we don't inline Bitmap nodes that
+ have a NULL key -- those nodes point to another
+ tree level, and we cannot simply move tree levels
+ up or down.
+ */
+ PyObject *key = child->b_array[0];
+ PyObject *val = child->b_array[1];
+
+ Py_INCREF(key);
+ new->b_array[new_i] = key;
+ Py_INCREF(val);
+ new->b_array[new_i + 1] = val;
+ }
+ else {
+ new->b_array[new_i] = NULL;
+ Py_INCREF(node);
+ new->b_array[new_i + 1] = (PyObject*)node;
+ }
+ }
+ else {
+
+#ifdef NDEBUG
+ if (IS_COLLISION_NODE(node)) {
+ assert(
+ (map_node_collision_count(
+ (MapNode_Collision*)node)) > 1);
+ }
+ else if (IS_ARRAY_NODE(node)) {
+ assert(((MapNode_Array*)node)->a_count >= 16);
+ }
+#endif
+
+ /* Just copy the node into our new Bitmap */
+ new->b_array[new_i] = NULL;
+ Py_INCREF(node);
+ new->b_array[new_i + 1] = (PyObject*)node;
+ }
+
+ new_i += 2;
+ }
+
+ new->b_bitmap = bitmap;
+ *new_node = (MapNode*)new; /* borrow */
+ return W_NEWNODE;
+ }
+
+ default:
+ abort();
+ }
+}
+
+static map_find_t
+map_node_array_find(MapNode_Array *self,
+ uint32_t shift, int32_t hash,
+ PyObject *key, PyObject **val)
+{
+ /* Lookup `key` in the Array node `self`. Set the value
+ for the found key to 'val'. */
+
+ uint32_t idx = map_mask(hash, shift);
+ MapNode *node;
+
+ node = self->a_array[idx];
+ if (node == NULL) {
+ return F_NOT_FOUND;
+ }
+
+ /* Dispatch to the generic map_node_find */
+ return map_node_find(node, shift + 5, hash, key, val);
+}
+
+static int
+map_node_array_traverse(MapNode_Array *self,
+ visitproc visit, void *arg)
+{
+ /* Array's tp_traverse */
+
+ Py_ssize_t i;
+
+ for (i = 0; i < HAMT_ARRAY_NODE_SIZE; i++) {
+ Py_VISIT(self->a_array[i]);
+ }
+
+ return 0;
+}
+
+static void
+map_node_array_dealloc(MapNode_Array *self)
+{
+ /* Array's tp_dealloc */
+
+ Py_ssize_t i;
+
+ PyObject_GC_UnTrack(self);
+ Py_TRASHCAN_SAFE_BEGIN(self)
+
+ for (i = 0; i < HAMT_ARRAY_NODE_SIZE; i++) {
+ Py_XDECREF(self->a_array[i]);
+ }
+
+ Py_TYPE(self)->tp_free((PyObject *)self);
+ Py_TRASHCAN_SAFE_END(self)
+}
+
+static int
+map_node_array_dump(MapNode_Array *node,
+ _PyUnicodeWriter *writer, int level)
+{
+ /* Debug build: __dump__() method implementation for Array nodes. */
+
+ Py_ssize_t i;
+
+ if (_map_dump_ident(writer, level + 1)) {
+ goto error;
+ }
+
+ if (_map_dump_format(writer, "ArrayNode(id=%p):\n", node)) {
+ goto error;
+ }
+
+ for (i = 0; i < HAMT_ARRAY_NODE_SIZE; i++) {
+ if (node->a_array[i] == NULL) {
+ continue;
+ }
+
+ if (_map_dump_ident(writer, level + 2)) {
+ goto error;
+ }
+
+ if (_map_dump_format(writer, "%d::\n", i)) {
+ goto error;
+ }
+
+ if (map_node_dump(node->a_array[i], writer, level + 1)) {
+ goto error;
+ }
+
+ if (_map_dump_format(writer, "\n")) {
+ goto error;
+ }
+ }
+
+ return 0;
+error:
+ return -1;
+}
+
+
+/////////////////////////////////// Node Dispatch
+
+
+static MapNode *
+map_node_assoc(MapNode *node,
+ uint32_t shift, int32_t hash,
+ PyObject *key, PyObject *val, int* added_leaf)
+{
+ /* Set key/value to the 'node' starting with the given shift/hash.
+ Return a new node, or the same node if key/value already
+ set.
+
+ added_leaf will be set to 1 if key/value wasn't in the
+ tree before.
+
+ This method automatically dispatches to the suitable
+ map_node_{nodetype}_assoc method.
+ */
+
+ if (IS_BITMAP_NODE(node)) {
+ return map_node_bitmap_assoc(
+ (MapNode_Bitmap *)node,
+ shift, hash, key, val, added_leaf);
+ }
+ else if (IS_ARRAY_NODE(node)) {
+ return map_node_array_assoc(
+ (MapNode_Array *)node,
+ shift, hash, key, val, added_leaf);
+ }
+ else {
+ assert(IS_COLLISION_NODE(node));
+ return map_node_collision_assoc(
+ (MapNode_Collision *)node,
+ shift, hash, key, val, added_leaf);
+ }
+}
+
+static map_without_t
+map_node_without(MapNode *node,
+ uint32_t shift, int32_t hash,
+ PyObject *key,
+ MapNode **new_node)
+{
+ if (IS_BITMAP_NODE(node)) {
+ return map_node_bitmap_without(
+ (MapNode_Bitmap *)node,
+ shift, hash, key,
+ new_node);
+ }
+ else if (IS_ARRAY_NODE(node)) {
+ return map_node_array_without(
+ (MapNode_Array *)node,
+ shift, hash, key,
+ new_node);
+ }
+ else {
+ assert(IS_COLLISION_NODE(node));
+ return map_node_collision_without(
+ (MapNode_Collision *)node,
+ shift, hash, key,
+ new_node);
+ }
+}
+
+static map_find_t
+map_node_find(MapNode *node,
+ uint32_t shift, int32_t hash,
+ PyObject *key, PyObject **val)
+{
+ /* Find the key in the node starting with the given shift/hash.
+
+ If a value is found, the result will be set to F_FOUND, and
+ *val will point to the found value object.
+
+ If a value wasn't found, the result will be set to F_NOT_FOUND.
+
+ If an exception occurs during the call, the result will be F_ERROR.
+
+ This method automatically dispatches to the suitable
+ map_node_{nodetype}_find method.
+ */
+
+ if (IS_BITMAP_NODE(node)) {
+ return map_node_bitmap_find(
+ (MapNode_Bitmap *)node,
+ shift, hash, key, val);
+
+ }
+ else if (IS_ARRAY_NODE(node)) {
+ return map_node_array_find(
+ (MapNode_Array *)node,
+ shift, hash, key, val);
+ }
+ else {
+ assert(IS_COLLISION_NODE(node));
+ return map_node_collision_find(
+ (MapNode_Collision *)node,
+ shift, hash, key, val);
+ }
+}
+
+static int
+map_node_dump(MapNode *node,
+ _PyUnicodeWriter *writer, int level)
+{
+ /* Debug build: __dump__() method implementation for a node.
+
+ This method automatically dispatches to the suitable
+ map_node_{nodetype})_dump method.
+ */
+
+ if (IS_BITMAP_NODE(node)) {
+ return map_node_bitmap_dump(
+ (MapNode_Bitmap *)node, writer, level);
+ }
+ else if (IS_ARRAY_NODE(node)) {
+ return map_node_array_dump(
+ (MapNode_Array *)node, writer, level);
+ }
+ else {
+ assert(IS_COLLISION_NODE(node));
+ return map_node_collision_dump(
+ (MapNode_Collision *)node, writer, level);
+ }
+}
+
+
+/////////////////////////////////// Iterators: Machinery
+
+
+static map_iter_t
+map_iterator_next(MapIteratorState *iter, PyObject **key, PyObject **val);
+
+
+static void
+map_iterator_init(MapIteratorState *iter, MapNode *root)
+{
+ for (uint32_t i = 0; i < _Py_HAMT_MAX_TREE_DEPTH; i++) {
+ iter->i_nodes[i] = NULL;
+ iter->i_pos[i] = 0;
+ }
+
+ iter->i_level = 0;
+
+ /* Note: we don't incref/decref nodes in i_nodes. */
+ iter->i_nodes[0] = root;
+}
+
+static map_iter_t
+map_iterator_bitmap_next(MapIteratorState *iter,
+ PyObject **key, PyObject **val)
+{
+ int8_t level = iter->i_level;
+
+ MapNode_Bitmap *node = (MapNode_Bitmap *)(iter->i_nodes[level]);
+ Py_ssize_t pos = iter->i_pos[level];
+
+ if (pos + 1 >= Py_SIZE(node)) {
+#ifdef NDEBUG
+ assert(iter->i_level >= 0);
+ iter->i_nodes[iter->i_level] = NULL;
+#endif
+ iter->i_level--;
+ return map_iterator_next(iter, key, val);
+ }
+
+ if (node->b_array[pos] == NULL) {
+ iter->i_pos[level] = pos + 2;
+
+ int8_t next_level = level + 1;
+ assert(next_level < _Py_HAMT_MAX_TREE_DEPTH);
+ iter->i_level = next_level;
+ iter->i_pos[next_level] = 0;
+ iter->i_nodes[next_level] = (MapNode *)
+ node->b_array[pos + 1];
+
+ return map_iterator_next(iter, key, val);
+ }
+
+ *key = node->b_array[pos];
+ *val = node->b_array[pos + 1];
+ iter->i_pos[level] = pos + 2;
+ return I_ITEM;
+}
+
+static map_iter_t
+map_iterator_collision_next(MapIteratorState *iter,
+ PyObject **key, PyObject **val)
+{
+ int8_t level = iter->i_level;
+
+ MapNode_Collision *node = (MapNode_Collision *)(iter->i_nodes[level]);
+ Py_ssize_t pos = iter->i_pos[level];
+
+ if (pos + 1 >= Py_SIZE(node)) {
+#ifdef NDEBUG
+ assert(iter->i_level >= 0);
+ iter->i_nodes[iter->i_level] = NULL;
+#endif
+ iter->i_level--;
+ return map_iterator_next(iter, key, val);
+ }
+
+ *key = node->c_array[pos];
+ *val = node->c_array[pos + 1];
+ iter->i_pos[level] = pos + 2;
+ return I_ITEM;
+}
+
+static map_iter_t
+map_iterator_array_next(MapIteratorState *iter,
+ PyObject **key, PyObject **val)
+{
+ int8_t level = iter->i_level;
+
+ MapNode_Array *node = (MapNode_Array *)(iter->i_nodes[level]);
+ Py_ssize_t pos = iter->i_pos[level];
+
+ if (pos >= HAMT_ARRAY_NODE_SIZE) {
+#ifdef NDEBUG
+ assert(iter->i_level >= 0);
+ iter->i_nodes[iter->i_level] = NULL;
+#endif
+ iter->i_level--;
+ return map_iterator_next(iter, key, val);
+ }
+
+ for (Py_ssize_t i = pos; i < HAMT_ARRAY_NODE_SIZE; i++) {
+ if (node->a_array[i] != NULL) {
+ iter->i_pos[level] = i + 1;
+
+ int8_t next_level = level + 1;
+ assert(next_level < _Py_HAMT_MAX_TREE_DEPTH);
+ iter->i_pos[next_level] = 0;
+ iter->i_nodes[next_level] = node->a_array[i];
+ iter->i_level = next_level;
+
+ return map_iterator_next(iter, key, val);
+ }
+ }
+
+#ifdef NDEBUG
+ assert(iter->i_level >= 0);
+ iter->i_nodes[iter->i_level] = NULL;
+#endif
+
+ iter->i_level--;
+ return map_iterator_next(iter, key, val);
+}
+
+static map_iter_t
+map_iterator_next(MapIteratorState *iter, PyObject **key, PyObject **val)
+{
+ if (iter->i_level < 0) {
+ return I_END;
+ }
+
+ assert(iter->i_level < _Py_HAMT_MAX_TREE_DEPTH);
+
+ MapNode *current = iter->i_nodes[iter->i_level];
+
+ if (IS_BITMAP_NODE(current)) {
+ return map_iterator_bitmap_next(iter, key, val);
+ }
+ else if (IS_ARRAY_NODE(current)) {
+ return map_iterator_array_next(iter, key, val);
+ }
+ else {
+ assert(IS_COLLISION_NODE(current));
+ return map_iterator_collision_next(iter, key, val);
+ }
+}
+
+
+/////////////////////////////////// HAMT high-level functions
+
+
+static MapObject *
+map_assoc(MapObject *o, PyObject *key, PyObject *val)
+{
+ int32_t key_hash;
+ int added_leaf = 0;
+ MapNode *new_root;
+ MapObject *new_o;
+
+ key_hash = map_hash(key);
+ if (key_hash == -1) {
+ return NULL;
+ }
+
+ new_root = map_node_assoc(
+ (MapNode *)(o->h_root),
+ 0, key_hash, key, val, &added_leaf);
+ if (new_root == NULL) {
+ return NULL;
+ }
+
+ if (new_root == o->h_root) {
+ Py_DECREF(new_root);
+ Py_INCREF(o);
+ return o;
+ }
+
+ new_o = map_alloc();
+ if (new_o == NULL) {
+ Py_DECREF(new_root);
+ return NULL;
+ }
+
+ new_o->h_root = new_root; /* borrow */
+ new_o->h_count = added_leaf ? o->h_count + 1 : o->h_count;
+
+ return new_o;
+}
+
+static MapObject *
+map_without(MapObject *o, PyObject *key)
+{
+ int32_t key_hash = map_hash(key);
+ if (key_hash == -1) {
+ return NULL;
+ }
+
+ MapNode *new_root;
+
+ map_without_t res = map_node_without(
+ (MapNode *)(o->h_root),
+ 0, key_hash, key,
+ &new_root);
+
+ switch (res) {
+ case W_ERROR:
+ return NULL;
+ case W_EMPTY:
+ return map_new();
+ case W_NOT_FOUND:
+ Py_INCREF(o);
+ return o;
+ case W_NEWNODE: {
+ assert(new_root != NULL);
+
+ MapObject *new_o = map_alloc();
+ if (new_o == NULL) {
+ Py_DECREF(new_root);
+ return NULL;
+ }
+
+ new_o->h_root = new_root; /* borrow */
+ new_o->h_count = o->h_count - 1;
+ assert(new_o->h_count >= 0);
+ return new_o;
+ }
+ default:
+ abort();
+ }
+}
+
+static map_find_t
+map_find(MapObject *o, PyObject *key, PyObject **val)
+{
+ if (o->h_count == 0) {
+ return F_NOT_FOUND;
+ }
+
+ int32_t key_hash = map_hash(key);
+ if (key_hash == -1) {
+ return F_ERROR;
+ }
+
+ return map_node_find(o->h_root, 0, key_hash, key, val);
+}
+
+static int
+map_eq(MapObject *v, MapObject *w)
+{
+ if (v == w) {
+ return 1;
+ }
+
+ if (v->h_count != w->h_count) {
+ return 0;
+ }
+
+ MapIteratorState iter;
+ map_iter_t iter_res;
+ map_find_t find_res;
+ PyObject *v_key;
+ PyObject *v_val;
+ PyObject *w_val;
+
+ map_iterator_init(&iter, v->h_root);
+
+ do {
+ iter_res = map_iterator_next(&iter, &v_key, &v_val);
+ if (iter_res == I_ITEM) {
+ find_res = map_find(w, v_key, &w_val);
+ switch (find_res) {
+ case F_ERROR:
+ return -1;
+
+ case F_NOT_FOUND:
+ return 0;
+
+ case F_FOUND: {
+ int cmp = PyObject_RichCompareBool(v_val, w_val, Py_EQ);
+ if (cmp < 0) {
+ return -1;
+ }
+ if (cmp == 0) {
+ return 0;
+ }
+ }
+ }
+ }
+ } while (iter_res != I_END);
+
+ return 1;
+}
+
+static Py_ssize_t
+map_len(MapObject *o)
+{
+ return o->h_count;
+}
+
+static MapObject *
+map_alloc(void)
+{
+ MapObject *o;
+ o = PyObject_GC_New(MapObject, &_Map_Type);
+ if (o == NULL) {
+ return NULL;
+ }
+ o->h_weakreflist = NULL;
+ PyObject_GC_Track(o);
+ return o;
+}
+
+static MapObject *
+map_new(void)
+{
+ if (_empty_map != NULL) {
+ /* HAMT is an immutable object so we can easily cache an
+ empty instance. */
+ Py_INCREF(_empty_map);
+ return _empty_map;
+ }
+
+ MapObject *o = map_alloc();
+ if (o == NULL) {
+ return NULL;
+ }
+
+ o->h_root = map_node_bitmap_new(0);
+ if (o->h_root == NULL) {
+ Py_DECREF(o);
+ return NULL;
+ }
+
+ o->h_count = 0;
+
+ if (_empty_map == NULL) {
+ Py_INCREF(o);
+ _empty_map = o;
+ }
+
+ return o;
+}
+
+static PyObject *
+map_dump(MapObject *self)
+{
+ _PyUnicodeWriter writer;
+
+ _PyUnicodeWriter_Init(&writer);
+
+ if (_map_dump_format(&writer, "HAMT(len=%zd):\n", self->h_count)) {
+ goto error;
+ }
+
+ if (map_node_dump(self->h_root, &writer, 0)) {
+ goto error;
+ }
+
+ return _PyUnicodeWriter_Finish(&writer);
+
+error:
+ _PyUnicodeWriter_Dealloc(&writer);
+ return NULL;
+}
+
+
+/////////////////////////////////// Iterators: Shared Iterator Implementation
+
+
+static int
+map_baseiter_tp_clear(MapIterator *it)
+{
+ Py_CLEAR(it->hi_obj);
+ return 0;
+}
+
+static void
+map_baseiter_tp_dealloc(MapIterator *it)
+{
+ PyObject_GC_UnTrack(it);
+ (void)map_baseiter_tp_clear(it);
+ PyObject_GC_Del(it);
+}
+
+static int
+map_baseiter_tp_traverse(MapIterator *it, visitproc visit, void *arg)
+{
+ Py_VISIT(it->hi_obj);
+ return 0;
+}
+
+static PyObject *
+map_baseiter_tp_iternext(MapIterator *it)
+{
+ PyObject *key;
+ PyObject *val;
+ map_iter_t res = map_iterator_next(&it->hi_iter, &key, &val);
+
+ switch (res) {
+ case I_END:
+ PyErr_SetNone(PyExc_StopIteration);
+ return NULL;
+
+ case I_ITEM: {
+ return (*(it->hi_yield))(key, val);
+ }
+
+ default: {
+ abort();
+ }
+ }
+}
+
+static Py_ssize_t
+map_baseiter_tp_len(MapIterator *it)
+{
+ return it->hi_obj->h_count;
+}
+
+static PyMappingMethods MapIterator_as_mapping = {
+ (lenfunc)map_baseiter_tp_len,
+};
+
+static PyObject *
+map_baseiter_new(PyTypeObject *type, binaryfunc yield, MapObject *o)
+{
+ MapIterator *it = PyObject_GC_New(MapIterator, type);
+ if (it == NULL) {
+ return NULL;
+ }
+
+ Py_INCREF(o);
+ it->hi_obj = o;
+ it->hi_yield = yield;
+
+ map_iterator_init(&it->hi_iter, o->h_root);
+
+ return (PyObject*)it;
+}
+
+#define ITERATOR_TYPE_SHARED_SLOTS \
+ .tp_basicsize = sizeof(MapIterator), \
+ .tp_itemsize = 0, \
+ .tp_as_mapping = &MapIterator_as_mapping, \
+ .tp_dealloc = (destructor)map_baseiter_tp_dealloc, \
+ .tp_getattro = PyObject_GenericGetAttr, \
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, \
+ .tp_traverse = (traverseproc)map_baseiter_tp_traverse, \
+ .tp_clear = (inquiry)map_baseiter_tp_clear, \
+ .tp_iter = PyObject_SelfIter, \
+ .tp_iternext = (iternextfunc)map_baseiter_tp_iternext,
+
+
+/////////////////////////////////// _MapItems_Type
+
+
+PyTypeObject _MapItems_Type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "items",
+ ITERATOR_TYPE_SHARED_SLOTS
+};
+
+static PyObject *
+map_iter_yield_items(PyObject *key, PyObject *val)
+{
+ return PyTuple_Pack(2, key, val);
+}
+
+static PyObject *
+map_new_iteritems(MapObject *o)
+{
+ return map_baseiter_new(
+ &_MapItems_Type, map_iter_yield_items, o);
+}
+
+
+/////////////////////////////////// _MapKeys_Type
+
+
+PyTypeObject _MapKeys_Type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "keys",
+ ITERATOR_TYPE_SHARED_SLOTS
+};
+
+static PyObject *
+map_iter_yield_keys(PyObject *key, PyObject *val)
+{
+ Py_INCREF(key);
+ return key;
+}
+
+static PyObject *
+map_new_iterkeys(MapObject *o)
+{
+ return map_baseiter_new(
+ &_MapKeys_Type, map_iter_yield_keys, o);
+}
+
+
+/////////////////////////////////// _MapValues_Type
+
+
+PyTypeObject _MapValues_Type = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "values",
+ ITERATOR_TYPE_SHARED_SLOTS
+};
+
+static PyObject *
+map_iter_yield_values(PyObject *key, PyObject *val)
+{
+ Py_INCREF(val);
+ return val;
+}
+
+static PyObject *
+map_new_itervalues(MapObject *o)
+{
+ return map_baseiter_new(
+ &_MapValues_Type, map_iter_yield_values, o);
+}
+
+
+/////////////////////////////////// _Map_Type
+
+
+static PyObject *
+map_dump(MapObject *self);
+
+
+static PyObject *
+map_tp_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ return (PyObject*)map_new();
+}
+
+static int
+map_tp_clear(MapObject *self)
+{
+ Py_CLEAR(self->h_root);
+ return 0;
+}
+
+
+static int
+map_tp_traverse(MapObject *self, visitproc visit, void *arg)
+{
+ Py_VISIT(self->h_root);
+ return 0;
+}
+
+static void
+map_tp_dealloc(MapObject *self)
+{
+ PyObject_GC_UnTrack(self);
+ if (self->h_weakreflist != NULL) {
+ PyObject_ClearWeakRefs((PyObject*)self);
+ }
+ (void)map_tp_clear(self);
+ Py_TYPE(self)->tp_free(self);
+}
+
+
+static PyObject *
+map_tp_richcompare(PyObject *v, PyObject *w, int op)
+{
+ if (!Map_Check(v) || !Map_Check(w) || (op != Py_EQ && op != Py_NE)) {
+ Py_RETURN_NOTIMPLEMENTED;
+ }
+
+ int res = map_eq((MapObject *)v, (MapObject *)w);
+ if (res < 0) {
+ return NULL;
+ }
+
+ if (op == Py_NE) {
+ res = !res;
+ }
+
+ if (res) {
+ Py_RETURN_TRUE;
+ }
+ else {
+ Py_RETURN_FALSE;
+ }
+}
+
+static int
+map_tp_contains(MapObject *self, PyObject *key)
+{
+ PyObject *val;
+ map_find_t res = map_find(self, key, &val);
+ switch (res) {
+ case F_ERROR:
+ return -1;
+ case F_NOT_FOUND:
+ return 0;
+ case F_FOUND:
+ return 1;
+ default:
+ abort();
+ }
+}
+
+static PyObject *
+map_tp_subscript(MapObject *self, PyObject *key)
+{
+ PyObject *val;
+ map_find_t res = map_find(self, key, &val);
+ switch (res) {
+ case F_ERROR:
+ return NULL;
+ case F_FOUND:
+ Py_INCREF(val);
+ return val;
+ case F_NOT_FOUND:
+ PyErr_SetObject(PyExc_KeyError, key);
+ return NULL;
+ default:
+ abort();
+ }
+}
+
+static Py_ssize_t
+map_tp_len(MapObject *self)
+{
+ return map_len(self);
+}
+
+static PyObject *
+map_tp_iter(MapObject *self)
+{
+ return map_new_iterkeys(self);
+}
+
+static PyObject *
+map_py_set(MapObject *self, PyObject *args)
+{
+ PyObject *key;
+ PyObject *val;
+
+ if (!PyArg_UnpackTuple(args, "set", 2, 2, &key, &val)) {
+ return NULL;
+ }
+
+ return (PyObject *)map_assoc(self, key, val);
+}
+
+static PyObject *
+map_py_get(MapObject *self, PyObject *args)
+{
+ PyObject *key;
+ PyObject *def = NULL;
+
+ if (!PyArg_UnpackTuple(args, "get", 1, 2, &key, &def)) {
+ return NULL;
+ }
+
+ PyObject *val = NULL;
+ map_find_t res = map_find(self, key, &val);
+ switch (res) {
+ case F_ERROR:
+ return NULL;
+ case F_FOUND:
+ Py_INCREF(val);
+ return val;
+ case F_NOT_FOUND:
+ if (def == NULL) {
+ Py_RETURN_NONE;
+ }
+ Py_INCREF(def);
+ return def;
+ default:
+ abort();
+ }
+}
+
+static PyObject *
+map_py_delete(MapObject *self, PyObject *key)
+{
+ return (PyObject *)map_without(self, key);
+}
+
+static PyObject *
+map_py_items(MapObject *self, PyObject *args)
+{
+ return map_new_iteritems(self);
+}
+
+static PyObject *
+map_py_values(MapObject *self, PyObject *args)
+{
+ return map_new_itervalues(self);
+}
+
+static PyObject *
+map_py_keys(MapObject *self, PyObject *args)
+{
+ return map_new_iterkeys(self);
+}
+
+static PyObject *
+map_py_dump(MapObject *self, PyObject *args)
+{
+ return map_dump(self);
+}
+
+
+static PyMethodDef Map_methods[] = {
+ {"set", (PyCFunction)map_py_set, METH_VARARGS, NULL},
+ {"get", (PyCFunction)map_py_get, METH_VARARGS, NULL},
+ {"delete", (PyCFunction)map_py_delete, METH_O, NULL},
+ {"items", (PyCFunction)map_py_items, METH_NOARGS, NULL},
+ {"keys", (PyCFunction)map_py_keys, METH_NOARGS, NULL},
+ {"values", (PyCFunction)map_py_values, METH_NOARGS, NULL},
+ {"__dump__", (PyCFunction)map_py_dump, METH_NOARGS, NULL},
+ {NULL, NULL}
+};
+
+static PySequenceMethods Map_as_sequence = {
+ 0, /* sq_length */
+ 0, /* sq_concat */
+ 0, /* sq_repeat */
+ 0, /* sq_item */
+ 0, /* sq_slice */
+ 0, /* sq_ass_item */
+ 0, /* sq_ass_slice */
+ (objobjproc)map_tp_contains, /* sq_contains */
+ 0, /* sq_inplace_concat */
+ 0, /* sq_inplace_repeat */
+};
+
+static PyMappingMethods Map_as_mapping = {
+ (lenfunc)map_tp_len, /* mp_length */
+ (binaryfunc)map_tp_subscript, /* mp_subscript */
+};
+
+PyTypeObject _Map_Type = {
+ PyVarObject_HEAD_INIT(&PyType_Type, 0)
+ "map",
+ sizeof(MapObject),
+ .tp_methods = Map_methods,
+ .tp_as_mapping = &Map_as_mapping,
+ .tp_as_sequence = &Map_as_sequence,
+ .tp_iter = (getiterfunc)map_tp_iter,
+ .tp_dealloc = (destructor)map_tp_dealloc,
+ .tp_getattro = PyObject_GenericGetAttr,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
+ .tp_richcompare = map_tp_richcompare,
+ .tp_traverse = (traverseproc)map_tp_traverse,
+ .tp_clear = (inquiry)map_tp_clear,
+ .tp_new = map_tp_new,
+ .tp_weaklistoffset = offsetof(MapObject, h_weakreflist),
+ .tp_hash = PyObject_HashNotImplemented,
+};
+
+
+/////////////////////////////////// Tree Node Types
+
+
+PyTypeObject _Map_ArrayNode_Type = {
+ PyVarObject_HEAD_INIT(&PyType_Type, 0)
+ "map_array_node",
+ sizeof(MapNode_Array),
+ 0,
+ .tp_dealloc = (destructor)map_node_array_dealloc,
+ .tp_getattro = PyObject_GenericGetAttr,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
+ .tp_traverse = (traverseproc)map_node_array_traverse,
+ .tp_free = PyObject_GC_Del,
+ .tp_hash = PyObject_HashNotImplemented,
+};
+
+PyTypeObject _Map_BitmapNode_Type = {
+ PyVarObject_HEAD_INIT(&PyType_Type, 0)
+ "map_bitmap_node",
+ sizeof(MapNode_Bitmap) - sizeof(PyObject *),
+ sizeof(PyObject *),
+ .tp_dealloc = (destructor)map_node_bitmap_dealloc,
+ .tp_getattro = PyObject_GenericGetAttr,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
+ .tp_traverse = (traverseproc)map_node_bitmap_traverse,
+ .tp_free = PyObject_GC_Del,
+ .tp_hash = PyObject_HashNotImplemented,
+};
+
+PyTypeObject _Map_CollisionNode_Type = {
+ PyVarObject_HEAD_INIT(&PyType_Type, 0)
+ "map_collision_node",
+ sizeof(MapNode_Collision) - sizeof(PyObject *),
+ sizeof(PyObject *),
+ .tp_dealloc = (destructor)map_node_collision_dealloc,
+ .tp_getattro = PyObject_GenericGetAttr,
+ .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
+ .tp_traverse = (traverseproc)map_node_collision_traverse,
+ .tp_free = PyObject_GC_Del,
+ .tp_hash = PyObject_HashNotImplemented,
+};
+
+
+static void
+module_free(void *m)
+{
+ Py_CLEAR(_empty_map);
+ Py_CLEAR(_empty_bitmap_node);
+}
+
+
+static struct PyModuleDef _mapmodule = {
+ PyModuleDef_HEAD_INIT, /* m_base */
+ "_map" , /* m_name */
+ NULL, /* m_doc */
+ -1, /* m_size */
+ NULL, /* m_methods */
+ NULL, /* m_slots */
+ NULL, /* m_traverse */
+ NULL, /* m_clear */
+ module_free, /* m_free */
+};
+
+
+PyMODINIT_FUNC
+PyInit__map(void)
+{
+ PyObject *m = PyModule_Create(&_mapmodule);
+
+ if ((PyType_Ready(&_Map_Type) < 0) ||
+ (PyType_Ready(&_Map_ArrayNode_Type) < 0) ||
+ (PyType_Ready(&_Map_BitmapNode_Type) < 0) ||
+ (PyType_Ready(&_Map_CollisionNode_Type) < 0) ||
+ (PyType_Ready(&_MapKeys_Type) < 0) ||
+ (PyType_Ready(&_MapValues_Type) < 0) ||
+ (PyType_Ready(&_MapItems_Type) < 0))
+ {
+ return 0;
+ }
+
+ Py_INCREF(&_Map_Type);
+ if (PyModule_AddObject(m, "Map",
+ (PyObject *)&_Map_Type) < 0)
+ {
+ Py_DECREF(&_Map_Type);
+ return NULL;
+ }
+
+ return m;
+}
diff --git a/immutables/_map.h b/immutables/_map.h
new file mode 100644
index 0000000..20999d7
--- /dev/null
+++ b/immutables/_map.h
@@ -0,0 +1,71 @@
+#ifndef IMMUTABLES_MAP_H
+#define IMMUTABLES_MAP_H
+
+#include "Python.h"
+
+#define _Py_HAMT_MAX_TREE_DEPTH 7
+
+
+#define Map_Check(o) (Py_TYPE(o) == &_Map_Type)
+
+
+/* Abstract tree node. */
+typedef struct {
+ PyObject_HEAD
+} MapNode;
+
+
+/* An HAMT immutable mapping collection. */
+typedef struct {
+ PyObject_HEAD
+ MapNode *h_root;
+ PyObject *h_weakreflist;
+ Py_ssize_t h_count;
+} MapObject;
+
+
+/* A struct to hold the state of depth-first traverse of the tree.
+
+ HAMT is an immutable collection. Iterators will hold a strong reference
+ to it, and every node in the HAMT has strong references to its children.
+
+ So for iterators, we can implement zero allocations and zero reference
+ inc/dec depth-first iteration.
+
+ - i_nodes: an array of seven pointers to tree nodes
+ - i_level: the current node in i_nodes
+ - i_pos: an array of positions within nodes in i_nodes.
+*/
+typedef struct {
+ MapNode *i_nodes[_Py_HAMT_MAX_TREE_DEPTH];
+ Py_ssize_t i_pos[_Py_HAMT_MAX_TREE_DEPTH];
+ int8_t i_level;
+} MapIteratorState;
+
+
+/* Base iterator object.
+
+ Contains the iteration state, a pointer to the HAMT tree,
+ and a pointer to the 'yield function'. The latter is a simple
+ function that returns a key/value tuple for the 'Items' iterator,
+ just a key for the 'Keys' iterator, and a value for the 'Values'
+ iterator.
+*/
+typedef struct {
+ PyObject_HEAD
+ MapObject *hi_obj;
+ MapIteratorState hi_iter;
+ binaryfunc hi_yield;
+} MapIterator;
+
+
+PyAPI_DATA(PyTypeObject) _Map_Type;
+PyAPI_DATA(PyTypeObject) _Map_ArrayNode_Type;
+PyAPI_DATA(PyTypeObject) _Map_BitmapNode_Type;
+PyAPI_DATA(PyTypeObject) _Map_CollisionNode_Type;
+PyAPI_DATA(PyTypeObject) _MapKeys_Type;
+PyAPI_DATA(PyTypeObject) _MapValues_Type;
+PyAPI_DATA(PyTypeObject) _MapItems_Type;
+
+
+#endif
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..4603e34
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+addopts = --capture=no --assert=plain --strict --tb native
+testpaths = tests
+filterwarnings = default
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..a190cd4
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,38 @@
+import platform
+import setuptools
+
+
+CFLAGS = ['-O2']
+
+if platform.uname().system != 'Windows':
+ CFLAGS.extend(['-fsigned-char', '-Wall', '-Wsign-compare', '-Wconversion'])
+
+
+setuptools.setup(
+ name='immutables',
+ description='Immutable Collections',
+ classifiers=[
+ 'License :: OSI Approved :: Apache Software License',
+ 'Intended Audience :: Developers',
+ 'Programming Language :: Python :: 3 :: Only',
+ 'Programming Language :: Python :: 3.6',
+ 'Operating System :: POSIX',
+ 'Operating System :: MacOS :: MacOS X',
+ 'Operating System :: Microsoft :: Windows',
+ ],
+ platforms=['POSIX'],
+ author='MagicStack Inc',
+ author_email='hello@magic.io',
+ url='https://github.com/MagicStack/immutables',
+ license='Apache License, Version 2.0',
+ packages=['immutables'],
+ provides=['immutables'],
+ include_package_data=True,
+ ext_modules=[
+ setuptools.Extension(
+ "immutables._map",
+ ["immutables/_map.c"],
+ extra_compile_args=CFLAGS)
+ ],
+ test_suite='tests.suite',
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..fa393d9
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,14 @@
+import sys
+import unittest
+
+
+def suite():
+ test_loader = unittest.TestLoader()
+ test_suite = test_loader.discover('.', pattern='test_*.py')
+ return test_suite
+
+
+if __name__ == '__main__':
+ runner = unittest.runner.TextTestRunner()
+ result = runner.run(suite())
+ sys.exit(not result.wasSuccessful())
diff --git a/tests/test_map.py b/tests/test_map.py
new file mode 100644
index 0000000..fdb6963
--- /dev/null
+++ b/tests/test_map.py
@@ -0,0 +1,710 @@
+import gc
+import random
+import unittest
+import weakref
+
+from immutables import Map
+
+
+class HashKey:
+ _crasher = None
+
+ def __init__(self, hash, name, *, error_on_eq_to=None):
+ assert hash != -1
+ self.name = name
+ self.hash = hash
+ self.error_on_eq_to = error_on_eq_to
+
+ def __repr__(self):
+ return f'<Key name:{self.name} hash:{self.hash}>'
+
+ def __hash__(self):
+ if self._crasher is not None and self._crasher.error_on_hash:
+ raise HashingError
+
+ return self.hash
+
+ def __eq__(self, other):
+ if not isinstance(other, HashKey):
+ return NotImplemented
+
+ if self._crasher is not None and self._crasher.error_on_eq:
+ raise EqError
+
+ if self.error_on_eq_to is not None and self.error_on_eq_to is other:
+ raise ValueError(f'cannot compare {self!r} to {other!r}')
+ if other.error_on_eq_to is not None and other.error_on_eq_to is self:
+ raise ValueError(f'cannot compare {other!r} to {self!r}')
+
+ return (self.name, self.hash) == (other.name, other.hash)
+
+
+class KeyStr(str):
+
+ def __hash__(self):
+ if HashKey._crasher is not None and HashKey._crasher.error_on_hash:
+ raise HashingError
+ return super().__hash__()
+
+ def __eq__(self, other):
+ if HashKey._crasher is not None and HashKey._crasher.error_on_eq:
+ raise EqError
+ return super().__eq__(other)
+
+
+class HaskKeyCrasher:
+
+ def __init__(self, *, error_on_hash=False, error_on_eq=False):
+ self.error_on_hash = error_on_hash
+ self.error_on_eq = error_on_eq
+
+ def __enter__(self):
+ if HashKey._crasher is not None:
+ raise RuntimeError('cannot nest crashers')
+ HashKey._crasher = self
+
+ def __exit__(self, *exc):
+ HashKey._crasher = None
+
+
+class HashingError(Exception):
+ pass
+
+
+class EqError(Exception):
+ pass
+
+
+class MapTest(unittest.TestCase):
+
+ def test_hashkey_helper_1(self):
+ k1 = HashKey(10, 'aaa')
+ k2 = HashKey(10, 'bbb')
+
+ self.assertNotEqual(k1, k2)
+ self.assertEqual(hash(k1), hash(k2))
+
+ d = dict()
+ d[k1] = 'a'
+ d[k2] = 'b'
+
+ self.assertEqual(d[k1], 'a')
+ self.assertEqual(d[k2], 'b')
+
+ def test_map_basics_1(self):
+ h = Map()
+ h = None # NoQA
+
+ def test_map_basics_2(self):
+ h = Map()
+ self.assertEqual(len(h), 0)
+
+ h2 = h.set('a', 'b')
+ self.assertIsNot(h, h2)
+ self.assertEqual(len(h), 0)
+ self.assertEqual(len(h2), 1)
+
+ self.assertIsNone(h.get('a'))
+ self.assertEqual(h.get('a', 42), 42)
+
+ self.assertEqual(h2.get('a'), 'b')
+
+ h3 = h2.set('b', 10)
+ self.assertIsNot(h2, h3)
+ self.assertEqual(len(h), 0)
+ self.assertEqual(len(h2), 1)
+ self.assertEqual(len(h3), 2)
+ self.assertEqual(h3.get('a'), 'b')
+ self.assertEqual(h3.get('b'), 10)
+
+ self.assertIsNone(h.get('b'))
+ self.assertIsNone(h2.get('b'))
+
+ self.assertIsNone(h.get('a'))
+ self.assertEqual(h2.get('a'), 'b')
+
+ h = h2 = h3 = None
+
+ def test_map_basics_3(self):
+ h = Map()
+ o = object()
+ h1 = h.set('1', o)
+ h2 = h1.set('1', o)
+ self.assertIs(h1, h2)
+
+ def test_map_basics_4(self):
+ h = Map()
+ h1 = h.set('key', [])
+ h2 = h1.set('key', [])
+ self.assertIsNot(h1, h2)
+ self.assertEqual(len(h1), 1)
+ self.assertEqual(len(h2), 1)
+ self.assertIsNot(h1.get('key'), h2.get('key'))
+
+ def test_map_collision_1(self):
+ k1 = HashKey(10, 'aaa')
+ k2 = HashKey(10, 'bbb')
+ k3 = HashKey(10, 'ccc')
+
+ h = Map()
+ h2 = h.set(k1, 'a')
+ h3 = h2.set(k2, 'b')
+
+ self.assertEqual(h.get(k1), None)
+ self.assertEqual(h.get(k2), None)
+
+ self.assertEqual(h2.get(k1), 'a')
+ self.assertEqual(h2.get(k2), None)
+
+ self.assertEqual(h3.get(k1), 'a')
+ self.assertEqual(h3.get(k2), 'b')
+
+ h4 = h3.set(k2, 'cc')
+ h5 = h4.set(k3, 'aa')
+
+ self.assertEqual(h3.get(k1), 'a')
+ self.assertEqual(h3.get(k2), 'b')
+ self.assertEqual(h4.get(k1), 'a')
+ self.assertEqual(h4.get(k2), 'cc')
+ self.assertEqual(h4.get(k3), None)
+ self.assertEqual(h5.get(k1), 'a')
+ self.assertEqual(h5.get(k2), 'cc')
+ self.assertEqual(h5.get(k2), 'cc')
+ self.assertEqual(h5.get(k3), 'aa')
+
+ self.assertEqual(len(h), 0)
+ self.assertEqual(len(h2), 1)
+ self.assertEqual(len(h3), 2)
+ self.assertEqual(len(h4), 2)
+ self.assertEqual(len(h5), 3)
+
+ def test_map_stress(self):
+ COLLECTION_SIZE = 7000
+ TEST_ITERS_EVERY = 647
+ CRASH_HASH_EVERY = 97
+ CRASH_EQ_EVERY = 11
+ RUN_XTIMES = 3
+
+ for _ in range(RUN_XTIMES):
+ h = Map()
+ d = dict()
+
+ for i in range(COLLECTION_SIZE):
+ key = KeyStr(i)
+
+ if not (i % CRASH_HASH_EVERY):
+ with HaskKeyCrasher(error_on_hash=True):
+ with self.assertRaises(HashingError):
+ h.set(key, i)
+
+ h = h.set(key, i)
+
+ if not (i % CRASH_EQ_EVERY):
+ with HaskKeyCrasher(error_on_eq=True):
+ with self.assertRaises(EqError):
+ h.get(KeyStr(i)) # really trigger __eq__
+
+ d[key] = i
+ self.assertEqual(len(d), len(h))
+
+ if not (i % TEST_ITERS_EVERY):
+ self.assertEqual(set(h.items()), set(d.items()))
+ self.assertEqual(len(h.items()), len(d.items()))
+
+ self.assertEqual(len(h), COLLECTION_SIZE)
+
+ for key in range(COLLECTION_SIZE):
+ self.assertEqual(h.get(KeyStr(key), 'not found'), key)
+
+ keys_to_delete = list(range(COLLECTION_SIZE))
+ random.shuffle(keys_to_delete)
+ for iter_i, i in enumerate(keys_to_delete):
+ key = KeyStr(i)
+
+ if not (iter_i % CRASH_HASH_EVERY):
+ with HaskKeyCrasher(error_on_hash=True):
+ with self.assertRaises(HashingError):
+ h.delete(key)
+
+ if not (iter_i % CRASH_EQ_EVERY):
+ with HaskKeyCrasher(error_on_eq=True):
+ with self.assertRaises(EqError):
+ h.delete(KeyStr(i))
+
+ h = h.delete(key)
+ self.assertEqual(h.get(key, 'not found'), 'not found')
+ del d[key]
+ self.assertEqual(len(d), len(h))
+
+ if iter_i == COLLECTION_SIZE // 2:
+ hm = h
+ dm = d.copy()
+
+ if not (iter_i % TEST_ITERS_EVERY):
+ self.assertEqual(set(h.keys()), set(d.keys()))
+ self.assertEqual(len(h.keys()), len(d.keys()))
+
+ self.assertEqual(len(d), 0)
+ self.assertEqual(len(h), 0)
+
+ # ============
+
+ for key in dm:
+ self.assertEqual(hm.get(str(key)), dm[key])
+ self.assertEqual(len(dm), len(hm))
+
+ for i, key in enumerate(keys_to_delete):
+ hm = hm.delete(str(key))
+ self.assertEqual(hm.get(str(key), 'not found'), 'not found')
+ dm.pop(str(key), None)
+ self.assertEqual(len(d), len(h))
+
+ if not (i % TEST_ITERS_EVERY):
+ self.assertEqual(set(h.values()), set(d.values()))
+ self.assertEqual(len(h.values()), len(d.values()))
+
+ self.assertEqual(len(d), 0)
+ self.assertEqual(len(h), 0)
+ self.assertEqual(list(h.items()), [])
+
+ def test_map_delete_1(self):
+ A = HashKey(100, 'A')
+ B = HashKey(101, 'B')
+ C = HashKey(102, 'C')
+ D = HashKey(103, 'D')
+ E = HashKey(104, 'E')
+ Z = HashKey(-100, 'Z')
+
+ Er = HashKey(103, 'Er', error_on_eq_to=D)
+
+ h = Map()
+ h = h.set(A, 'a')
+ h = h.set(B, 'b')
+ h = h.set(C, 'c')
+ h = h.set(D, 'd')
+ h = h.set(E, 'e')
+
+ orig_len = len(h)
+
+ # BitmapNode(size=10 bitmap=0b111110000 id=0x10eadc618):
+ # <Key name:A hash:100>: 'a'
+ # <Key name:B hash:101>: 'b'
+ # <Key name:C hash:102>: 'c'
+ # <Key name:D hash:103>: 'd'
+ # <Key name:E hash:104>: 'e'
+
+ h = h.delete(C)
+ self.assertEqual(len(h), orig_len - 1)
+
+ with self.assertRaisesRegex(ValueError, 'cannot compare'):
+ h.delete(Er)
+
+ h = h.delete(D)
+ self.assertEqual(len(h), orig_len - 2)
+
+ h2 = h.delete(Z)
+ self.assertIs(h2, h)
+
+ h = h.delete(A)
+ self.assertEqual(len(h), orig_len - 3)
+
+ self.assertEqual(h.get(A, 42), 42)
+ self.assertEqual(h.get(B), 'b')
+ self.assertEqual(h.get(E), 'e')
+
+ def test_map_delete_2(self):
+ A = HashKey(100, 'A')
+ B = HashKey(201001, 'B')
+ C = HashKey(101001, 'C')
+ D = HashKey(103, 'D')
+ E = HashKey(104, 'E')
+ Z = HashKey(-100, 'Z')
+
+ Er = HashKey(201001, 'Er', error_on_eq_to=B)
+
+ h = Map()
+ h = h.set(A, 'a')
+ h = h.set(B, 'b')
+ h = h.set(C, 'c')
+ h = h.set(D, 'd')
+ h = h.set(E, 'e')
+
+ orig_len = len(h)
+
+ # BitmapNode(size=8 bitmap=0b1110010000):
+ # <Key name:A hash:100>: 'a'
+ # <Key name:D hash:103>: 'd'
+ # <Key name:E hash:104>: 'e'
+ # NULL:
+ # BitmapNode(size=4 bitmap=0b100000000001000000000):
+ # <Key name:B hash:201001>: 'b'
+ # <Key name:C hash:101001>: 'c'
+
+ with self.assertRaisesRegex(ValueError, 'cannot compare'):
+ h.delete(Er)
+
+ h = h.delete(Z)
+ self.assertEqual(len(h), orig_len)
+
+ h = h.delete(C)
+ self.assertEqual(len(h), orig_len - 1)
+
+ h = h.delete(B)
+ self.assertEqual(len(h), orig_len - 2)
+
+ h = h.delete(A)
+ self.assertEqual(len(h), orig_len - 3)
+
+ self.assertEqual(h.get(D), 'd')
+ self.assertEqual(h.get(E), 'e')
+
+ h = h.delete(A)
+ h = h.delete(B)
+ h = h.delete(D)
+ h = h.delete(E)
+ self.assertEqual(len(h), 0)
+
+ def test_map_delete_3(self):
+ A = HashKey(100, 'A')
+ B = HashKey(101, 'B')
+ C = HashKey(100100, 'C')
+ D = HashKey(100100, 'D')
+ E = HashKey(104, 'E')
+
+ h = Map()
+ h = h.set(A, 'a')
+ h = h.set(B, 'b')
+ h = h.set(C, 'c')
+ h = h.set(D, 'd')
+ h = h.set(E, 'e')
+
+ orig_len = len(h)
+
+ # BitmapNode(size=6 bitmap=0b100110000):
+ # NULL:
+ # BitmapNode(size=4 bitmap=0b1000000000000000000001000):
+ # <Key name:A hash:100>: 'a'
+ # NULL:
+ # CollisionNode(size=4 id=0x108572410):
+ # <Key name:C hash:100100>: 'c'
+ # <Key name:D hash:100100>: 'd'
+ # <Key name:B hash:101>: 'b'
+ # <Key name:E hash:104>: 'e'
+
+ h = h.delete(A)
+ self.assertEqual(len(h), orig_len - 1)
+
+ h = h.delete(E)
+ self.assertEqual(len(h), orig_len - 2)
+
+ self.assertEqual(h.get(C), 'c')
+ self.assertEqual(h.get(B), 'b')
+
+ def test_map_delete_4(self):
+ A = HashKey(100, 'A')
+ B = HashKey(101, 'B')
+ C = HashKey(100100, 'C')
+ D = HashKey(100100, 'D')
+ E = HashKey(100100, 'E')
+
+ h = Map()
+ h = h.set(A, 'a')
+ h = h.set(B, 'b')
+ h = h.set(C, 'c')
+ h = h.set(D, 'd')
+ h = h.set(E, 'e')
+
+ orig_len = len(h)
+
+ # BitmapNode(size=4 bitmap=0b110000):
+ # NULL:
+ # BitmapNode(size=4 bitmap=0b1000000000000000000001000):
+ # <Key name:A hash:100>: 'a'
+ # NULL:
+ # CollisionNode(size=6 id=0x10515ef30):
+ # <Key name:C hash:100100>: 'c'
+ # <Key name:D hash:100100>: 'd'
+ # <Key name:E hash:100100>: 'e'
+ # <Key name:B hash:101>: 'b'
+
+ h = h.delete(D)
+ self.assertEqual(len(h), orig_len - 1)
+
+ h = h.delete(E)
+ self.assertEqual(len(h), orig_len - 2)
+
+ h = h.delete(C)
+ self.assertEqual(len(h), orig_len - 3)
+
+ h = h.delete(A)
+ self.assertEqual(len(h), orig_len - 4)
+
+ h = h.delete(B)
+ self.assertEqual(len(h), 0)
+
+ def test_map_delete_5(self):
+ h = Map()
+
+ keys = []
+ for i in range(17):
+ key = HashKey(i, str(i))
+ keys.append(key)
+ h = h.set(key, f'val-{i}')
+
+ collision_key16 = HashKey(16, '18')
+ h = h.set(collision_key16, 'collision')
+
+ # ArrayNode(id=0x10f8b9318):
+ # 0::
+ # BitmapNode(size=2 count=1 bitmap=0b1):
+ # <Key name:0 hash:0>: 'val-0'
+ #
+ # ... 14 more BitmapNodes ...
+ #
+ # 15::
+ # BitmapNode(size=2 count=1 bitmap=0b1):
+ # <Key name:15 hash:15>: 'val-15'
+ #
+ # 16::
+ # BitmapNode(size=2 count=1 bitmap=0b1):
+ # NULL:
+ # CollisionNode(size=4 id=0x10f2f5af8):
+ # <Key name:16 hash:16>: 'val-16'
+ # <Key name:18 hash:16>: 'collision'
+
+ self.assertEqual(len(h), 18)
+
+ h = h.delete(keys[2])
+ self.assertEqual(len(h), 17)
+
+ h = h.delete(collision_key16)
+ self.assertEqual(len(h), 16)
+ h = h.delete(keys[16])
+ self.assertEqual(len(h), 15)
+
+ h = h.delete(keys[1])
+ self.assertEqual(len(h), 14)
+ h = h.delete(keys[1])
+ self.assertEqual(len(h), 14)
+
+ for key in keys:
+ h = h.delete(key)
+ self.assertEqual(len(h), 0)
+
+ def test_map_items_1(self):
+ A = HashKey(100, 'A')
+ B = HashKey(201001, 'B')
+ C = HashKey(101001, 'C')
+ D = HashKey(103, 'D')
+ E = HashKey(104, 'E')
+ F = HashKey(110, 'F')
+
+ h = Map()
+ h = h.set(A, 'a')
+ h = h.set(B, 'b')
+ h = h.set(C, 'c')
+ h = h.set(D, 'd')
+ h = h.set(E, 'e')
+ h = h.set(F, 'f')
+
+ it = h.items()
+ self.assertEqual(
+ set(list(it)),
+ {(A, 'a'), (B, 'b'), (C, 'c'), (D, 'd'), (E, 'e'), (F, 'f')})
+
+ def test_map_items_2(self):
+ A = HashKey(100, 'A')
+ B = HashKey(101, 'B')
+ C = HashKey(100100, 'C')
+ D = HashKey(100100, 'D')
+ E = HashKey(100100, 'E')
+ F = HashKey(110, 'F')
+
+ h = Map()
+ h = h.set(A, 'a')
+ h = h.set(B, 'b')
+ h = h.set(C, 'c')
+ h = h.set(D, 'd')
+ h = h.set(E, 'e')
+ h = h.set(F, 'f')
+
+ it = h.items()
+ self.assertEqual(
+ set(list(it)),
+ {(A, 'a'), (B, 'b'), (C, 'c'), (D, 'd'), (E, 'e'), (F, 'f')})
+
+ def test_map_keys_1(self):
+ A = HashKey(100, 'A')
+ B = HashKey(101, 'B')
+ C = HashKey(100100, 'C')
+ D = HashKey(100100, 'D')
+ E = HashKey(100100, 'E')
+ F = HashKey(110, 'F')
+
+ h = Map()
+ h = h.set(A, 'a')
+ h = h.set(B, 'b')
+ h = h.set(C, 'c')
+ h = h.set(D, 'd')
+ h = h.set(E, 'e')
+ h = h.set(F, 'f')
+
+ self.assertEqual(set(list(h.keys())), {A, B, C, D, E, F})
+ self.assertEqual(set(list(h)), {A, B, C, D, E, F})
+
+ def test_map_items_3(self):
+ h = Map()
+ self.assertEqual(len(h.items()), 0)
+ self.assertEqual(list(h.items()), [])
+
+ def test_map_eq_1(self):
+ A = HashKey(100, 'A')
+ B = HashKey(101, 'B')
+ C = HashKey(100100, 'C')
+ D = HashKey(100100, 'D')
+ E = HashKey(120, 'E')
+
+ h1 = Map()
+ h1 = h1.set(A, 'a')
+ h1 = h1.set(B, 'b')
+ h1 = h1.set(C, 'c')
+ h1 = h1.set(D, 'd')
+
+ h2 = Map()
+ h2 = h2.set(A, 'a')
+
+ self.assertFalse(h1 == h2)
+ self.assertTrue(h1 != h2)
+
+ h2 = h2.set(B, 'b')
+ self.assertFalse(h1 == h2)
+ self.assertTrue(h1 != h2)
+
+ h2 = h2.set(C, 'c')
+ self.assertFalse(h1 == h2)
+ self.assertTrue(h1 != h2)
+
+ h2 = h2.set(D, 'd2')
+ self.assertFalse(h1 == h2)
+ self.assertTrue(h1 != h2)
+
+ h2 = h2.set(D, 'd')
+ self.assertTrue(h1 == h2)
+ self.assertFalse(h1 != h2)
+
+ h2 = h2.set(E, 'e')
+ self.assertFalse(h1 == h2)
+ self.assertTrue(h1 != h2)
+
+ h2 = h2.delete(D)
+ self.assertFalse(h1 == h2)
+ self.assertTrue(h1 != h2)
+
+ h2 = h2.set(E, 'd')
+ self.assertFalse(h1 == h2)
+ self.assertTrue(h1 != h2)
+
+ def test_map_eq_2(self):
+ A = HashKey(100, 'A')
+ Er = HashKey(100, 'Er', error_on_eq_to=A)
+
+ h1 = Map()
+ h1 = h1.set(A, 'a')
+
+ h2 = Map()
+ h2 = h2.set(Er, 'a')
+
+ with self.assertRaisesRegex(ValueError, 'cannot compare'):
+ h1 == h2
+
+ with self.assertRaisesRegex(ValueError, 'cannot compare'):
+ h1 != h2
+
+ def test_map_gc_1(self):
+ A = HashKey(100, 'A')
+
+ h = Map()
+ h = h.set(0, 0) # empty Map node is memoized in _map.c
+ ref = weakref.ref(h)
+
+ a = []
+ a.append(a)
+ a.append(h)
+ b = []
+ a.append(b)
+ b.append(a)
+ h = h.set(A, b)
+
+ del h, a, b
+
+ gc.collect()
+ gc.collect()
+ gc.collect()
+
+ self.assertIsNone(ref())
+
+ def test_map_gc_2(self):
+ A = HashKey(100, 'A')
+
+ h = Map()
+ h = h.set(A, 'a')
+ h = h.set(A, h)
+
+ ref = weakref.ref(h)
+ hi = h.items()
+ next(hi)
+
+ del h, hi
+
+ gc.collect()
+ gc.collect()
+ gc.collect()
+
+ self.assertIsNone(ref())
+
+ def test_map_in_1(self):
+ A = HashKey(100, 'A')
+ AA = HashKey(100, 'A')
+
+ B = HashKey(101, 'B')
+
+ h = Map()
+ h = h.set(A, 1)
+
+ self.assertTrue(A in h)
+ self.assertFalse(B in h)
+
+ with self.assertRaises(EqError):
+ with HaskKeyCrasher(error_on_eq=True):
+ AA in h
+
+ with self.assertRaises(HashingError):
+ with HaskKeyCrasher(error_on_hash=True):
+ AA in h
+
+ def test_map_getitem_1(self):
+ A = HashKey(100, 'A')
+ AA = HashKey(100, 'A')
+
+ B = HashKey(101, 'B')
+
+ h = Map()
+ h = h.set(A, 1)
+
+ self.assertEqual(h[A], 1)
+ self.assertEqual(h[AA], 1)
+
+ with self.assertRaises(KeyError):
+ h[B]
+
+ with self.assertRaises(EqError):
+ with HaskKeyCrasher(error_on_eq=True):
+ h[AA]
+
+ with self.assertRaises(HashingError):
+ with HaskKeyCrasher(error_on_hash=True):
+ h[AA]
+
+
+if __name__ == "__main__":
+ unittest.main()