hamt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +24 -0
- data/lib/hamt/version.rb +5 -0
- data/lib/hamt.rb +307 -0
- metadata +46 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c5295a7442e167cb677bc71da0c89ed99ed90e91b40ffcd2eccf0221ab95b836
|
|
4
|
+
data.tar.gz: bc3d7a19547ce390a03ad73d10e221993f96b0f7d82f2ac0de4acbd076e836b2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1ddd15320f29fb1e3ea594afd8f1b5c7476902c67eb29ad0eb30dde112ea6cd00a5d49dbb2a49adbba6640a734e686074516a187903552c3ec63f1b49d120876
|
|
7
|
+
data.tar.gz: 1d230de9f31182bbd61376015fbd4c440056ce8e7a5c1fa296af4585e54a95584e70446fe6a20c8822ade77a20feb995ad154be733378006325c16a3a53cfa11
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Hawthorn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# HAMT
|
|
2
|
+
|
|
3
|
+
A persistent (immutable) Hash Array Mapped Trie for Ruby.
|
|
4
|
+
|
|
5
|
+
`set` and `delete` return a new HAMT that shares structure with the old one;
|
|
6
|
+
nothing is ever mutated. Keys are compared the way Ruby's Hash compares them:
|
|
7
|
+
by `#hash`, then `#eql?`.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
a = HAMT[x: 1]
|
|
11
|
+
b = a.set(:y, 2)
|
|
12
|
+
a[:y] # => nil (a is untouched)
|
|
13
|
+
b[:y] # => 2
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
gem install hamt
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## License
|
|
23
|
+
|
|
24
|
+
MIT
|
data/lib/hamt/version.rb
ADDED
data/lib/hamt.rb
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hamt/version"
|
|
4
|
+
|
|
5
|
+
# A persistent (immutable) Hash Array Mapped Trie.
|
|
6
|
+
#
|
|
7
|
+
# `set` and `delete` return a new HAMT that shares structure with the old one;
|
|
8
|
+
# nothing is ever mutated and every node is frozen. Keys are compared the way
|
|
9
|
+
# Ruby's Hash compares them: by #hash, then #eql?.
|
|
10
|
+
#
|
|
11
|
+
# a = HAMT[x: 1]
|
|
12
|
+
# b = a.set(:y, 2)
|
|
13
|
+
# a[:y] # => nil (a is untouched)
|
|
14
|
+
# b[:y] # => 2
|
|
15
|
+
#
|
|
16
|
+
class HAMT
|
|
17
|
+
include Enumerable
|
|
18
|
+
|
|
19
|
+
BITS = 5 # 5 bits per level => 32-way branches
|
|
20
|
+
MASK = (1 << BITS) - 1
|
|
21
|
+
|
|
22
|
+
NOT_FOUND = Object.new
|
|
23
|
+
private_constant :NOT_FOUND
|
|
24
|
+
|
|
25
|
+
# In a key slot: the value slot holds a sub-node, not a value.
|
|
26
|
+
SUBNODE = Object.new
|
|
27
|
+
private_constant :SUBNODE
|
|
28
|
+
|
|
29
|
+
def self.popcount(int)
|
|
30
|
+
n = 0
|
|
31
|
+
while int != 0
|
|
32
|
+
int &= int - 1
|
|
33
|
+
n += 1
|
|
34
|
+
end
|
|
35
|
+
n
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# array packs two cells per filled slot: [key, value] or [SUBNODE, child].
|
|
39
|
+
class Node
|
|
40
|
+
attr_reader :bitmap, :array
|
|
41
|
+
|
|
42
|
+
def initialize(bitmap, array)
|
|
43
|
+
@bitmap = bitmap
|
|
44
|
+
@array = array
|
|
45
|
+
freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def get(key, hash, shift, default)
|
|
49
|
+
bit = 1 << ((hash >> shift) & MASK)
|
|
50
|
+
return default if (bitmap & bit).zero?
|
|
51
|
+
i = 2 * index(bit)
|
|
52
|
+
k = array[i]
|
|
53
|
+
if k.equal?(SUBNODE)
|
|
54
|
+
array[i + 1].get(key, hash, shift + BITS, default)
|
|
55
|
+
elsif k.eql?(key)
|
|
56
|
+
array[i + 1]
|
|
57
|
+
else
|
|
58
|
+
default
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def put(key, hash, shift, value, result)
|
|
63
|
+
bit = 1 << ((hash >> shift) & MASK)
|
|
64
|
+
i = 2 * index(bit)
|
|
65
|
+
if (bitmap & bit).zero?
|
|
66
|
+
result.count += 1
|
|
67
|
+
Node.new(bitmap | bit, insert2(array, i, key, value))
|
|
68
|
+
elsif (k = array[i]).equal?(SUBNODE)
|
|
69
|
+
child = array[i + 1]
|
|
70
|
+
new_child = child.put(key, hash, shift + BITS, value, result)
|
|
71
|
+
new_child.equal?(child) ? self : Node.new(bitmap, store1(array, i + 1, new_child))
|
|
72
|
+
elsif k.eql?(key)
|
|
73
|
+
return self if array[i + 1].equal?(value)
|
|
74
|
+
Node.new(bitmap, store1(array, i + 1, value))
|
|
75
|
+
else
|
|
76
|
+
# different key, same slot: push the existing pair down a level
|
|
77
|
+
ev = array[i + 1]
|
|
78
|
+
ehash = k.hash
|
|
79
|
+
child =
|
|
80
|
+
if ehash == hash
|
|
81
|
+
result.count += 1
|
|
82
|
+
Collision.new(hash, [k, ev, key, value].freeze)
|
|
83
|
+
else
|
|
84
|
+
sub = Node.new(1 << ((ehash >> (shift + BITS)) & MASK), [k, ev].freeze)
|
|
85
|
+
sub.put(key, hash, shift + BITS, value, result)
|
|
86
|
+
end
|
|
87
|
+
Node.new(bitmap, store2(array, i, SUBNODE, child))
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def delete(key, hash, shift)
|
|
92
|
+
bit = 1 << ((hash >> shift) & MASK)
|
|
93
|
+
return self if (bitmap & bit).zero?
|
|
94
|
+
i = 2 * index(bit)
|
|
95
|
+
k = array[i]
|
|
96
|
+
if k.equal?(SUBNODE)
|
|
97
|
+
child = array[i + 1]
|
|
98
|
+
new_child = child.delete(key, hash, shift + BITS)
|
|
99
|
+
return self if new_child.equal?(child)
|
|
100
|
+
if new_child.nil?
|
|
101
|
+
rest = bitmap & ~bit
|
|
102
|
+
rest.zero? ? nil : Node.new(rest, remove2(array, i))
|
|
103
|
+
else
|
|
104
|
+
Node.new(bitmap, store1(array, i + 1, new_child))
|
|
105
|
+
end
|
|
106
|
+
elsif k.eql?(key)
|
|
107
|
+
rest = bitmap & ~bit
|
|
108
|
+
rest.zero? ? nil : Node.new(rest, remove2(array, i))
|
|
109
|
+
else
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def each(&block)
|
|
115
|
+
i = 0
|
|
116
|
+
n = array.size
|
|
117
|
+
while i < n
|
|
118
|
+
k = array[i]
|
|
119
|
+
if k.equal?(SUBNODE)
|
|
120
|
+
array[i + 1].each(&block)
|
|
121
|
+
else
|
|
122
|
+
block.call([k, array[i + 1]])
|
|
123
|
+
end
|
|
124
|
+
i += 2
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private def index(bit) = HAMT.popcount(bitmap & (bit - 1))
|
|
129
|
+
|
|
130
|
+
# [*a] beats Array#dup here: dup pays for rb_obj_dup_setup, splat is a
|
|
131
|
+
# raw copy (see bench/array_copy.rb).
|
|
132
|
+
private def insert2(a, i, x, y) = [*a].insert(i, x, y).freeze
|
|
133
|
+
|
|
134
|
+
private def store1(a, i, x)
|
|
135
|
+
b = [*a]
|
|
136
|
+
b[i] = x
|
|
137
|
+
b.freeze
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private def store2(a, i, x, y)
|
|
141
|
+
b = [*a]
|
|
142
|
+
b[i] = x
|
|
143
|
+
b[i + 1] = y
|
|
144
|
+
b.freeze
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private def remove2(a, i)
|
|
148
|
+
b = [*a]
|
|
149
|
+
b.slice!(i, 2)
|
|
150
|
+
b.freeze
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
private_constant :Node
|
|
154
|
+
|
|
155
|
+
# Flat [k, v, k, v, ...] bucket for keys with a fully-equal hash.
|
|
156
|
+
class Collision
|
|
157
|
+
attr_reader :hash, :array
|
|
158
|
+
|
|
159
|
+
def initialize(hash, array)
|
|
160
|
+
@hash = hash
|
|
161
|
+
@array = array
|
|
162
|
+
freeze
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def get(key, _hash, _shift, default)
|
|
166
|
+
i = 0
|
|
167
|
+
n = array.size
|
|
168
|
+
while i < n
|
|
169
|
+
return array[i + 1] if array[i].eql?(key)
|
|
170
|
+
i += 2
|
|
171
|
+
end
|
|
172
|
+
default
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def put(key, hash, shift, value, result)
|
|
176
|
+
return split(shift).put(key, hash, shift, value, result) unless hash == self.hash
|
|
177
|
+
i = 0
|
|
178
|
+
n = array.size
|
|
179
|
+
while i < n
|
|
180
|
+
if array[i].eql?(key)
|
|
181
|
+
return self if array[i + 1].equal?(value)
|
|
182
|
+
b = [*array]
|
|
183
|
+
b[i + 1] = value
|
|
184
|
+
return Collision.new(hash, b.freeze)
|
|
185
|
+
end
|
|
186
|
+
i += 2
|
|
187
|
+
end
|
|
188
|
+
result.count += 1
|
|
189
|
+
Collision.new(hash, ([*array] << key << value).freeze)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def delete(key, _hash, _shift)
|
|
193
|
+
i = 0
|
|
194
|
+
n = array.size
|
|
195
|
+
while i < n
|
|
196
|
+
if array[i].eql?(key)
|
|
197
|
+
return nil if n == 2
|
|
198
|
+
b = [*array]
|
|
199
|
+
b.slice!(i, 2)
|
|
200
|
+
return Collision.new(hash, b.freeze)
|
|
201
|
+
end
|
|
202
|
+
i += 2
|
|
203
|
+
end
|
|
204
|
+
self
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def each
|
|
208
|
+
i = 0
|
|
209
|
+
n = array.size
|
|
210
|
+
while i < n
|
|
211
|
+
yield [array[i], array[i + 1]]
|
|
212
|
+
i += 2
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private def split(shift) = Node.new(1 << ((hash >> shift) & MASK), [SUBNODE, self].freeze)
|
|
217
|
+
end
|
|
218
|
+
private_constant :Collision
|
|
219
|
+
|
|
220
|
+
def self.[](enum = [])
|
|
221
|
+
enum.reduce(new) { |h, (k, v)| h.set(k, v) }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def initialize(root = nil, count = 0)
|
|
225
|
+
@root = root
|
|
226
|
+
@count = count
|
|
227
|
+
freeze
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
attr_accessor :root, :count
|
|
231
|
+
alias size count
|
|
232
|
+
alias length count
|
|
233
|
+
def empty? = @count.zero?
|
|
234
|
+
|
|
235
|
+
def get(key, default = nil)
|
|
236
|
+
@root ? @root.get(key, key.hash, 0, default) : default
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def [](key)
|
|
240
|
+
@root ? @root.get(key, key.hash, 0, nil) : nil
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def key?(key)
|
|
244
|
+
return false unless @root
|
|
245
|
+
!@root.get(key, key.hash, 0, NOT_FOUND).equal?(NOT_FOUND)
|
|
246
|
+
end
|
|
247
|
+
alias has_key? key?
|
|
248
|
+
alias include? key?
|
|
249
|
+
|
|
250
|
+
def fetch(key, default = NOT_FOUND)
|
|
251
|
+
value = @root ? @root.get(key, key.hash, 0, NOT_FOUND) : NOT_FOUND
|
|
252
|
+
return value unless NOT_FOUND.equal?(value)
|
|
253
|
+
return yield(key) if block_given?
|
|
254
|
+
return default unless NOT_FOUND.equal?(default)
|
|
255
|
+
raise KeyError, "key not found: #{key.inspect}"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def set(key, value)
|
|
259
|
+
hash = key.hash
|
|
260
|
+
return HAMT.new(Node.new(1 << (hash & MASK), [key, value].freeze), 1) if @root.nil?
|
|
261
|
+
|
|
262
|
+
result = HAMT.allocate
|
|
263
|
+
result.count = @count
|
|
264
|
+
root = @root.put(key, hash, 0, value, result)
|
|
265
|
+
return self if root.equal?(@root)
|
|
266
|
+
result.root = root
|
|
267
|
+
result.freeze
|
|
268
|
+
result
|
|
269
|
+
end
|
|
270
|
+
alias store set
|
|
271
|
+
alias put set
|
|
272
|
+
|
|
273
|
+
def delete(key)
|
|
274
|
+
return self unless @root
|
|
275
|
+
root = @root.delete(key, key.hash, 0)
|
|
276
|
+
root.equal?(@root) ? self : HAMT.new(root, @count - 1)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def each(&block)
|
|
280
|
+
return enum_for(:each) { @count } unless block_given?
|
|
281
|
+
@root&.each(&block)
|
|
282
|
+
self
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def merge(*others)
|
|
286
|
+
others.reduce(self) do |acc, other|
|
|
287
|
+
other.reduce(acc) do |h, (k, v)|
|
|
288
|
+
if block_given? && !(old = h.get(k, NOT_FOUND)).equal?(NOT_FOUND)
|
|
289
|
+
h.set(k, yield(k, old, v))
|
|
290
|
+
else
|
|
291
|
+
h.set(k, v)
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
def keys = map { |k, _| k }
|
|
297
|
+
def values = map { |_, v| v }
|
|
298
|
+
def to_h = each_with_object({}) { |(k, v), h| h[k] = v }
|
|
299
|
+
|
|
300
|
+
def ==(other)
|
|
301
|
+
other.is_a?(HAMT) && other.count == @count &&
|
|
302
|
+
all? { |k, v| other.get(k, NOT_FOUND) == v }
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def inspect = "HAMT[#{map { |k, v| "#{k.inspect}=>#{v.inspect}" }.join(', ')}]"
|
|
306
|
+
alias to_s inspect
|
|
307
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: hamt
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- John Hawthorn
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: A persistent (immutable) Hash Array Mapped Trie
|
|
13
|
+
email:
|
|
14
|
+
- john@hawthorn.email
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- LICENSE.txt
|
|
20
|
+
- README.md
|
|
21
|
+
- lib/hamt.rb
|
|
22
|
+
- lib/hamt/version.rb
|
|
23
|
+
homepage: https://github.com/jhawthorn/hamt
|
|
24
|
+
licenses:
|
|
25
|
+
- MIT
|
|
26
|
+
metadata:
|
|
27
|
+
homepage_uri: https://github.com/jhawthorn/hamt
|
|
28
|
+
source_code_uri: https://github.com/jhawthorn/hamt
|
|
29
|
+
rdoc_options: []
|
|
30
|
+
require_paths:
|
|
31
|
+
- lib
|
|
32
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
33
|
+
requirements:
|
|
34
|
+
- - ">="
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: '3.0'
|
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '0'
|
|
42
|
+
requirements: []
|
|
43
|
+
rubygems_version: 4.0.10
|
|
44
|
+
specification_version: 4
|
|
45
|
+
summary: A persistent (immutable) Hash Array Mapped Trie
|
|
46
|
+
test_files: []
|