@amritk/nish 0.11.0 → 0.13.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.
- package/README.md +43 -23
- package/bin/launcher.js +45 -41
- package/bin/packaging.js +39 -33
- package/docs/AI.md +135 -11
- package/docs/INSTALL.md +29 -13
- package/package.json +10 -6
- package/runtime/nish.d.ts +22 -1
- package/runtime/nish.h +31 -11
- package/runtime/nish.mjs +16 -3
- package/runtime/{runtime_os.c → runtime-os.c} +1 -1
- package/runtime/{runtime_parallel.c → runtime-parallel.c} +112 -4
- package/runtime/{runtime_wasm.c → runtime-wasm.c} +6 -1
- package/runtime/runtime.c +5 -5
- package/runtime/shim.mjs +6 -6
- package/scripts/bootstrap.sh +29 -29
- package/scripts/build.sh +14 -9
- package/scripts/changelog-gen.mjs +260 -192
- package/scripts/ci-profile.mjs +81 -68
- package/scripts/codes-registry.js +25 -13
- package/scripts/gen-diagnostic-codes.mjs +86 -69
- package/scripts/nish-compiler.sh +2 -0
- package/scripts/platform-package.mjs +26 -26
- package/scripts/postinstall.mjs +46 -36
- package/scripts/size-report.sh +3 -3
- package/scripts/smoke.sh +1 -1
- package/std/README.md +16 -11
- package/std/collections.ts +221 -178
- package/std/json.ts +136 -136
- package/std/map.ts +39 -0
- package/std/pair.ts +2 -2
- package/std/testing.ts +67 -67
- package/std/text.ts +54 -54
- package/std/threads.ts +126 -38
package/std/collections.ts
CHANGED
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
* what the 24-bit index field holds as an index plus one. Node's own `Map` and
|
|
48
48
|
* `Set` hold one more, 2^24, and throw on the next insert.
|
|
49
49
|
*/
|
|
50
|
-
const INDEX_CAP: i32 = 16777215
|
|
50
|
+
const INDEX_CAP: i32 = 16777215
|
|
51
51
|
|
|
52
52
|
/** The first bucket count. A power of two, as every bucket count is. */
|
|
53
|
-
const INITIAL_SLOTS: i32 = 8
|
|
53
|
+
const INITIAL_SLOTS: i32 = 8
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* The key's hash, never 0: FNV-1a over a string's bytes, murmur3's `fmix32`
|
|
@@ -60,11 +60,11 @@ const INITIAL_SLOTS: i32 = 8;
|
|
|
60
60
|
* constants are public domain.
|
|
61
61
|
*
|
|
62
62
|
* The body is never emitted. Every call is lowered in place, per key type, by
|
|
63
|
-
* `
|
|
63
|
+
* `src/emit-map.ts`; the constant is what the checker and the whole-program
|
|
64
64
|
* facts see, and the facts of the function that calls it are what decide its
|
|
65
65
|
* attributes, because every call is inside a probe that reads the table.
|
|
66
66
|
*/
|
|
67
|
-
const hashKey = <K>(
|
|
67
|
+
const hashKey = <K>(_key: K): u32 => 1
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* JavaScript's key equality, SameValueZero: `===`, except that NaN equals NaN.
|
|
@@ -72,19 +72,19 @@ const hashKey = <K>(key: K): u32 => 1;
|
|
|
72
72
|
* `icmp` for an integer, a boolean, an enum and a class instance, and for a
|
|
73
73
|
* float `a == b` or both unordered.
|
|
74
74
|
*/
|
|
75
|
-
const sameKey = <K>(a: K, b: K): boolean => a === b
|
|
75
|
+
const sameKey = <K>(a: K, b: K): boolean => a === b
|
|
76
76
|
|
|
77
77
|
/** The first bucket for `h`: its low bits, folded with the high half. */
|
|
78
|
-
const homeBucket = (h: u32, mask: i32): i32 => toI32(h ^ (h >>> 16)) & mask
|
|
78
|
+
const homeBucket = (h: u32, mask: i32): i32 => toI32(h ^ (h >>> 16)) & mask
|
|
79
79
|
|
|
80
80
|
/** The bucket word for entry `index` of hash `h`. */
|
|
81
|
-
const slotWord = (h: u32, index: i32): u32 => ((h >>> 24) << 24) | toU32(index + 1)
|
|
81
|
+
const slotWord = (h: u32, index: i32): u32 => ((h >>> 24) << 24) | toU32(index + 1)
|
|
82
82
|
|
|
83
83
|
/** A probe that found entry `index`, pointed at by `bucket`. Never negative. */
|
|
84
|
-
const foundAt = (bucket: i32, index: i32): i64 => (toI64(bucket) << 32) | toI64(index)
|
|
84
|
+
const foundAt = (bucket: i32, index: i32): i64 => (toI64(bucket) << 32) | toI64(index)
|
|
85
85
|
|
|
86
86
|
/** A probe that stopped at the empty `bucket` for hash `h`. Always negative. */
|
|
87
|
-
const absentAt = (bucket: i32, h: u32): i64 => toI64(-1) - ((toI64(bucket) << 32) | toI64(h))
|
|
87
|
+
const absentAt = (bucket: i32, h: u32): i64 => toI64(-1) - ((toI64(bucket) << 32) | toI64(h))
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* The one probe, for both classes: linear probing from `h`'s home bucket to
|
|
@@ -94,26 +94,32 @@ const absentAt = (bucket: i32, h: u32): i64 => toI64(-1) - ((toI64(bucket) << 32
|
|
|
94
94
|
* empty, so the walk ends.
|
|
95
95
|
*/
|
|
96
96
|
const probeTable = <K>(slots: u32[], mask: i32, hashes: u32[], keys: K[], key: K): i64 => {
|
|
97
|
-
const h = hashKey(key)
|
|
98
|
-
const fingerprint = h >>> 24
|
|
99
|
-
let bucket = homeBucket(h, mask)
|
|
97
|
+
const h = hashKey(key)
|
|
98
|
+
const fingerprint = h >>> 24
|
|
99
|
+
let bucket = homeBucket(h, mask)
|
|
100
100
|
// The length is read in the condition rather than once: the key compare is
|
|
101
101
|
// a call, and a call ends every length fact the bounds proof holds.
|
|
102
102
|
while (bucket >= 0 && bucket < toI32(slots.length)) {
|
|
103
|
-
const word = slots[bucket]
|
|
103
|
+
const word = slots[bucket]
|
|
104
104
|
if (word === 0) {
|
|
105
|
-
return absentAt(bucket, h)
|
|
105
|
+
return absentAt(bucket, h)
|
|
106
106
|
}
|
|
107
107
|
if (word >>> 24 === fingerprint) {
|
|
108
|
-
const at = toI32(word & 16777215) - 1
|
|
109
|
-
if (
|
|
110
|
-
|
|
108
|
+
const at = toI32(word & 16777215) - 1
|
|
109
|
+
if (
|
|
110
|
+
at >= 0 &&
|
|
111
|
+
at < toI32(hashes.length) &&
|
|
112
|
+
hashes[at] === h &&
|
|
113
|
+
at < toI32(keys.length) &&
|
|
114
|
+
sameKey(keys[at], key)
|
|
115
|
+
) {
|
|
116
|
+
return foundAt(bucket, at)
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
|
-
bucket = (bucket + 1) & mask
|
|
119
|
+
bucket = (bucket + 1) & mask
|
|
114
120
|
}
|
|
115
|
-
panic("collections: a probe ran out of buckets")
|
|
116
|
-
}
|
|
121
|
+
panic("collections: a probe ran out of buckets")
|
|
122
|
+
}
|
|
117
123
|
|
|
118
124
|
/**
|
|
119
125
|
* Point the first empty bucket from `h`'s home at entry `index`. A rebuild's
|
|
@@ -121,47 +127,47 @@ const probeTable = <K>(slots: u32[], mask: i32, hashes: u32[], keys: K[], key: K
|
|
|
121
127
|
* compared, and the stored hash is all it needs.
|
|
122
128
|
*/
|
|
123
129
|
const fileEntry = (slots: u32[], mask: i32, h: u32, index: i32): void => {
|
|
124
|
-
const word = slotWord(h, index)
|
|
125
|
-
let bucket = homeBucket(h, mask)
|
|
130
|
+
const word = slotWord(h, index)
|
|
131
|
+
let bucket = homeBucket(h, mask)
|
|
126
132
|
while (bucket >= 0 && bucket < toI32(slots.length)) {
|
|
127
133
|
if (slots[bucket] === 0) {
|
|
128
|
-
slots[bucket] = word
|
|
129
|
-
return
|
|
134
|
+
slots[bucket] = word
|
|
135
|
+
return
|
|
130
136
|
}
|
|
131
|
-
bucket = (bucket + 1) & mask
|
|
137
|
+
bucket = (bucket + 1) & mask
|
|
132
138
|
}
|
|
133
|
-
}
|
|
139
|
+
}
|
|
134
140
|
|
|
135
141
|
/** Slide the live entries of `items` down over the dead ones, in order, and drop the tail. */
|
|
136
142
|
const compactEntries = <T>(items: T[], hashes: u32[]): void => {
|
|
137
|
-
const used = toI32(items.length)
|
|
138
|
-
let to: i32 = 0
|
|
143
|
+
const used = toI32(items.length)
|
|
144
|
+
let to: i32 = 0
|
|
139
145
|
for (let from: i32 = 0; from < used && from < toI32(hashes.length); from++) {
|
|
140
146
|
if (hashes[from] !== 0 && to >= 0 && to < used && from < toI32(items.length)) {
|
|
141
|
-
items[to] = items[from]
|
|
142
|
-
to
|
|
147
|
+
items[to] = items[from]
|
|
148
|
+
to++
|
|
143
149
|
}
|
|
144
150
|
}
|
|
145
151
|
while (toI32(items.length) > to) {
|
|
146
|
-
items.pop()
|
|
152
|
+
items.pop()
|
|
147
153
|
}
|
|
148
|
-
}
|
|
154
|
+
}
|
|
149
155
|
|
|
150
156
|
/** The stored hashes compacted the same way, last, since the two above read them. */
|
|
151
157
|
const compactHashes = (hashes: u32[]): void => {
|
|
152
|
-
const used = toI32(hashes.length)
|
|
153
|
-
let to: i32 = 0
|
|
158
|
+
const used = toI32(hashes.length)
|
|
159
|
+
let to: i32 = 0
|
|
154
160
|
for (let from: i32 = 0; from < used; from++) {
|
|
155
|
-
const h = hashes[from]
|
|
161
|
+
const h = hashes[from]
|
|
156
162
|
if (h !== 0 && to >= 0 && to < used) {
|
|
157
|
-
hashes[to] = h
|
|
158
|
-
to
|
|
163
|
+
hashes[to] = h
|
|
164
|
+
to++
|
|
159
165
|
}
|
|
160
166
|
}
|
|
161
167
|
while (toI32(hashes.length) > to) {
|
|
162
|
-
hashes.pop()
|
|
168
|
+
hashes.pop()
|
|
163
169
|
}
|
|
164
|
-
}
|
|
170
|
+
}
|
|
165
171
|
|
|
166
172
|
/**
|
|
167
173
|
* The bucket table after a rebuild of `live` entries out of `used`. More than
|
|
@@ -171,13 +177,13 @@ const compactHashes = (hashes: u32[]): void => {
|
|
|
171
177
|
* the table never shrinks.
|
|
172
178
|
*/
|
|
173
179
|
const rebuiltSlots = (slots: u32[], live: i32, used: i32): u32[] => {
|
|
174
|
-
const n = toI32(slots.length)
|
|
180
|
+
const n = toI32(slots.length)
|
|
175
181
|
if (live * 2 < used) {
|
|
176
|
-
clearSlots(slots)
|
|
177
|
-
return slots
|
|
182
|
+
clearSlots(slots)
|
|
183
|
+
return slots
|
|
178
184
|
}
|
|
179
|
-
return new Array<u32>(n * 2)
|
|
180
|
-
}
|
|
185
|
+
return new Array<u32>(n * 2)
|
|
186
|
+
}
|
|
181
187
|
|
|
182
188
|
/**
|
|
183
189
|
* Re-file every live entry from its stored hash. After a compaction none is
|
|
@@ -185,14 +191,14 @@ const rebuiltSlots = (slots: u32[], live: i32, used: i32): u32[] => {
|
|
|
185
191
|
* keeps its place and takes no bucket, since no probe can find it.
|
|
186
192
|
*/
|
|
187
193
|
const refile = (slots: u32[], hashes: u32[]): void => {
|
|
188
|
-
const mask = toI32(slots.length) - 1
|
|
194
|
+
const mask = toI32(slots.length) - 1
|
|
189
195
|
for (let i: i32 = 0; i < toI32(hashes.length); i++) {
|
|
190
|
-
const h = hashes[i]
|
|
196
|
+
const h = hashes[i]
|
|
191
197
|
if (h !== 0) {
|
|
192
|
-
fileEntry(slots, mask, h, i)
|
|
198
|
+
fileEntry(slots, mask, h, i)
|
|
193
199
|
}
|
|
194
200
|
}
|
|
195
|
-
}
|
|
201
|
+
}
|
|
196
202
|
|
|
197
203
|
/**
|
|
198
204
|
* `delete`'s write through a found probe result: the bucket becomes a
|
|
@@ -200,15 +206,15 @@ const refile = (slots: u32[], hashes: u32[]): void => {
|
|
|
200
206
|
* until a rebuild compacts them away (§6.1).
|
|
201
207
|
*/
|
|
202
208
|
const killEntry = (slots: u32[], hashes: u32[], found: i64): void => {
|
|
203
|
-
const at = toI32(found)
|
|
204
|
-
const bucket = toI32(found >> 32)
|
|
209
|
+
const at = toI32(found)
|
|
210
|
+
const bucket = toI32(found >> 32)
|
|
205
211
|
if (bucket >= 0 && bucket < toI32(slots.length)) {
|
|
206
|
-
slots[bucket] = 16777216
|
|
212
|
+
slots[bucket] = 16777216
|
|
207
213
|
}
|
|
208
214
|
if (at >= 0 && at < toI32(hashes.length)) {
|
|
209
|
-
hashes[at] = 0
|
|
215
|
+
hashes[at] = 0
|
|
210
216
|
}
|
|
211
|
-
}
|
|
217
|
+
}
|
|
212
218
|
|
|
213
219
|
/**
|
|
214
220
|
* Zero every element in place: the buckets, which emptying a table and
|
|
@@ -217,9 +223,9 @@ const killEntry = (slots: u32[], hashes: u32[], found: i64): void => {
|
|
|
217
223
|
*/
|
|
218
224
|
const clearSlots = (slots: u32[]): void => {
|
|
219
225
|
for (let i: i32 = 0; i < toI32(slots.length); i++) {
|
|
220
|
-
slots[i] = 0
|
|
226
|
+
slots[i] = 0
|
|
221
227
|
}
|
|
222
|
-
}
|
|
228
|
+
}
|
|
223
229
|
|
|
224
230
|
/**
|
|
225
231
|
* The index of the first live entry at `from` or after it, or -1 when there is
|
|
@@ -231,18 +237,18 @@ const clearSlots = (slots: u32[]): void => {
|
|
|
231
237
|
const nextLive = (hashes: u32[], from: i32): i32 => {
|
|
232
238
|
for (let i: i32 = from; i >= 0 && i < toI32(hashes.length); i++) {
|
|
233
239
|
if (hashes[i] !== 0) {
|
|
234
|
-
return i
|
|
240
|
+
return i
|
|
235
241
|
}
|
|
236
242
|
}
|
|
237
|
-
return -1
|
|
238
|
-
}
|
|
243
|
+
return -1
|
|
244
|
+
}
|
|
239
245
|
|
|
240
246
|
/** Drop every element of `items`, keeping its capacity. */
|
|
241
247
|
const truncate = <T>(items: T[]): void => {
|
|
242
248
|
while (toI32(items.length) > 0) {
|
|
243
|
-
items.pop()
|
|
249
|
+
items.pop()
|
|
244
250
|
}
|
|
245
|
-
}
|
|
251
|
+
}
|
|
246
252
|
|
|
247
253
|
/**
|
|
248
254
|
* Point bucket `bucket`, the empty one an absent probe stopped at, at the entry
|
|
@@ -251,11 +257,11 @@ const truncate = <T>(items: T[]): void => {
|
|
|
251
257
|
*/
|
|
252
258
|
const fileAppended = (slots: u32[], mask: i32, bucket: i32, h: u32, used: i32): void => {
|
|
253
259
|
if (bucket >= 0 && bucket < toI32(slots.length)) {
|
|
254
|
-
slots[bucket] = slotWord(h, used - 1)
|
|
260
|
+
slots[bucket] = slotWord(h, used - 1)
|
|
255
261
|
} else {
|
|
256
|
-
fileEntry(slots, mask, h, used - 1)
|
|
262
|
+
fileEntry(slots, mask, h, used - 1)
|
|
257
263
|
}
|
|
258
|
-
}
|
|
264
|
+
}
|
|
259
265
|
|
|
260
266
|
/**
|
|
261
267
|
* A key-value table in insertion order, with JavaScript's semantics: keys are
|
|
@@ -265,60 +271,60 @@ const fileAppended = (slots: u32[], mask: i32, bucket: i32, h: u32, used: i32):
|
|
|
265
271
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: this is the global `Map`, which the compiler loads for a program that names it
|
|
266
272
|
export class Map<K, V> {
|
|
267
273
|
/** How many entries are live. The one field a program may read, and it may not write it. */
|
|
268
|
-
size: number = 0
|
|
274
|
+
size: number = 0
|
|
269
275
|
/** Bucket -> fingerprint and entry index plus one; see the header. */
|
|
270
|
-
slots: u32[]
|
|
276
|
+
slots: u32[]
|
|
271
277
|
/** `slots.length - 1`: the table is a power of two. */
|
|
272
|
-
mask: i32 = 7
|
|
278
|
+
mask: i32 = 7
|
|
273
279
|
/** Live entries, as an `i32` for the load and compaction arithmetic. */
|
|
274
|
-
live: i32 = 0
|
|
275
|
-
entryKeys: K[]
|
|
276
|
-
entryValues: V[]
|
|
280
|
+
live: i32 = 0
|
|
281
|
+
entryKeys: K[]
|
|
282
|
+
entryValues: V[]
|
|
277
283
|
/** The full hash of each entry; 0 once the entry is deleted. */
|
|
278
|
-
entryHashes: u32[]
|
|
284
|
+
entryHashes: u32[]
|
|
279
285
|
/**
|
|
280
286
|
* How many `for...of` loops are walking the table now. While it is above 0
|
|
281
287
|
* a rebuild doubles rather than compacts, and `clear` marks entries dead
|
|
282
288
|
* rather than truncating, so no entry moves under a walk's cursor (§6.2).
|
|
283
289
|
*/
|
|
284
|
-
walks: i32 = 0
|
|
290
|
+
walks: i32 = 0
|
|
285
291
|
|
|
286
292
|
constructor() {
|
|
287
|
-
this.slots = new Array<u32>(INITIAL_SLOTS)
|
|
288
|
-
this.entryKeys = []
|
|
289
|
-
this.entryValues = []
|
|
290
|
-
this.entryHashes = []
|
|
293
|
+
this.slots = new Array<u32>(INITIAL_SLOTS)
|
|
294
|
+
this.entryKeys = []
|
|
295
|
+
this.entryValues = []
|
|
296
|
+
this.entryHashes = []
|
|
291
297
|
}
|
|
292
298
|
|
|
293
299
|
/** The packed probe result for `key`: see `probeTable`, `foundAt` and `absentAt`. */
|
|
294
300
|
probe(key: K): i64 {
|
|
295
|
-
return probeTable(this.slots, this.mask, this.entryHashes, this.entryKeys, key)
|
|
301
|
+
return probeTable(this.slots, this.mask, this.entryHashes, this.entryKeys, key)
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
has(key: K): boolean {
|
|
299
|
-
return this.probe(key) >= 0
|
|
305
|
+
return this.probe(key) >= 0
|
|
300
306
|
}
|
|
301
307
|
|
|
302
308
|
/** Set `key` to `value`, in place when it is there and at the end when it is not: one probe. */
|
|
303
309
|
set(key: K, value: V): Map<K, V> {
|
|
304
|
-
const found = this.probe(key)
|
|
310
|
+
const found = this.probe(key)
|
|
305
311
|
if (found >= 0) {
|
|
306
|
-
this.setValueAt(toI32(found), value)
|
|
312
|
+
this.setValueAt(toI32(found), value)
|
|
307
313
|
} else {
|
|
308
|
-
this.insertAt(found, key, value)
|
|
314
|
+
this.insertAt(found, key, value)
|
|
309
315
|
}
|
|
310
|
-
return this
|
|
316
|
+
return this
|
|
311
317
|
}
|
|
312
318
|
|
|
313
319
|
delete(key: K): boolean {
|
|
314
|
-
const found = this.probe(key)
|
|
320
|
+
const found = this.probe(key)
|
|
315
321
|
if (found < 0) {
|
|
316
|
-
return false
|
|
322
|
+
return false
|
|
317
323
|
}
|
|
318
|
-
killEntry(this.slots, this.entryHashes, found)
|
|
319
|
-
this.live = this.live - 1
|
|
320
|
-
this.size = this.size - 1
|
|
321
|
-
return true
|
|
324
|
+
killEntry(this.slots, this.entryHashes, found)
|
|
325
|
+
this.live = this.live - 1
|
|
326
|
+
this.size = this.size - 1
|
|
327
|
+
return true
|
|
322
328
|
}
|
|
323
329
|
|
|
324
330
|
/**
|
|
@@ -326,16 +332,16 @@ export class Map<K, V> {
|
|
|
326
332
|
* truncated, or, while a loop walks the table, marked dead and kept (§6.1).
|
|
327
333
|
*/
|
|
328
334
|
clear(): void {
|
|
329
|
-
clearSlots(this.slots)
|
|
335
|
+
clearSlots(this.slots)
|
|
330
336
|
if (this.walks > 0) {
|
|
331
|
-
clearSlots(this.entryHashes)
|
|
337
|
+
clearSlots(this.entryHashes) // every entry dead, and the count kept
|
|
332
338
|
} else {
|
|
333
|
-
truncate(this.entryKeys)
|
|
334
|
-
truncate(this.entryValues)
|
|
335
|
-
truncate(this.entryHashes)
|
|
339
|
+
truncate(this.entryKeys)
|
|
340
|
+
truncate(this.entryValues)
|
|
341
|
+
truncate(this.entryHashes)
|
|
336
342
|
}
|
|
337
|
-
this.live = 0
|
|
338
|
-
this.size = 0
|
|
343
|
+
this.live = 0
|
|
344
|
+
this.size = 0
|
|
339
345
|
}
|
|
340
346
|
|
|
341
347
|
/**
|
|
@@ -346,66 +352,66 @@ export class Map<K, V> {
|
|
|
346
352
|
* that leaves the loop. Two stores a loop, and none per entry.
|
|
347
353
|
*/
|
|
348
354
|
walkOpen(): void {
|
|
349
|
-
this.walks = this.walks + 1
|
|
355
|
+
this.walks = this.walks + 1
|
|
350
356
|
}
|
|
351
357
|
|
|
352
358
|
walkNext(from: i32): i32 {
|
|
353
|
-
return nextLive(this.entryHashes, from)
|
|
359
|
+
return nextLive(this.entryHashes, from)
|
|
354
360
|
}
|
|
355
361
|
|
|
356
362
|
walkClose(): void {
|
|
357
|
-
this.walks = this.walks - 1
|
|
363
|
+
this.walks = this.walks - 1
|
|
358
364
|
}
|
|
359
365
|
|
|
360
366
|
/** The key of entry `index`, which `walkNext` answered. */
|
|
361
367
|
keyAt(index: i32): K {
|
|
362
368
|
if (index < 0 || index >= toI32(this.entryKeys.length)) {
|
|
363
|
-
panic("Map: no entry at this index")
|
|
369
|
+
panic("Map: no entry at this index")
|
|
364
370
|
}
|
|
365
|
-
return this.entryKeys[index]
|
|
371
|
+
return this.entryKeys[index]
|
|
366
372
|
}
|
|
367
373
|
|
|
368
374
|
/** The value of the entry a probe found, or a walk reached. */
|
|
369
375
|
valueAt(index: i32): V {
|
|
370
376
|
if (index < 0 || index >= toI32(this.entryValues.length)) {
|
|
371
|
-
panic("Map: no entry at this index")
|
|
377
|
+
panic("Map: no entry at this index")
|
|
372
378
|
}
|
|
373
|
-
return this.entryValues[index]
|
|
379
|
+
return this.entryValues[index]
|
|
374
380
|
}
|
|
375
381
|
|
|
376
382
|
/** Overwrite the value of the entry a probe found; it keeps its place. */
|
|
377
383
|
setValueAt(index: i32, value: V): void {
|
|
378
384
|
if (index >= 0 && index < toI32(this.entryValues.length)) {
|
|
379
|
-
this.entryValues[index] = value
|
|
385
|
+
this.entryValues[index] = value
|
|
380
386
|
}
|
|
381
387
|
}
|
|
382
388
|
|
|
383
389
|
/** Append an entry for `key` at the empty bucket an absent probe result names, reusing its hash. */
|
|
384
390
|
insertAt(absent: i64, key: K, value: V): void {
|
|
385
|
-
const packed = -1 - absent
|
|
386
|
-
let bucket = toI32(packed >> 32)
|
|
387
|
-
const h = toU32(packed)
|
|
391
|
+
const packed = -1 - absent
|
|
392
|
+
let bucket = toI32(packed >> 32)
|
|
393
|
+
const h = toU32(packed)
|
|
388
394
|
if (toI32(this.entryKeys.length) >= INDEX_CAP) {
|
|
389
395
|
// Dead entries hold the cap: compact them away, then find the bucket
|
|
390
396
|
// again. A walk defers compaction, so under one the cap is full.
|
|
391
397
|
if (this.live >= INDEX_CAP || this.walks > 0) {
|
|
392
|
-
panic("Map maximum size exceeded")
|
|
398
|
+
panic("Map maximum size exceeded")
|
|
393
399
|
}
|
|
394
|
-
this.rebuild()
|
|
395
|
-
bucket = -1
|
|
400
|
+
this.rebuild()
|
|
401
|
+
bucket = -1
|
|
396
402
|
}
|
|
397
|
-
this.entryKeys.push(key)
|
|
398
|
-
this.entryValues.push(value)
|
|
399
|
-
this.entryHashes.push(h)
|
|
400
|
-
this.live = this.live + 1
|
|
401
|
-
this.size = this.size + 1
|
|
403
|
+
this.entryKeys.push(storedKey(key))
|
|
404
|
+
this.entryValues.push(value)
|
|
405
|
+
this.entryHashes.push(h)
|
|
406
|
+
this.live = this.live + 1
|
|
407
|
+
this.size = this.size + 1
|
|
402
408
|
// Every entry takes a bucket, live or dead, until a rebuild, which files
|
|
403
409
|
// the new entry with the rest; otherwise it takes the bucket the probe found.
|
|
404
|
-
const used = toI32(this.entryKeys.length)
|
|
410
|
+
const used = toI32(this.entryKeys.length)
|
|
405
411
|
if (used * 4 > toI32(this.slots.length) * 3) {
|
|
406
|
-
this.rebuild()
|
|
412
|
+
this.rebuild()
|
|
407
413
|
} else {
|
|
408
|
-
fileAppended(this.slots, this.mask, bucket, h, used)
|
|
414
|
+
fileAppended(this.slots, this.mask, bucket, h, used)
|
|
409
415
|
}
|
|
410
416
|
}
|
|
411
417
|
|
|
@@ -415,17 +421,44 @@ export class Map<K, V> {
|
|
|
415
421
|
* first rebuild after the walk compacts (§6.2).
|
|
416
422
|
*/
|
|
417
423
|
rebuild(): void {
|
|
418
|
-
const used = toI32(this.entryKeys.length)
|
|
419
|
-
const walking = this.walks > 0
|
|
420
|
-
const slots = rebuiltSlots(this.slots, walking ? used : this.live, used)
|
|
424
|
+
const used = toI32(this.entryKeys.length)
|
|
425
|
+
const walking = this.walks > 0
|
|
426
|
+
const slots = rebuiltSlots(this.slots, walking ? used : this.live, used)
|
|
421
427
|
if (!walking && this.live < used) {
|
|
422
|
-
compactEntries(this.entryKeys, this.entryHashes)
|
|
423
|
-
compactEntries(this.entryValues, this.entryHashes)
|
|
424
|
-
compactHashes(this.entryHashes)
|
|
428
|
+
compactEntries(this.entryKeys, this.entryHashes)
|
|
429
|
+
compactEntries(this.entryValues, this.entryHashes)
|
|
430
|
+
compactHashes(this.entryHashes)
|
|
431
|
+
}
|
|
432
|
+
this.slots = slots
|
|
433
|
+
this.mask = toI32(slots.length) - 1
|
|
434
|
+
refile(slots, this.entryHashes)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* `reserve(m, n)` from `nish/map` (docs/wp32-map.md §9.2): grow the bucket
|
|
439
|
+
* table until `n` entries, dead ones included, fit under the load bound, so
|
|
440
|
+
* that the inserts up to `n` rebuild nothing. It only ever grows the buckets
|
|
441
|
+
* and re-files them from the stored hashes, so no entry moves and it is as
|
|
442
|
+
* safe under a walk as the doubling a walk already allows (§6.2). A count
|
|
443
|
+
* that is not positive, or not a number, does nothing, as `reserve` does
|
|
444
|
+
* under Node; one past the entry cap is the cap.
|
|
445
|
+
*/
|
|
446
|
+
reserveSlots(n: number): void {
|
|
447
|
+
if (!(n > 0)) {
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
const want: i32 = n >= 16777215 ? INDEX_CAP : toI32(n)
|
|
451
|
+
const have = toI32(this.slots.length)
|
|
452
|
+
let size = have
|
|
453
|
+
while (want * 4 > size * 3) {
|
|
454
|
+
size = size * 2
|
|
455
|
+
}
|
|
456
|
+
if (size > have) {
|
|
457
|
+
const slots = new Array<u32>(size)
|
|
458
|
+
this.slots = slots
|
|
459
|
+
this.mask = size - 1
|
|
460
|
+
refile(slots, this.entryHashes)
|
|
425
461
|
}
|
|
426
|
-
this.slots = slots;
|
|
427
|
-
this.mask = toI32(slots.length) - 1;
|
|
428
|
-
refile(slots, this.entryHashes);
|
|
429
462
|
}
|
|
430
463
|
}
|
|
431
464
|
|
|
@@ -433,116 +466,126 @@ export class Map<K, V> {
|
|
|
433
466
|
// biome-ignore lint/suspicious/noShadowRestrictedNames: this is the global `Set`, which the compiler loads for a program that names it
|
|
434
467
|
export class Set<T> {
|
|
435
468
|
/** How many elements are live. The one field a program may read, and it may not write it. */
|
|
436
|
-
size: number = 0
|
|
437
|
-
slots: u32[]
|
|
438
|
-
mask: i32 = 7
|
|
439
|
-
live: i32 = 0
|
|
440
|
-
entryKeys: T[]
|
|
441
|
-
entryHashes: u32[]
|
|
469
|
+
size: number = 0
|
|
470
|
+
slots: u32[]
|
|
471
|
+
mask: i32 = 7
|
|
472
|
+
live: i32 = 0
|
|
473
|
+
entryKeys: T[]
|
|
474
|
+
entryHashes: u32[]
|
|
442
475
|
/** `Map`'s walk count: how many `for...of` loops are walking the table now. */
|
|
443
|
-
walks: i32 = 0
|
|
476
|
+
walks: i32 = 0
|
|
444
477
|
|
|
445
478
|
constructor() {
|
|
446
|
-
this.slots = new Array<u32>(INITIAL_SLOTS)
|
|
447
|
-
this.entryKeys = []
|
|
448
|
-
this.entryHashes = []
|
|
479
|
+
this.slots = new Array<u32>(INITIAL_SLOTS)
|
|
480
|
+
this.entryKeys = []
|
|
481
|
+
this.entryHashes = []
|
|
449
482
|
}
|
|
450
483
|
|
|
451
484
|
probe(key: T): i64 {
|
|
452
|
-
return probeTable(this.slots, this.mask, this.entryHashes, this.entryKeys, key)
|
|
485
|
+
return probeTable(this.slots, this.mask, this.entryHashes, this.entryKeys, key)
|
|
453
486
|
}
|
|
454
487
|
|
|
455
488
|
has(key: T): boolean {
|
|
456
|
-
return this.probe(key) >= 0
|
|
489
|
+
return this.probe(key) >= 0
|
|
457
490
|
}
|
|
458
491
|
|
|
459
492
|
/** Add `key` at the end when it is not there already: one probe. */
|
|
460
493
|
add(key: T): Set<T> {
|
|
461
|
-
const found = this.probe(key)
|
|
494
|
+
const found = this.probe(key)
|
|
462
495
|
if (found < 0) {
|
|
463
|
-
this.insertAt(found, key)
|
|
496
|
+
this.insertAt(found, key)
|
|
464
497
|
}
|
|
465
|
-
return this
|
|
498
|
+
return this
|
|
466
499
|
}
|
|
467
500
|
|
|
468
501
|
delete(key: T): boolean {
|
|
469
|
-
const found = this.probe(key)
|
|
502
|
+
const found = this.probe(key)
|
|
470
503
|
if (found < 0) {
|
|
471
|
-
return false
|
|
504
|
+
return false
|
|
472
505
|
}
|
|
473
|
-
killEntry(this.slots, this.entryHashes, found)
|
|
474
|
-
this.live = this.live - 1
|
|
475
|
-
this.size = this.size - 1
|
|
476
|
-
return true
|
|
506
|
+
killEntry(this.slots, this.entryHashes, found)
|
|
507
|
+
this.live = this.live - 1
|
|
508
|
+
this.size = this.size - 1
|
|
509
|
+
return true
|
|
477
510
|
}
|
|
478
511
|
|
|
479
512
|
clear(): void {
|
|
480
|
-
clearSlots(this.slots)
|
|
513
|
+
clearSlots(this.slots)
|
|
481
514
|
if (this.walks > 0) {
|
|
482
|
-
clearSlots(this.entryHashes)
|
|
515
|
+
clearSlots(this.entryHashes) // every entry dead, and the count kept
|
|
483
516
|
} else {
|
|
484
|
-
truncate(this.entryKeys)
|
|
485
|
-
truncate(this.entryHashes)
|
|
517
|
+
truncate(this.entryKeys)
|
|
518
|
+
truncate(this.entryHashes)
|
|
486
519
|
}
|
|
487
|
-
this.live = 0
|
|
488
|
-
this.size = 0
|
|
520
|
+
this.live = 0
|
|
521
|
+
this.size = 0
|
|
489
522
|
}
|
|
490
523
|
|
|
491
524
|
/** `Map`'s walk: `for (const x of s)`, `s.keys()` and `s.values()` are all this one. */
|
|
492
525
|
walkOpen(): void {
|
|
493
|
-
this.walks = this.walks + 1
|
|
526
|
+
this.walks = this.walks + 1
|
|
494
527
|
}
|
|
495
528
|
|
|
496
529
|
walkNext(from: i32): i32 {
|
|
497
|
-
return nextLive(this.entryHashes, from)
|
|
530
|
+
return nextLive(this.entryHashes, from)
|
|
498
531
|
}
|
|
499
532
|
|
|
500
533
|
walkClose(): void {
|
|
501
|
-
this.walks = this.walks - 1
|
|
534
|
+
this.walks = this.walks - 1
|
|
502
535
|
}
|
|
503
536
|
|
|
504
537
|
keyAt(index: i32): T {
|
|
505
538
|
if (index < 0 || index >= toI32(this.entryKeys.length)) {
|
|
506
|
-
panic("Set: no entry at this index")
|
|
539
|
+
panic("Set: no entry at this index")
|
|
507
540
|
}
|
|
508
|
-
return this.entryKeys[index]
|
|
541
|
+
return this.entryKeys[index]
|
|
509
542
|
}
|
|
510
543
|
|
|
511
544
|
insertAt(absent: i64, key: T): void {
|
|
512
|
-
const packed = -1 - absent
|
|
513
|
-
let bucket = toI32(packed >> 32)
|
|
514
|
-
const h = toU32(packed)
|
|
545
|
+
const packed = -1 - absent
|
|
546
|
+
let bucket = toI32(packed >> 32)
|
|
547
|
+
const h = toU32(packed)
|
|
515
548
|
if (toI32(this.entryKeys.length) >= INDEX_CAP) {
|
|
516
549
|
if (this.live >= INDEX_CAP || this.walks > 0) {
|
|
517
|
-
panic("Set maximum size exceeded")
|
|
550
|
+
panic("Set maximum size exceeded")
|
|
518
551
|
}
|
|
519
|
-
this.rebuild()
|
|
520
|
-
bucket = -1
|
|
552
|
+
this.rebuild()
|
|
553
|
+
bucket = -1
|
|
521
554
|
}
|
|
522
|
-
this.entryKeys.push(key)
|
|
523
|
-
this.entryHashes.push(h)
|
|
524
|
-
this.live = this.live + 1
|
|
525
|
-
this.size = this.size + 1
|
|
555
|
+
this.entryKeys.push(storedKey(key))
|
|
556
|
+
this.entryHashes.push(h)
|
|
557
|
+
this.live = this.live + 1
|
|
558
|
+
this.size = this.size + 1
|
|
526
559
|
// Every entry takes a bucket, live or dead, until a rebuild, which files
|
|
527
560
|
// the new entry with the rest; otherwise it takes the bucket the probe found.
|
|
528
|
-
const used = toI32(this.entryKeys.length)
|
|
561
|
+
const used = toI32(this.entryKeys.length)
|
|
529
562
|
if (used * 4 > toI32(this.slots.length) * 3) {
|
|
530
|
-
this.rebuild()
|
|
563
|
+
this.rebuild()
|
|
531
564
|
} else {
|
|
532
|
-
fileAppended(this.slots, this.mask, bucket, h, used)
|
|
565
|
+
fileAppended(this.slots, this.mask, bucket, h, used)
|
|
533
566
|
}
|
|
534
567
|
}
|
|
535
568
|
|
|
536
569
|
rebuild(): void {
|
|
537
|
-
const used = toI32(this.entryKeys.length)
|
|
538
|
-
const walking = this.walks > 0
|
|
539
|
-
const slots = rebuiltSlots(this.slots, walking ? used : this.live, used)
|
|
570
|
+
const used = toI32(this.entryKeys.length)
|
|
571
|
+
const walking = this.walks > 0
|
|
572
|
+
const slots = rebuiltSlots(this.slots, walking ? used : this.live, used)
|
|
540
573
|
if (!walking && this.live < used) {
|
|
541
|
-
compactEntries(this.entryKeys, this.entryHashes)
|
|
542
|
-
compactHashes(this.entryHashes)
|
|
574
|
+
compactEntries(this.entryKeys, this.entryHashes)
|
|
575
|
+
compactHashes(this.entryHashes)
|
|
543
576
|
}
|
|
544
|
-
this.slots = slots
|
|
545
|
-
this.mask = toI32(slots.length) - 1
|
|
546
|
-
refile(slots, this.entryHashes)
|
|
577
|
+
this.slots = slots
|
|
578
|
+
this.mask = toI32(slots.length) - 1
|
|
579
|
+
refile(slots, this.entryHashes)
|
|
547
580
|
}
|
|
548
581
|
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* The key as an entry stores it: a float's -0 becomes +0, as ECMA-262's
|
|
585
|
+
* `Map.prototype.set` and `Set.prototype.add` store it, so a walk over the
|
|
586
|
+
* keys yields +0 whichever zero was inserted. Lowered in place, like `hashKey`
|
|
587
|
+
* and `sameKey`: `fadd` of +0 for a float, and nothing at all for every other
|
|
588
|
+
* key. It sits last in the file so that adding it moved no line a `-g` build
|
|
589
|
+
* of an earlier function records.
|
|
590
|
+
*/
|
|
591
|
+
const storedKey = <K>(key: K): K => key
|