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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +24 -0
  4. data/lib/hamt/version.rb +5 -0
  5. data/lib/hamt.rb +307 -0
  6. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class HAMT
4
+ VERSION = "0.1.0"
5
+ end
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: []