@amritk/nish 0.10.0 → 0.11.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 CHANGED
@@ -49,22 +49,19 @@ Requirements: Node.js 22.18+ and, to produce binaries, clang (LLVM 18) + lld;
49
49
  per-OS install commands are in [docs/INSTALL.md](docs/INSTALL.md).
50
50
 
51
51
  ```bash
52
- # Once this is published, the whole install is either of:
53
- # npm install -g @amritk/nish
54
- # curl -fsSL https://raw.githubusercontent.com/amritk/nish/main/install.sh | sh
52
+ npm install -g @amritk/nish # or per project: npm i -D @amritk/nish
53
+ npx @amritk/nish --version # or run it without installing
54
+ # or, with no Node at all:
55
+ curl -fsSL https://raw.githubusercontent.com/amritk/nish/main/install.sh | sh
55
56
  # `nish` on the registry is an unrelated package from 2014, so this one is
56
- # scoped; the command it installs is still `nish`. Nothing is published yet,
57
- # so take it from a release -- the npm tarball, plus the prebuilt native
58
- # compiler for your machine, which is what the scoped install would fetch.
59
- base=https://github.com/amritk/nish/releases/download/v0.4.0
60
- curl -LO $base/amritk-nish-0.4.0.tgz
61
- curl -LO $base/amritk-nish-x86_64-linux-0.4.0.tgz # or the row for your machine
62
- npm install -g ./amritk-nish-0.4.0.tgz ./amritk-nish-x86_64-linux-0.4.0.tgz
57
+ # scoped; the command it installs is still `nish`. npm fetches the prebuilt
58
+ # native compiler for your machine alongside it.
63
59
  ```
64
60
 
65
61
  > [!NOTE]
66
- > **Install the pair.** The main package is a launcher and carries no compiler:
67
- > installed on its own it has nothing to hand over to and says so, exiting 3.
62
+ > **From a release tarball rather than the registry, install the pair.** The
63
+ > main package is a launcher and carries no compiler: installed on its own it
64
+ > has nothing to hand over to and says so, exiting 3.
68
65
  > The same is true on a platform this project publishes no binary for — musl,
69
66
  > FreeBSD, 32-bit anything — which since 0.6.0 gets a diagnostic naming the four
70
67
  > that do rather than the TypeScript compiler under Node that used to ship
@@ -550,4 +547,5 @@ coding agents are in [AGENTS.md](AGENTS.md) and [`.claude/`](.claude/).
550
547
 
551
548
  ## License
552
549
 
553
- MIT, see [LICENSE](LICENSE).
550
+ MIT, see [LICENSE](LICENSE). Third-party code in the repository keeps its own
551
+ licence; see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
package/docs/AI.md CHANGED
@@ -111,14 +111,14 @@ rejects. This table is the highest-value part of the page.
111
111
  | `x == y` | `Loose equality is forbidden` | `x === y` |
112
112
  | `throw new Error(m)` | `` `throw` is forbidden `` | `return Err(m)`, or `panic(m)` to end the process |
113
113
  | `try { … } catch { … }` | `` `try`/`catch`/`finally` is forbidden `` | `Result<T, E>` and `isErr()` |
