├── .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 | 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 | 17 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 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 | --------------------------------------------------------------------------------