@bradensbay/globals-core 0.2.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 (98) hide show
  1. package/README.md +69 -0
  2. package/dist/src/allocator.d.ts +78 -0
  3. package/dist/src/allocator.d.ts.map +1 -0
  4. package/dist/src/allocator.js +211 -0
  5. package/dist/src/allocator.js.map +1 -0
  6. package/dist/src/arena.d.ts +81 -0
  7. package/dist/src/arena.d.ts.map +1 -0
  8. package/dist/src/arena.js +224 -0
  9. package/dist/src/arena.js.map +1 -0
  10. package/dist/src/checksum.d.ts +5 -0
  11. package/dist/src/checksum.d.ts.map +1 -0
  12. package/dist/src/checksum.js +35 -0
  13. package/dist/src/checksum.js.map +1 -0
  14. package/dist/src/draft.d.ts +48 -0
  15. package/dist/src/draft.d.ts.map +1 -0
  16. package/dist/src/draft.js +427 -0
  17. package/dist/src/draft.js.map +1 -0
  18. package/dist/src/errors.d.ts +56 -0
  19. package/dist/src/errors.d.ts.map +1 -0
  20. package/dist/src/errors.js +69 -0
  21. package/dist/src/errors.js.map +1 -0
  22. package/dist/src/external.d.ts +52 -0
  23. package/dist/src/external.d.ts.map +1 -0
  24. package/dist/src/external.js +68 -0
  25. package/dist/src/external.js.map +1 -0
  26. package/dist/src/hamt.d.ts +87 -0
  27. package/dist/src/hamt.d.ts.map +1 -0
  28. package/dist/src/hamt.js +643 -0
  29. package/dist/src/hamt.js.map +1 -0
  30. package/dist/src/history.d.ts +45 -0
  31. package/dist/src/history.d.ts.map +1 -0
  32. package/dist/src/history.js +49 -0
  33. package/dist/src/history.js.map +1 -0
  34. package/dist/src/index.d.ts +55 -0
  35. package/dist/src/index.d.ts.map +1 -0
  36. package/dist/src/index.js +38 -0
  37. package/dist/src/index.js.map +1 -0
  38. package/dist/src/inspect.d.ts +74 -0
  39. package/dist/src/inspect.d.ts.map +1 -0
  40. package/dist/src/inspect.js +127 -0
  41. package/dist/src/inspect.js.map +1 -0
  42. package/dist/src/layout.d.ts +130 -0
  43. package/dist/src/layout.d.ts.map +1 -0
  44. package/dist/src/layout.js +149 -0
  45. package/dist/src/layout.js.map +1 -0
  46. package/dist/src/liveness.d.ts +48 -0
  47. package/dist/src/liveness.d.ts.map +1 -0
  48. package/dist/src/liveness.js +92 -0
  49. package/dist/src/liveness.js.map +1 -0
  50. package/dist/src/owner.d.ts +130 -0
  51. package/dist/src/owner.d.ts.map +1 -0
  52. package/dist/src/owner.js +368 -0
  53. package/dist/src/owner.js.map +1 -0
  54. package/dist/src/reader.d.ts +122 -0
  55. package/dist/src/reader.d.ts.map +1 -0
  56. package/dist/src/reader.js +325 -0
  57. package/dist/src/reader.js.map +1 -0
  58. package/dist/src/readers.d.ts +60 -0
  59. package/dist/src/readers.d.ts.map +1 -0
  60. package/dist/src/readers.js +122 -0
  61. package/dist/src/readers.js.map +1 -0
  62. package/dist/src/retained.d.ts +39 -0
  63. package/dist/src/retained.d.ts.map +1 -0
  64. package/dist/src/retained.js +99 -0
  65. package/dist/src/retained.js.map +1 -0
  66. package/dist/src/schema.d.ts +82 -0
  67. package/dist/src/schema.d.ts.map +1 -0
  68. package/dist/src/schema.js +18 -0
  69. package/dist/src/schema.js.map +1 -0
  70. package/dist/src/store.d.ts +83 -0
  71. package/dist/src/store.d.ts.map +1 -0
  72. package/dist/src/store.js +127 -0
  73. package/dist/src/store.js.map +1 -0
  74. package/dist/src/strings.d.ts +37 -0
  75. package/dist/src/strings.d.ts.map +1 -0
  76. package/dist/src/strings.js +144 -0
  77. package/dist/src/strings.js.map +1 -0
  78. package/dist/src/tags.d.ts +47 -0
  79. package/dist/src/tags.d.ts.map +1 -0
  80. package/dist/src/tags.js +53 -0
  81. package/dist/src/tags.js.map +1 -0
  82. package/dist/src/values.d.ts +90 -0
  83. package/dist/src/values.d.ts.map +1 -0
  84. package/dist/src/values.js +458 -0
  85. package/dist/src/values.js.map +1 -0
  86. package/dist/src/vector.d.ts +64 -0
  87. package/dist/src/vector.d.ts.map +1 -0
  88. package/dist/src/vector.js +387 -0
  89. package/dist/src/vector.js.map +1 -0
  90. package/dist/src/verify.d.ts +38 -0
  91. package/dist/src/verify.d.ts.map +1 -0
  92. package/dist/src/verify.js +179 -0
  93. package/dist/src/verify.js.map +1 -0
  94. package/dist/src/view.d.ts +66 -0
  95. package/dist/src/view.d.ts.map +1 -0
  96. package/dist/src/view.js +278 -0
  97. package/dist/src/view.js.map +1 -0
  98. package/package.json +28 -0
