├── .gitignore
├── .idea
├── compiler.xml
├── copyright
│ └── profiles_settings.xml
├── encodings.xml
├── inspectionProfiles
│ ├── Project_Default.xml
│ └── profiles_settings.xml
├── libraries
│ └── KotlinJavaRuntime.xml
├── misc.xml
├── modules.xml
├── pegdown-doclet.xml
└── vcs.xml
├── LICENSE
├── README.md
├── makefile
├── src
└── norswap
│ └── triemap
│ ├── BitmapNode.kt
│ ├── Bitmaps.kt
│ ├── Change.kt
│ ├── CollisionNode.kt
│ ├── ImmutableMap.kt
│ ├── TrieIterator.kt
│ ├── TrieMap.kt
│ ├── TrieNode.kt
│ └── Utils.kt
├── test
└── norswap
│ └── triemap
│ └── Test.kt
└── triemap.iml
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /out/
3 | /.idea/workspace.xml
4 | /.idea/uiDesigner.xml
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/libraries/KotlinJavaRuntime.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/pegdown-doclet.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Nicolas LAURENT
2 | All rights reserved.
3 |
4 | The BSD 3-Clause License
5 |
6 | Redistribution and use in source and binary forms, with or without modification,
7 | are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its contributors
17 | may be used to endorse or promote products derived from this software without
18 | specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Trie Map
2 |
3 | An compressed HAMT (CHAMP) implementation, as devised by Steindorfer & Vinju.
4 | Their own (far more complete) implementation is [available here] as a library
5 | named Capsule (along with links to related papers).
6 |
7 | I implemented this to learn about the inner workings of the data structure. As such it may
8 | be valuable for learning, but do not use for serious work (have a look at Capsule instead).
9 |
10 | This has `put`, `get`, `size` and an iterator implemented.
11 | Everything is immutable (transients not available).
12 |
13 | Requires Kotlin 1.0.3+
14 |
15 | [available here]: https://github.com/usethesource/capsule
16 |
17 | ## Ideas
18 |
19 | An hash-array map trie (HAMT) is a trie over the hashes of its keys.
20 | They are implemented by the `TrieMap` class.
21 |
22 | Each `TrieMap` instance holds a reference to its root node.
23 |
24 | Nodes come in two flavour:
25 |
26 | - Bitmap Nodes
27 |
28 | These are mixed nodes: they hold both references to map entries and to sub-nodes.
29 |
30 | Both entries and sub-nodes are held within a single object array.
31 |
32 | Each bitmap node covers 5 bits of the key hash (since 2^5 = 32 = number of bits in the bitmap).
33 | If these 5 bits are enough to determine a unique object, then the entry is held within the
34 | object array. Otherwise, the entry points to a sub-node. If the bitmap node covers the last
35 | five bits, the sub-node is necessarily a collision node.
36 |
37 | The node has two bitmaps: one for entries and one for sub-nodes.
38 |
39 | One bit set in a bitmap indicates there is a value/sub-node for the prefix formed by the
40 | bitmap node prefix + the 5-bit block represented by the position of the bit.
41 |
42 | The array is compact: no slot is unused. The start of the array is used to store
43 | entries. Each entries occupies multiple 2 slots of the array, one for the key and one for the
44 | value. The end of the array is taken by sub-nodes.
45 |
46 | To determine the index of an entry/sub-node, each set bit in a bitmap is assigned an index.
47 | The index of a bit is defined as the number of lower-order bits in the bitmap.
48 |
49 | Entry indices are multiplied by 2 and indexed from the start of the array,
50 | while sub-node indices are index from the end of the array.
51 |
52 | - Collision Nodes
53 |
54 | These nodes collect multiple values whose key have the same hash.
55 | Keys are distinguished via the `equals` method.
56 |
57 | One of the tricky point in the algorithms is removal. When an entry is removed in a node so that
58 | it only contains a single entry, this node will either become the new root of the trie, or
59 | will be "inlined" in one of its ancestors. The singleton node is inlined in the first of its
60 | ancestor that does have other entries or sub-nodes. If no such ancestor exists, it becomes the new
61 | root node.
62 |
63 | In this implementation, singleton nodes are always bitmap nodes: when the second-to-last node is
64 | removed from a collision node, a bitmap node with a single item is returned.
65 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------------------------------
2 |
3 | default: build
4 |
5 | # SPEC -------------------------------------------------------------------------
6 |
7 | NAME := triemap
8 | VERSION := 0.1.0
9 |
10 | # PATHS ------------------------------------------------------------------------
11 |
12 | ifeq ($(OS) , Windows_NT)
13 | SEP := ;
14 | else
15 | SEP := :
16 | endif
17 |
18 | output := out/production/$(NAME)
19 | test := out/test/$(NAME)
20 | cp := "$(output)"
21 | testcp := "$(output)$(SEP)$(test)"
22 |
23 | # CLEAN ------------------------------------------------------------------------
24 |
25 | clean:
26 | rm -rf out
27 |
28 | # BUILD & TEST -----------------------------------------------------------------
29 |
30 | build:
31 | mkdir -p $(output)
32 | kotlinc -cp $(cp) src -d $(output)
33 | mkdir -p $(test)
34 | kotlinc -cp $(testcp) test -d $(test)
35 |
36 | test:
37 | kotlin -cp $(testcp) norswap.triemap.TestKt
38 |
39 | # JAR --------------------------------------------------------------------------
40 |
41 | jar:
42 | find out -name .DS_Store -type f -delete
43 | jar cf out/$(NAME)-$(VERSION).jar -C $(output) .
44 |
45 | # ------------------------------------------------------------------------------
46 |
47 | .PHONY: \
48 | default \
49 | clean \
50 | build \
51 | build-tests \
52 | test \
53 | jar
54 |
55 | # ------------------------------------------------------------------------------
56 |
--------------------------------------------------------------------------------
/src/norswap/triemap/BitmapNode.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 | package norswap.triemap
3 | import norswap.triemap.TrieNode.SizePredicate.*
4 |
5 | class BitmapNode (nodeMap: Int, entryMap: Int, items: Array): TrieNode()
6 | {
7 | // ---------------------------------------------------------------------------------------------
8 |
9 | private val nodeMap = nodeMap
10 | private val entryMap = entryMap
11 | private val items = items
12 |
13 | // ---------------------------------------------------------------------------------------------
14 |
15 | internal inline fun isEntry (bit: Int)
16 | = (entryMap and bit) != 0
17 |
18 | internal inline fun isNode (bit: Int)
19 | = (nodeMap and bit) != 0
20 |
21 | // ---------------------------------------------------------------------------------------------
22 |
23 | private inline fun entryCount()
24 | = Integer.bitCount(entryMap)
25 |
26 | private inline fun nodeCount()
27 | = Integer.bitCount(nodeMap)
28 |
29 | // ---------------------------------------------------------------------------------------------
30 |
31 | internal inline fun entryIndex (bit: Int)
32 | = index(entryMap, bit)
33 |
34 | internal inline fun key (i: Int): K
35 | = items[2 * i].cast()
36 |
37 | internal inline fun value (i: Int): V
38 | = items[2 * i + 1].cast()
39 |
40 | internal inline fun node (bit: Int): TrieNode
41 | = items[itemNodeIndex(bit)].cast()
42 |
43 | // ---------------------------------------------------------------------------------------------
44 |
45 | internal inline fun itemEntryIndex (bit: Int)
46 | = 2 * entryIndex(bit)
47 |
48 | private inline fun itemNodeIndex (bit: Int, size: Int = items.size)
49 | = size - 1 - index(nodeMap, bit)
50 |
51 | // ---------------------------------------------------------------------------------------------
52 |
53 | override val sizePred: TrieNode.SizePredicate =
54 | if (nodeCount() != 0)
55 | MANY
56 | else when (entryCount()) {
57 | 0 -> ZERO
58 | 1 -> ONE
59 | else -> MANY
60 | }
61 |
62 | // ---------------------------------------------------------------------------------------------
63 |
64 | override fun find (k: K, h: Int, shift: Int): V?
65 | {
66 | val bit = bitFor(h, shift)
67 |
68 | if (isEntry(bit)) {
69 | val i = entryIndex(bit)
70 | return if (key(i) == k) value(i) else null
71 | }
72 |
73 | if (isNode(bit))
74 | return node(bit).find(k, h, shift + PARTITION_SIZE)
75 |
76 | return null
77 | }
78 |
79 | // ---------------------------------------------------------------------------------------------
80 |
81 | override fun updated (k: K, v: V, h: Int, shift: Int, change: Change): TrieNode
82 | {
83 | val bit = bitFor(h, shift)
84 |
85 | if (isEntry(bit))
86 | {
87 | val i = entryIndex(bit)
88 | val k1 = key(i)
89 | val v1 = value(i)
90 |
91 | if (k == k1)
92 | {
93 | change.replaced(v1)
94 | return updateValue(bit, v1)
95 | }
96 | else
97 | {
98 | val h1 = k1.hashCode()
99 | val subNode = merge(k, v, h, k1, v1, h1, shift + PARTITION_SIZE)
100 | change.changed()
101 | return entryToNode(bit, subNode)
102 | }
103 | }
104 | else if (isNode(bit))
105 | {
106 | val subNode = node(bit).updated(k, v, h, shift + PARTITION_SIZE, change)
107 | return if (change.changed) updateNode(bit, subNode) else this
108 | }
109 | else
110 | {
111 | change.changed()
112 | return insertEntry(bit, k, v)
113 | }
114 | }
115 |
116 | // ---------------------------------------------------------------------------------------------
117 |
118 | private fun updateValue (bit: Int, v: V): TrieNode
119 | {
120 | val i = itemEntryIndex(bit) + 1
121 | val items1 = items.clone()
122 | items1[i] = v
123 | return BitmapNode(nodeMap, entryMap, items1)
124 | }
125 |
126 | // ---------------------------------------------------------------------------------------------
127 |
128 | private fun entryToNode (bit: Int, node: TrieNode): TrieNode
129 | {
130 | val size1 = items.size - 1
131 | val items1 = array(size1)
132 |
133 | val i0 = itemEntryIndex(bit)
134 | val i1 = itemNodeIndex(bit, size1)
135 |
136 | assert(i0 <= i1)
137 | System.arraycopy(items, 0, items1, 0, i0)
138 | System.arraycopy(items, i0 + 2, items1, i0, i1 - i0)
139 | System.arraycopy(items, i1 + 2, items1, i1 + 1, items.size - i1 - 2)
140 | items1[i1] = node
141 |
142 | return BitmapNode(nodeMap or bit, entryMap xor bit, items1)
143 | }
144 |
145 | // ---------------------------------------------------------------------------------------------
146 |
147 | private fun updateNode (bit: Int, node: TrieNode): TrieNode
148 | {
149 | val i = itemNodeIndex(bit)
150 | val nodes1 = items.clone()
151 | nodes1[i] = node
152 | return BitmapNode(nodeMap, entryMap, nodes1)
153 | }
154 |
155 | // ---------------------------------------------------------------------------------------------
156 |
157 | private fun insertEntry (bit: Int, k: K, v: V): TrieNode
158 | {
159 | val i = itemEntryIndex(bit)
160 | val nodes1 = array(items.size + 2)
161 |
162 | System.arraycopy(items, 0, nodes1, 0, i)
163 | System.arraycopy(items, i, nodes1, i + 2, items.size - i)
164 | nodes1[i] = k
165 | nodes1[i + 1] = v
166 |
167 | return BitmapNode(nodeMap, entryMap or bit, nodes1)
168 | }
169 |
170 | // ---------------------------------------------------------------------------------------------
171 |
172 | private fun merge (k1: K, v1: V, h1: Int, k2: K, v2: V, h2: Int, shift: Int): TrieNode
173 | {
174 | if (shift > HASH_LEN) {
175 | assert(h1 == h2)
176 | return CollisionNode(array(k1, k2), array(v1, v2), h1)
177 | }
178 |
179 | val p1 = part(h1, shift)
180 | val p2 = part(h2, shift)
181 |
182 | if (p1 != p2)
183 | {
184 | val entryMap1 = bit(p1) or bit(p2)
185 |
186 | if (p1 < p2)
187 | return BitmapNode(0, entryMap1, arrayOf(k1, v1, k2, v2))
188 | else
189 | return BitmapNode(0, entryMap1, arrayOf(k2, v2, k1, v1))
190 | }
191 | else
192 | {
193 | val subNode = merge(k1, v1, h1, k2, v2, h2, shift + PARTITION_SIZE)
194 | return BitmapNode(bit(p1), 0, arrayOf(subNode))
195 | }
196 | }
197 |
198 | // ---------------------------------------------------------------------------------------------
199 |
200 | override fun removed (k: K, h: Int, shift: Int, change: Change): TrieNode
201 | {
202 | val bit = bitFor(h, shift)
203 |
204 | if (isEntry(bit))
205 | {
206 | val i = entryIndex(bit)
207 |
208 | if (key(i) != k)
209 | return this
210 |
211 | val v = value(i)
212 | change.replaced(v)
213 |
214 | if (entryCount() != 2 || nodeCount() != 0)
215 | return removeEntry(bit)
216 |
217 | // Compute the bit with shift = 0.
218 | // Either the node will become the root (shift = 0),
219 | // or the node will be inlined (then only the bit count is used).
220 |
221 | val entryMap1 =
222 | if (shift == 0) // remove the bit for delete node
223 | entryMap xor bit
224 | else // entries will have the same bit at shift=0
225 | bitFor(h, 0)
226 |
227 | if (i == 0)
228 | return BitmapNode(0, entryMap1, arrayOf(key(1), value(1)))
229 | else
230 | return BitmapNode(0, entryMap1, arrayOf(key(0), value(0)))
231 | }
232 | else if (isNode(bit))
233 | {
234 | val subNode = node(bit).removed(k, h, shift + PARTITION_SIZE, change)
235 |
236 | if (!change.changed)
237 | return this
238 |
239 | when (subNode.sizePred) {
240 | ONE ->
241 | if (entryCount() == 0 && nodeCount() == 1)
242 | // Single sub-node with single entry, will become root or be inlined.
243 | return subNode
244 | else
245 | // Inline sub-node in this node.
246 | return nodeToEntry(bit, subNode)
247 | MANY ->
248 | return updateNode(bit, subNode)
249 | ZERO ->
250 | throw IllegalStateException("Sub-node must have at least one element.")
251 | }
252 | }
253 |
254 | return this
255 | }
256 |
257 | // ---------------------------------------------------------------------------------------------
258 |
259 | private fun removeEntry (bit: Int): TrieNode
260 | {
261 | val i = itemEntryIndex(bit)
262 | val items1 = array(items.size - 2)
263 |
264 | System.arraycopy(items, 0, items1, 0, i)
265 | System.arraycopy(items, i + 2, items1, i, items.size - i - 2)
266 |
267 | return BitmapNode(nodeMap, entryMap xor bit, items1)
268 | }
269 |
270 | // ---------------------------------------------------------------------------------------------
271 |
272 | private fun nodeToEntry (bit: Int, node: TrieNode): TrieNode
273 | {
274 | val items1 = array(items.size + 1)
275 |
276 | val i0 = itemEntryIndex(bit)
277 | val i1 = itemNodeIndex(bit)
278 |
279 | assert(i0 <= i1)
280 | System.arraycopy(items, 0, items1, 0, i0)
281 | System.arraycopy(items, i0, items1, i0 + 2, i1 - i0)
282 | System.arraycopy(items, i1 + 1, items1, i1 + 2, items.size - i1 - 1)
283 |
284 | assertT (node as BitmapNode)
285 |
286 | items1[i0] = node.key(0)
287 | items1[i0 + 1] = node.value(0)
288 |
289 | return BitmapNode(nodeMap xor bit, entryMap or bit, items1)
290 | }
291 |
292 | // ---------------------------------------------------------------------------------------------
293 |
294 | override fun hashCode(): Int
295 | {
296 | var h = 0
297 | h = h * 31 + nodeMap
298 | h = h * 31 + entryMap
299 | h = h * 31 + items.hash()
300 | return h
301 | }
302 |
303 | // ---------------------------------------------------------------------------------------------
304 |
305 | override fun equals (other: Any?)
306 | = other === this
307 | || other is BitmapNode<*, *>
308 | && nodeMap == other.nodeMap
309 | && entryMap == other.entryMap
310 | && items eq other.items
311 |
312 | // ---------------------------------------------------------------------------------------------
313 | }
314 |
--------------------------------------------------------------------------------
/src/norswap/triemap/Bitmaps.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 | package norswap.triemap
3 |
4 | // -------------------------------------------------------------------------------------------------
5 |
6 | /**
7 | * Length of hashes.
8 | */
9 | val HASH_LEN = 32
10 |
11 | // -------------------------------------------------------------------------------------------------
12 |
13 | /**
14 | * Partition size, i.e. number of bits of the hash covered by each [BitmapNode].
15 | */
16 | val PARTITION_SIZE = 5
17 |
18 | // -------------------------------------------------------------------------------------------------
19 |
20 | /**
21 | * Number of different value a partition can take, i.e. `2 ^ PARTITION_SIZE`.
22 | * The range of values is `[0, PARTITION_RANGE[`.
23 | */
24 | val PARTITION_RANGE = 32
25 |
26 | // -------------------------------------------------------------------------------------------------
27 |
28 | /**
29 | * Mask to isolate the lower [PARTITION_SIZE] bits.
30 | */
31 | val PARTITION_MASK = 0b11111
32 |
33 | // -------------------------------------------------------------------------------------------------
34 |
35 | /**
36 | * Returns the partition of [h] generated by [shift]: its [PARTITION_SIZE] lowest bits
37 | * after shifting out its [shift] lowest bits.
38 | */
39 | inline fun part (h: Int, shift: Int)
40 | = (h ushr shift) and PARTITION_MASK
41 |
42 | // -------------------------------------------------------------------------------------------------
43 |
44 | /**
45 | * Returns an integer with a single bit set at the given position.
46 | */
47 | inline fun bit (pos: Int)
48 | = 1 shl pos
49 |
50 | // -------------------------------------------------------------------------------------------------
51 |
52 | /**
53 | * Returns the bit for the partition of [h] generated by [shift].
54 | */
55 | inline fun bitFor (h: Int, shift: Int)
56 | = bit(part(h, shift))
57 |
58 | // -------------------------------------------------------------------------------------------------
59 |
60 | /**
61 | * Returns the "index" of the given bit in the bitmap.
62 | * The index of a bit is defined as the number of lower-order bits in the bitmap.
63 | */
64 | inline fun index (bitmap: Int, bit: Int)
65 | = Integer.bitCount(bitmap and (bit - 1))
66 |
67 | // -------------------------------------------------------------------------------------------------
68 |
--------------------------------------------------------------------------------
/src/norswap/triemap/Change.kt:
--------------------------------------------------------------------------------
1 | package norswap.triemap
2 |
3 | /**
4 | * Used to report that the tree was changed:
5 | * - entry inserted
6 | * - entry removed
7 | * - value for key replaced
8 | *
9 | * In the last case, [replaced] holds the old value.
10 | */
11 | class Change
12 | {
13 | // ---------------------------------------------------------------------------------------------
14 |
15 | var replaced: V? = null
16 | var changed = false
17 |
18 | // ---------------------------------------------------------------------------------------------
19 |
20 | /**
21 | * Call if the element count changed.
22 | */
23 | fun changed()
24 | {
25 | changed = true
26 | }
27 |
28 | // ---------------------------------------------------------------------------------------------
29 |
30 | /**
31 | * Call with the old value if it was replaced by a new one.
32 | */
33 | fun replaced (v: V)
34 | {
35 | changed = true
36 | this.replaced = v
37 | }
38 |
39 | // ---------------------------------------------------------------------------------------------
40 | }
41 |
--------------------------------------------------------------------------------
/src/norswap/triemap/CollisionNode.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 | package norswap.triemap
3 |
4 | class CollisionNode (ks: Array, vs: Array, h: Int): TrieNode()
5 | {
6 | // ---------------------------------------------------------------------------------------------
7 |
8 | private val ks = ks
9 | private val vs = vs
10 | private val h = h
11 |
12 | // ---------------------------------------------------------------------------------------------
13 |
14 | internal inline fun key (i: Int): K
15 | = ks[i]
16 |
17 | internal inline fun value (i: Int): V
18 | = vs[i]
19 |
20 | // ---------------------------------------------------------------------------------------------
21 |
22 | override val sizePred = SizePredicate.MANY
23 |
24 | // ---------------------------------------------------------------------------------------------
25 |
26 | val size: Int
27 | get() = ks.size
28 |
29 | // ---------------------------------------------------------------------------------------------
30 |
31 | override fun find (k: K, h: Int, shift: Int): V?
32 | {
33 | assert(this.h == h)
34 | val i = ks.index(k)
35 | return if (i >= 0) vs[i] else null
36 | }
37 |
38 | // ---------------------------------------------------------------------------------------------
39 |
40 | override fun updated (k: K, v: V, h: Int, shift: Int, change: Change): TrieNode
41 | {
42 | assert(this.h == h)
43 | val i = ks.index(k)
44 |
45 | if (i >= 0)
46 | {
47 | val v1 = vs[i]
48 |
49 | if (v1 == v)
50 | return this
51 |
52 | val vs1 = vs.clone()
53 | vs1[i] = v1
54 |
55 | change.replaced(v1)
56 | return CollisionNode(ks, vs1, h)
57 | }
58 | else
59 | {
60 | val ks1 = copyOf(ks, ks.size + 1)
61 | ks1[ks.size] = k
62 |
63 | val vs1 = copyOf(vs, vs.size + 1)
64 | vs1[vs.size] = v
65 |
66 | change.changed()
67 | return CollisionNode(ks1, vs1, h)
68 | }
69 | }
70 |
71 | // ---------------------------------------------------------------------------------------------
72 |
73 | override fun removed (k: K, h: Int, shift: Int, change: Change): TrieNode
74 | {
75 | assert(this.h == h)
76 | val i = ks.index(k)
77 |
78 | if (i < 0)
79 | return this
80 |
81 | val v0 = vs[i]
82 | change.replaced(v0)
83 |
84 | if (ks.size == 1)
85 | {
86 | return emptyNode()
87 | }
88 | else if (ks.size == 2)
89 | {
90 | val k1 = if (i == 0) ks[1] else ks[0]
91 | val v1 = if (i == 0) vs[1] else vs[0]
92 |
93 | // Compute the bit with shift = 0.
94 | // Either the node will become the root (shift = 0),
95 | // or the node will be inlined (then only the bit count is used).
96 |
97 | return emptyNode().updated(k1, v1, k1.hashCode(), 0, change)
98 | }
99 | else
100 | {
101 | val ks1 = arrayT(ks.size - 1)
102 | System.arraycopy(ks, 0, ks1, 0, i)
103 | System.arraycopy(ks, i + 1, ks1, i, ks.size - i - 1)
104 |
105 | val vs1 = arrayT(vs.size - 1)
106 | System.arraycopy(vs, 0, vs1, 0, i)
107 | System.arraycopy(vs, i + 1, vs1, i, vs.size - i - 1)
108 |
109 | return CollisionNode(ks1, vs1, h)
110 | }
111 | }
112 |
113 | // ---------------------------------------------------------------------------------------------
114 |
115 | override fun hashCode(): Int
116 | {
117 | var h = 0
118 | h = h * 31 + this.h
119 | h = h * 31 + ks.hash()
120 | h = h * 31 + vs.hash()
121 | return h
122 | }
123 |
124 | // ---------------------------------------------------------------------------------------------
125 |
126 | @Suppress("UNCHECKED_CAST")
127 | override fun equals (other: Any?)
128 | = other === this
129 | || other is CollisionNode<*, *>
130 | && h == other.h
131 | && ks.size == other.ks.size
132 | && expr e@ {
133 | for (i in ks.indices) {
134 | val k0 = ks[i]
135 | val v0 = vs[i]
136 | val v1 = (other as CollisionNode).find(k0, k0.hashCode(), 0)
137 | if (v0 != v1)
138 | return@e false
139 | }
140 | true
141 | }
142 |
143 | // ---------------------------------------------------------------------------------------------
144 | }
145 |
--------------------------------------------------------------------------------
/src/norswap/triemap/ImmutableMap.kt:
--------------------------------------------------------------------------------
1 | package norswap.triemap
2 |
3 | /**
4 | * Minimal interface for immutable maps.
5 | */
6 | interface ImmutableMap
7 | {
8 | // ---------------------------------------------------------------------------------------------
9 |
10 | operator fun get (k: K): V?
11 |
12 | // ---------------------------------------------------------------------------------------------
13 |
14 | fun put (k: K, v: V): ImmutableMap
15 |
16 | // ---------------------------------------------------------------------------------------------
17 |
18 | fun remove (k: K): ImmutableMap
19 |
20 | // ---------------------------------------------------------------------------------------------
21 |
22 | val size: Int
23 |
24 | // ---------------------------------------------------------------------------------------------
25 |
26 | val empty: Boolean
27 | get() = size == 0
28 |
29 | // ---------------------------------------------------------------------------------------------
30 | }
31 |
--------------------------------------------------------------------------------
/src/norswap/triemap/TrieIterator.kt:
--------------------------------------------------------------------------------
1 | package norswap.triemap
2 | import java.util.ArrayDeque
3 | import java.util.NoSuchElementException
4 |
5 | class TrieIterator (root: TrieNode): Iterator>
6 | {
7 | // ---------------------------------------------------------------------------------------------
8 |
9 | private val nodes = ArrayDeque>()
10 | private val indices = ArrayDeque()
11 |
12 | private var node = root
13 | private var i = 0
14 |
15 | // ---------------------------------------------------------------------------------------------
16 |
17 | override fun hasNext()
18 | = !nodes.isEmpty()
19 |
20 | // ---------------------------------------------------------------------------------------------
21 |
22 | init { advance() }
23 |
24 | // ---------------------------------------------------------------------------------------------
25 |
26 | /**
27 | * `(node, i) = pop(nodes, indices)`
28 | */
29 | private fun pop(): Boolean
30 | {
31 | if (nodes.isEmpty())
32 | return false
33 |
34 | node = nodes.pop()
35 | i = indices.pop()
36 | return true
37 | }
38 |
39 | // ---------------------------------------------------------------------------------------------
40 |
41 | /**
42 | * ````
43 | * top(nodes, indices) = (node, i + 1)
44 | * (node, i) = (node1, 0)
45 | * ````
46 | */
47 | private fun push (node1: TrieNode)
48 | {
49 | nodes.push(node)
50 | indices.push(i + 1)
51 | node = node1
52 | i = 0
53 | }
54 |
55 | // ---------------------------------------------------------------------------------------------
56 |
57 | fun advance()
58 | {
59 | outer@ while (true)
60 | {
61 | val n = node
62 | if (n is CollisionNode)
63 | {
64 | if (i < n.size)
65 | return
66 | if (!pop())
67 | return
68 | else
69 | continue
70 | }
71 | else if (n is BitmapNode)
72 | {
73 | inner@ while (true)
74 | {
75 | if (i == PARTITION_RANGE)
76 | if (!pop())
77 | return
78 | else
79 | continue@outer
80 |
81 | val bit = bit(i)
82 |
83 | if (n.isEntry(bit)) {
84 | return
85 | }
86 | else if (n.isNode(bit)) {
87 | push(n.node(bit))
88 | continue@outer
89 | }
90 | else {
91 | ++ i
92 | continue@inner
93 | }
94 | }
95 | }
96 | }
97 | }
98 |
99 | // ---------------------------------------------------------------------------------------------
100 |
101 | private inline fun nextGen(
102 | collision: CollisionNode.() -> T,
103 | bitmap: BitmapNode.() -> T)
104 | : T
105 | {
106 | if (nodes.isEmpty())
107 | throw NoSuchElementException()
108 |
109 | val n = node
110 |
111 | val out = when (n)
112 | {
113 | is CollisionNode -> n.collision()
114 | is BitmapNode -> n.bitmap()
115 | else -> throw Error()
116 | }
117 |
118 | ++ i
119 | advance()
120 | return out
121 | }
122 |
123 | // ---------------------------------------------------------------------------------------------
124 |
125 | override fun next(): Pair
126 | = nextGen (
127 | collision = { Pair(key(i), value(i)) },
128 | bitmap = { val j = entryIndex(bit(i)) ; Pair(key(j), value(j)) }
129 | )
130 |
131 | // ---------------------------------------------------------------------------------------------
132 |
133 | fun nextKey(): K
134 | = nextGen (
135 | collision = { key(i) },
136 | bitmap = { key(bit(i)) }
137 | )
138 |
139 | // ---------------------------------------------------------------------------------------------
140 |
141 | fun nextValue(): V
142 | = nextGen (
143 | collision = { value(i) },
144 | bitmap = { value(bit(i)) }
145 | )
146 |
147 | // ---------------------------------------------------------------------------------------------
148 | }
149 |
--------------------------------------------------------------------------------
/src/norswap/triemap/TrieMap.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UNCHECKED_CAST")
2 | package norswap.triemap
3 | import kotlin.text.removeSuffix
4 |
5 | /**
6 | * An immutable map backed by a hash array mapped trie (HAMT).
7 | */
8 | class TrieMap private constructor (root: TrieNode, h: Int, size: Int)
9 | : ImmutableMap, Iterable>
10 | {
11 | // ---------------------------------------------------------------------------------------------
12 |
13 | companion object
14 | {
15 | private val EMPTY_MAP = TrieMap(TrieNode.emptyNode(), 0, 0)
16 |
17 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
18 |
19 | /**
20 | * Returns an empty trie map. This does not trigger any memory allocation.
21 | */
22 | operator fun invoke(): TrieMap
23 | = EMPTY_MAP as TrieMap
24 |
25 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
26 |
27 | /**
28 | * Returns a trie map whose keys and values are passed in [keyValuePairs].
29 | * e.g. `TrieMap(k0, v0, k1, v1, k2, v2)`
30 | */
31 | operator fun invoke (vararg keyValuePairs: Any): TrieMap
32 | {
33 | if (keyValuePairs.size % 2 != 0)
34 | throw IllegalArgumentException(
35 | "Length of argument list is uneven: no key/value pairs.")
36 |
37 | var result = TrieMap()
38 |
39 | var i = 0
40 | while (i < keyValuePairs.size)
41 | {
42 | val k = keyValuePairs[i] as K
43 | val v = keyValuePairs[i + 1] as V
44 | result = result.put(k, v)
45 | i += 2
46 | }
47 |
48 | return result
49 | }
50 | }
51 |
52 | // ---------------------------------------------------------------------------------------------
53 |
54 | private val root = root
55 | private val h = h
56 | override val size = size
57 |
58 | // ---------------------------------------------------------------------------------------------
59 |
60 | override fun get (k: K): V?
61 | = root.find(k, k.hashCode(), 0)
62 |
63 | // ---------------------------------------------------------------------------------------------
64 |
65 | override fun put (k: K, v: V): TrieMap
66 | {
67 | val kh = k.hashCode()
68 | val change = Change()
69 | val root1 = root.updated(k, v, kh, 0, change)
70 |
71 | if (!change.changed)
72 | return this
73 |
74 | val rep = change.replaced
75 | val vh1 = v.hashCode()
76 |
77 | if (rep != null) {
78 | val vh0 = rep.hashCode()
79 | val h1 = h + (kh xor vh1) - (kh xor vh0)
80 | return TrieMap(root1, h1, size)
81 | }
82 | else {
83 | val h1 = h + (kh xor vh1)
84 | return TrieMap(root1, h1, size + 1)
85 | }
86 | }
87 |
88 | // ---------------------------------------------------------------------------------------------
89 |
90 | override fun remove (k: K): TrieMap
91 | {
92 | val kh = k.hashCode()
93 | val change = Change()
94 | val root1 = root.removed(k, kh, 0, change)
95 |
96 | val rep = change.replaced
97 |
98 | if (rep != null) {
99 | val vh = rep.hashCode()
100 | return TrieMap(root1, h - (kh xor vh), size - 1)
101 | }
102 |
103 | return this
104 | }
105 |
106 | // ---------------------------------------------------------------------------------------------
107 |
108 | override fun hashCode() = h
109 |
110 | // ---------------------------------------------------------------------------------------------
111 |
112 | override fun equals (other: Any?)
113 | = other === this
114 | || other is TrieMap<*, *>
115 | && other.size == size
116 | && other.h == h
117 | && other.root == root
118 | || other is Map<*, *>
119 | && other.size == size
120 | && expr e@ {
121 | for (e in other.entries) {
122 | val k = e.key as K
123 | val v = get(k)
124 | if (v != e.value)
125 | return@e false
126 | }
127 | true
128 | }
129 |
130 | // ---------------------------------------------------------------------------------------------
131 |
132 | override fun iterator() = TrieIterator(root)
133 |
134 | // ---------------------------------------------------------------------------------------------
135 |
136 | override fun toString(): String
137 | {
138 | val b = StringBuilder()
139 | b += "{"
140 | for ((k, v) in this) b += " $k -> $v,"
141 | if (!empty) {
142 | b.removeSuffix(",")
143 | b += " "
144 | }
145 | b += "}"
146 | return b.toString()
147 | }
148 |
149 | // ---------------------------------------------------------------------------------------------
150 | }
151 |
--------------------------------------------------------------------------------
/src/norswap/triemap/TrieNode.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE", "CAST_NEVER_SUCCEEDS")
2 | package norswap.triemap
3 |
4 | abstract class TrieNode
5 | {
6 | // ---------------------------------------------------------------------------------------------
7 |
8 | companion object
9 | {
10 | private val EMPTY_NODE
11 | = BitmapNode(0, 0, emptyArray())
12 |
13 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
14 |
15 | inline internal fun emptyNode()
16 | = EMPTY_NODE as TrieNode
17 | }
18 |
19 | // ---------------------------------------------------------------------------------------------
20 |
21 | abstract val sizePred: SizePredicate
22 |
23 | // ---------------------------------------------------------------------------------------------
24 |
25 | enum class SizePredicate { ZERO, ONE, MANY }
26 |
27 | // ---------------------------------------------------------------------------------------------
28 |
29 | abstract fun find (k: K, h: Int, shift: Int): V?
30 |
31 | // ---------------------------------------------------------------------------------------------
32 |
33 | abstract fun updated (k: K, v: V, h: Int, shift: Int, change: Change): TrieNode
34 |
35 | // ---------------------------------------------------------------------------------------------
36 |
37 | abstract fun removed (k: K, h: Int, shift: Int, change: Change): TrieNode
38 |
39 | // ---------------------------------------------------------------------------------------------
40 | }
41 |
--------------------------------------------------------------------------------
/src/norswap/triemap/Utils.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("NOTHING_TO_INLINE")
2 | package norswap.triemap
3 | import java.util.Arrays
4 |
5 | // -------------------------------------------------------------------------------------------------
6 |
7 | /**
8 | * Like [indexOf], except inlined & more efficient.
9 | */
10 | inline fun Array.index(it: T): Int
11 | {
12 | var i = 0
13 | while (i < this.size) {
14 | if (this[i] == it) return i
15 | ++ i
16 | }
17 | return -1
18 | }
19 |
20 | // -------------------------------------------------------------------------------------------------
21 |
22 | @Suppress("SENSELESS_COMPARISON")
23 | /**
24 | * Usage `assertT(thing as MyType)`.
25 | *
26 | * When a cast appears in an expression, Kotlin enables smart-casting for the remainder of the
27 | * scope. This simply enables this form of casting with no run-time overhead when assertions
28 | * are turned off.
29 | */
30 | inline fun assertT(any: Any) {
31 | assert(any != null)
32 | }
33 |
34 | // -------------------------------------------------------------------------------------------------
35 |
36 | /**
37 | * Returns the result of [f].
38 | * The point is to allow statements in an expression context.
39 | */
40 | inline fun expr(f: () -> T): T = f()
41 |
42 | // -------------------------------------------------------------------------------------------------
43 |
44 | /**
45 | * Shorthand for [StringBuilder.append].
46 | */
47 | operator inline fun StringBuilder.plusAssign(o: Any?) { append(o) }
48 |
49 | // -------------------------------------------------------------------------------------------------
50 |
51 | /**
52 | * Shorthand for [Arrays.hashCode].
53 | */
54 | inline fun Array<*>.hash()
55 | = Arrays.hashCode(this)
56 |
57 | // -------------------------------------------------------------------------------------------------
58 |
59 | /**
60 | * Shorthand for [Arrays.equals].
61 | */
62 | infix inline fun Array<*>.eq(other: Array<*>)
63 | = Arrays.equals(this, other)
64 |
65 | // -------------------------------------------------------------------------------------------------
66 |
67 | /**
68 | * To create an array with items whose type is non-reifiable ([arrayOf] chokes on these cases).
69 | */
70 | @Suppress("UNCHECKED_CAST")
71 | inline fun array(vararg items: T)
72 | = items as Array
73 |
74 | // -------------------------------------------------------------------------------------------------
75 |
76 | /**
77 | * Allocate a null-initialized array (circumvents Kotlin nullability checks).
78 | */
79 | @Suppress("CAST_NEVER_SUCCEEDS")
80 | inline fun array(size: Int): Array
81 | = arrayOfNulls(size) as Array
82 |
83 | // -------------------------------------------------------------------------------------------------
84 |
85 | /**
86 | * Allocates an null-initialized array of type T (circumvents Kotlin nullability checks).
87 | */
88 | @Suppress("UNCHECKED_CAST")
89 | inline fun arrayT(size: Int)
90 | = arrayOfNulls(size) as Array
91 |
92 | // -------------------------------------------------------------------------------------------------
93 |
94 | /**
95 | * Just like [Arrays.copyOf], but does not mess withclasses, hence faster (on the other hand,
96 | * less safe: using an Object array delays or inhibits class casts exceptions).
97 | */
98 | @Suppress("UNCHECKED_CAST")
99 | inline fun copyOf(array: Array, size: Int): Array
100 | {
101 | val array1 = arrayOfNulls(size) as Array
102 | val copy = if (size < array.size) size else array.size
103 | System.arraycopy(array, 0, array1, 0, copy)
104 | return array1
105 | }
106 |
107 | // -------------------------------------------------------------------------------------------------
108 |
109 | /**
110 | * Casts the receiver to [T].
111 | *
112 | * This is more useful than regular casts because it enables casts to non-denotable types
113 | * through type inference.
114 | */
115 | @Suppress("UNCHECKED_CAST")
116 | inline fun Any?.cast()
117 | = this as T
118 |
119 | // -------------------------------------------------------------------------------------------------
120 |
--------------------------------------------------------------------------------
/test/norswap/triemap/Test.kt:
--------------------------------------------------------------------------------
1 | package norswap.triemap
2 | import java.math.BigInteger
3 | import java.util.HashMap
4 | import java.util.Random
5 |
6 | // -------------------------------------------------------------------------------------------------
7 |
8 | fun main (args: Array)
9 | {
10 | while (true)
11 | doSomething()
12 | }
13 |
14 | // -------------------------------------------------------------------------------------------------
15 |
16 | val m1 = HashMap()
17 | var m2 = TrieMap()
18 |
19 | val random = Random()
20 |
21 | // -------------------------------------------------------------------------------------------------
22 |
23 | fun check (b: Boolean)
24 | {
25 | if (!b) {
26 | println(m1)
27 | println(m2)
28 | println(m2.empty)
29 | println(m2.size)
30 | throw AssertionError()
31 | }
32 | }
33 |
34 | // -------------------------------------------------------------------------------------------------
35 |
36 | fun verify()
37 | {
38 | check(m1.size == m2.size)
39 |
40 | for (k in m1.keys) {
41 | check(m1[k] == m2[k])
42 | }
43 | }
44 |
45 | // -------------------------------------------------------------------------------------------------
46 |
47 | fun doSomething()
48 | {
49 | if (random.nextBoolean()) // add
50 | {
51 | val n = when (random.nextInt(3)) {
52 | 0 -> { println("adding 1") ; 1 }
53 | 1 -> { println("adding 10") ; 10 }
54 | else -> { println("adding 100") ; 100 }
55 | }
56 |
57 | for (i in 1..n) {
58 | val str = BigInteger(130, random).toString(32)
59 | val int = random.nextInt()
60 | m1.put(str, int)
61 | m2 = m2.put(str, int)
62 | }
63 | }
64 | else // remove
65 | {
66 | val n = when (random.nextInt(3)) {
67 | 0 -> { println("remove 1") ; 1 }
68 | 1 -> { println("remove 9") ; 9 }
69 | else -> { println("remove 90") ; 90 }
70 | }
71 |
72 | for (i in 1..n) {
73 | if (m1.isEmpty()) {
74 | check(m2.empty)
75 | break
76 | }
77 |
78 | val j = random.nextInt(m1.size)
79 | val k = m1.keys.asSequence().drop(j).iterator().next()
80 | m1.remove(k)
81 | m2 = m2.remove(k)
82 | check (m2.get(k) == null)
83 | }
84 | }
85 |
86 | println(m2.size)
87 | verify()
88 | }
89 |
90 | // -------------------------------------------------------------------------------------------------
91 |
--------------------------------------------------------------------------------
/triemap.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------