@amritk/nish 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/AI.md CHANGED
@@ -37,6 +37,7 @@ and it is the only authority.
37
37
  nish program.ts --json # one JSON object per diagnostic, on stdout
38
38
  nish program.ts -o out.ll # emit LLVM IR
39
39
  nish program.ts --link prog # build a native binary (needs clang)
40
+ nish run program.ts a b # build into a cache, then run it with `a b`
40
41
  nish --help # the full flag list, stdout, exit 0
41
42
  ```
42
43
 
@@ -58,7 +59,9 @@ exclusive:
58
59
  `NL0000` a diagnostic with no rule yet.
59
60
  - **Exit codes**: `0` ok, `1` the program was rejected, `2` usage, `3` the C
60
61
  toolchain is unusable, `70` an internal compiler error — that last one is a
61
- bug in `nish`, not in your program, and is worth reporting.
62
+ bug in `nish`, not in your program, and is worth reporting. `nish run`
63
+ answers the same codes until the program starts, and the program's own
64
+ status after that.
62
65
  - Every failure is a `--json` object, toolchain and internal errors included,
63
66
  so you never have to parse stderr to find out why a run failed.
64
67
 
@@ -195,16 +198,42 @@ const good = (a: i32, b: i64): i64 => {
195
198
  };
196
199
  ```
197
200
 
198
- **`integer` is reserved, not a type.** It is held for ranged integers
199
- (`integer<Lo, Hi>`), which the compiler does not build yet, so writing it as a
200
- type is refused. Declaring a type alias, enum, class, interface or function
201
- named `integer` is refused too. Use `i32`. A value named `integer` (a local
202
- or a module constant) is fine.
201
+ **`integer<Lo, Hi>` is an `i32` that stays in `[Lo, Hi]`.** The bounds are two
202
+ integer literals (a sign allowed) inside `i32`, and it is an `i32` everywhere
203
+ the machine can see. Putting a value *into* one is checked: an `i32` or
204
+ another range is compared once and the program panics (exit 1) outside the
205
+ range, a literal outside it is a compile error, and a literal inside it or a
206
+ narrower range costs nothing. So does a value a loop condition or a guard
207
+ already bounds (`b` below), and `toI32` of a `u8`; a check left inside a loop
208
+ is a performance warning (NL9013) naming the guard that removes it. A range
209
+ bounds an index too: `integer<0, 255>`, like `u8`, needs only a length guard of
210
+ 256. Taking one *out* is free: every operator reads
211
+ it as `i32`, a `const` keeps the range and a `let` widens to `i32`. A `u8` is
212
+ not an `integer<0, 255>` (convert with `toI32`), and a `declare function` may
213
+ not mention a range. Put the range on the value you use, not on a loop
214
+ counter, which has to leave the range to end the loop.
203
215
 
204
- ```ts nish:err NL2333
205
- const clamp = (x: integer): i32 => x;
216
+ ```ts nish:ok
217
+ const getByte = (buf: u8[], i: integer<0, 255>): u8 => buf[i];
218
+
219
+ const sumAll = (buf: u8[]): i32 => {
220
+ let sum = 0;
221
+ for (let i = 0; i < 256 && i < buf.length; i++) {
222
+ const b: integer<0, 255> = i;
223
+ sum = sum + toI32(getByte(buf, b));
224
+ }
225
+ return sum;
226
+ };
227
+ ```
228
+
229
+ ```ts nish:err-body NL2384
230
+ const digit: integer<0, 9> = 12;
206
231
  ```
207
232
 
233
+ Declaring a type alias, enum, class, interface or function named `integer` is
234
+ refused, because the name is the type. A value named `integer` (a local or a
235
+ module constant) is fine.
236
+
208
237
  ```ts nish:err NL2332