114
- | `xs.map(f)`, `.filter`, `.reduce`, `.forEach`, `.slice`, `.sort`, `.shift` | `` Unknown method `map` on i32[] (supported: push, pop, indexOf, join) `` | a `for` loop; there are no function values to pass |
114
+ | `xs.map(f)`, `.filter`, `.reduce`, `.forEach`, `.slice`, `.sort`, `.shift` | `` Unknown method `map` on i32[] (supported: push, pop, indexOf, join) `` | a `for` loop, or a top-level `map(xs, f)` with a [function parameter](#function-parameters) |
115
115
  | `s.toUpperCase()`, `s.split()`, `s.trim()`, `s.replace()` | `` Unknown method … on string `` | index bytes with `charCodeAt` / `substring` |
116
- | `a?.b`, `a ?? b` | forbidden | `if (a !== null)` first |
116
+ | `a?.b`, `a ?? b` | forbidden, except `m.get(k) ?? d` | `if (a !== null)` first |
117
117
  | `x as T`, `<T>x`, `x!` | `Unsupported expression in Phase 1: AsExpression` | there are no casts; `implements` is the only widening |
118
118
  | `any`, `unknown` | forbidden | name the real type |
119
- | `undefined` | forbidden | `null`, with a `T \| null` type |
119
+ | `undefined` | forbidden, except `x === undefined` on a `Map.get` result | `null`, with a `T \| null` type |
120
120
  | `let total = 0` at the top level | `` Top-level `let` is not supported `` | a module `const`, or a local |
121
- | a callback: `xs.forEach(f)`, `(cb: (n: i32) => i32)` | `` Unsupported type `(n: i32) => i32` `` | there are **no function values**; inline the body or write a loop |
121
+ | a callback stored or returned: `const cb = (n: i32) => n`, a field `cb: (n: i32) => i32` | `An arrow may only be written as the argument for a function-typed parameter` / `` … is a function type, which may only annotate a parameter of a top-level function `` | there are **no function values**; a callback the call names is a [function parameter](#function-parameters) |
122
122
  | `type Pair<T>` (a generic alias) | `` expected `=`, found `<` `` (a syntax error) | a generic **function**, **class**, **interface** and **method** all work — see below; an alias renames a type that already exists, so it has nothing to specialise |
123
123
  | `constructor<T>(x: T)` | a syntax error: `a constructor cannot have type parameters` | put the parameter on the class (`class Box<T>`), or on a method |
124
124
  | `h.get<i32>(7)` (type argument at a method call) | `Type arguments are not written at a call site in Nish` | `h.get(7)` — a generic method infers like a generic function |
@@ -136,7 +136,9 @@ rejects. This table is the highest-value part of the page.
136
136
  | `export default f` | `` `export default` / `export =` are not supported `` | `export const f = …` |
137
137
  | `export type T = …`, `export enum K` | cannot be exported | declare the alias/enum in each module that needs it |
138
138
  | `new Date()`, `Date.now()` | `` Unknown builtin `Date.now` `` | `monotonicNanos()` for elapsed time; there is no wall clock and no calendar |
139
- | `JSON.parse`, `RegExp`, `Map`, `Set`, `Promise` | unknown / forbidden | none of these exist; write them or restructure |
139
+ | `JSON.parse`, `RegExp`, `Promise` | unknown / forbidden | none of these exist; write them or restructure |
140
+ | `for (const [k, v] of m)`, `m.forEach(...)`, `new Map(entries)` | each refused by name | `Map` and `Set` exist, with `size`, `get`, `set` / `add`, `has`, `delete`, `clear`, and `keys()` / `values()` in a `for...of` — see [Map and Set](#map-and-set) |
141
+ | `let v = m.get(k)`, `f(m.get(k))`, `m.get(k) + 1` | `` `m.get(k)` is `i32 \| undefined` and cannot be held in a `let` `` (and one message per place) | `m.get(k) ?? 0`, or `const v = m.get(k); if (v !== undefined) { … }` |
140
142
  | `let x = 5; x = "s"` | `Cannot initialize …` | types never change and never convert implicitly |
141
143
 
142
144
  A worked pair. This is the single most common rejection:
@@ -351,7 +353,8 @@ export const main = (): i32 => {
351
353
  ```
352
354
 
353
355
  - Reading anything off an un-narrowed nullable is an error: narrow with
354
- `!== null` first. `?.` and `??` are forbidden outright.
356
+ `!== null` first. `?.` is forbidden outright, and so is `??`, except after
357
+ `Map.get` (see [Map and Set](#map-and-set)).
355
358
  - Narrowing applies to a **local or parameter**, never a property path. `if
356
359
  (n.next !== null) n.next.v` is rejected — copy into a local first.
357
360
  - A narrowing ends at any assignment to the variable, and is dropped before a
@@ -373,11 +376,14 @@ const double = (n: i32): i32 => n * 2; // a concise body is that one `return`
373
376
 
374
377
  - A function is a **module-level `const` bound to an arrow**. `let` is
375
378
  rejected; annotating the `const` is rejected (the annotation would be a
376
- function type, and those are forbidden). The `function` keyword is accepted
377
- as the legacy spelling and compiles to identical IR.
379
+ function type, which only a parameter may have). The `function` keyword is
380
+ accepted as the legacy spelling and compiles to identical IR.
378
381
  - **Every parameter and the return type must be annotated.**
379
382
  - **A function is not a value.** `const alias = double` is
380
- `` Unknown identifier `double` ``. No callbacks, no function types, ever.
383
+ `` Unknown identifier `double` ``. A function may be *passed* only for a
384
+ function-typed parameter, where it is resolved at compile time — see
385
+ [Function parameters](#function-parameters) — and never stored, returned or
386
+ held in a field, a local or an array.
381
387
  - **Parameters are immutable**: `p = 1`, `p++`, `p += 1` are all rejected. Copy
382
388
  into a `let` first.
383
389
  - A non-`void` function must return on every path.
@@ -605,6 +611,115 @@ class Box<T> {
605
611
  export const main = (): i32 => new Box<i32>(1).value;
606
612
  ```
607
613
 
614
+ ### Function parameters
615
+
616
+ A parameter of a top-level function may have a **function type**. Its argument
617
+ is a top-level function named at the call, or an arrow written right there, and
618
+ each one is compiled into its **own copy** of the function, with a direct call
619
+ where the parameter is called — there is no function pointer at run time.
620
+
621
+ ```ts nish:ok
622
+ const map = <T, U>(xs: T[], f: (x: T) => U): U[] => {
623
+ const out: U[] = [];
624
+ for (const x of xs) {
625
+ out.push(f(x));
626
+ }
627
+ return out;
628
+ };
629
+
630
+ const fold = <T>(xs: T[], f: (acc: T, x: T) => T, identity: T): T => {
631
+ let acc = identity;
632
+ for (const x of xs) {
633
+ acc = f(acc, x);
634
+ }
635
+ return acc;
636
+ };
637
+
638
+ const add = (a: i32, b: i32): i32 => a + b;
639
+
640
+ export const main = (): i32 => {
641
+ const doubled = map([1, 2, 3], (n) => n * 2); // U is bound from the body
642
+ return fold(doubled, add, 0) - 12; // 0
643
+ };
644
+ ```
645
+
646
+ - **Call it, or pass it on to another function parameter. Nothing else**:
647
+ `const g = f`, `return f` and `[f]` are
648
+ `` `f` is a function parameter and can only be called or passed on as a function argument ``.
649
+ - **An arrow captures nothing.** It sees its own parameters and top-level
650
+ names; reading a local of the function around it is
651
+ `` The arrow reads `k`, which belongs to the function it is written in ``.
652
+ Pass the value in as another argument of the template instead.
653
+ - **The argument's type must match exactly**: `(x: i32) => i64` for a
654
+ `(x: i32) => i32` parameter is refused. A generic function cannot be passed
655
+ by name — write `(x) => identity(x)`.
656
+ - **Annotate what cannot be inferred**: an arrow parameter whose type no value
657
+ argument binds, and the return type of a block-bodied arrow whose result is a
658
+ type parameter.
659
+ - **Only a top-level function** may have one: not a method, a constructor, a
660
+ field, a local, a return type or an alias.
661
+
662
+ ```ts nish:err NL2335
663
+ const apply = (f: (x: i32) => i32, x: i32): i32 => f(x);
664
+
665
+ export const main = (): i32 => {
666
+ const k = 3;
667
+ return apply((x) => x + k, 1); // captures `k`: pass it to `apply` instead
668
+ };
669
+ ```
670
+
671
+ ### Data parallelism: `nish/threads`
672
+
673
+ `parallelMapInto(src, dst, f)` writes `f(src[i])` into `dst[i]`, and
674
+ `parallelReduce(src, f, identity)` folds `src` with `f` — each on as many
675
+ threads as the array is long enough for. `f` is a [function parameter](#function-parameters).
676
+ Importing the module compiles the program with `--threads`; there is nothing
677
+ else to switch on and no thread count to choose. (A plain block: a program
678
+ importing a `nish/` module writes two `.ll` files, so this one is compiled by
679
+ `tests/link/par_map` and `tests/link/par_reduce` instead.)
680
+
681
+ ```ts
682
+ import { parallelMapInto, parallelReduce } from "nish/threads";
683
+
684
+ const square = (x: f64): f64 => x * x;
685
+
686
+ export const main = (): i32 => {
687
+ const src: f64[] = [1.0, 2.0, 3.0];
688
+ const dst: f64[] = [0.0, 0.0, 0.0]; // at least src.length, or it panics
689
+ parallelMapInto(src, dst, square);
690
+ const total = parallelReduce(dst, (a, b) => a + b, 0.0); // 14, the same bits on any core count
691
+ return toI32(total);
692
+ };
693
+ ```
694
+
695
+ - **`f` writes nothing its caller can see.** No field or element store
696
+ through its argument and no `console.log` — each is refused at the call,
697
+ naming the write. Reading is fine; so are a bounds check, an integer
698
+ division and `panic`.
699
+ - **What `f` allocates is freed after every element.** A string, array or
700
+ object built on the way to the result compiles, with performance warning
701
+ NL9012: each element pays for building it and for the release. Storing an
702
+ allocation into another object (NL2352) and touching `Arena` (NL2351) are
703
+ refused, because either would make that release unsound.
704
+ - **The result is a number, a `boolean` or an enum.** A string or an object
705
+ made on another thread would be freed with it.
706
+ - **`dst` may not be reachable from an element of `src`**: mapping `Row[]` into
707
+ `f64[]` is refused when `Row` holds an `f64[]`, because it could be `dst`.
708
+ - **A reduce's `f` is associative and `identity` is its identity** — `0` for
709
+ `+`, `1` for `*`. The array is folded in fixed blocks and the blocks combined
710
+ in order, so `-` or a wrong identity is refused when `f` is an arrow of one
711
+ operator; a named function is trusted.
712
+ - A short array runs on the calling thread at the cost of the loop, so there
713
+ is no reason to guard a call by size. How short is sized from what `f`
714
+ costs: a cheap body is divided only past about a million elements, one with
715
+ a loop far sooner.
716
+
717
+ ```ts nish:err NL2350
718
+ import { parallelReduce } from "nish/threads";
719
+
720
+ export const main = (): i32 => parallelReduce([1, 2, 3], (a, b) => a + b, 1); // `+` folds from 0
721
+ ```
722
+
608
723
  ### Calling C
609
724
 
610
725
  `declare function name(params): T;` declares a C function this program calls but
@@ -889,8 +1004,8 @@ export const main = (): i32 => {
889
1004
  - `++` / `--` work on **numeric mutable locals only**, not fields or elements.
890
1005
  - Compound assignment `+= -= *= /= %=` requires a numeric target (so `+=` never
891
1006
  concatenates strings); the bitwise forms need an integer target.
892
- - **Forbidden**: `,` `??` `?.` `in` `instanceof` `typeof` `delete` `void expr`
893
- `==` `!=` `**` unary `+`.
1007
+ - **Forbidden**: `,` `?.` `in` `instanceof` `typeof` `delete` `void expr`
1008
+ `==` `!=` `**` unary `+`, and `??` on anything but a `Map.get` result.
894
1009
  - Element access `a[i]` needs an array and a numeric index, and is
895
1010
  bounds-checked (negative indices fail too). String-keyed access is forbidden.
896
1011
  The check comes off where the compiler proves the index in range — a loop
@@ -956,6 +1071,94 @@ bytes, no `.`/`..`), `spawnSync(argv)`, `spawnSyncTo(argv, outPath, errPath)`,
956
1071
  **Arena.** `Arena.mark()`, `Arena.release(m)`, `Arena.reset()`, `Arena.used()`
957
1072
  — see below.
958
1073
 
1074
+ ### Map and Set
1075
+
1076
+ `Map<K, V>` and `Set<T>` are global, as in JavaScript: no import, insertion
1077
+ order, SameValueZero keys (`-0` is `+0`, `NaN` finds `NaN`). What they have is
1078
+ exactly `size` (a read-only `number`), `get(k)`, `set(k, v)` / `add(x)` (both
1079
+ answer the receiver, so they chain), `has(k)`, `delete(k)` (answers whether it
1080
+ was there) and `clear()`, and `keys()` / `values()` as the iterable of a
1081
+ `for...of` and nowhere else. `entries` and `forEach` are refused by name.
1082
+
1083
+ ```ts nish:ok-body
1084
+ const seen = new Set<string>();
1085
+ const counts: Map<string, i32> = new Map();
1086
+ seen.add("a").add("b").add("a");
1087
+ counts.set("a", 1).set("b", 2);
1088
+ counts.delete("b");
1089
+ console.log(`${seen.size} ${counts.size} ${seen.has("b")}`);
1090
+ ```
1091
+
1092
+ - Write the type arguments on `new`, or annotate the declaration and write
1093
+ `new Map()`. `new Map(entries)` and `new Set(array)` are refused: start empty
1094
+ and `set` / `add` in a loop.
1095
+ - A key is a string, any number type, a `boolean`, an enum or a class instance
1096
+ (by identity). An interface, an array, a nullable type or a `Result` is not a
1097
+ key; a value is anything but `void` or an interface.
1098
+ - A module that declares its own `Map` or `Set` keeps it, but then no other
1099
+ module of the program may name the global one.
1100
+
1101
+ `m.get(k)` is `V | undefined`, as under `tsc`, and it is read in exactly one of
1102
+ three ways: a default with `??`, a `const` tested with `!== undefined` (or
1103
+ `=== undefined` and an early `return`), or a bare test. Each is one probe.
1104
+
1105
+ ```ts nish:ok-body
1106
+ const counts = new Map<string, i32>();
1107
+ for (const w of ["a", "b", "a"]) {
1108
+ counts.set(w, (counts.get(w) ?? 0) + 1);
1109
+ }
1110
+ const a = counts.get("a");
1111
+ if (a !== undefined) {
1112
+ console.log(`${a} ${counts.get("z") === undefined}`); // 2 true
1113
+ }
1114
+ ```
1115
+
1116
+ - Anywhere else — a `let`, an argument, a `return`, a field or element, a
1117
+ template hole, an arithmetic operand, the default of another `??`, a `const`
1118
+ annotated `i32` — the maybe is refused with a message naming the place.
1119
+ Default it with `??` first: `m.get(a) ?? (m.get(b) ?? 0)`.
1120
+ - The only spelling of the type is `const a: i32 | undefined = m.get(k)`.
1121
+ - `??` does not mix with `||` or `&&` without parentheses, and its default must
1122
+ be the value type. For a `Map<K, Node | null>` it replaces a stored `null`
1123
+ too, but the result is still `Node | null`.
1124
+
1125
+ ```ts nish:err-body NL2361
1126
+ const m = new Map<string, i32>();
1127
+ let v = m.get("a");
1128
+ ```
1129
+
1130
+ Walk a map with `for (const k of m.keys())` or `for (const v of m.values())`,
1131
+ and a set with `for (const x of s)`: insertion order, deleted keys skipped. You
1132
+ may `set`, `delete` or `clear` the table inside the walk and get exactly what
1133
+ JavaScript gives (a new key is visited later in the same walk; a key deleted
1134
+ before it is reached is not).
1135
+
1136
+ ```ts nish:ok-body
1137
+ const ages = new Map<string, i32>();
1138
+ ages.set("ada", 36).set("alan", 41);
1139
+ ages.delete("alan");
1140
+ ages.set("grace", 85);
1141
+ let total = 0;
1142
+ for (const name of ages.keys()) {
1143
+ console.log(name); // ada, then grace
1144
+ }
1145
+ for (const age of ages.values()) {
1146
+ total += age;
1147
+ }
1148
+ console.log(`${total}`); // 121
1149
+ ```
1150
+
1151
+ - An iterator is not a value: `keys()` / `values()` stored, passed, returned or
1152
+ spread are refused. Pass the map itself and walk it where it is used.
1153
+ - `for (const e of m)` over a `Map` itself is refused, because it would need
1154
+ `[key, value]` destructuring: walk `m.keys()` and call `m.get(k)`.
1155
+
1156
+ ```ts nish:err-body NL2375
1157
+ const m = new Map<string, i32>();
1158
+ for (const e of m) {
1159
+ }
1160
+ ```
1161
+
959
1162
  ## Memory
960
1163
 
961
1164
  There is **no garbage collector and no `free`**, and nothing about this changes
@@ -1017,8 +1220,9 @@ and return `Pair<A, B>`: an object literal `{ first, second }` at the return,
1017
1220
  exactly as above. It is for returning two values, not storing them — a struct
1018
1221
  kept in an array or a field wants named fields.
1019
1222
 
1020
- **A map or a set** — neither exists. Use parallel arrays with a linear scan, or
1021
- an array indexed by a small integer key, and write the lookup out.
1223
+ **A map or a set** — the global `Map` and `Set` ([Map and Set](#map-and-set)).
1224
+ Read a value with `m.get(k) ?? d`, and keep the keys in an array beside it when
1225
+ you need to walk them, since there is no iteration yet.
1022
1226
 
1023
1227
  **Shared behaviour over several shapes** — there is no inheritance and no
1024
1228
  dispatch. Give each class the interface's fields **first**, `implements` it,
@@ -1064,8 +1268,8 @@ Run it. `nish file.ts --json` is one command and it is the only proof.
1064
1268
  in an object-literal property or a method argument expecting a width?
1065
1269
  6. Is every `Result` read, and is every `.value` behind an `isOk()`?
1066
1270
  7. Is every nullable narrowed with `!== null` before it is touched?
1067
- 8. Did you use `throw`, `try`, `any`, `undefined`, `?.`, `??`, a cast, a
1068
- callback, a generic *alias*, or `extends`?
1271
+ 8. Did you use `throw`, `try`, `any`, `undefined` or `??` (outside `Map.get`), `?.`, a cast, a
1272
+ stored or returned callback, a generic *alias*, or `extends`?
1069
1273
  9. If you are adding to this repository: `npm run check` and `npm test` green,
1070
1274
  and a new construct ships a golden `.ll`, an `llvm-as` pass, a native round
1071
1275
  trip, a negative test, its `LANGUAGE.md` rule and cookbook entry, and a
package/docs/INSTALL.md CHANGED
@@ -234,12 +234,13 @@ routes are
234
234
  If you need one of these to be a first-class install, that is the conversation
235
235
  to have on the issue tracker rather than a workaround to discover here.
236
236
 
237
- **Nothing is published to the registry yet.** The installer is built and
238
- tested, and what is left is a person publishing a release under it, which
239
- [docs/wp12-release.md](wp12-release.md#release-procedure) step 4 is about.
240
- Until that happens, install from a release.
237
+ **On the registry from 0.10.0.** Every release publishes the main package and
238
+ its platform packages to npm from `release.yml`, by trusted publishing
239
+ ([docs/wp12-release.md](wp12-release.md#release-procedure) step 4), so
240
+ `npm install -g @amritk/nish` is the whole install. The rest of this section is
241
+ for a machine that cannot reach the registry, or a release before 0.10.0.
241
242
 
242
- From a release tarball on GitHub — the same package `npm publish` would upload,
243
+ From a release tarball on GitHub — the same package `npm publish` uploads,
243
244
  carried by the release instead of the registry. The `Release` workflow attaches
244
245
  the npm tarball to the release it builds for a `v*` tag. `npm pack` names it
245
246
  after `package.json#name`, so it is `amritk-nish-<version>.tgz` from 0.4.0 on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Nish: an ahead-of-time compiler from a static subset of TypeScript to LLVM IR",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -74,10 +74,10 @@
74
74
  "nish"
75
75
  ],
76
76
  "optionalDependencies": {
77
- "@amritk/nish-x86_64-linux": "0.10.0",
78
- "@amritk/nish-aarch64-linux": "0.10.0",
79
- "@amritk/nish-aarch64-darwin": "0.10.0",
80
- "@amritk/nish-x86_64-darwin": "0.10.0"
77
+ "@amritk/nish-x86_64-linux": "0.11.0",
78
+ "@amritk/nish-aarch64-linux": "0.11.0",
79
+ "@amritk/nish-aarch64-darwin": "0.11.0",
80
+ "@amritk/nish-x86_64-darwin": "0.11.0"
81
81
  },
82
82
  "devDependencies": {
83
83
  "@biomejs/biome": "2.5.12",
@@ -0,0 +1,23 @@
1
+ Boost Software License - Version 1.0 - August 17th, 2003
2
+
3
+ Permission is hereby granted, free of charge, to any person or organization
4
+ obtaining a copy of the software and accompanying documentation covered by
5
+ this license (the "Software") to use, reproduce, display, distribute,
6
+ execute, and transmit the Software, and to prepare derivative works of the
7
+ Software, and to permit third-parties to whom the Software is furnished to
8
+ do so, all subject to the following:
9
+
10
+ The copyright notices in the Software and this entire statement, including
11
+ the above license grant, this restriction and the following disclaimer,
12
+ must be included in all copies of the Software, in whole or in part, and
13
+ all derivative works of the Software, unless such copies or derivative
14
+ works are solely in the form of machine-executable object code generated by
15
+ a source language processor.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
20
+ SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
21
+ FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
22
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23
+ DEALINGS IN THE SOFTWARE.
package/runtime/nish.d.ts CHANGED
@@ -25,6 +25,15 @@
25
25
  * So a program that `tsc` accepts may still be rejected by `nish`; the
26
26
  * reverse should never happen, and a case where it does is a bug in this file.
27
27
  * `docs/LANGUAGE.md` is the normative description.
28
+ *
29
+ * **`Map` and `Set` are not declared here**, although `nish` has them as
30
+ * globals: the `"lib": ["ES2022"]` a program is checked against already
31
+ * declares JavaScript's, and a second declaration would clash with it.
32
+ * `nish`'s are that surface less what it defers — no `entries`, no `forEach`,
33
+ * no `for...of` over a `Map` itself — with `keys()` and `values()` admitted
34
+ * only as the iterable of a `for...of`, and `get`'s `V | undefined` only as a
35
+ * `const`'s initialiser, the left of `??` or an operand of `=== undefined`,
36
+ * none of which `tsc` can know (docs/LANGUAGE.md -> `Map` and `Set`).
28
37
  */
29
38
 
30
39
  // ---- Numeric widths (docs/LANGUAGE.md -> Types) ------------------------------
package/runtime/nish.h CHANGED
@@ -179,7 +179,15 @@ void nish_append_file(const nish_str *path, const nish_str *data);
179
179
  * `push` that grows it copies the elements into the arena and leaves your
180
180
  * buffer behind; the callee must not retain the pointer beyond the call.
181
181
  * A returned array lives in the arena (valid until the next reset/release):
182
- * copy `len` elements out of `data` before recycling. */
182
+ * copy `len` elements out of `data` before recycling.
183
+ *
184
+ * An array field stored inside its object (`self/inline_arrays.ts`) is this
185
+ * header followed by its `K` slots, `struct { nish_array h; T slots[K]; }`
186
+ * with `h.data == (char *)slots` and `h.cap == K`; `self/runtime.ts`'s
187
+ * `ARRAY_TYPE` and `self/structs.ts`'s `INLINE_HEADER_BYTES` are the same 24
188
+ * bytes, and `tests/layout/inline_array.c` holds the three to it. It only
189
+ * happens where no header, `.d.ts` or N-API shim describes the class, so a
190
+ * host that includes a generated header never meets one. */
183
191
  typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
184
192
  /* A fresh arena array of `len` uninitialised elements (`len == cap`), for a
185
193
  * host that wants the runtime to own the storage (the wasm loader does).
package/runtime/runtime.c CHANGED
@@ -277,6 +277,13 @@ nish_str *nish_str_from_u64(uint64_t v) { return str_from_digits(v, 0); }
277
277
 
278
278
  /* ---- Shortest round-trip digits (Ryu)
279
279
 
280
+ Adapted from Ryu (https://github.com/ulfjack/ryu), ryu/d2s.c and
281
+ ryu/common.h. Copyright 2018 Ulf Adams. Used under the Boost Software
282
+ License, Version 1.0; see runtime/LICENSE-ryu. The code from here to the end
283
+ of `nish_shortest_digits` follows upstream's `d2d()` and its helpers, which
284
+ is why it keeps their names, constants and comments; the two tables are
285
+ regenerated by scripts/gen-pow5-tables.py rather than copied.
286
+
280
287
  `String(x)` in JavaScript prints the *fewest* digits that read back as the
281
288
  same double, and the language promises that spelling. Reaching it by asking
282
289
  snprintf for k digits and strtod whether they round-trip -- which is what
@@ -1079,7 +1086,9 @@ nish_str *nish_str_from_f64(double v) {
1079
1086
  return nish_str_new(out, o - out);
1080
1087
  }
1081
1088
 
1082
- /* ---- Math.random: xorshift64*, seeded lazily from time and pid.
1089
+ /* ---- Math.random: xorshift64*, seeded lazily from time and pid. The shifts
1090
+ and the multiplier are Marsaglia's and Vigna's published constants, which
1091
+ are public domain.
1083
1092
  Thread-local under -DNISH_THREADS (WP20 T0 §3.2): a shared seed word is a
1084
1093
  race, and a per-thread one also makes each worker's stream its own rather
1085
1094
  than an interleaving of everyone's. Two threads that start in the same
@@ -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 nothing the compiler knows about: a module in this directory is an
5
- ordinary Nish source file, compiled as part of whatever program imports it,
6
- and subject to the same rules as `examples/` or `self/`
7
- ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md) is the style guide).
4
+ here and — with two exceptions, `threads.ts` and `collections.ts` — nothing
5
+ 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,8 @@ 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` and `sameKey` are lowered by the compiler per key type |
16
+ | [`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
17
 
16
18
  ## How a program imports it
17
19
 
@@ -0,0 +1,548 @@
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(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
+ /** A set of keys in insertion order: `Map`'s table with no values, and the same probe. */
433
+ // biome-ignore lint/suspicious/noShadowRestrictedNames: this is the global `Set`, which the compiler loads for a program that names it
434
+ export class Set<T> {
435
+ /** 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[];
442
+ /** `Map`'s walk count: how many `for...of` loops are walking the table now. */
443
+ walks: i32 = 0;
444
+
445
+ constructor() {
446
+ this.slots = new Array<u32>(INITIAL_SLOTS);
447
+ this.entryKeys = [];
448
+ this.entryHashes = [];
449
+ }
450
+
451
+ probe(key: T): i64 {
452
+ return probeTable(this.slots, this.mask, this.entryHashes, this.entryKeys, key);
453
+ }
454
+
455
+ has(key: T): boolean {
456
+ return this.probe(key) >= 0;
457
+ }
458
+
459
+ /** Add `key` at the end when it is not there already: one probe. */
460
+ add(key: T): Set<T> {
461
+ const found = this.probe(key);
462
+ if (found < 0) {
463
+ this.insertAt(found, key);
464
+ }
465
+ return this;
466
+ }
467
+
468
+ delete(key: T): boolean {
469
+ const found = this.probe(key);
470
+ if (found < 0) {
471
+ return false;
472
+ }
473
+ killEntry(this.slots, this.entryHashes, found);
474
+ this.live = this.live - 1;
475
+ this.size = this.size - 1;
476
+ return true;
477
+ }
478
+
479
+ clear(): void {
480
+ clearSlots(this.slots);
481
+ if (this.walks > 0) {
482
+ clearSlots(this.entryHashes); // every entry dead, and the count kept
483
+ } else {
484
+ truncate(this.entryKeys);
485
+ truncate(this.entryHashes);
486
+ }
487
+ this.live = 0;
488
+ this.size = 0;
489
+ }
490
+
491
+ /** `Map`'s walk: `for (const x of s)`, `s.keys()` and `s.values()` are all this one. */
492
+ walkOpen(): void {
493
+ this.walks = this.walks + 1;
494
+ }
495
+
496
+ walkNext(from: i32): i32 {
497
+ return nextLive(this.entryHashes, from);
498
+ }
499
+
500
+ walkClose(): void {
501
+ this.walks = this.walks - 1;
502
+ }
503
+
504
+ keyAt(index: i32): T {
505
+ if (index < 0 || index >= toI32(this.entryKeys.length)) {
506
+ panic("Set: no entry at this index");
507
+ }
508
+ return this.entryKeys[index];
509
+ }
510
+
511
+ insertAt(absent: i64, key: T): void {
512
+ const packed = -1 - absent;
513
+ let bucket = toI32(packed >> 32);
514
+ const h = toU32(packed);
515
+ if (toI32(this.entryKeys.length) >= INDEX_CAP) {
516
+ if (this.live >= INDEX_CAP || this.walks > 0) {
517
+ panic("Set maximum size exceeded");
518
+ }
519
+ this.rebuild();
520
+ bucket = -1;
521
+ }
522
+ this.entryKeys.push(key);
523
+ this.entryHashes.push(h);
524
+ this.live = this.live + 1;
525
+ this.size = this.size + 1;
526
+ // Every entry takes a bucket, live or dead, until a rebuild, which files
527
+ // the new entry with the rest; otherwise it takes the bucket the probe found.
528
+ const used = toI32(this.entryKeys.length);
529
+ if (used * 4 > toI32(this.slots.length) * 3) {
530
+ this.rebuild();
531
+ } else {
532
+ fileAppended(this.slots, this.mask, bucket, h, used);
533
+ }
534
+ }
535
+
536
+ 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);
540
+ if (!walking && this.live < used) {
541
+ compactEntries(this.entryKeys, this.entryHashes);
542
+ compactHashes(this.entryHashes);
543
+ }
544
+ this.slots = slots;
545
+ this.mask = toI32(slots.length) - 1;
546
+ refile(slots, this.entryHashes);
547
+ }
548
+ }
package/std/threads.ts ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * `std/threads` — data parallelism, and nothing else (docs/wp29-thread-surface.md §4.1).
3
+ *
4
+ * import { parallelMapInto, parallelReduce } from "nish/threads";
5
+ *
6
+ * parallelMapInto(src, dst, (x) => x * 3);
7
+ * const total = parallelReduce(src, (a, b) => a + b, 0);
8
+ *
9
+ * **What is written here is the meaning, not the implementation.** Each body
10
+ * below is the sequential program, and it is what runs under Node, what
11
+ * `npm run check` type-checks, and what the compiler checks the call against.
12
+ * The compiler then recognises the two exported templates by module and name
13
+ * and lowers an instance of either onto `nish_parallel_range`
14
+ * (runtime/runtime_parallel.c): it replaces the one call that walks the whole
15
+ * range — `mapRange` for a map, `reduceBlocks` for a reduce — with a region that
16
+ * hands each thread a contiguous piece of it, and emits the rest of the body as
17
+ * written. So the length check, its message and the order a reduce combines in
18
+ * are this file's, whichever way the program is compiled.
19
+ *
20
+ * What makes the region safe is checked at the call, not trusted
21
+ * (docs/LANGUAGE.md, "Data parallelism"): the function passed as `f` may write
22
+ * nothing its caller could observe, may allocate only temporaries it drops
23
+ * before it returns — they are given back after every element — `dst` may not
24
+ * be reachable from an element of `src`, and the result type is a number or a
25
+ * `boolean`. Importing this module compiles the program with `--threads`,
26
+ * because every worker needs an arena of its own.
27
+ *
28
+ * A reduce is deterministic. `src` is split into `min(64, ceil(n / BLOCK))`
29
+ * blocks, each block is folded from `identity`, and the block results are
30
+ * combined left to right — here, on one thread, and by the compiled program on
31
+ * any number of them — so an `f64` sum is the same bits on one core or sixty
32
+ * four. That needs `f` to be associative and `identity` to be its identity,
33
+ * which the checker enforces for an arrow whose body is one operator on its two
34
+ * parameters and cannot see through a named function.
35
+ */
36
+
37
+ /**
38
+ * Elements per block of a reduce: 2^20, about a millisecond of simple work
39
+ * (docs/wp20-threads.md §8e). It decides the blocking, and so the answer's
40
+ * bits: an array this short is one block, folded as the loop it would have
41
+ * been. It is independent of the map's grain (`mapGrain` in
42
+ * `self/parallel.ts`), which decides only how a map is divided and may
43
+ * change without changing any result; this one may not.
44
+ */
45
+ const BLOCK: i32 = 1048576;
46
+
47
+ /** The most blocks a reduce is split into: the partitioner's own ceiling on threads. */
48
+ const MAX_BLOCKS: i32 = 64;
49
+
50
+ /**
51
+ * Writes `f(src[i])` into `dst[i]` for every `i` in `[lo, hi)`: one thread's
52
+ * share of a map. Each length is read in the loop condition and `dst`'s again
53
+ * after the call, because a call ends every length fact the bounds proof holds
54
+ * (docs/LANGUAGE.md, "Arrays") and this is what keeps both accesses unchecked.
55
+ * The caller has already checked that the range is inside both arrays, so
56
+ * neither test is ever the one that stops the loop.
57
+ */
58
+ const mapRange = <T, U>(src: T[], dst: U[], f: (x: T) => U, lo: i32, hi: i32): void => {
59
+ for (let i: i32 = lo; i >= 0 && i < hi && i < toI32(src.length); i++) {
60
+ const y = f(src[i]);
61
+ if (i < toI32(dst.length)) {
62
+ dst[i] = y;
63
+ }
64
+ }
65
+ };
66
+
67
+ /** `f` folded over `src[lo..hi)` from `identity`: one block of a reduce. */
68
+ const reduceRange = <T>(src: T[], f: (acc: T, x: T) => T, identity: T, lo: i32, hi: i32): T => {
69
+ let acc = identity;
70
+ for (let i: i32 = lo; i >= 0 && i < hi && i < toI32(src.length); i++) {
71
+ acc = f(acc, src[i]);
72
+ }
73
+ return acc;
74
+ };
75
+
76
+ /** How many blocks a reduce over `n` elements is split into: `min(MAX_BLOCKS, ceil(n / BLOCK))`. */
77
+ const reduceBlockCount = (n: i32): i32 => {
78
+ const wanted: i32 = toI32((toI64(n) + toI64(BLOCK) - 1) / toI64(BLOCK));
79
+ return wanted < MAX_BLOCKS ? wanted : MAX_BLOCKS;
80
+ };
81
+
82
+ /** Where block `k` of `blocks` over `n` elements starts; block `blocks` starts at `n`. */
83
+ const reduceBlockStart = (n: i32, blocks: i32, k: i32): i32 => toI32((toI64(n) * toI64(k)) / toI64(blocks));
84
+
85
+ /** Folds blocks `[lo, hi)` of `src` into `partials`, one result per block: one thread's share of a reduce. */
86
+ const reduceBlocks = <T>(
87
+ src: T[],
88
+ f: (acc: T, x: T) => T,
89
+ identity: T,
90
+ partials: T[],
91
+ lo: i32,
92
+ hi: i32
93
+ ): void => {
94
+ const n: i32 = toI32(src.length);
95
+ const blocks: i32 = toI32(partials.length);
96
+ for (let k: i32 = lo; k >= 0 && k < hi && k < blocks; k++) {
97
+ const partial = reduceRange(
98
+ src,
99
+ f,
100
+ identity,
101
+ reduceBlockStart(n, blocks, k),
102
+ reduceBlockStart(n, blocks, k + 1)
103
+ );
104
+ if (k < toI32(partials.length)) {
105
+ partials[k] = partial;
106
+ }
107
+ }
108
+ };
109
+
110
+ /**
111
+ * The panic of a map whose `dst` is shorter than its `src`. A function of its
112
+ * own so that the message it builds is its allocation and not the map's: an
113
+ * allocation anywhere in `parallelMapInto` would give every call an arena mark
114
+ * and release, which is most of what a map over a few elements costs.
115
+ */
116
+ const dstTooShort = (have: i32, want: i32): void => {
117
+ panic(`parallelMapInto: dst has ${have} elements and src has ${want}`);
118
+ };
119
+
120
+ /**
121
+ * `dst[i] = f(src[i])` for every index of `src`, on as many threads as the
122
+ * machine has and the length is worth. `dst` must be at least as long as
123
+ * `src`, which is checked once, before any element is written.
124
+ */
125
+ export const parallelMapInto = <T, U>(src: T[], dst: U[], f: (x: T) => U): void => {
126
+ const n: i32 = toI32(src.length);
127
+ if (toI32(dst.length) < n) {
128
+ dstTooShort(toI32(dst.length), n);
129
+ }
130
+ mapRange(src, dst, f, 0, n);
131
+ };
132
+
133
+ /**
134
+ * `f` folded over `src`, blockwise from `identity` and then left to right over
135
+ * the blocks, so the answer does not depend on how many threads computed it.
136
+ * An empty `src` answers `identity`.
137
+ */
138
+ export const parallelReduce = <T>(src: T[], f: (acc: T, x: T) => T, identity: T): T => {
139
+ const blocks: i32 = reduceBlockCount(toI32(src.length));
140
+ if (blocks === 0) {
141
+ return identity;
142
+ }
143
+ const partials = new Array<T>(blocks);
144
+ reduceBlocks(src, f, identity, partials, 0, blocks);
145
+ let acc = partials[0];
146
+ for (let k: i32 = 1; k < toI32(partials.length); k++) {
147
+ acc = f(acc, partials[k]);
148
+ }
149
+ return acc;
150
+ };