@@ -0,0 +1,643 @@
1
+ import { ArenaCorruptError } from "./errors.js";
2
+ import { WORD } from "./layout.js";
3
+ import { Tag } from "./tags.js";
4
+ import { decodeString, stringEquals, stringHash } from "./strings.js";
5
+ /**
6
+ * A hash array mapped trie, used for objects, maps, and sets.
7
+ *
8
+ * The point of it is structural sharing. Setting one field of an object with ten thousand
9
+ * keys copies the path from the root to that key, which is at most seven nodes, rather than
10
+ * the whole record. That is what keeps write cost logarithmic and makes retention of several
11
+ * versions affordable.
12
+ *
13
+ * Node layout, at the payload offset the allocator returns:
14
+ *
15
+ * word 0 kind, BITMAP or COLLISION
16
+ * BITMAP word 1 dataMap, word 2 nodeMap
17
+ * then popcount(dataMap) entries of three words: key tag, key payload, and a
18
+ * value slot packed as two words
19
+ * then popcount(nodeMap) child offsets, one word each
20
+ * COLLISION word 1 entry count, word 2 the shared hash
21
+ * then count entries of four words: key tag, key payload, value tag, value
22
+ * payload
23
+ *
24
+ * An explicit kind word costs four bytes per node and buys two things: collision nodes are
25
+ * distinguishable without stealing a bitmap value, and a wild offset that happens to land on
26
+ * plausible bitmaps is rejected rather than walked.
27
+ */
28
+ export const HAMT_BITMAP = 0x484d4200;
29
+ export const HAMT_COLLISION = 0x484d4201;
30
+ const KIND = 0;
31
+ const MAP_A = 1;
32
+ const MAP_B = 2;
33
+ const HEADER_WORDS = 3;
34
+ const ENTRY_WORDS = 4;
35
+ const BITS = 5;
36
+ const WIDTH = 1 << BITS;
37
+ const MASK = WIDTH - 1;
38
+ /** Six full levels use thirty bits. The seventh level carries the remaining two. */
39
+ const MAX_SHIFT = 30;
40
+ export const EMPTY_NODE = 0;
41
+ function popcount(bits) {
42
+ let value = bits - ((bits >>> 1) & 0x55555555);
43
+ value = (value & 0x33333333) + ((value >>> 2) & 0x33333333);
44
+ value = (value + (value >>> 4)) & 0x0f0f0f0f;
45
+ return (Math.imul(value, 0x01010101) >>> 24) & 0x3f;
46
+ }
47
+ function fragment(hash, shift) {
48
+ return (hash >>> shift) & MASK;
49
+ }
50
+ /** The hash a key slot contributes to the trie. */
51
+ export function keyHash(arena, key) {
52
+ switch (key.tag) {
53
+ case Tag.String:
54
+ return stringHash(arena, key.payload);
55
+ case Tag.Int32:
56
+ return Math.imul(key.payload, 0x9e3779b1) | 0;
57
+ case Tag.Double: {
58
+ arena.checkBlock(key.payload, "map key");
59
+ const value = arena.floats[key.payload / 8];
60
+ // Integral doubles must hash like the int32 that equals them, or a map keyed on 2 ** 40
61
+ // and one keyed on a small integer would disagree with SameValueZero.
62
+ if (Number.isInteger(value) && Math.abs(value) <= 2147483647) {
63
+ return Math.imul(value, 0x9e3779b1) | 0;
64
+ }
65
+ const bytes = new Float64Array(1);
66
+ bytes[0] = value;
67
+ const words = new Int32Array(bytes.buffer);
68
+ return (Math.imul(words[0], 0x9e3779b1) ^ words[1]) | 0;
69
+ }
70
+ case Tag.True:
71
+ return 0x7a5b3c1d;
72
+ case Tag.False:
73
+ return 0x1d3c5b7a;
74
+ case Tag.Null:
75
+ return 0x4e554c4c;
76
+ case Tag.Undefined:
77
+ return 0x554e4446;
78
+ default:
79
+ throw new ArenaCorruptError(`tag ${key.tag} cannot be a key`, { actual: key.tag });
80
+ }
81
+ }
82
+ /**
83
+ * Key equality, SameValueZero.
84
+ *
85
+ * Interned strings compare by offset, which is the whole reason for interning: an object
86
+ * property lookup is an integer compare rather than a character walk. Doubles have to be
87
+ * compared by value, because two equal doubles live in two different blocks.
88
+ */
89
+ export function keyEquals(arena, a, b) {
90
+ if (a.tag === b.tag) {
91
+ if (a.tag === Tag.Double) {
92
+ arena.checkBlock(a.payload, "map key");
93
+ arena.checkBlock(b.payload, "map key");
94
+ const left = arena.floats[a.payload / 8];
95
+ const right = arena.floats[b.payload / 8];
96
+ return left === right || (Number.isNaN(left) && Number.isNaN(right));
97
+ }
98
+ return a.payload === b.payload;
99
+ }
100
+ // An int32 and a double can hold the same number, so the cross tag case is real.
101
+ if (a.tag === Tag.Int32 && b.tag === Tag.Double) {
102
+ arena.checkBlock(b.payload, "map key");
103
+ return arena.floats[b.payload / 8] === a.payload;
104
+ }
105
+ if (a.tag === Tag.Double && b.tag === Tag.Int32) {
106
+ arena.checkBlock(a.payload, "map key");
107
+ return arena.floats[a.payload / 8] === b.payload;
108
+ }
109
+ return false;
110
+ }
111
+ function readKind(arena, node) {
112
+ const byteSize = arena.checkBlock(node, "hamt node");
113
+ if (byteSize < HEADER_WORDS * WORD) {
114
+ throw new ArenaCorruptError(`hamt node at ${node} is too small`, { offset: node });
115
+ }
116
+ const kind = arena.words[node / WORD + KIND];
117
+ if (kind !== HAMT_BITMAP && kind !== HAMT_COLLISION) {
118
+ throw new ArenaCorruptError(`hamt node at ${node} has kind ${kind}`, { offset: node });
119
+ }
120
+ return kind;
121
+ }
122
+ /**
123
+ * The entry count of a collision node, validated against the block that holds it.
124
+ *
125
+ * The bitmap node has always had this check through its popcount, which cannot exceed 32. The
126
+ * collision node stores its count as a plain integer, and nothing validated it, so a corrupt
127
+ * value sent every walker off to visit two billion entries. The loops that consume it have no
128
+ * budget check inside them, so this is where it has to be caught.
129
+ */
130
+ function collisionCount(arena, node) {
131
+ const byteSize = arena.checkBlock(node, "hamt collision node");
132
+ const count = arena.words[node / WORD + MAP_A];
133
+ if (count < 0 || (HEADER_WORDS + count * ENTRY_WORDS) * WORD > byteSize) {
134
+ throw new ArenaCorruptError(`hamt collision node at ${node} claims ${count} entries, which does not fit in ` +
135
+ `${byteSize} bytes`, { offset: node, actual: count });
136
+ }
137
+ return count;
138
+ }
139
+ function bitmapLayout(arena, node) {
140
+ const base = node / WORD;
141
+ const dataCount = popcount(arena.words[base + MAP_A]);
142
+ const nodeCount = popcount(arena.words[base + MAP_B]);
143
+ const needed = (HEADER_WORDS + dataCount * ENTRY_WORDS + nodeCount) * WORD;
144
+ const byteSize = arena.checkBlock(node, "hamt bitmap node");
145
+ if (needed > byteSize) {
146
+ throw new ArenaCorruptError(`hamt node at ${node} claims ${dataCount} entries and ${nodeCount} children, which does ` +
147
+ `not fit in ${byteSize} bytes`, { offset: node });
148
+ }
149
+ return { dataCount, nodeCount, base };
150
+ }
151
+ function allocBitmap(context, dataMap, nodeMap, dataCount, nodeCount) {
152
+ const bytes = (HEADER_WORDS + dataCount * ENTRY_WORDS + nodeCount) * WORD;
153
+ const offset = context.allocator.allocate(bytes);
154
+ context.allocated.push(offset);
155
+ const words = context.arena.words;
156
+ const base = offset / WORD;
157
+ words[base + KIND] = HAMT_BITMAP;
158
+ words[base + MAP_A] = dataMap;
159
+ words[base + MAP_B] = nodeMap;
160
+ return offset;
161
+ }
162
+ function dataIndex(base, index) {
163
+ return base + HEADER_WORDS + index * ENTRY_WORDS;
164
+ }
165
+ function childIndex(base, dataCount, index) {
166
+ return base + HEADER_WORDS + dataCount * ENTRY_WORDS + index;
167
+ }
168
+ export function emptyNode(context) {
169
+ return allocBitmap(context, 0, 0, 0, 0);
170
+ }
171
+ /** Look a key up. Returns undefined when it is absent, which a null value slot cannot be. */
172
+ export function hamtGet(arena, node, key, hash) {
173
+ let current = node;
174
+ let shift = 0;
175
+ for (;;) {
176
+ if (current === EMPTY_NODE)
177
+ return undefined;
178
+ const kind = readKind(arena, current);
179
+ if (kind === HAMT_COLLISION) {
180
+ const base = current / WORD;
181
+ const count = collisionCount(arena, current);
182
+ for (let i = 0; i < count; i += 1) {
183
+ const at = dataIndex(base, i);
184
+ const candidate = {
185
+ tag: arena.words[at],
186
+ payload: arena.words[at + 1],
187
+ };
188
+ if (keyEquals(arena, candidate, key)) {
189
+ return { tag: arena.words[at + 2], payload: arena.words[at + 3] };
190
+ }
191
+ }
192
+ return undefined;
193
+ }
194
+ const { dataCount, base } = bitmapLayout(arena, current);
195
+ const bit = 1 << fragment(hash, shift);
196
+ const dataMap = arena.words[base + MAP_A];
197
+ const nodeMap = arena.words[base + MAP_B];
198
+ if ((dataMap & bit) !== 0) {
199
+ const index = popcount(dataMap & (bit - 1));
200
+ const at = dataIndex(base, index);
201
+ const candidate = {
202
+ tag: arena.words[at],
203
+ payload: arena.words[at + 1],
204
+ };
205
+ if (!keyEquals(arena, candidate, key))
206
+ return undefined;
207
+ return { tag: arena.words[at + 2], payload: arena.words[at + 3] };
208
+ }
209
+ if ((nodeMap & bit) === 0)
210
+ return undefined;
211
+ const index = popcount(nodeMap & (bit - 1));
212
+ current = arena.words[childIndex(base, dataCount, index)];
213
+ shift += BITS;
214
+ if (shift > MAX_SHIFT + BITS) {
215
+ throw new ArenaCorruptError("hamt lookup exceeded the maximum depth", { offset: node });
216
+ }
217
+ }
218
+ }
219
+ function copyEntry(words, from, to) {
220
+ words[to] = words[from];
221
+ words[to + 1] = words[from + 1];
222
+ words[to + 2] = words[from + 2];
223
+ words[to + 3] = words[from + 3];
224
+ }
225
+ function writeEntry(words, at, key, value) {
226
+ words[at] = key.tag;
227
+ words[at + 1] = key.payload;
228
+ words[at + 2] = value.tag;
229
+ words[at + 3] = value.payload;
230
+ }
231
+ function allocCollision(context, hash, count) {
232
+ const offset = context.allocator.allocate((HEADER_WORDS + count * ENTRY_WORDS) * WORD);
233
+ context.allocated.push(offset);
234
+ const words = context.arena.words;
235
+ const base = offset / WORD;
236
+ words[base + KIND] = HAMT_COLLISION;
237
+ words[base + MAP_A] = count;
238
+ words[base + MAP_B] = hash;
239
+ return offset;
240
+ }
241
+ /** Build a node holding two entries whose hashes differ somewhere at or below `shift`. */
242
+ function mergeEntries(context, shift, keyA, valueA, hashA, keyB, valueB, hashB) {
243
+ if (shift > MAX_SHIFT) {
244
+ const node = allocCollision(context, hashA, 2);
245
+ const words = context.arena.words;
246
+ const base = node / WORD;
247
+ writeEntry(words, dataIndex(base, 0), keyA, valueA);
248
+ writeEntry(words, dataIndex(base, 1), keyB, valueB);
249
+ return node;
250
+ }
251
+ const fragmentA = fragment(hashA, shift);
252
+ const fragmentB = fragment(hashB, shift);
253
+ if (fragmentA === fragmentB) {
254
+ const child = mergeEntries(context, shift + BITS, keyA, valueA, hashA, keyB, valueB, hashB);
255
+ const node = allocBitmap(context, 0, 1 << fragmentA, 0, 1);
256
+ context.arena.words[childIndex(node / WORD, 0, 0)] = child;
257
+ return node;
258
+ }
259
+ const dataMap = (1 << fragmentA) | (1 << fragmentB);
260
+ const node = allocBitmap(context, dataMap, 0, 2, 0);
261
+ const words = context.arena.words;
262
+ const base = node / WORD;
263
+ const firstIsA = fragmentA < fragmentB;
264
+ writeEntry(words, dataIndex(base, 0), firstIsA ? keyA : keyB, firstIsA ? valueA : valueB);
265
+ writeEntry(words, dataIndex(base, 1), firstIsA ? keyB : keyA, firstIsA ? valueB : valueA);
266
+ return node;
267
+ }
268
+ /**
269
+ * Insert or replace a key, returning the new root of this subtree.
270
+ *
271
+ * Every node on the path from the root to the key is copied and the original is pushed onto
272
+ * the retired list. Nothing already published is mutated, which is what makes a reader
273
+ * holding an older root safe without any coordination.
274
+ */
275
+ export function hamtAssoc(context, node, key, hash, value, shift = 0) {
276
+ const arena = context.arena;
277
+ if (node === EMPTY_NODE) {
278
+ const bit = 1 << fragment(hash, shift);
279
+ const created = allocBitmap(context, bit, 0, 1, 0);
280
+ writeEntry(arena.words, dataIndex(created / WORD, 0), key, value);
281
+ return created;
282
+ }
283
+ const kind = readKind(arena, node);
284
+ if (kind === HAMT_COLLISION) {
285
+ const base = node / WORD;
286
+ const count = collisionCount(arena, node);
287
+ const storedHash = arena.words[base + MAP_B];
288
+ let existing = -1;
289
+ for (let i = 0; i < count; i += 1) {
290
+ const at = dataIndex(base, i);
291
+ const candidate = {
292
+ tag: arena.words[at],
293
+ payload: arena.words[at + 1],
294
+ };
295
+ if (keyEquals(arena, candidate, key)) {
296
+ existing = i;
297
+ break;
298
+ }
299
+ }
300
+ const newCount = existing === -1 ? count + 1 : count;
301
+ const created = allocCollision(context, storedHash, newCount);
302
+ const words = arena.words;
303
+ const newBase = created / WORD;
304
+ for (let i = 0; i < count; i += 1) {
305
+ copyEntry(words, dataIndex(base, i), dataIndex(newBase, i));
306
+ }
307
+ writeEntry(words, dataIndex(newBase, existing === -1 ? count : existing), key, value);
308
+ context.retired.push(node);
309
+ return created;
310
+ }
311
+ const { dataCount, nodeCount, base } = bitmapLayout(arena, node);
312
+ const dataMap = arena.words[base + MAP_A];
313
+ const nodeMap = arena.words[base + MAP_B];
314
+ const bit = 1 << fragment(hash, shift);
315
+ if ((nodeMap & bit) !== 0) {
316
+ const index = popcount(nodeMap & (bit - 1));
317
+ const childOffset = arena.words[childIndex(base, dataCount, index)];
318
+ const newChild = hamtAssoc(context, childOffset, key, hash, value, shift + BITS);
319
+ if (newChild === childOffset)
320
+ return node;
321
+ const created = allocBitmap(context, dataMap, nodeMap, dataCount, nodeCount);
322
+ const words = arena.words;
323
+ const newBase = created / WORD;
324
+ for (let i = 0; i < dataCount; i += 1) {
325
+ copyEntry(words, dataIndex(base, i), dataIndex(newBase, i));
326
+ }
327
+ for (let i = 0; i < nodeCount; i += 1) {
328
+ words[childIndex(newBase, dataCount, i)] = words[childIndex(base, dataCount, i)];
329
+ }
330
+ words[childIndex(newBase, dataCount, index)] = newChild;
331
+ context.retired.push(node);
332
+ return created;
333
+ }
334
+ if ((dataMap & bit) !== 0) {
335
+ const index = popcount(dataMap & (bit - 1));
336
+ const at = dataIndex(base, index);
337
+ const existingKey = {
338
+ tag: arena.words[at],
339
+ payload: arena.words[at + 1],
340
+ };
341
+ if (keyEquals(arena, existingKey, key)) {
342
+ const created = allocBitmap(context, dataMap, nodeMap, dataCount, nodeCount);
343
+ const words = arena.words;
344
+ const newBase = created / WORD;
345
+ for (let i = 0; i < dataCount; i += 1) {
346
+ copyEntry(words, dataIndex(base, i), dataIndex(newBase, i));
347
+ }
348
+ for (let i = 0; i < nodeCount; i += 1) {
349
+ words[childIndex(newBase, dataCount, i)] = words[childIndex(base, dataCount, i)];
350
+ }
351
+ writeEntry(words, dataIndex(newBase, index), key, value);
352
+ context.retired.push(node);
353
+ return created;
354
+ }
355
+ // Two different keys want the same slot. Push both down a level.
356
+ const existingValue = {
357
+ tag: arena.words[at + 2],
358
+ payload: arena.words[at + 3],
359
+ };
360
+ const child = mergeEntries(context, shift + BITS, existingKey, existingValue, keyHash(arena, existingKey), key, value, hash);
361
+ const newDataMap = dataMap & ~bit;
362
+ const newNodeMap = nodeMap | bit;
363
+ const created = allocBitmap(context, newDataMap, newNodeMap, dataCount - 1, nodeCount + 1);
364
+ const words = arena.words;
365
+ const newBase = created / WORD;
366
+ let target = 0;
367
+ for (let i = 0; i < dataCount; i += 1) {
368
+ if (i === index)
369
+ continue;
370
+ copyEntry(words, dataIndex(base, i), dataIndex(newBase, target));
371
+ target += 1;
372
+ }
373
+ const childSlot = popcount(newNodeMap & (bit - 1));
374
+ let source = 0;
375
+ for (let i = 0; i < nodeCount + 1; i += 1) {
376
+ if (i === childSlot) {
377
+ words[childIndex(newBase, dataCount - 1, i)] = child;
378
+ continue;
379
+ }
380
+ words[childIndex(newBase, dataCount - 1, i)] = words[childIndex(base, dataCount, source)];
381
+ source += 1;
382
+ }
383
+ context.retired.push(node);
384
+ return created;
385
+ }
386
+ // The slot is free, so the entry goes inline.
387
+ const index = popcount(dataMap & (bit - 1));
388
+ const created = allocBitmap(context, dataMap | bit, nodeMap, dataCount + 1, nodeCount);
389
+ const words = arena.words;
390
+ const newBase = created / WORD;
391
+ for (let i = 0; i < index; i += 1) {
392
+ copyEntry(words, dataIndex(base, i), dataIndex(newBase, i));
393
+ }
394
+ writeEntry(words, dataIndex(newBase, index), key, value);
395
+ for (let i = index; i < dataCount; i += 1) {
396
+ copyEntry(words, dataIndex(base, i), dataIndex(newBase, i + 1));
397
+ }
398
+ for (let i = 0; i < nodeCount; i += 1) {
399
+ words[childIndex(newBase, dataCount + 1, i)] = words[childIndex(base, dataCount, i)];
400
+ }
401
+ context.retired.push(node);
402
+ return created;
403
+ }
404
+ /** Remove a key. Returns the node unchanged when the key is absent. */
405
+ export function hamtDissoc(context, node, key, hash, shift = 0) {
406
+ const arena = context.arena;
407
+ if (node === EMPTY_NODE)
408
+ return node;
409
+ const kind = readKind(arena, node);
410
+ if (kind === HAMT_COLLISION) {
411
+ const base = node / WORD;
412
+ const count = collisionCount(arena, node);
413
+ const storedHash = arena.words[base + MAP_B];
414
+ let existing = -1;
415
+ for (let i = 0; i < count; i += 1) {
416
+ const at = dataIndex(base, i);
417
+ const candidate = {
418
+ tag: arena.words[at],
419
+ payload: arena.words[at + 1],
420
+ };
421
+ if (keyEquals(arena, candidate, key)) {
422
+ existing = i;
423
+ break;
424
+ }
425
+ }
426
+ if (existing === -1)
427
+ return node;
428
+ const created = allocCollision(context, storedHash, count - 1);
429
+ const words = arena.words;
430
+ const newBase = created / WORD;
431
+ let target = 0;
432
+ for (let i = 0; i < count; i += 1) {
433
+ if (i === existing)
434
+ continue;
435
+ copyEntry(words, dataIndex(base, i), dataIndex(newBase, target));
436
+ target += 1;
437
+ }
438
+ context.retired.push(node);
439
+ return created;
440
+ }
441
+ const { dataCount, nodeCount, base } = bitmapLayout(arena, node);
442
+ const dataMap = arena.words[base + MAP_A];
443
+ const nodeMap = arena.words[base + MAP_B];
444
+ const bit = 1 << fragment(hash, shift);
445
+ if ((dataMap & bit) !== 0) {
446
+ const index = popcount(dataMap & (bit - 1));
447
+ const at = dataIndex(base, index);
448
+ const candidate = {
449
+ tag: arena.words[at],
450
+ payload: arena.words[at + 1],
451
+ };
452
+ if (!keyEquals(arena, candidate, key))
453
+ return node;
454
+ const created = allocBitmap(context, dataMap & ~bit, nodeMap, dataCount - 1, nodeCount);
455
+ const words = arena.words;
456
+ const newBase = created / WORD;
457
+ let target = 0;
458
+ for (let i = 0; i < dataCount; i += 1) {
459
+ if (i === index)
460
+ continue;
461
+ copyEntry(words, dataIndex(base, i), dataIndex(newBase, target));
462
+ target += 1;
463
+ }
464
+ for (let i = 0; i < nodeCount; i += 1) {
465
+ words[childIndex(newBase, dataCount - 1, i)] = words[childIndex(base, dataCount, i)];
466
+ }
467
+ context.retired.push(node);
468
+ return created;
469
+ }
470
+ if ((nodeMap & bit) === 0)
471
+ return node;
472
+ const index = popcount(nodeMap & (bit - 1));
473
+ const childOffset = arena.words[childIndex(base, dataCount, index)];
474
+ const newChild = hamtDissoc(context, childOffset, key, hash, shift + BITS);
475
+ if (newChild === childOffset)
476
+ return node;
477
+ const created = allocBitmap(context, dataMap, nodeMap, dataCount, nodeCount);
478
+ const words = arena.words;
479
+ const newBase = created / WORD;
480
+ for (let i = 0; i < dataCount; i += 1) {
481
+ copyEntry(words, dataIndex(base, i), dataIndex(newBase, i));
482
+ }
483
+ for (let i = 0; i < nodeCount; i += 1) {
484
+ words[childIndex(newBase, dataCount, i)] = words[childIndex(base, dataCount, i)];
485
+ }
486
+ words[childIndex(newBase, dataCount, index)] = newChild;
487
+ context.retired.push(node);
488
+ return created;
489
+ }
490
+ /**
491
+ * The deepest a correct trie can be.
492
+ *
493
+ * Seven levels of five bits covers a 32 bit hash, plus one for a collision node. A traversal
494
+ * that goes deeper is walking a cycle, which a corrupt child pointer can create.
495
+ */
496
+ const MAX_DEPTH = 8;
497
+ /**
498
+ * The most entries a traversal will visit before giving up.
499
+ *
500
+ * A depth bound alone is not enough, and this is the part that is easy to get wrong. Eight
501
+ * levels with thirty two children each is 32 to the eighth, about a trillion visits, so a
502
+ * corrupt trie that fans out and points back at itself stays inside the depth bound and still
503
+ * exhausts the heap. The fuzzer found this by running the process out of memory after ten
504
+ * minutes rather than by throwing.
505
+ *
506
+ * An entry occupies four words inside a node, so a buffer cannot hold more than its length
507
+ * divided by sixteen of them however it is arranged.
508
+ */
509
+ function traversalBudget(arena) {
510
+ return Math.ceil(arena.byteLength / 16);
511
+ }
512
+ /** Every entry in the subtree. Order is the trie order, which is stable for a given set. */
513
+ export function hamtEntries(arena, node, out = [], depth = 0, budget = traversalBudget(arena)) {
514
+ if (node === EMPTY_NODE)
515
+ return out;
516
+ if (depth > MAX_DEPTH) {
517
+ throw new ArenaCorruptError(`hamt traversal exceeded ${MAX_DEPTH} levels`, { offset: node });
518
+ }
519
+ if (out.length > budget) {
520
+ throw new ArenaCorruptError(`hamt traversal exceeded ${budget} entries, which is more than the buffer can hold`, { offset: node });
521
+ }
522
+ const kind = readKind(arena, node);
523
+ const base = node / WORD;
524
+ if (kind === HAMT_COLLISION) {
525
+ const count = collisionCount(arena, node);
526
+ for (let i = 0; i < count; i += 1) {
527
+ const at = dataIndex(base, i);
528
+ out.push({
529
+ key: { tag: arena.words[at], payload: arena.words[at + 1] },
530
+ value: { tag: arena.words[at + 2], payload: arena.words[at + 3] },
531
+ });
532
+ }
533
+ return out;
534
+ }
535
+ const { dataCount, nodeCount } = bitmapLayout(arena, node);
536
+ for (let i = 0; i < dataCount; i += 1) {
537
+ const at = dataIndex(base, i);
538
+ out.push({
539
+ key: { tag: arena.words[at], payload: arena.words[at + 1] },
540
+ value: { tag: arena.words[at + 2], payload: arena.words[at + 3] },
541
+ });
542
+ }
543
+ for (let i = 0; i < nodeCount; i += 1) {
544
+ hamtEntries(arena, arena.words[childIndex(base, dataCount, i)], out, depth + 1, budget);
545
+ }
546
+ return out;
547
+ }
548
+ export function hamtSize(arena, node, depth = 0) {
549
+ if (node === EMPTY_NODE)
550
+ return 0;
551
+ if (depth > MAX_DEPTH) {
552
+ throw new ArenaCorruptError(`hamt traversal exceeded ${MAX_DEPTH} levels`, { offset: node });
553
+ }
554
+ const kind = readKind(arena, node);
555
+ const base = node / WORD;
556
+ if (kind === HAMT_COLLISION)
557
+ return collisionCount(arena, node);
558
+ const { dataCount, nodeCount } = bitmapLayout(arena, node);
559
+ let total = dataCount;
560
+ for (let i = 0; i < nodeCount; i += 1) {
561
+ total += hamtSize(arena, arena.words[childIndex(base, dataCount, i)], depth + 1);
562
+ }
563
+ return total;
564
+ }
565
+ /** Every node in the subtree, for retiring a whole structure the writer is discarding. */
566
+ export function hamtNodes(arena, node, out = [], depth = 0, budget = traversalBudget(arena)) {
567
+ if (node === EMPTY_NODE)
568
+ return out;
569
+ if (depth > MAX_DEPTH) {
570
+ throw new ArenaCorruptError(`hamt traversal exceeded ${MAX_DEPTH} levels`, { offset: node });
571
+ }
572
+ if (out.length > budget) {
573
+ throw new ArenaCorruptError(`hamt traversal exceeded ${budget} nodes, which is more than the buffer can hold`, { offset: node });
574
+ }
575
+ out.push(node);
576
+ if (readKind(arena, node) === HAMT_COLLISION)
577
+ return out;
578
+ const base = node / WORD;
579
+ const { dataCount, nodeCount } = bitmapLayout(arena, node);
580
+ for (let i = 0; i < nodeCount; i += 1) {
581
+ hamtNodes(arena, arena.words[childIndex(base, dataCount, i)], out, depth + 1, budget);
582
+ }
583
+ return out;
584
+ }
585
+ /** Property names of an object node, as strings. */
586
+ export function hamtKeyStrings(arena, node) {
587
+ return hamtEntries(arena, node).map((entry) => {
588
+ if (entry.key.tag !== Tag.String) {
589
+ throw new ArenaCorruptError("object key is not a string", { actual: entry.key.tag });
590
+ }
591
+ return decodeString(arena, entry.key.payload);
592
+ });
593
+ }
594
+ /**
595
+ * Look a string key up without interning it first.
596
+ *
597
+ * A reader cannot intern, because interning is a write, so it cannot compare keys by offset
598
+ * the way the writer does. It can still walk the trie by hash and compare only the one
599
+ * record it lands on, which is the same logarithmic walk with a character comparison at the
600
+ * end instead of an integer one.
601
+ */
602
+ export function hamtGetString(arena, node, text, hash) {
603
+ let current = node;
604
+ let shift = 0;
605
+ for (;;) {
606
+ if (current === EMPTY_NODE)
607
+ return undefined;
608
+ const kind = readKind(arena, current);
609
+ const base = current / WORD;
610
+ if (kind === HAMT_COLLISION) {
611
+ const count = collisionCount(arena, current);
612
+ for (let i = 0; i < count; i += 1) {
613
+ const at = dataIndex(base, i);
614
+ if (arena.words[at] !== Tag.String)
615
+ continue;
616
+ if (!stringEquals(arena, arena.words[at + 1], text))
617
+ continue;
618
+ return { tag: arena.words[at + 2], payload: arena.words[at + 3] };
619
+ }
620
+ return undefined;
621
+ }
622
+ const { dataCount } = bitmapLayout(arena, current);
623
+ const bit = 1 << fragment(hash, shift);
624
+ const dataMap = arena.words[base + MAP_A];
625
+ const nodeMap = arena.words[base + MAP_B];
626
+ if ((dataMap & bit) !== 0) {
627
+ const at = dataIndex(base, popcount(dataMap & (bit - 1)));
628
+ if (arena.words[at] !== Tag.String)
629
+ return undefined;
630
+ if (!stringEquals(arena, arena.words[at + 1], text))
631
+ return undefined;
632
+ return { tag: arena.words[at + 2], payload: arena.words[at + 3] };
633
+ }
634
+ if ((nodeMap & bit) === 0)
635
+ return undefined;
636
+ current = arena.words[childIndex(base, dataCount, popcount(nodeMap & (bit - 1)))];
637
+ shift += BITS;
638
+ if (shift > MAX_SHIFT + BITS) {
639
+ throw new ArenaCorruptError("hamt lookup exceeded the maximum depth", { offset: node });
640
+ }
641
+ }
642
+ }
643
+ //# sourceMappingURL=hamt.js.map