209
238
  class integer {
210
239
  value: i32 = 0;
@@ -314,6 +343,8 @@ Narrowing follows the same engine as `T | null` below: it applies to a
314
343
  **variable**, never a property path; it ends at any assignment to that
315
344
  variable; and it is dropped before a loop that assigns it. `if (r.isOk()) A
316
345
  else B` narrows in `A`, and after the `if` when `B` cannot fall through.
346
+ The proof stays with `r`: `const y = r`, `c ? r : q` and `[r]` are plain
347
+ `Result`s, so test `y` itself before `y.value`.
317
348
 
318
349
  `panic(message)` is the other ending: message to stderr, exit 1. It is for an
319
350
  invariant that cannot hold, not for a failure a caller should handle. It
@@ -357,8 +388,9 @@ export const main = (): i32 => {
357
388
  `Map.get` (see [Map and Set](#map-and-set)).
358
389
  - Narrowing applies to a **local or parameter**, never a property path. `if
359
390
  (n.next !== null) n.next.v` is rejected — copy into a local first.
360
- - A narrowing ends at any assignment to the variable, and is dropped before a
361
- loop whose body, condition or update assigns it.
391
+ - A narrowing ends at any assignment to the variable, including one in an
392
+ earlier operand of the same `&&` / `||` chain, and is dropped before a loop
393
+ whose body, condition or update assigns it.
362
394
  - Two nullables cannot be compared with each other; compare each with `null`.
363
395
  - `new Array<T | null>(n)` is allowed: the zero fill *is* `null`.
364
396
 
@@ -675,7 +707,7 @@ export const main = (): i32 => {
675
707
  threads as the array is long enough for. `f` is a [function parameter](#function-parameters).
676
708
  Importing the module compiles the program with `--threads`; there is nothing
677
709
  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
710
+ importing `nish/threads` writes two `.ll` files, so this one is compiled by
679
711
  `tests/link/par_map` and `tests/link/par_reduce` instead.)
680
712
 
681
713
  ```ts
@@ -720,6 +752,62 @@ import { parallelReduce } from "nish/threads";
720
752
  export const main = (): i32 => parallelReduce([1, 2, 3], (a, b) => a + b, 1); // `+` folds from 0
721
753
  ```
722
754
 
755
+ ### Scoped tasks: `using s = scope()`
756
+
757
+ Different functions at once, one thread each: open a scope with `using`, give
758
+ it tasks with `spawn(entry, arg, dst, at)`, and every task has run and stored
759
+ `entry(arg)` into `dst[at]` when the block ends. (A plain block, for the same
760
+ reason as the one above: `tests/link/thread_scope_basic` compiles it.)
761
+
762
+ ```ts
763
+ import { scope } from "nish/threads";
764
+
765
+ const sumOf = (xs: f64[]): f64 => xs[0] + xs[1];
766
+ const square = (n: i32): i32 => n * n;
767
+
768
+ export const main = (): i32 => {
769
+ const xs: f64[] = [1.5, 2.5];
770
+ const sums: f64[] = [0.0];
771
+ const squares: i32[] = [0];
772
+ {
773
+ using s = scope();
774
+ s.spawn(sumOf, xs, sums, 0);
775
+ s.spawn(square, 12, squares, 0);
776
+ } // joined here, on every exit
777
+ console.log(`${sums[0]} ${squares[0]}`); // 4 144
778
+ return 0;
779
+ };
780
+ ```
781
+
782
+ - **The tasks run when the block ends**, together, and each answer is stored
783
+ after the last one finishes. So read a destination after the block. A
784
+ destination is a `const` bound to a fresh array (`[0, 0]`, `new Array`) that
785
+ you only index; never pass it on. Between the first `spawn` and the block's
786
+ end, don't read a destination, and don't write memory a task could read (a
787
+ store into an argument, a call that writes through its argument).
788
+ - **`using` takes only `scope()`**, and `scope()` only comes from `using`.
789
+ The scope is only ever the receiver of a `spawn` statement: never pass it,
790
+ store it or return it.
791
+ - **The task is a named top-level function**, not an arrow, and it follows the
792
+ data-parallel rules: it writes nothing anybody else can see, answers a number,
793
+ a `boolean` or an enum, and leaves `Arena` alone. A task cannot open a scope
794
+ of its own or call `parallelMapInto`.
795
+ - **Any argument will do** — an array, an object — because nothing writes
796
+ memory while the tasks run. There is no thread count: one task, one thread.
797
+
798
+ ```ts nish:err NL2388
799
+ import { scope } from "nish/threads";
800
+
801
+ const one = (n: i32): i32 => n + 1;
802
+
803
+ export const main = (): i32 => {
804
+ const out: i32[] = [0];
805
+ const s = scope(); // a scope must be introduced by `using`
806
+ s.spawn(one, 1, out, 0);
807
+ return out[0];
808
+ };
809
+ ```
810
+
723
811
  ### Calling C
724
812
 
725
813
  `declare function name(params): T;` declares a C function this program calls but
@@ -952,6 +1040,11 @@ export const main = (): i32 => {
952
1040
  annotation must match **exactly**. `const` freezes the binding, not the
953
1041
  contents: `xs[0] = 1` and `xs.push(1)` on a `const xs` are fine.
954
1042
  - `var` is forbidden. Destructuring is not supported.
1043
+ - **Semicolons are optional**, where TypeScript would insert one: at a line
1044
+ break, before `}` and at the end of the file. It is TypeScript's rule, so a
1045
+ line starting with `(` or `[` continues the one before it, and `return`
1046
+ followed by a line break returns nothing. Here, the value left on the next
1047
+ line is an unreachable-code error rather than a silent bug.
955
1048
  - `for (init; cond; update)` with every clause optional, and
956
1049
  `for (const x of xs)` over an **array** only — `x` gets the element type and
957
1050
  must not be annotated. The array's `length` is re-read each iteration, so a
@@ -1159,6 +1252,37 @@ for (const e of m) {
1159
1252
  }
1160
1253
  ```
1161
1254
 
1255
+ Asking about one key twice costs one probe when the second call writes
1256
+ through the first: `m.set(k, E)` whose `E` reads `m.get(k)` or `m.has(k)`
1257
+ once, and `if (m.has(k)) { m.set(k, E); … }`, `if (!m.has(k)) { m.set(k, E); … }`
1258
+ or `if (!s.has(x)) { s.add(x); … }` with the write first in the branch. The
1259
+ receiver and key must be a local, a parameter or a `this.<field>` path (the key
1260
+ may be a literal), spelled the same in both calls, and nothing in between may
1261
+ call anything, allocate (`new`, a literal array or object, a template, a
1262
+ string `+`) or assign. Anything else still compiles, as one probe per call.
1263
+ `nish/map` adds `getOrInsert(m, k, v)` — the value of `k`, or `v` after
1264
+ inserting it — and `reserve(m, n)`, which sizes the table for `n` entries; both
1265
+ are one module with the program and run unchanged under Node.
1266
+
1267
+ ```ts nish:ok
1268
+ import { getOrInsert, reserve } from "nish/map";
1269
+
1270
+ export const main = (): i32 => {
1271
+ const words = ["to", "be", "or", "not", "to", "be"];
1272
+ const counts = new Map<string, i32>();
1273
+ reserve(counts, words.length);
1274
+ for (const w of words) {
1275
+ counts.set(w, (counts.get(w) ?? 0) + 1); // one probe per word
1276
+ }
1277
+ const first = new Map<string, i32>();
1278
+ for (let i = 0; i < words.length; i++) {
1279
+ getOrInsert(first, words[i], i); // one probe; inserts only the first time
1280
+ }
1281
+ console.log(`${counts.get("to") ?? 0} ${first.get("be") ?? -1}`); // 2 1
1282
+ return 0;
1283
+ };
1284
+ ```
1285
+
1162
1286
  ## Memory
1163
1287
 
1164
1288
  There is **no garbage collector and no `free`**, and nothing about this changes
package/docs/INSTALL.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `nish` compiles a static subset of TypeScript to LLVM IR and, with
4
4
  `--link`, to a native binary. The compiler is itself a native binary, written
5
- in Nish (`self/`), and needs nothing to run; the `--link` step (and anything
5
+ in Nish (`src/`), and needs nothing to run; the `--link` step (and anything
6
6
  else that turns `.ll` into machine code) needs an LLVM toolchain.
7
7
 
8
8
  ## 1. Prerequisites
@@ -142,7 +142,7 @@ The npm route installs the **native** compiler, and it is a download rather than
142
142
  build: nothing is compiled on your machine. The package declares one
143
143
  `nish-<os>-<arch>` package per supported platform as an `optionalDependencies`
144
144
  entry with `os` and `cpu` set, so npm fetches exactly the one that matches and
145
- skips the rest. Each of those carries the self-hosted compiler — `self/`
145
+ skips the rest. Each of those carries the self-hosted compiler — `src/`
146
146
  compiled by itself — already built, `--verify`d and smoke-tested on a machine
147
147
  of its own architecture by the release workflow.
148
148
 
@@ -198,7 +198,7 @@ nish: no prebuilt compiler for freebsd/x64
198
198
 
199
199
  Until 0.6.0 it ran the TypeScript compiler that shipped in the same package
200
200
  instead — the same compiler by every test here, about eight times slower, and
201
- no C toolchain needed. That compiler was `src/`, which was deleted in R6
201
+ no C toolchain needed. That compiler was stage0, which was deleted in R6
202
202
  ([wp19](wp19-stage0-retirement.md)), so there is nothing left in the package to
203
203
  fall back to. The cost is stated where the rest of that deletion's costs are,
204
204
  in [wp19 §6](wp19-stage0-retirement.md#6-what-retirement-costs-stated-plainly),
@@ -218,7 +218,7 @@ routes are
218
218
  # in the glibc container, where build/nish runs
219
219
  build/nish app.ts -o app.ll
220
220
  # on the musl host, with its own clang and the runtime from this repository
221
- clang app.ll runtime/runtime.c runtime/runtime_os.c -lm -o app
221
+ clang app.ll runtime/runtime.c runtime/runtime-os.c -lm -o app
222
222
  ```
223
223
 
224
224
  `--link`ing inside the container is the mistake to avoid: it shells out to the
@@ -268,7 +268,7 @@ npm install -g ./amritk-nish-0.4.0.tgz ./amritk-nish-x86_64-linux-0.4.0.tgz
268
268
  ```
269
269
 
270
270
  As a native compiler, which needs no Node at all. A release also attaches the
271
- self-hosted compiler — the binary `self/` produces by compiling itself — one
271
+ self-hosted compiler — the binary `src/` produces by compiling itself — one
272
272
  per supported platform, from the version named in the last column:
273
273
 
274
274
  | Asset | For | Attached from |
@@ -326,7 +326,7 @@ release exists, take its version from
326
326
  Unpack it and run `bin/nish` from wherever you like; put that on `PATH` if you
327
327
  want it there. Keep the directory intact rather than moving the binary out of
328
328
  it: `--link` runs `scripts/build.sh` and compiles the C runtime
329
- (`runtime/runtime.c` and `runtime/runtime_os.c`, the system-call half), and the
329
+ (`runtime/runtime.c` and `runtime/runtime-os.c`, the system-call half), and the
330
330
  compiler finds all of them relative to its own location — `bin/nish` alone in a
331
331
  directory can still emit IR with `-o`, but `--link` will tell you it cannot
332
332
  find `scripts/build.sh`.
@@ -367,23 +367,23 @@ Building the IR yourself rather than through `--link` means naming the runtime
367
367
  on the `clang` line, and it is two files:
368
368
 
369
369
  ```bash
370
- clang app.ll runtime/runtime.c runtime/runtime_os.c -lm -o app
370
+ clang app.ll runtime/runtime.c runtime/runtime-os.c -lm -o app
371
371
  ```
372
372
 
373
373
  `runtime.c` is the half every program touches — the arena, strings, arrays,
374
- number formatting, the panics — and `runtime_os.c` is the half that wraps the
374
+ number formatting, the panics — and `runtime-os.c` is the half that wraps the
375
375
  system calls: files, directories, subprocesses, `getenv`, the monotonic clock.
376
376
  They are separate so that each carries its own measured size ceiling
377
377
  ([docs/wp7-runtime.md](wp7-runtime.md)); nothing in the core calls into the
378
378
  system-call half, so an older line that names `runtime.c` alone still links a
379
379
  program that reads no files and spawns nothing. `scripts/build.sh` compiles
380
- `runtime_os.c` beside any `runtime.c` it is handed, so a build that goes
380
+ `runtime-os.c` beside any `runtime.c` it is handed, so a build that goes
381
381
  through it — every `--link`, and every `--profile` recipe in these documents —
382
382
  needs to name only the one.
383
383
 
384
384
  ## 2a. What `npm run build` does
385
385
 
386
- `self/` is the compiler, written in Nish, and it compiles itself
386
+ `src/` is the compiler, written in Nish, and it compiles itself
387
387
  ([docs/wp14-selfhost.md](wp14-selfhost.md)). From a checkout, with clang on
388
388
  `PATH`, `npm run build` runs `scripts/bootstrap.sh` with the seed from §2:
389
389
 
@@ -406,11 +406,11 @@ binary itself. It looks for `scripts/build.sh` and the two `runtime/*.c` files
406
406
  one level up from wherever it was invoked, then in the working directory, so
407
407
  it wants a checkout or an installed package around it the way `nish` does.
408
408
 
409
- Because the seed is the last release, `self/` may only *use* in its own
409
+ Because the seed is the last release, `src/` may only *use* in its own
410
410
  source the constructs that release compiles. A new construct is implemented
411
- in `self/` and becomes usable inside `self/` from the next release on; CI's
411
+ in `src/` and becomes usable inside `src/` from the next release on; CI's
412
412
  `bootstrap` job is what checks that the released seed still builds stage1.
413
- Until R6 the seed was a TypeScript compiler in `src/`, run under Node; it was
413
+ Until R6 the seed was a TypeScript compiler, stage0, run under Node; it was
414
414
  deleted once the native one answered every flag it did
415
415
  ([wp19](wp19-stage0-retirement.md)).
416
416
 
@@ -446,6 +446,21 @@ Other build profiles: `--profile size` (smallest binary), `--profile debug`
446
446
  (no optimisation, symbols kept). Run `nish --help` for every flag, and
447
447
  see the [README](../README.md) for the language subset.
448
448
 
449
+ To use a program as a script rather than keep the binary, run it:
450
+
451
+ ```bash
452
+ nish run hello.ts
453
+ # hello from Nish
454
+ ```
455
+
456
+ `nish run [options] <file.ts> [args ...]` compiles the file, links it into
457
+ `$XDG_CACHE_HOME/nish/run` (or `~/.cache/nish/run`) when that program has not
458
+ been linked before, and starts it with the arguments after the file. The first
459
+ run of a new edit pays for one link, and later runs start the cached binary.
460
+ It needs clang only for that link. It needs `HOME` or `XDG_CACHE_HOME` set,
461
+ and refuses the run without them rather than keep a binary in a shared
462
+ directory.
463
+
449
464
  ## 4. Exit codes
450
465
 
451
466
  | Code | Meaning |
@@ -454,6 +469,7 @@ see the [README](../README.md) for the language subset.
454
469
  | 1 | the program was rejected: compile error (`file:line:col: error: ...`), missing input file, or an `-o` layout that does not fit the module count |
455
470
  | 2 | usage error: unknown flag, missing argument, no input files |
456
471
  | 3 | toolchain error: `--link` found no `clang` (`CC` overrides), or `scripts/build.sh` failed (its output is shown; the `.ll` files are still written) — or the `nish` command found no prebuilt compiler for this platform, or one that would not start. Under `--json` all of these are one `NL0002` object |
472
+ | | `nish run` answers these codes until the program starts, and the program's own exit status (`128 + n` for a signal) after that. With neither `HOME` nor `XDG_CACHE_HOME` set it answers 3 |
457
473
  | 70 | internal compiler error: an unexpected exception. Please report it at <https://github.com/amritk/nish/issues> with the input and command line; `NISH_DEBUG=1` prints the stack trace |
458
474
 
459
475
  ## Troubleshooting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
@@ -20,6 +20,7 @@
20
20
  "!scripts/arrow-verify.mjs",
21
21
  "!scripts/arrowify.mjs",
22
22
  "!scripts/fetch-seed.sh",
23
+ "!scripts/check-filenames.mjs",
23
24
  "!scripts/check-pr-body.mjs",
24
25
  "!scripts/check-pr-body.test.mjs",
25
26
  "!scripts/changelog-gen.test.mjs",
@@ -45,7 +46,9 @@
45
46
  "test:nish": "build/nish tests/nish/run.ts -o build/nish-runner.ir/ --link build/nish-runner && build/nish-runner",
46
47
  "test:cli": "build/nish tests/nish/cli.ts -o build/nish-cli.ir/ --link build/nish-cli && build/nish-cli",
47
48
  "test:node": "node tests/differential/unmodified.js",
48
- "lint": "biome check --formatter-enabled=false .",
49
+ "lint": "node scripts/check-filenames.mjs && biome check .",
50
+ "lint:fix": "biome check --write .",
51
+ "lint:dead": "knip --no-progress",
49
52
  "format": "biome format --write .",
50
53
  "prepublishOnly": "npm run check && npm test",
51
54
  "changelog": "node scripts/changelog-gen.mjs"
@@ -74,14 +77,15 @@
74
77
  "nish"
75
78
  ],
76
79
  "optionalDependencies": {
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"
80
+ "@amritk/nish-x86_64-linux": "0.13.0",
81
+ "@amritk/nish-aarch64-linux": "0.13.0",
82
+ "@amritk/nish-aarch64-darwin": "0.13.0",
83
+ "@amritk/nish-x86_64-darwin": "0.13.0"
81
84
  },
82
85
  "devDependencies": {
83
86
  "@biomejs/biome": "2.5.12",
84
87
  "@types/node": "^22.0.0",
88
+ "knip": "6.38.0",
85
89
  "typescript": "^5.6.0"
86
90
  }
87
91
  }
package/runtime/nish.d.ts CHANGED
@@ -34,6 +34,8 @@
34
34
  * only as the iterable of a `for...of`, and `get`'s `V | undefined` only as a
35
35
  * `const`'s initialiser, the left of `??` or an operand of `=== undefined`,
36
36
  * none of which `tsc` can know (docs/LANGUAGE.md -> `Map` and `Set`).
37
+ * `nish/map`'s `reserve` and `getOrInsert` are not declared here either: they
38
+ * are ordinary source, `std/map.ts`, which `tsconfig.json` maps `nish/*` to.
37
39
  */
38
40
 
39
41
  // ---- Numeric widths (docs/LANGUAGE.md -> Types) ------------------------------
@@ -52,6 +54,12 @@ type u64 = number;
52
54
  type f32 = number;
53
55
  type f64 = number;
54
56
 
57
+ // ---- Ranged integers (docs/wp31-ranged-integers.md) --------------------------
58
+ //
59
+ // An `i32` the compiler knows lies in `[Lo, Hi]`. The bounds are numeric
60
+ // literal types, and `tsc` checks only that; the range itself is `nish`'s.
61
+ type integer<Lo extends number, Hi extends number> = number;
62
+
55
63
  // ---- Result (docs/LANGUAGE.md -> Result and error handling) ------------------
56
64
  //
57
65
  // Modelled as the tagged union TypeScript would use anyway, intersected with
@@ -102,6 +110,19 @@ declare function Err<T, E>(error: E): Result<T, E>;
102
110
  // `Process` is not that lucky, so a project that needs `@types/node` for other
103
111
  // reasons should drop this file's `process` rather than fight it.
104
112
 
113
+ /**
114
+ * The disposable protocol `using` reads (WP29 P2, docs/wp29-thread-surface.md
115
+ * §5): declared here so that a program using `nish/threads`'s scope needs no
116
+ * `"ESNext.Disposable"` in its `lib`. `nish` itself takes `using` only for a
117
+ * `scope()`, and `[Symbol.dispose]` only in `nish/threads`.
118
+ */
119
+ interface SymbolConstructor {
120
+ readonly dispose: unique symbol;
121
+ }
122
+ interface Disposable {
123
+ [Symbol.dispose](): void;
124
+ }
125
+
105
126
  interface Console {
106
127
  /** `x` and a newline to stdout. Statement position; exactly one argument. */
107
128
  log(x: string | number | boolean): void;
@@ -150,7 +171,7 @@ declare function writeError(s: string): void;
150
171
  * `message` and a newline to stderr, then exit 1. Terminates control flow, so
151
172
  * it is `never`: that is what lets `tsc` agree that a function ending in a
152
173
  * `panic` returns, and that `x` is not null after `if (x === null) { panic(...); }`
153
- * — the guard-then-panic shape `self/` uses everywhere in place of an assert.
174
+ * — the guard-then-panic shape `src/` uses everywhere in place of an assert.
154
175
  */
155
176
  declare function panic(message: string): never;
156
177
  /**
package/runtime/nish.h CHANGED
@@ -2,10 +2,10 @@
2
2
  *
3
3
  * Include this from C drivers, N-API shims, or any other host that links the C
4
4
  * runtime next to compiled Nish modules. Everything here is a contract shared
5
- * with `self/runtime.ts` (the IR side) and the implementation, which is
5
+ * with `src/runtime.ts` (the IR side) and the implementation, which is
6
6
  * two translation units: `runtime/runtime.c` holds the core every program
7
7
  * touches — the arena, strings, arrays, number formatting, the panics — and
8
- * `runtime/runtime_os.c` holds everything that wraps a system call: the file
8
+ * `runtime/runtime-os.c` holds everything that wraps a system call: the file
9
9
  * functions, the directory and subprocess calls, `nish_getenv`, the monotonic
10
10
  * clock, `nish_platform` / `nish_arch`. They are separate so that each carries
11
11
  * its own measured code-size ceiling (docs/wp7-runtime.md, "Runtime additions
@@ -41,7 +41,7 @@ extern "C" {
41
41
  * ELF refuses to link that against a non-TLS definition. `scripts/build.sh
42
42
  * --threads` passes the macro to every input, which is how `nish --threads
43
43
  * --link` keeps the two halves in step. The same definition is in
44
- * runtime/runtime.c and runtime/runtime_wasm.c. */
44
+ * runtime/runtime.c and runtime/runtime-wasm.c. */
45
45
  #ifdef NISH_THREADS
46
46
  #define NISH_TLS _Thread_local
47
47
  #else
@@ -73,7 +73,7 @@ typedef struct nish_str {
73
73
  * `off` and `cap` are `uint64_t` and not `size_t` because that `i64` is what
74
74
  * the inlined fast path bumps and compares on every target: under wasm32 a
75
75
  * `size_t` pair would put them at bytes 4 and 8 while compiled code reads 8
76
- * and 16. runtime.c and runtime_wasm.c static-assert these offsets. */
76
+ * and 16. runtime.c and runtime-wasm.c static-assert these offsets. */
77
77
  struct nish_arena {
78
78
  char *buf;
79
79
  uint64_t off;
@@ -140,7 +140,7 @@ nish_str *nish_str_from_u64(uint64_t v);
140
140
 
141
141
  /* Process and file I/O (WP7). `nish_exit` never returns; the file functions
142
142
  * print a message to stderr and exit(1) on a fatal error. Everything from here
143
- * to the end of the clock section below is implemented in runtime_os.c, with
143
+ * to the end of the clock section below is implemented in runtime-os.c, with
144
144
  * `nish_random` and `nish_argv_init` the exceptions: neither asks the operating
145
145
  * system anything (the clock only seeds the first, and the entry point hands the
146
146
  * second its arguments), so both stay with the core. `nish_random` keeps one seed
@@ -181,11 +181,11 @@ void nish_append_file(const nish_str *path, const nish_str *data);
181
181
  * A returned array lives in the arena (valid until the next reset/release):
182
182
  * copy `len` elements out of `data` before recycling.
183
183
  *
184
- * An array field stored inside its object (`self/inline_arrays.ts`) is this
184
+ * An array field stored inside its object (`src/inline-arrays.ts`) is this
185
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
186
+ * with `h.data == (char *)slots` and `h.cap == K`; `src/runtime.ts`'s
187
+ * `ARRAY_TYPE` and `src/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
189
  * happens where no header, `.d.ts` or N-API shim describes the class, so a
190
190
  * host that includes a generated header never meets one. */
191
191
  typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
@@ -260,7 +260,7 @@ int32_t nish_spawn(const nish_array *argv);
260
260
  * paths must differ: each is opened separately with its own offset, so naming
261
261
  * one file twice makes the streams overwrite each other instead of
262
262
  * interleaving; capture them apart and concatenate to merge them. One `static`
263
- * implementation in runtime_os.c backs both spawn builtins, which is what keeps
263
+ * implementation in runtime-os.c backs both spawn builtins, which is what keeps
264
264
  * the argument vector, the wait and the signal convention written once. */
265
265
  int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_str *err);
266
266
 
@@ -316,7 +316,7 @@ double nish_parse_number(const nish_str *s, int32_t mode);
316
316
  /* Checked integer division (Rust semantics): the failed-check path. */
317
317
  void nish_panic_div(bool by_zero);
318
318
 
319
- /* ---- Parallel work (WP20 T1 / wp29 stage P1), runtime/runtime_parallel.c ----
319
+ /* ---- Parallel work (WP20 T1 / wp29 stage P1), runtime/runtime-parallel.c ----
320
320
  *
321
321
  * One region of work, divided. `nish_parallel_range` calls `body(lo, hi, ctx)`
322
322
  * once per chunk of a partition of `[0, len)`: contiguous chunks, at most one
@@ -341,6 +341,26 @@ typedef void (*nish_par_body)(int64_t lo, int64_t hi, void *ctx);
341
341
  int64_t nish_cpu_count(void);
342
342
  void nish_parallel_range(nish_par_body body, void *ctx, int64_t len, int64_t grain);
343
343
 
344
+ /* ---- A scope's tasks (wp29 stage P2), runtime/runtime-parallel.c ----
345
+ *
346
+ * `nish_scope_spawn` files one task under `scope`, an address the caller keeps
347
+ * alive until it joins: it copies `size` bytes of `payload` and keeps them with
348
+ * `run` and `finish`, and runs nothing yet. `nish_scope_join` takes every task
349
+ * filed under `scope`, runs `run(payload)` for each -- the first on the calling
350
+ * thread and each other on a thread of its own, which frees its arena before it
351
+ * exits -- waits for all of them, and then calls `finish(payload)` for each on
352
+ * the calling thread, in the order they were filed. Without `-DNISH_THREADS`,
353
+ * or when a thread cannot be created, a task runs on the calling thread, so
354
+ * running short of threads costs speed and never an answer.
355
+ *
356
+ * The preconditions are the language's (docs/LANGUAGE.md, "Scoped tasks"):
357
+ * `run` writes nothing another task or the caller can see, its result is a
358
+ * scalar it writes into its own payload, and only `finish`, on the calling
359
+ * thread, stores into memory the caller owns. */
360
+ typedef void (*nish_task_fn)(void *payload);
361
+ void nish_scope_spawn(void *scope, nish_task_fn run, nish_task_fn finish, const void *payload, int64_t size);
362
+ void nish_scope_join(void *scope);
363
+
344
364
  #ifdef __cplusplus
345
365
  }
346
366
  #endif
package/runtime/nish.mjs CHANGED
@@ -28,9 +28,9 @@
28
28
  * so is every string offset. ASCII agrees; nothing else does.
29
29
  * - **`a[i]` is unchecked**: out of range is `undefined` here and an exit-1
30
30
  * panic natively. Only a program that indexes out of range can tell.
31
- * - **Method dispatch is virtual under Node** and static natively, so an
32
- * override reached through a base-typed value differs (LANGUAGE.md,
33
- * "Method dispatch is static").
31
+ * - **A record put into an array is shared here** and copied natively, and
32
+ * `slice`, `new Array<T>(n)` and the typed-array names each differ too;
33
+ * the document lists them.
34
34
  * - **`orReturn()` does not propagate.** It throws a marker the rewriter's
35
35
  * `try`/`catch` turns into an early `return`; unmodified there is no
36
36
  * `catch`, so it escapes. `Ok`/`Err`/`isOk`/`isErr`/`value`/`error`/
@@ -54,6 +54,7 @@
54
54
  * harness already uses, so the two cannot drift apart: a semantic fixed there
55
55
  * is fixed here in the same commit.
56
56
  */
57
+ import { registerHooks } from "node:module";
57
58
  import * as shim from "./shim.mjs";
58
59
 
59
60
  /** Install `value` as a global unless the program declared its own. */
@@ -141,3 +142,15 @@ provide("Arena", {
141
142
  reset: shim.arenaReset,
142
143
  used: shim.arenaUsed,
143
144
  });
145
+
146
+ // The standard library. A program imports it as `nish/<module>`, which the
147
+ // compiler resolves to `std/<module>.ts` beside itself; this does the same for
148
+ // Node, relative to this file rather than to the program, so the same
149
+ // specifier works from any directory. Only the `nish/` package is answered
150
+ // here, and every other specifier goes to Node as it was written.
151
+ registerHooks({
152
+ resolve: (specifier, context, next) =>
153
+ specifier.startsWith("nish/")
154
+ ? next(new URL(`../std/${specifier.slice("nish/".length)}.ts`, import.meta.url).href, context)
155
+ : next(specifier, context),
156
+ });
@@ -319,7 +319,7 @@ nish_str *nish_realpath(const nish_str *path) {
319
319
  this file is compiled — a cross build compiles the runtime for the target,
320
320
  so the answer is the target's — which is why each is a string in constant
321
321
  data handed back by address: no allocation and no load, and `readnone` on
322
- the declaration (self/runtime.ts) is a fact rather than a hope. The
322
+ the declaration (src/runtime.ts) is a fact rather than a hope. The
323
323
  spellings are Node's, so a program reads the same answer from this runtime
324
324
  and from `runtime/shim.mjs`; anything neither branch names is "unknown",
325
325
  which is what `--target host` then refuses. Contracts: nish.h.