@amritk/nish 0.10.0 → 0.12.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 +29 -13
- package/docs/AI.md +259 -16
- package/docs/INSTALL.md +22 -5
- package/package.json +7 -6
- package/runtime/LICENSE-ryu +23 -0
- package/runtime/nish.d.ts +11 -0
- package/runtime/nish.h +9 -1
- package/runtime/nish.mjs +13 -0
- package/runtime/runtime.c +10 -1
- package/scripts/ci-profile.mjs +3 -2
- package/scripts/gen-pow5-tables.py +5 -0
- package/std/README.md +17 -10
- package/std/collections.ts +585 -0
- package/std/map.ts +37 -0
- package/std/threads.ts +150 -0
package/scripts/ci-profile.mjs
CHANGED
|
@@ -67,10 +67,11 @@ const takeLine = (line) => {
|
|
|
67
67
|
|
|
68
68
|
const onData = (data) => {
|
|
69
69
|
buffered += data;
|
|
70
|
-
let i;
|
|
71
|
-
while (
|
|
70
|
+
let i = buffered.indexOf("\n");
|
|
71
|
+
while (i >= 0) {
|
|
72
72
|
takeLine(buffered.slice(0, i));
|
|
73
73
|
buffered = buffered.slice(i + 1);
|
|
74
|
+
i = buffered.indexOf("\n");
|
|
74
75
|
}
|
|
75
76
|
};
|
|
76
77
|
|
|
@@ -8,6 +8,11 @@ Prints the C to stdout; paste it over the `NISH_POW5_INV_SPLIT` and
|
|
|
8
8
|
the ~10 KB of constants in the runtime are *derived*, with exact integer
|
|
9
9
|
arithmetic, rather than transcribed from somewhere -- so a reader can check
|
|
10
10
|
them rather than trust them. See docs/wp15-performance.md section 7a.
|
|
11
|
+
|
|
12
|
+
The code that reads the tables is adapted from Ryu (ryu/d2s.c and
|
|
13
|
+
ryu/common.h, Copyright 2018 Ulf Adams) under the Boost Software License,
|
|
14
|
+
Version 1.0; runtime/LICENSE-ryu has the text and THIRD_PARTY_NOTICES.md the
|
|
15
|
+
entry.
|
|
11
16
|
"""
|
|
12
17
|
INV_BITCOUNT = 125
|
|
13
18
|
BITCOUNT = 125
|
package/std/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# `std/` — the standard library
|
|
2
2
|
|
|
3
3
|
Nish modules written in Nish, for Nish programs to import. There is no magic
|
|
4
|
-
here and
|
|
5
|
-
ordinary Nish source file, compiled as part of
|
|
6
|
-
and subject to the same rules as `examples/` or
|
|
7
|
-
([`docs/LANGUAGE.md`](../docs/LANGUAGE.md) is the style guide).
|
|
4
|
+
here and — with three exceptions, `threads.ts`, `collections.ts` and `map.ts` —
|
|
5
|
+
nothing the compiler knows about: a module in this directory is an ordinary Nish source file, compiled as part of
|
|
6
|
+
whatever program imports it, and subject to the same rules as `examples/` or
|
|
7
|
+
`self/` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md) is the style guide).
|
|
8
8
|
|
|
9
9
|
| Module | What it is |
|
|
10
10
|
| --- | --- |
|
|
@@ -12,6 +12,9 @@ and subject to the same rules as `examples/` or `self/`
|
|
|
12
12
|
| [`text.ts`](./text.ts) | the string operations a program would otherwise write inline: `splitLines`, `splitWhitespace`, `trim` and its halves, `contains`, `replaceAll`, and `firstDifference` over two arrays of lines |
|
|
13
13
|
| [`json.ts`](./json.ts) | `jsonField(object, name)`: the value of one field of one flat JSON object, which is the shape the compiler's own `--json` diagnostics have. A reader and not a parser — it answers text, answers `null` for a field that is not there, and does not validate |
|
|
14
14
|
| [`pair.ts`](./pair.ts) | `Pair<A, B>`: an interface with `first` and `second`, for a function that answers two values from one call. A type and nothing else — the caller writes an object literal at the return — and for returning two values rather than storing them side by side |
|
|
15
|
+
| [`collections.ts`](./collections.ts) | the global `Map<K, V>` and `Set<T>`: insertion-ordered tables whose buckets carry a hash fingerprint beside the entry index and whose entries keep their full hash, so every `get`, `set`, `add`, `has` and `delete` is one probe. `get` is not a method here: its `V | undefined` never crosses a call, so the compiler lowers it to `probe` and, where the key was found, `valueAt`. Nor are `keys()` and `values()`: an iterator is not a value, so a `for...of` over one is lowered to `walkOpen`, `walkNext`, `keyAt` or `valueAt`, and `walkClose`, and a count of live walks defers compaction until no loop is walking the table. A program never imports it: naming `Map` or `Set` loads it, and the compiler emits what a module uses of it into that module ([`docs/wp32-map.md`](../docs/wp32-map.md), [`docs/LANGUAGE.md`](../docs/LANGUAGE.md#map-and-set)). Its `hashKey`, `sameKey` and `storedKey` are lowered by the compiler per key type |
|
|
16
|
+
| [`map.ts`](./map.ts) | `reserve(m, n)` and `getOrInsert(m, k, v)` for the global `Map`. Their bodies are the meaning, and what runs under Node: `reserve` does nothing, and `getOrInsert` is a `get`, and a `set` of `v` when the key was missing. Natively the compiler lowers every call in place — `reserve` to the table's `reserveSlots`, which grows the buckets once so that `n` entries fit without a rebuild, and `getOrInsert` to one `probe` and a `valueAt` or an `insertAt` through its answer — so, like `collections.ts`, it writes no `.ll` of its own ([`docs/wp32-map.md`](../docs/wp32-map.md) §9.2, [`docs/LANGUAGE.md`](../docs/LANGUAGE.md#map-and-set)) |
|
|
17
|
+
| [`threads.ts`](./threads.ts) | `parallelMapInto(src, dst, f)` and `parallelReduce(src, f, identity)`: a function over every element of an array, on as many threads as the length is worth. Its bodies are the sequential meaning, which is what runs under Node; the compiler recognises the two templates by module and name, lowers the one loop in each onto `nish_parallel_range`, holds the function to the rules that make that safe, and compiles an importing program with `--threads` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#data-parallelism-nishthreads)). `tests/link/par_*` are its programs |
|
|
15
18
|
|
|
16
19
|
## How a program imports it
|
|
17
20
|
|
|
@@ -62,12 +65,16 @@ reasons that are only true of it:
|
|
|
62
65
|
package and is versioned with it, so "the `std/` beside this binary" is not a
|
|
63
66
|
guess a resolver makes — it is the only `std/` that can be correct for the
|
|
64
67
|
compiler reading it. No version can be skewed against it.
|
|
65
|
-
- It **is** what Node
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
- It **is** what Node and `tsc` resolve. The package is published as
|
|
69
|
+
`@amritk/nish`, so a bare `nish/text` is not a package self-reference on its
|
|
70
|
+
own; `runtime/nish.mjs`, the prelude a program runs under Node with
|
|
71
|
+
(`node --experimental-strip-types --import ./runtime/nish.mjs`), resolves
|
|
72
|
+
`nish/<module>` to `std/<module>.ts` beside itself, and the repository's
|
|
73
|
+
`tsconfig.json` maps `nish/*` to `./std/*`, which is what gives `tsc` and an
|
|
74
|
+
editor go-to-definition into the real source. `package.json` still declares
|
|
75
|
+
`"./*": "./std/*.ts"` in `exports`, so `@amritk/nish/text` reaches the same
|
|
76
|
+
file. The compiler short-circuits to that answer rather than walking
|
|
77
|
+
`node_modules` to reach it.
|
|
71
78
|
|
|
72
79
|
So this is WP21's first slice rather than a detour around it: the spelling is
|
|
73
80
|
the one WP21 specifies, and what is still missing is resolution for specifiers
|
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `std/collections` — the global `Map` and `Set` (docs/wp32-map.md).
|
|
3
|
+
*
|
|
4
|
+
* A program does not import this module. Naming `Map` or `Set` is what loads
|
|
5
|
+
* it, unless the module declares or imports a `Map` or `Set` of its own, and
|
|
6
|
+
* the compiler emits every instance used, and every function here that it
|
|
7
|
+
* reaches, into the module that uses it as `internal` functions. So this file
|
|
8
|
+
* writes no `.ll` of its own, and a one-file program that names `Map` is still
|
|
9
|
+
* one module (§4.1). Under Node, and under `tsc`, `Map` and `Set` are the
|
|
10
|
+
* platform's, so nothing here runs there.
|
|
11
|
+
*
|
|
12
|
+
* **The layout is §2's.** Entries sit in insertion order in parallel arrays,
|
|
13
|
+
* `entryKeys`, `entryValues` and `entryHashes`, and `entryHashes` holds the
|
|
14
|
+
* full 32-bit hash of each. The bucket table `slots` is open addressing with
|
|
15
|
+
* linear probing, and a bucket is one `u32`:
|
|
16
|
+
*
|
|
17
|
+
* bits 31..24 the top eight bits of the key's hash (the fingerprint)
|
|
18
|
+
* bits 23..0 the entry's index plus one
|
|
19
|
+
*
|
|
20
|
+
* An empty bucket is 0. A deleted entry's bucket is `0x01000000` (16777216),
|
|
21
|
+
* fingerprint 1 and index field 0, which no live entry has; its stored hash is
|
|
22
|
+
* set to 0, which is why a computed hash of 0 is moved to 1. A probe reads the
|
|
23
|
+
* entry arrays only when a bucket's fingerprint matches, and compares the
|
|
24
|
+
* stored hash before the key, so a miss almost never touches a key, and a hit
|
|
25
|
+
* compares one. Growth and compaction re-file the buckets from the stored
|
|
26
|
+
* hashes and never hash a key again.
|
|
27
|
+
*
|
|
28
|
+
* **Every operation is one probe.** `probe` answers a packed `i64`: the entry
|
|
29
|
+
* it found and the bucket that points at it, or the empty bucket it stopped at
|
|
30
|
+
* and the key's hash. `set` and `add` insert through that result, and nothing
|
|
31
|
+
* here asks `has` and then `set`. `probe`, `valueAt`, `setValueAt` and
|
|
32
|
+
* `insertAt` are the pieces a fused lookup writes through, and they write
|
|
33
|
+
* nothing but what their names say: `probe` writes no memory at all.
|
|
34
|
+
*
|
|
35
|
+
* A program sees only the JavaScript members (§7), `size`, `get`, `set`/`add`,
|
|
36
|
+
* `has`, `delete` and `clear`, and `keys()` and `values()` as the iterable of
|
|
37
|
+
* a `for...of`. Neither `get` nor the iterators has a method here: `get`'s
|
|
38
|
+
* `V | undefined` never crosses a call, so the compiler lowers it to `probe`
|
|
39
|
+
* and, where found, `valueAt` (§3.2), and an iterator is not a value, so a
|
|
40
|
+
* `for...of` over one is lowered to `walkOpen`, `walkNext`, `keyAt` or
|
|
41
|
+
* `valueAt`, and `walkClose` (§6.2). The rest of this file is refused by name
|
|
42
|
+
* outside it.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The most entries a table holds, dead ones included until a rebuild: 2^24 - 1,
|
|
47
|
+
* what the 24-bit index field holds as an index plus one. Node's own `Map` and
|
|
48
|
+
* `Set` hold one more, 2^24, and throw on the next insert.
|
|
49
|
+
*/
|
|
50
|
+
const INDEX_CAP: i32 = 16777215;
|
|
51
|
+
|
|
52
|
+
/** The first bucket count. A power of two, as every bucket count is. */
|
|
53
|
+
const INITIAL_SLOTS: i32 = 8;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The key's hash, never 0: FNV-1a over a string's bytes, murmur3's `fmix32`
|
|
57
|
+
* for an integer of 32 bits or fewer, `fmix64` folded to 32 bits for a 64-bit
|
|
58
|
+
* integer, a float (normalised first, so that -0 and +0, and every NaN, hash
|
|
59
|
+
* alike) and a class instance's address (§5.2). FNV, MurmurHash3 and their
|
|
60
|
+
* constants are public domain.
|
|
61
|
+
*
|
|
62
|
+
* The body is never emitted. Every call is lowered in place, per key type, by
|
|
63
|
+
* `self/emit_map.ts`; the constant is what the checker and the whole-program
|
|
64
|
+
* facts see, and the facts of the function that calls it are what decide its
|
|
65
|
+
* attributes, because every call is inside a probe that reads the table.
|
|
66
|
+
*/
|
|
67
|
+
const hashKey = <K>(key: K): u32 => 1;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* JavaScript's key equality, SameValueZero: `===`, except that NaN equals NaN.
|
|
71
|
+
* Lowered in place per key type as well: `nish_str_eq` for a string, one
|
|
72
|
+
* `icmp` for an integer, a boolean, an enum and a class instance, and for a
|
|
73
|
+
* float `a == b` or both unordered.
|
|
74
|
+
*/
|
|
75
|
+
const sameKey = <K>(a: K, b: K): boolean => a === b;
|
|
76
|
+
|
|
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;
|
|
79
|
+
|
|
80
|
+
/** The bucket word for entry `index` of hash `h`. */
|
|
81
|
+
const slotWord = (h: u32, index: i32): u32 => ((h >>> 24) << 24) | toU32(index + 1);
|
|
82
|
+
|
|
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);
|
|
85
|
+
|
|
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));
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The one probe, for both classes: linear probing from `h`'s home bucket to
|
|
91
|
+
* the bucket pointing at `key`, or to the first empty one. A tombstone is
|
|
92
|
+
* walked past — its index field is 0, so its entry index is -1 and the range
|
|
93
|
+
* test turns it away — and the load bound keeps a quarter of the buckets
|
|
94
|
+
* empty, so the walk ends.
|
|
95
|
+
*/
|
|
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);
|
|
100
|
+
// The length is read in the condition rather than once: the key compare is
|
|
101
|
+
// a call, and a call ends every length fact the bounds proof holds.
|
|
102
|
+
while (bucket >= 0 && bucket < toI32(slots.length)) {
|
|
103
|
+
const word = slots[bucket];
|
|
104
|
+
if (word === 0) {
|
|
105
|
+
return absentAt(bucket, h);
|
|
106
|
+
}
|
|
107
|
+
if (word >>> 24 === fingerprint) {
|
|
108
|
+
const at = toI32(word & 16777215) - 1;
|
|
109
|
+
if (at >= 0 && at < toI32(hashes.length) && hashes[at] === h && at < toI32(keys.length) && sameKey(keys[at], key)) {
|
|
110
|
+
return foundAt(bucket, at);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
bucket = (bucket + 1) & mask;
|
|
114
|
+
}
|
|
115
|
+
panic("collections: a probe ran out of buckets");
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Point the first empty bucket from `h`'s home at entry `index`. A rebuild's
|
|
120
|
+
* re-filing: every key is already known to be distinct, so no key is
|
|
121
|
+
* compared, and the stored hash is all it needs.
|
|
122
|
+
*/
|
|
123
|
+
const fileEntry = (slots: u32[], mask: i32, h: u32, index: i32): void => {
|
|
124
|
+
const word = slotWord(h, index);
|
|
125
|
+
let bucket = homeBucket(h, mask);
|
|
126
|
+
while (bucket >= 0 && bucket < toI32(slots.length)) {
|
|
127
|
+
if (slots[bucket] === 0) {
|
|
128
|
+
slots[bucket] = word;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
bucket = (bucket + 1) & mask;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/** Slide the live entries of `items` down over the dead ones, in order, and drop the tail. */
|
|
136
|
+
const compactEntries = <T>(items: T[], hashes: u32[]): void => {
|
|
137
|
+
const used = toI32(items.length);
|
|
138
|
+
let to: i32 = 0;
|
|
139
|
+
for (let from: i32 = 0; from < used && from < toI32(hashes.length); from++) {
|
|
140
|
+
if (hashes[from] !== 0 && to >= 0 && to < used && from < toI32(items.length)) {
|
|
141
|
+
items[to] = items[from];
|
|
142
|
+
to++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
while (toI32(items.length) > to) {
|
|
146
|
+
items.pop();
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** The stored hashes compacted the same way, last, since the two above read them. */
|
|
151
|
+
const compactHashes = (hashes: u32[]): void => {
|
|
152
|
+
const used = toI32(hashes.length);
|
|
153
|
+
let to: i32 = 0;
|
|
154
|
+
for (let from: i32 = 0; from < used; from++) {
|
|
155
|
+
const h = hashes[from];
|
|
156
|
+
if (h !== 0 && to >= 0 && to < used) {
|
|
157
|
+
hashes[to] = h;
|
|
158
|
+
to++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
while (toI32(hashes.length) > to) {
|
|
162
|
+
hashes.pop();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The bucket table after a rebuild of `live` entries out of `used`. More than
|
|
168
|
+
* half dead compacts at the same size and clears the table in place — a new
|
|
169
|
+
* array would leave the old one in the arena, once per compaction, which a
|
|
170
|
+
* table that churns forever cannot afford (§6.1). Otherwise it doubles, and
|
|
171
|
+
* the table never shrinks.
|
|
172
|
+
*/
|
|
173
|
+
const rebuiltSlots = (slots: u32[], live: i32, used: i32): u32[] => {
|
|
174
|
+
const n = toI32(slots.length);
|
|
175
|
+
if (live * 2 < used) {
|
|
176
|
+
clearSlots(slots);
|
|
177
|
+
return slots;
|
|
178
|
+
}
|
|
179
|
+
return new Array<u32>(n * 2);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Re-file every live entry from its stored hash. After a compaction none is
|
|
184
|
+
* dead; after a rebuild during a walk, which does not compact, a dead entry
|
|
185
|
+
* keeps its place and takes no bucket, since no probe can find it.
|
|
186
|
+
*/
|
|
187
|
+
const refile = (slots: u32[], hashes: u32[]): void => {
|
|
188
|
+
const mask = toI32(slots.length) - 1;
|
|
189
|
+
for (let i: i32 = 0; i < toI32(hashes.length); i++) {
|
|
190
|
+
const h = hashes[i];
|
|
191
|
+
if (h !== 0) {
|
|
192
|
+
fileEntry(slots, mask, h, i);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* `delete`'s write through a found probe result: the bucket becomes a
|
|
199
|
+
* tombstone and the entry's stored hash 0. The key and value stay in the entry
|
|
200
|
+
* until a rebuild compacts them away (§6.1).
|
|
201
|
+
*/
|
|
202
|
+
const killEntry = (slots: u32[], hashes: u32[], found: i64): void => {
|
|
203
|
+
const at = toI32(found);
|
|
204
|
+
const bucket = toI32(found >> 32);
|
|
205
|
+
if (bucket >= 0 && bucket < toI32(slots.length)) {
|
|
206
|
+
slots[bucket] = 16777216;
|
|
207
|
+
}
|
|
208
|
+
if (at >= 0 && at < toI32(hashes.length)) {
|
|
209
|
+
hashes[at] = 0;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Zero every element in place: the buckets, which emptying a table and
|
|
215
|
+
* compacting one both start with, and under a walk the stored hashes, which
|
|
216
|
+
* is how `clear` marks every entry dead without moving one (§6.1).
|
|
217
|
+
*/
|
|
218
|
+
const clearSlots = (slots: u32[]): void => {
|
|
219
|
+
for (let i: i32 = 0; i < toI32(slots.length); i++) {
|
|
220
|
+
slots[i] = 0;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The index of the first live entry at `from` or after it, or -1 when there is
|
|
226
|
+
* none: a `for...of` walk's step. It reads the entry count on every call, so
|
|
227
|
+
* an entry appended during the walk is reached, and it skips a dead entry,
|
|
228
|
+
* whose stored hash is 0, so a key deleted before the walk reaches it is not
|
|
229
|
+
* visited (docs/wp32-map.md §6.2).
|
|
230
|
+
*/
|
|
231
|
+
const nextLive = (hashes: u32[], from: i32): i32 => {
|
|
232
|
+
for (let i: i32 = from; i >= 0 && i < toI32(hashes.length); i++) {
|
|
233
|
+
if (hashes[i] !== 0) {
|
|
234
|
+
return i;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return -1;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/** Drop every element of `items`, keeping its capacity. */
|
|
241
|
+
const truncate = <T>(items: T[]): void => {
|
|
242
|
+
while (toI32(items.length) > 0) {
|
|
243
|
+
items.pop();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Point bucket `bucket`, the empty one an absent probe stopped at, at the entry
|
|
249
|
+
* just appended as index `used - 1`; or, when a rebuild made room first and
|
|
250
|
+
* moved the buckets (`bucket` is -1), file it from its hash.
|
|
251
|
+
*/
|
|
252
|
+
const fileAppended = (slots: u32[], mask: i32, bucket: i32, h: u32, used: i32): void => {
|
|
253
|
+
if (bucket >= 0 && bucket < toI32(slots.length)) {
|
|
254
|
+
slots[bucket] = slotWord(h, used - 1);
|
|
255
|
+
} else {
|
|
256
|
+
fileEntry(slots, mask, h, used - 1);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* A key-value table in insertion order, with JavaScript's semantics: keys are
|
|
262
|
+
* compared by SameValueZero, a key set again keeps its place, and a deleted key
|
|
263
|
+
* set again goes to the end.
|
|
264
|
+
*/
|
|
265
|
+
// biome-ignore lint/suspicious/noShadowRestrictedNames: this is the global `Map`, which the compiler loads for a program that names it
|
|
266
|
+
export class Map<K, V> {
|
|
267
|
+
/** How many entries are live. The one field a program may read, and it may not write it. */
|
|
268
|
+
size: number = 0;
|
|
269
|
+
/** Bucket -> fingerprint and entry index plus one; see the header. */
|
|
270
|
+
slots: u32[];
|
|
271
|
+
/** `slots.length - 1`: the table is a power of two. */
|
|
272
|
+
mask: i32 = 7;
|
|
273
|
+
/** Live entries, as an `i32` for the load and compaction arithmetic. */
|
|
274
|
+
live: i32 = 0;
|
|
275
|
+
entryKeys: K[];
|
|
276
|
+
entryValues: V[];
|
|
277
|
+
/** The full hash of each entry; 0 once the entry is deleted. */
|
|
278
|
+
entryHashes: u32[];
|
|
279
|
+
/**
|
|
280
|
+
* How many `for...of` loops are walking the table now. While it is above 0
|
|
281
|
+
* a rebuild doubles rather than compacts, and `clear` marks entries dead
|
|
282
|
+
* rather than truncating, so no entry moves under a walk's cursor (§6.2).
|
|
283
|
+
*/
|
|
284
|
+
walks: i32 = 0;
|
|
285
|
+
|
|
286
|
+
constructor() {
|
|
287
|
+
this.slots = new Array<u32>(INITIAL_SLOTS);
|
|
288
|
+
this.entryKeys = [];
|
|
289
|
+
this.entryValues = [];
|
|
290
|
+
this.entryHashes = [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** The packed probe result for `key`: see `probeTable`, `foundAt` and `absentAt`. */
|
|
294
|
+
probe(key: K): i64 {
|
|
295
|
+
return probeTable(this.slots, this.mask, this.entryHashes, this.entryKeys, key);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
has(key: K): boolean {
|
|
299
|
+
return this.probe(key) >= 0;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Set `key` to `value`, in place when it is there and at the end when it is not: one probe. */
|
|
303
|
+
set(key: K, value: V): Map<K, V> {
|
|
304
|
+
const found = this.probe(key);
|
|
305
|
+
if (found >= 0) {
|
|
306
|
+
this.setValueAt(toI32(found), value);
|
|
307
|
+
} else {
|
|
308
|
+
this.insertAt(found, key, value);
|
|
309
|
+
}
|
|
310
|
+
return this;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
delete(key: K): boolean {
|
|
314
|
+
const found = this.probe(key);
|
|
315
|
+
if (found < 0) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
killEntry(this.slots, this.entryHashes, found);
|
|
319
|
+
this.live = this.live - 1;
|
|
320
|
+
this.size = this.size - 1;
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Empty the table in place: the buckets are zeroed and the entries
|
|
326
|
+
* truncated, or, while a loop walks the table, marked dead and kept (§6.1).
|
|
327
|
+
*/
|
|
328
|
+
clear(): void {
|
|
329
|
+
clearSlots(this.slots);
|
|
330
|
+
if (this.walks > 0) {
|
|
331
|
+
clearSlots(this.entryHashes); // every entry dead, and the count kept
|
|
332
|
+
} else {
|
|
333
|
+
truncate(this.entryKeys);
|
|
334
|
+
truncate(this.entryValues);
|
|
335
|
+
truncate(this.entryHashes);
|
|
336
|
+
}
|
|
337
|
+
this.live = 0;
|
|
338
|
+
this.size = 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The four pieces a `for...of` over `keys()` or `values()` is lowered to
|
|
343
|
+
* (`emitForOf`, docs/wp32-map.md §6.2): `walkOpen` where the loop is
|
|
344
|
+
* entered, `walkNext` for the first live entry and after each pass,
|
|
345
|
+
* `keyAt` or `valueAt` for the loop variable, and `walkClose` on every edge
|
|
346
|
+
* that leaves the loop. Two stores a loop, and none per entry.
|
|
347
|
+
*/
|
|
348
|
+
walkOpen(): void {
|
|
349
|
+
this.walks = this.walks + 1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
walkNext(from: i32): i32 {
|
|
353
|
+
return nextLive(this.entryHashes, from);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
walkClose(): void {
|
|
357
|
+
this.walks = this.walks - 1;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** The key of entry `index`, which `walkNext` answered. */
|
|
361
|
+
keyAt(index: i32): K {
|
|
362
|
+
if (index < 0 || index >= toI32(this.entryKeys.length)) {
|
|
363
|
+
panic("Map: no entry at this index");
|
|
364
|
+
}
|
|
365
|
+
return this.entryKeys[index];
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** The value of the entry a probe found, or a walk reached. */
|
|
369
|
+
valueAt(index: i32): V {
|
|
370
|
+
if (index < 0 || index >= toI32(this.entryValues.length)) {
|
|
371
|
+
panic("Map: no entry at this index");
|
|
372
|
+
}
|
|
373
|
+
return this.entryValues[index];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Overwrite the value of the entry a probe found; it keeps its place. */
|
|
377
|
+
setValueAt(index: i32, value: V): void {
|
|
378
|
+
if (index >= 0 && index < toI32(this.entryValues.length)) {
|
|
379
|
+
this.entryValues[index] = value;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Append an entry for `key` at the empty bucket an absent probe result names, reusing its hash. */
|
|
384
|
+
insertAt(absent: i64, key: K, value: V): void {
|
|
385
|
+
const packed = -1 - absent;
|
|
386
|
+
let bucket = toI32(packed >> 32);
|
|
387
|
+
const h = toU32(packed);
|
|
388
|
+
if (toI32(this.entryKeys.length) >= INDEX_CAP) {
|
|
389
|
+
// Dead entries hold the cap: compact them away, then find the bucket
|
|
390
|
+
// again. A walk defers compaction, so under one the cap is full.
|
|
391
|
+
if (this.live >= INDEX_CAP || this.walks > 0) {
|
|
392
|
+
panic("Map maximum size exceeded");
|
|
393
|
+
}
|
|
394
|
+
this.rebuild();
|
|
395
|
+
bucket = -1;
|
|
396
|
+
}
|
|
397
|
+
this.entryKeys.push(storedKey(key));
|
|
398
|
+
this.entryValues.push(value);
|
|
399
|
+
this.entryHashes.push(h);
|
|
400
|
+
this.live = this.live + 1;
|
|
401
|
+
this.size = this.size + 1;
|
|
402
|
+
// Every entry takes a bucket, live or dead, until a rebuild, which files
|
|
403
|
+
// the new entry with the rest; otherwise it takes the bucket the probe found.
|
|
404
|
+
const used = toI32(this.entryKeys.length);
|
|
405
|
+
if (used * 4 > toI32(this.slots.length) * 3) {
|
|
406
|
+
this.rebuild();
|
|
407
|
+
} else {
|
|
408
|
+
fileAppended(this.slots, this.mask, bucket, h, used);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Compact or double, then re-file the buckets from the stored hashes (§6.1).
|
|
414
|
+
* While a loop walks the table it always doubles and moves no entry, and the
|
|
415
|
+
* first rebuild after the walk compacts (§6.2).
|
|
416
|
+
*/
|
|
417
|
+
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);
|
|
421
|
+
if (!walking && this.live < used) {
|
|
422
|
+
compactEntries(this.entryKeys, this.entryHashes);
|
|
423
|
+
compactEntries(this.entryValues, this.entryHashes);
|
|
424
|
+
compactHashes(this.entryHashes);
|
|
425
|
+
}
|
|
426
|
+
this.slots = slots;
|
|
427
|
+
this.mask = toI32(slots.length) - 1;
|
|
428
|
+
refile(slots, this.entryHashes);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* `reserve(m, n)` from `nish/map` (docs/wp32-map.md §9.2): grow the bucket
|
|
433
|
+
* table until `n` entries, dead ones included, fit under the load bound, so
|
|
434
|
+
* that the inserts up to `n` rebuild nothing. It only ever grows the buckets
|
|
435
|
+
* and re-files them from the stored hashes, so no entry moves and it is as
|
|
436
|
+
* safe under a walk as the doubling a walk already allows (§6.2). A count
|
|
437
|
+
* that is not positive, or not a number, does nothing, as `reserve` does
|
|
438
|
+
* under Node; one past the entry cap is the cap.
|
|
439
|
+
*/
|
|
440
|
+
reserveSlots(n: number): void {
|
|
441
|
+
if (!(n > 0)) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const want: i32 = n >= 16777215 ? INDEX_CAP : toI32(n);
|
|
445
|
+
const have = toI32(this.slots.length);
|
|
446
|
+
let size = have;
|
|
447
|
+
while (want * 4 > size * 3) {
|
|
448
|
+
size = size * 2;
|
|
449
|
+
}
|
|
450
|
+
if (size > have) {
|
|
451
|
+
const slots = new Array<u32>(size);
|
|
452
|
+
this.slots = slots;
|
|
453
|
+
this.mask = size - 1;
|
|
454
|
+
refile(slots, this.entryHashes);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** A set of keys in insertion order: `Map`'s table with no values, and the same probe. */
|
|
460
|
+
// biome-ignore lint/suspicious/noShadowRestrictedNames: this is the global `Set`, which the compiler loads for a program that names it
|
|
461
|
+
export class Set<T> {
|
|
462
|
+
/** How many elements are live. The one field a program may read, and it may not write it. */
|
|
463
|
+
size: number = 0;
|
|
464
|
+
slots: u32[];
|
|
465
|
+
mask: i32 = 7;
|
|
466
|
+
live: i32 = 0;
|
|
467
|
+
entryKeys: T[];
|
|
468
|
+
entryHashes: u32[];
|
|
469
|
+
/** `Map`'s walk count: how many `for...of` loops are walking the table now. */
|
|
470
|
+
walks: i32 = 0;
|
|
471
|
+
|
|
472
|
+
constructor() {
|
|
473
|
+
this.slots = new Array<u32>(INITIAL_SLOTS);
|
|
474
|
+
this.entryKeys = [];
|
|
475
|
+
this.entryHashes = [];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
probe(key: T): i64 {
|
|
479
|
+
return probeTable(this.slots, this.mask, this.entryHashes, this.entryKeys, key);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
has(key: T): boolean {
|
|
483
|
+
return this.probe(key) >= 0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Add `key` at the end when it is not there already: one probe. */
|
|
487
|
+
add(key: T): Set<T> {
|
|
488
|
+
const found = this.probe(key);
|
|
489
|
+
if (found < 0) {
|
|
490
|
+
this.insertAt(found, key);
|
|
491
|
+
}
|
|
492
|
+
return this;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
delete(key: T): boolean {
|
|
496
|
+
const found = this.probe(key);
|
|
497
|
+
if (found < 0) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
killEntry(this.slots, this.entryHashes, found);
|
|
501
|
+
this.live = this.live - 1;
|
|
502
|
+
this.size = this.size - 1;
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
clear(): void {
|
|
507
|
+
clearSlots(this.slots);
|
|
508
|
+
if (this.walks > 0) {
|
|
509
|
+
clearSlots(this.entryHashes); // every entry dead, and the count kept
|
|
510
|
+
} else {
|
|
511
|
+
truncate(this.entryKeys);
|
|
512
|
+
truncate(this.entryHashes);
|
|
513
|
+
}
|
|
514
|
+
this.live = 0;
|
|
515
|
+
this.size = 0;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** `Map`'s walk: `for (const x of s)`, `s.keys()` and `s.values()` are all this one. */
|
|
519
|
+
walkOpen(): void {
|
|
520
|
+
this.walks = this.walks + 1;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
walkNext(from: i32): i32 {
|
|
524
|
+
return nextLive(this.entryHashes, from);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
walkClose(): void {
|
|
528
|
+
this.walks = this.walks - 1;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
keyAt(index: i32): T {
|
|
532
|
+
if (index < 0 || index >= toI32(this.entryKeys.length)) {
|
|
533
|
+
panic("Set: no entry at this index");
|
|
534
|
+
}
|
|
535
|
+
return this.entryKeys[index];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
insertAt(absent: i64, key: T): void {
|
|
539
|
+
const packed = -1 - absent;
|
|
540
|
+
let bucket = toI32(packed >> 32);
|
|
541
|
+
const h = toU32(packed);
|
|
542
|
+
if (toI32(this.entryKeys.length) >= INDEX_CAP) {
|
|
543
|
+
if (this.live >= INDEX_CAP || this.walks > 0) {
|
|
544
|
+
panic("Set maximum size exceeded");
|
|
545
|
+
}
|
|
546
|
+
this.rebuild();
|
|
547
|
+
bucket = -1;
|
|
548
|
+
}
|
|
549
|
+
this.entryKeys.push(storedKey(key));
|
|
550
|
+
this.entryHashes.push(h);
|
|
551
|
+
this.live = this.live + 1;
|
|
552
|
+
this.size = this.size + 1;
|
|
553
|
+
// Every entry takes a bucket, live or dead, until a rebuild, which files
|
|
554
|
+
// the new entry with the rest; otherwise it takes the bucket the probe found.
|
|
555
|
+
const used = toI32(this.entryKeys.length);
|
|
556
|
+
if (used * 4 > toI32(this.slots.length) * 3) {
|
|
557
|
+
this.rebuild();
|
|
558
|
+
} else {
|
|
559
|
+
fileAppended(this.slots, this.mask, bucket, h, used);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
rebuild(): void {
|
|
564
|
+
const used = toI32(this.entryKeys.length);
|
|
565
|
+
const walking = this.walks > 0;
|
|
566
|
+
const slots = rebuiltSlots(this.slots, walking ? used : this.live, used);
|
|
567
|
+
if (!walking && this.live < used) {
|
|
568
|
+
compactEntries(this.entryKeys, this.entryHashes);
|
|
569
|
+
compactHashes(this.entryHashes);
|
|
570
|
+
}
|
|
571
|
+
this.slots = slots;
|
|
572
|
+
this.mask = toI32(slots.length) - 1;
|
|
573
|
+
refile(slots, this.entryHashes);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* The key as an entry stores it: a float's -0 becomes +0, as ECMA-262's
|
|
579
|
+
* `Map.prototype.set` and `Set.prototype.add` store it, so a walk over the
|
|
580
|
+
* keys yields +0 whichever zero was inserted. Lowered in place, like `hashKey`
|
|
581
|
+
* and `sameKey`: `fadd` of +0 for a float, and nothing at all for every other
|
|
582
|
+
* key. It sits last in the file so that adding it moved no line a `-g` build
|
|
583
|
+
* of an earlier function records.
|
|
584
|
+
*/
|
|
585
|
+
const storedKey = <K>(key: K): K => key;
|