@amritk/nish 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -23
- package/bin/launcher.js +45 -41
- package/bin/packaging.js +39 -33
- package/docs/AI.md +135 -11
- package/docs/INSTALL.md +29 -13
- package/package.json +10 -6
- package/runtime/nish.d.ts +22 -1
- package/runtime/nish.h +31 -11
- package/runtime/nish.mjs +16 -3
- package/runtime/{runtime_os.c → runtime-os.c} +1 -1
- package/runtime/{runtime_parallel.c → runtime-parallel.c} +112 -4
- package/runtime/{runtime_wasm.c → runtime-wasm.c} +6 -1
- package/runtime/runtime.c +5 -5
- package/runtime/shim.mjs +6 -6
- package/scripts/bootstrap.sh +29 -29
- package/scripts/build.sh +14 -9
- package/scripts/changelog-gen.mjs +260 -192
- package/scripts/ci-profile.mjs +81 -68
- package/scripts/codes-registry.js +25 -13
- package/scripts/gen-diagnostic-codes.mjs +86 -69
- package/scripts/nish-compiler.sh +2 -0
- package/scripts/platform-package.mjs +26 -26
- package/scripts/postinstall.mjs +46 -36
- package/scripts/size-report.sh +3 -3
- package/scripts/smoke.sh +1 -1
- package/std/README.md +16 -11
- package/std/collections.ts +221 -178
- package/std/json.ts +136 -136
- package/std/map.ts +39 -0
- package/std/pair.ts +2 -2
- package/std/testing.ts +67 -67
- package/std/text.ts +54 -54
- package/std/threads.ts +126 -38
package/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
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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:
|
|
205
|
-
const
|
|
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,
|
|
361
|
-
|
|
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
|
|
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 (`
|
|
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 — `
|
|
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
|
|
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/
|
|
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 `
|
|
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/
|
|
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/
|
|
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 `
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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, `
|
|
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 `
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
78
|
-
"@amritk/nish-aarch64-linux": "0.
|
|
79
|
-
"@amritk/nish-aarch64-darwin": "0.
|
|
80
|
-
"@amritk/nish-x86_64-darwin": "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 `
|
|
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 `
|
|
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/
|
|
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/
|
|
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
|
|
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
|
|
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 (`
|
|
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`; `
|
|
187
|
-
* `ARRAY_TYPE` and `
|
|
188
|
-
* bytes, and `tests/layout/
|
|
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
|
|
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/
|
|
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
|
-
* - **
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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 (
|
|
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.
|