@amritk/nish 0.12.0 → 0.14.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.
Files changed (42) hide show
  1. package/README.md +27 -23
  2. package/bin/launcher.js +45 -41
  3. package/bin/packaging.js +39 -33
  4. package/docs/AI.md +157 -35
  5. package/docs/INSTALL.md +13 -13
  6. package/llms.txt +1 -1
  7. package/package.json +9 -6
  8. package/runtime/nish.d.ts +85 -1
  9. package/runtime/nish.h +62 -11
  10. package/runtime/nish.mjs +38 -3
  11. package/runtime/runtime-host.c +178 -0
  12. package/runtime/{runtime_os.c → runtime-os.c} +16 -1
  13. package/runtime/{runtime_parallel.c → runtime-parallel.c} +112 -4
  14. package/runtime/{runtime_wasm.c → runtime-wasm.c} +6 -1
  15. package/runtime/runtime.c +5 -5
  16. package/runtime/shim.mjs +128 -6
  17. package/scripts/bootstrap.sh +29 -29
  18. package/scripts/build.sh +19 -13
  19. package/scripts/changelog-gen.mjs +260 -192
  20. package/scripts/ci-profile.mjs +80 -68
  21. package/scripts/codes-registry.js +25 -13
  22. package/scripts/gen-diagnostic-codes.mjs +124 -85
  23. package/scripts/nish-compiler.sh +2 -0
  24. package/scripts/platform-package.mjs +26 -26
  25. package/scripts/postinstall.mjs +46 -36
  26. package/scripts/size-report.sh +3 -3
  27. package/scripts/smoke.sh +1 -1
  28. package/std/README.md +42 -2
  29. package/std/collections.ts +194 -188
  30. package/std/crypto/base64url.ts +145 -0
  31. package/std/crypto/ct.ts +64 -0
  32. package/std/crypto/hkdf.ts +118 -0
  33. package/std/crypto/hmac.ts +155 -0
  34. package/std/crypto/sha256.ts +444 -0
  35. package/std/crypto/sha512.ts +510 -0
  36. package/std/crypto/x25519.ts +494 -0
  37. package/std/json.ts +136 -136
  38. package/std/map.ts +9 -7
  39. package/std/pair.ts +2 -2
  40. package/std/testing.ts +67 -67
  41. package/std/text.ts +54 -54
  42. package/std/threads.ts +126 -38
package/docs/AI.md CHANGED
@@ -46,16 +46,18 @@ stdout, nothing on stderr, every field 1-based with `endLine`/`endColumn`
46
46
  exclusive:
47
47
 
48
48
  ```json
49
- {"file":"p.ts","line":3,"column":17,"endLine":3,"endColumn":20,"severity":"error","code":"NL2249","message":"Unknown method `map` on i32[] (supported: push, pop, indexOf, join)"}
49
+ {"file":"p.ts","line":3,"column":17,"endLine":3,"endColumn":20,"severity":"error","code":"NL2249","message":"Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill)"}
50
50
  ```
51
51
 
52
52
  - **Key on `code`, never on `message`.** A code is a promise: `NL2249` means
53
53
  the same rule next release. The prose may improve; the code may not.
54
- - **`severity`** is `"error"` or `"performance"`. A performance warning never
55
- changes the exit code — it is advice, not a rejection.
54
+ - **`severity`** is `"error"`, `"performance"` or `"portability"`. A warning
55
+ of either kind never changes the exit code — it is advice, not a rejection.
56
+ Portability warnings, the sites where the program's TypeScript reading
57
+ answers differently, print only under `--warn-portability`.
56
58
  - **Bands**: `NL1xxx` the Phase 0 forbidden-syntax sweep, `NL2xxx` the checker,
57
- `NL3xxx` the driver and modules, `NL4xxx` the interop sidecars, `NL9xxx`
58
- performance, `NL0001` syntax, `NL0002` toolchain, `NL0003` internal,
59
+ `NL3xxx` the driver and modules, `NL4xxx` the interop sidecars, `NL8xxx`
60
+ portability, `NL9xxx` performance, `NL0001` syntax, `NL0002` toolchain, `NL0003` internal,
59
61
  `NL0000` a diagnostic with no rule yet.
60
62
  - **Exit codes**: `0` ok, `1` the program was rejected, `2` usage, `3` the C
61
63
  toolchain is unusable, `70` an internal compiler error — that last one is a
@@ -114,31 +116,39 @@ rejects. This table is the highest-value part of the page.
114
116
  | `x == y` | `Loose equality is forbidden` | `x === y` |
115
117
  | `throw new Error(m)` | `` `throw` is forbidden `` | `return Err(m)`, or `panic(m)` to end the process |
116
118
  | `try { … } catch { … }` | `` `try`/`catch`/`finally` is forbidden `` | `Result<T, E>` and `isErr()` |
117
- | `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) |
119
+ | `xs.map(f)`, `.filter`, `.reduce`, `.forEach`, `.slice`, `.sort`, `.shift` | `` Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill) `` | a `for` loop, or a top-level `map(xs, f)` with a [function parameter](#function-parameters) |
118
120
  | `s.toUpperCase()`, `s.split()`, `s.trim()`, `s.replace()` | `` Unknown method … on string `` | index bytes with `charCodeAt` / `substring` |
119
121
  | `a?.b`, `a ?? b` | forbidden, except `m.get(k) ?? d` | `if (a !== null)` first |
120
- | `x as T`, `<T>x`, `x!` | `Unsupported expression in Phase 1: AsExpression` | there are no casts; `implements` is the only widening |
122
+ | `x as T`, `<T>x`, `x satisfies T` | `Unsupported expression in Phase 1: AsExpression` (to `any` or `unknown`: `` Type assertion to `any` is forbidden ``); `x!` is a syntax error | there are no casts; `implements` is the only widening |
123
+ | `typeof x`, `x instanceof C`, `"k" in o`, `delete o.k`, `void 0`, `a, b` | each refused by name (`` `typeof` is forbidden ``, …) | there is no runtime type information: a value's type is its declared one |
124
+ | `a ** b`, `[...xs]`, `[1, , 2]` | `` Unsupported binary operator `**` `` / `Spread in array literals is not supported` / `Holes in array literals are not supported` | `Math.pow` on `f64`, a loop, every element written out |
121
125
  | `any`, `unknown` | forbidden | name the real type |
122
126
  | `undefined` | forbidden, except `x === undefined` on a `Map.get` result | `null`, with a `T \| null` type |
123
127
  | `let total = 0` at the top level | `` Top-level `let` is not supported `` | a module `const`, or a local |
128
+ | `for (const k in o)` | `` `for...in` is forbidden `` | `for (const x of xs)` over an array, or `for (const k of m.keys())` over a `Map` |
124
129
  | 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) |
125
- | `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 |
130
+ | `type Pair<T>` (a generic alias) | `Generic type parameters are forbidden on a type alias` | 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 |
126
131
  | `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 |
127
132
  | `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 |
128
133
  | `<T>(p: T) => p.x` (a member of a type parameter) | `` Cannot read `x` of `T`: an unconstrained type parameter has no members `` | `<T extends Point>(p: T) => p.x`, where `Point` is a class or interface that declares `x` |
129
134
  | `identity<i32>(7)` (type argument at a call) | `Type arguments are not written at a call site in Nish` | `identity(7)` — `T` is inferred from the arguments |
130
135
  | `async` / `await` / `Promise` | forbidden (no event loop) | the I/O builtins are synchronous |
136
+ | `namespace N { }`, `declare global { }`, `@decorator` | forbidden | one module per file; a plain function in place of a decorator |
137
+ | `keyof T`, `<T = i32>` (a default type argument) | `` Unsupported type `keyof T` `` / `a default type argument … is not supported` | name the type; a type argument is always inferred |
131
138
  | `class B extends A` | `` `extends` is not supported: Nish has no inheritance `` | repeat the fields and `implements` an interface |
132
- | `static` members, getters/setters | not supported | module `const`s and plain methods |
139
+ | `static` members, `get x()` / `set x(v)` | `` … `static` members are not supported `` / `` Getters and setters are not supported in class `C` (use a method) `` | module `const`s and plain methods |
140
+ | `const [a, b] = xs`, `({ x }: Point) =>`, `(n = 1)`, `(n?: i32)`, `(...ns: i32[])` | `Destructuring is not supported` / `Destructured parameters are not supported` / `Optional/default parameters are not supported` / `Rest parameters are not supported` | one name per binding, every parameter passed; an array for a variable count |
141
+ | `function f(): void;` (an overload), `export default function ()` | `Functions must have a body` / `Functions must be named` | one named function, with its body |
142
+ | a method in an `interface` | `` Interface `I` cannot declare methods (interfaces describe layout only) `` | fields only; a top-level function over the interface |
143
+ | `abstract class`, `declare class`, `constructor(public x: i32)`, `interface B extends A`, `[k: string]: T` | `Abstract classes are not supported` / `` `declare class` is not supported `` / `Parameter properties … are not supported; declare the field and assign it` / ``Interface inheritance (`extends`) is not supported; list every field`` / `Index signatures are not supported …` | a plain class with its fields declared and assigned in the constructor; every field listed; a `Map` for a keyed table |
144
+ | `{ m() { … } }`, `{ "a": 1 }`, `const C = class { }` | ``Unsupported object literal member: MethodDeclaration (only `key: value`)`` / `Object literal keys must be plain identifiers` / `Classes must be named` | fields with identifier keys, a top-level function over the interface, a named top-level class |
133
145
  | `type Pair = { a: i32 }` (inline object type) | `` Unsupported type `{ a: i32 }` `` | declare an `interface` |
134
146
  | `A \| B` unions | `` Union types other than `T \| null` are forbidden `` | one type, or an `interface` prefix |
135
147
  | `String(n)`, `n.toString()` | `` Unknown function `String` `` / `` Unknown method `toString` on i32 `` | `` `${n}` `` |
136
148
  | `xs.length = 0` | `` Cannot assign to `length` of i32[] (array length is read-only; use `push`) `` | build a new array |
137
- | `for (const k in o)` | `Unsupported statement in Phase 1: ForInStatement` | `for (const x of xs)` over an array |
138
149
  | `import { readFileSync } from "fs"` | `` Cannot find package `fs` `` — a bare specifier is a **package name**, so it is looked for in `node_modules`; one that is installed but has no `nish` condition is `` Package `fs` has no Nish entry point `` | `readFileSync` is a global; no import needed (or `import { readFileSync } from "nish:fs"`) |
139
150
  | `export default f` | `` `export default` / `export =` are not supported `` | `export const f = …` |
140
- | `export type T = …`, `export enum K` | cannot be exported | declare the alias/enum in each module that needs it |
141
- | `new Date()`, `Date.now()` | `` Unknown builtin `Date.now` `` | `monotonicNanos()` for elapsed time; there is no wall clock and no calendar |
151
+ | `new Date()` | `` `new Date()` is refused: Nish has no `Date` object; the one `Date` member is `Date.now()`, the wall clock in milliseconds `` | `Date.now()` for the wall clock, `monotonicNanos()` for elapsed time; there is no calendar |
142
152
  | `JSON.parse`, `RegExp`, `Promise` | unknown / forbidden | none of these exist; write them or restructure |
143
153
  | `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) |
144
154
  | `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) { … }` |
@@ -198,16 +208,42 @@ const good = (a: i32, b: i64): i64 => {
198
208
  };
199
209
  ```
200
210
 
201
- **`integer` is reserved, not a type.** It is held for ranged integers
202
- (`integer<Lo, Hi>`), which the compiler does not build yet, so writing it as a
203
- type is refused. Declaring a type alias, enum, class, interface or function
204
- named `integer` is refused too. Use `i32`. A value named `integer` (a local
205
- or a module constant) is fine.
211
+ **`integer<Lo, Hi>` is an `i32` that stays in `[Lo, Hi]`.** The bounds are two
212
+ integer literals (a sign allowed) inside `i32`, and it is an `i32` everywhere
213
+ the machine can see. Putting a value *into* one is checked: an `i32` or
214
+ another range is compared once and the program panics (exit 1) outside the
215
+ range, a literal outside it is a compile error, and a literal inside it or a
216
+ narrower range costs nothing. So does a value a loop condition or a guard
217
+ already bounds (`b` below), and `toI32` of a `u8`; a check left inside a loop
218
+ is a performance warning (NL9013) naming the guard that removes it. A range
219
+ bounds an index too: `integer<0, 255>`, like `u8`, needs only a length guard of
220
+ 256. Taking one *out* is free: every operator reads
221
+ it as `i32`, a `const` keeps the range and a `let` widens to `i32`. A `u8` is
222
+ not an `integer<0, 255>` (convert with `toI32`), and a `declare function` may
223
+ not mention a range. Put the range on the value you use, not on a loop
224
+ counter, which has to leave the range to end the loop.
206
225
 
207
- ```ts nish:err NL2333
208
- const clamp = (x: integer): i32 => x;
226
+ ```ts nish:ok
227
+ const getByte = (buf: u8[], i: integer<0, 255>): u8 => buf[i];
228
+
229
+ const sumAll = (buf: u8[]): i32 => {
230
+ let sum = 0;
231
+ for (let i = 0; i < 256 && i < buf.length; i++) {
232
+ const b: integer<0, 255> = i;
233
+ sum = sum + toI32(getByte(buf, b));
234
+ }
235
+ return sum;
236
+ };
209
237
  ```
210
238
 
239
+ ```ts nish:err-body NL2384
240
+ const digit: integer<0, 9> = 12;
241
+ ```
242
+
243
+ Declaring a type alias, enum, class, interface or function named `integer` is
244
+ refused, because the name is the type. A value named `integer` (a local or a
245
+ module constant) is fine.
246
+
211
247
  ```ts nish:err NL2332
212
248
  class integer {
213
249
  value: i32 = 0;
@@ -317,6 +353,8 @@ Narrowing follows the same engine as `T | null` below: it applies to a
317
353
  **variable**, never a property path; it ends at any assignment to that
318
354
  variable; and it is dropped before a loop that assigns it. `if (r.isOk()) A
319
355
  else B` narrows in `A`, and after the `if` when `B` cannot fall through.
356
+ The proof stays with `r`: `const y = r`, `c ? r : q` and `[r]` are plain
357
+ `Result`s, so test `y` itself before `y.value`.
320
358
 
321
359
  `panic(message)` is the other ending: message to stderr, exit 1. It is for an
322
360
  invariant that cannot hold, not for a failure a caller should handle. It
@@ -360,8 +398,9 @@ export const main = (): i32 => {
360
398
  `Map.get` (see [Map and Set](#map-and-set)).
361
399
  - Narrowing applies to a **local or parameter**, never a property path. `if
362
400
  (n.next !== null) n.next.v` is rejected — copy into a local first.
363
- - A narrowing ends at any assignment to the variable, and is dropped before a
364
- loop whose body, condition or update assigns it.
401
+ - A narrowing ends at any assignment to the variable, including one in an
402
+ earlier operand of the same `&&` / `||` chain, and is dropped before a loop
403
+ whose body, condition or update assigns it.
365
404
  - Two nullables cannot be compared with each other; compare each with `null`.
366
405
  - `new Array<T | null>(n)` is allowed: the zero fill *is* `null`.
367
406
 
@@ -723,6 +762,62 @@ import { parallelReduce } from "nish/threads";
723
762
  export const main = (): i32 => parallelReduce([1, 2, 3], (a, b) => a + b, 1); // `+` folds from 0
724
763
  ```
725
764
 
765
+ ### Scoped tasks: `using s = scope()`
766
+
767
+ Different functions at once, one thread each: open a scope with `using`, give
768
+ it tasks with `spawn(entry, arg, dst, at)`, and every task has run and stored
769
+ `entry(arg)` into `dst[at]` when the block ends. (A plain block, for the same
770
+ reason as the one above: `tests/link/thread_scope_basic` compiles it.)
771
+
772
+ ```ts
773
+ import { scope } from "nish/threads";
774
+
775
+ const sumOf = (xs: f64[]): f64 => xs[0] + xs[1];
776
+ const square = (n: i32): i32 => n * n;
777
+
778
+ export const main = (): i32 => {
779
+ const xs: f64[] = [1.5, 2.5];
780
+ const sums: f64[] = [0.0];
781
+ const squares: i32[] = [0];
782
+ {
783
+ using s = scope();
784
+ s.spawn(sumOf, xs, sums, 0);
785
+ s.spawn(square, 12, squares, 0);
786
+ } // joined here, on every exit
787
+ console.log(`${sums[0]} ${squares[0]}`); // 4 144
788
+ return 0;
789
+ };
790
+ ```
791
+
792
+ - **The tasks run when the block ends**, together, and each answer is stored
793
+ after the last one finishes. So read a destination after the block. A
794
+ destination is a `const` bound to a fresh array (`[0, 0]`, `new Array`) that
795
+ you only index; never pass it on. Between the first `spawn` and the block's
796
+ end, don't read a destination, and don't write memory a task could read (a
797
+ store into an argument, a call that writes through its argument).
798
+ - **`using` takes only `scope()`**, and `scope()` only comes from `using`.
799
+ The scope is only ever the receiver of a `spawn` statement: never pass it,
800
+ store it or return it.
801
+ - **The task is a named top-level function**, not an arrow, and it follows the
802
+ data-parallel rules: it writes nothing anybody else can see, answers a number,
803
+ a `boolean` or an enum, and leaves `Arena` alone. A task cannot open a scope
804
+ of its own or call `parallelMapInto`.
805
+ - **Any argument will do** — an array, an object — because nothing writes
806
+ memory while the tasks run. There is no thread count: one task, one thread.
807
+
808
+ ```ts nish:err NL2388
809
+ import { scope } from "nish/threads";
810
+
811
+ const one = (n: i32): i32 => n + 1;
812
+
813
+ export const main = (): i32 => {
814
+ const out: i32[] = [0];
815
+ const s = scope(); // a scope must be introduced by `using`
816
+ s.spawn(one, 1, out, 0);
817
+ return out[0];
818
+ };
819
+ ```
820
+
726
821
  ### Calling C
727
822
 
728
823
  `declare function name(params): T;` declares a C function this program calls but
@@ -911,15 +1006,21 @@ export const main = (): i32 => {
911
1006
  point: `Kind` and `i32` never convert in either direction. Members must be
912
1007
  numeric literals. `===` and `!==` are the **only** operators — no arithmetic,
913
1008
  no bitwise, so **bit flags stay `i32` module constants**.
914
- - **Neither can be exported.** Both are module-local; declare them in every
915
- module that needs them. A class or interface is what crosses a module
916
- boundary.
1009
+ - **Both can be exported.** `export enum Kind` is imported like a function,
1010
+ `import { Kind } from "./kinds"` (renamed with `as` if you like), and in the
1011
+ importer it *is* the exporter's `Kind`: the members fold to the exporter's
1012
+ integers and a `Kind` value crosses a call as itself. A `Kind` a second
1013
+ module declares for itself is a different type, however alike the two are.
1014
+ `export type Conn = Socket | null` is imported the same way and is, in the
1015
+ importer, the type it names; its right-hand side is resolved in the module
1016
+ that wrote it, so the importer need not import `Socket`. A module does not
1017
+ re-export an enum or alias it imported.
917
1018
  - Both are top-level only.
918
1019
 
919
1020
  ### Modules
920
1021
 
921
- - `export` goes on `const` (function or constant), `class`, and `interface`
922
- declarations. No `export default`, no `export { … }`, no `export *`.
1022
+ - `export` goes on `const` (function or constant), `class`, `interface`,
1023
+ `enum` and `type` declarations. No `export default`, no `export { … }`, no `export *`.
923
1024
  - The only import form is a **named import**:
924
1025
  `import { square, cube as pow3 } from "./math"`. `.ts` is optional. Default
925
1026
  imports, namespace imports, side-effect imports and type-only imports are all
@@ -940,8 +1041,8 @@ export const main = (): i32 => {
940
1041
  `"engines": {"nish": ">=X.Y.Z"}` is above this compiler `` needs a newer
941
1042
  compiler ``, and a `package.json` that is not JSON names its line and
942
1043
  column — each with its own `--json` code (`NL3017`–`NL3021`).
943
- - Functions may be renamed on import; classes and interfaces may not — the type
944
- name is part of the ABI.
1044
+ - Functions and enums may be renamed on import; classes and interfaces may
1045
+ not — the type name is part of the ABI.
945
1046
  - Import cycles are allowed; a shared dependency is compiled once.
946
1047
  - **A function name is unique across the whole program**, exported or not.
947
1048
  - An `export`ed function is an external C-ABI symbol; every other function gets
@@ -963,7 +1064,8 @@ export const main = (): i32 => {
963
1064
  - `for (init; cond; update)` with every clause optional, and
964
1065
  `for (const x of xs)` over an **array** only — `x` gets the element type and
965
1066
  must not be annotated. The array's `length` is re-read each iteration, so a
966
- `push` inside the body extends the loop.
1067
+ `push` inside the body extends the loop. `for...in`, `for await` and
1068
+ `for (x of xs)` over an existing `x` are each refused by name.
967
1069
  - `switch` takes an **integer or enum** discriminant; every `case` label is an
968
1070
  integer constant expression of that type. **There is no implicit
969
1071
  fallthrough** — a clause with statements ends in `break`, `return`,
@@ -1048,9 +1150,19 @@ constants `Math.PI` / `Math.E`. The f64-only ones reject an `i32`: write
1048
1150
  `Number(x)`, `parseInt(s)` (base 10, `i32`, no `NaN` — no digits give `0`),
1049
1151
  `parseFloat(s)`.
1050
1152
 
1153
+ **Constant time.** `ctSelect(mask, a, b)` is `(a & mask) | (b & ~mask)` and
1154
+ `ctEq(a, b)` is all-ones when equal and zero otherwise, both over `u32` or
1155
+ `u64` only, every operand one type. The mask sits behind an optimisation
1156
+ barrier, so neither ever becomes a branch; select and compare on a secret with
1157
+ these, never with `if`, `?:`, `===` or `table[secret]`, which they cannot fix.
1158
+
1051
1159
  **Arrays.** `a.length` (read-only), `a.push(v)`, `a.pop()` (panics when empty —
1052
1160
  there is no `undefined` to return), `a.indexOf(v)`, `a.join(sep)` — **`join` is
1053
- `string[]` only**. That is every array method there is.
1161
+ `string[]` only** — and, on an array of numbers only, `dst.set(src[, offset])`
1162
+ (one `memmove`; a range past the end panics) and `a.fill(v[, start[, end]])`
1163
+ (ends clamped and counted back from the end when negative, as in JavaScript;
1164
+ never panics). Both are statements. That is every array method there is. A
1165
+ window into a buffer is `(buf, offset, length)`; there is no view type.
1054
1166
 
1055
1167
  **Strings.** `s.length` (**bytes**), `s.charCodeAt(i)` (the byte, bounds-checked),
1056
1168
  `s.substring(start[, end])`, `s.slice(start[, end])`, `s.indexOf(sub)`,
@@ -1066,15 +1178,24 @@ in half is possible.
1066
1178
  `process.platform`, `process.arch`.
1067
1179
 
1068
1180
  **Files and the system.** `readFileSync(path)` (exits 1 on failure),
1069
- `readFileSyncOrNull(path)` (`string | null`), `writeFileSync(path, data)`,
1181
+ `readFileSyncOrNull(path)` (`string | null`), `readFileBytesSync(path)`
1182
+ (`u8[] | null`, the bytes as they are on disk), `writeFileSync(path, data)`,
1070
1183
  `appendFileSync(path, data)`, `mkdirSync(path)` (one level, `boolean`),
1071
1184
  `isDirectorySync(path)`, `readdirSync(path)` (`string[] | null`, sorted by
1072
1185
  bytes, no `.`/`..`), `spawnSync(argv)`, `spawnSyncTo(argv, outPath, errPath)`,
1073
1186
  `getenv(name)` (`string | null` — unset and empty are different answers),
1074
1187
  `realpathSync(path)` (`string | null`; symbolic links resolved, absolute, and
1075
1188
  `null` when it does not resolve),
1076
- `monotonicNanos()` (`i64`; elapsed time only, **there is no wall clock and no
1077
- `Date`**).
1189
+ `monotonicNanos()` (`i64`; elapsed time only).
1190
+
1191
+ **The host.** `Date.now()` (an `f64` of whole milliseconds, the wall clock —
1192
+ **the only `Date` there is**: `new Date()`, `Date.parse` and the rest are
1193
+ refused), `crypto.getRandomValues(bytes)` (a statement over a `u8[]` only, at
1194
+ most 65,536 bytes a call, from the kernel's CSPRNG; more panics),
1195
+ `statMtimeSync(path)` (an `f64` of milliseconds, **NaN** — not `null` — when the
1196
+ path cannot be stat'd, so test `m !== m`), and `signalFd()` / `readSignal(fd)`:
1197
+ call `signalFd()` once at the top of `main`, then `readSignal(fd)` blocks until
1198
+ SIGTERM or SIGINT and answers 15 or 2. None of them exists on a wasm target.
1078
1199
 
1079
1200
  **Arena.** `Arena.mark()`, `Arena.release(m)`, `Arena.reset()`, `Arena.used()`
1080
1201
  — see below.
@@ -1291,8 +1412,9 @@ export const main = (): i32 => {
1291
1412
  **Optional parameters** — there are none. Write two functions with different
1292
1413
  names, or take the value and document the sentinel.
1293
1414
 
1294
- **A wall clock, regex, JSON, threads of your own** — not in the language. Say
1295
- so rather than emitting code that cannot compile.
1415
+ **A calendar, regex, JSON, threads of your own** — not in the language (the
1416
+ wall clock is `Date.now()`, and that is all of `Date`). Say so rather than
1417
+ emitting code that cannot compile.
1296
1418
 
1297
1419
  ## Before you say it compiles
1298
1420
 
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
 
package/llms.txt CHANGED
@@ -26,7 +26,7 @@ the compiler.
26
26
  - [Rules card](https://raw.githubusercontent.com/amritk/nish/main/docs/AI.md): the whole language as rules, sized for one read — the traps first, then types, `Result`, nullables, declarations, statements, the complete builtin inventory, and recipes for what is missing. Ships in the npm package as `docs/AI.md`.
27
27
  - [Language reference](https://raw.githubusercontent.com/amritk/nish/main/docs/LANGUAGE.md): normative and exhaustive. Every rule cites the test that pins it and quotes the exact rejection message. Settles any disagreement with the rules card.
28
28
  - [Ambient declarations](https://raw.githubusercontent.com/amritk/nish/main/runtime/nish.d.ts): reference this from `tsconfig.json` and `tsc --strict`, your editor and your language server accept `Result<T, E>`, `i32` and the rest. `nish` is still the authority — `tsc` cannot see the flow rules.
29
- - [Standard library](https://raw.githubusercontent.com/amritk/nish/main/std/README.md): `std/testing`, a suite a program drives to check itself and answer an exit code; `std/text`, the `split` / `trim` / `replace` the language does not have; `std/json`, the value of one field of one flat JSON object — enough to read this compiler's own `--json` output. Source rather than a built library, imported by relative path, and shipped in the npm package under `std/`.
29
+ - [Standard library](https://raw.githubusercontent.com/amritk/nish/main/std/README.md): `std/testing`, a suite a program drives to check itself and answer an exit code; `std/text`, the `split` / `trim` / `replace` the language does not have; `std/json`, the value of one field of one flat JSON object — enough to read this compiler's own `--json` output; `nish/crypto`, SHA-256, SHA-384 and SHA-512, HMAC, HKDF, a constant-time compare, base64url and X25519, one module each (`nish/crypto/sha256` and so on). Source rather than a built library, imported as `nish/<module>`, and shipped in the npm package under `std/`.
30
30
  - [FAQ](https://raw.githubusercontent.com/amritk/nish/main/docs/FAQ.md): why no `any`, why `i32`, how to get JavaScript numbers, why no GC, overflow and `--wrapping`, what errors look like.
31
31
  - [Install](https://raw.githubusercontent.com/amritk/nish/main/docs/INSTALL.md): prerequisites per OS, hello world, exit codes, troubleshooting.
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish",
3
- "version": "0.12.0",
3
+ "version": "0.14.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",
@@ -46,7 +46,9 @@
46
46
  "test:nish": "build/nish tests/nish/run.ts -o build/nish-runner.ir/ --link build/nish-runner && build/nish-runner",
47
47
  "test:cli": "build/nish tests/nish/cli.ts -o build/nish-cli.ir/ --link build/nish-cli && build/nish-cli",
48
48
  "test:node": "node tests/differential/unmodified.js",
49
- "lint": "node scripts/check-filenames.mjs --advisory && 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",
50
52
  "format": "biome format --write .",
51
53
  "prepublishOnly": "npm run check && npm test",
52
54
  "changelog": "node scripts/changelog-gen.mjs"
@@ -75,14 +77,15 @@
75
77
  "nish"
76
78
  ],
77
79
  "optionalDependencies": {
78
- "@amritk/nish-x86_64-linux": "0.12.0",
79
- "@amritk/nish-aarch64-linux": "0.12.0",
80
- "@amritk/nish-aarch64-darwin": "0.12.0",
81
- "@amritk/nish-x86_64-darwin": "0.12.0"
80
+ "@amritk/nish-x86_64-linux": "0.14.0",
81
+ "@amritk/nish-aarch64-linux": "0.14.0",
82
+ "@amritk/nish-aarch64-darwin": "0.14.0",
83
+ "@amritk/nish-x86_64-darwin": "0.14.0"
82
84
  },
83
85
  "devDependencies": {
84
86
  "@biomejs/biome": "2.5.12",
85
87
  "@types/node": "^22.0.0",
88
+ "knip": "6.38.0",
86
89
  "typescript": "^5.6.0"
87
90
  }
88
91
  }
package/runtime/nish.d.ts CHANGED
@@ -54,6 +54,12 @@ type u64 = number;
54
54
  type f32 = number;
55
55
  type f64 = number;
56
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
+
57
63
  // ---- Result (docs/LANGUAGE.md -> Result and error handling) ------------------
58
64
  //
59
65
  // Modelled as the tagged union TypeScript would use anyway, intersected with
@@ -104,6 +110,19 @@ declare function Err<T, E>(error: E): Result<T, E>;
104
110
  // `Process` is not that lucky, so a project that needs `@types/node` for other
105
111
  // reasons should drop this file's `process` rather than fight it.
106
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
+
107
126
  interface Console {
108
127
  /** `x` and a newline to stdout. Statement position; exactly one argument. */
109
128
  log(x: string | number | boolean): void;
@@ -142,6 +161,18 @@ declare function toF64(x: number | boolean): f64;
142
161
  declare function f64ToBits(x: f64): i64;
143
162
  declare function bitsToF64(bits: i64): f64;
144
163
 
164
+ // ---- Constant time (WP34 N6) ------------------------------------------------
165
+
166
+ /**
167
+ * `(a & mask) | (b & ~mask)` with the mask hidden from the optimiser, so it is
168
+ * never turned into a branch: `a` for an all-ones mask, `b` for zero. Every
169
+ * operand is one type, `u32` or `u64`. Both are `number` here, so one generic
170
+ * declaration stands for the two.
171
+ */
172
+ declare function ctSelect<T extends u32 | u64>(mask: T, a: T, b: T): T;
173
+ /** All-ones of the operands' type when `a === b`, zero otherwise, without a branch. */
174
+ declare function ctEq<T extends u32 | u64>(a: T, b: T): T;
175
+
145
176
  // ---- Streams and files (globals: Nish has no package resolution) ---------
146
177
 
147
178
  /** `s` to stdout with no trailing newline and no conversion. */
@@ -152,7 +183,7 @@ declare function writeError(s: string): void;
152
183
  * `message` and a newline to stderr, then exit 1. Terminates control flow, so
153
184
  * it is `never`: that is what lets `tsc` agree that a function ending in a
154
185
  * `panic` returns, and that `x` is not null after `if (x === null) { panic(...); }`
155
- * — the guard-then-panic shape `self/` uses everywhere in place of an assert.
186
+ * — the guard-then-panic shape `src/` uses everywhere in place of an assert.
156
187
  */
157
188
  declare function panic(message: string): never;
158
189
  /**
@@ -163,6 +194,12 @@ declare function panic(message: string): never;
163
194
  declare function readFileSync(path: string): string;
164
195
  /** The same read, answering `null` for every path the other exits over. */
165
196
  declare function readFileSyncOrNull(path: string): string | null;
197
+ /**
198
+ * The file's bytes as they are on disk — no UTF-8 assumed, so a zero byte and
199
+ * a byte of 0x80 or above survive — or `null` for every path
200
+ * `readFileSyncOrNull` answers `null` for.
201
+ */
202
+ declare function readFileBytesSync(path: string): u8[] | null;
166
203
  declare function writeFileSync(path: string, data: string): void;
167
204
  declare function appendFileSync(path: string, data: string): void;
168
205
  /** One directory, not recursive; whether a directory is there afterwards. */
@@ -194,6 +231,34 @@ declare function getenv(name: string): string | null;
194
231
  * origin is arbitrary, so only the difference between two reads is meaningful.
195
232
  */
196
233
  declare function monotonicNanos(): i64;
234
+ /**
235
+ * The modification time of `path` in milliseconds since the epoch, with the
236
+ * sub-millisecond fraction the file system keeps (Node's `mtimeMs`), or NaN
237
+ * when it cannot be stat'd. Follows a symbolic link; a directory has one too.
238
+ */
239
+ declare function statMtimeSync(path: string): f64;
240
+ /**
241
+ * A descriptor that becomes readable when SIGTERM or SIGINT arrives, whichever
242
+ * thread the signal lands on, made once (every call answers the same one), or -1.
243
+ */
244
+ declare function signalFd(): i32;
245
+ /**
246
+ * Block until SIGTERM or SIGINT arrives and answer its number, 15 or 2; -1 for
247
+ * any `fd` that is not `signalFd()`'s. No reading under Node, which throws.
248
+ */
249
+ declare function readSignal(fd: i32): i32;
250
+
251
+ // ---- `Date` and `crypto` (WP34 N3) -----------------------------------------------
252
+ //
253
+ // `lib.es2022` already declares `Date`, whose `now()` is Nish's one member of it,
254
+ // so nothing is added: `tsc` accepting `new Date()` is `tsc` not being Nish's
255
+ // checker. `crypto` is a Web API that `lib.es2022` leaves out, so it is
256
+ // declared with its one member, typed as Nish types it.
257
+
258
+ declare var crypto: {
259
+ /** Every byte of `bytes` from the system's CSPRNG; at most 65,536 bytes a call. */
260
+ getRandomValues(bytes: u8[]): void;
261
+ };
197
262
 
198
263
  // ---- The builtin modules (`nish:`) ---------------------------------------------
199
264
  //
@@ -210,6 +275,7 @@ declare function monotonicNanos(): i64;
210
275
  declare module "nish:fs" {
211
276
  export function readFileSync(path: string): string;
212
277
  export function readFileSyncOrNull(path: string): string | null;
278
+ export function readFileBytesSync(path: string): u8[] | null;
213
279
  export function writeFileSync(path: string, data: string): void;
214
280
  export function appendFileSync(path: string, data: string): void;
215
281
  /** `true` when the directory was created, `false` when it already existed. */
@@ -221,6 +287,8 @@ declare module "nish:fs" {
221
287
  */
222
288
  export function readdirSync(path: string): string[] | null;
223
289
  export function realpathSync(path: string): string | null;
290
+ /** Node's `mtimeMs` for the path, or NaN when it cannot be stat'd. */
291
+ export function statMtimeSync(path: string): f64;
224
292
  }
225
293
 
226
294
  declare module "nish:process" {
@@ -238,6 +306,10 @@ declare module "nish:process" {
238
306
  * difference between two reads is meaningful.
239
307
  */
240
308
  export function monotonicNanos(): i64;
309
+ /** A descriptor readable when SIGTERM or SIGINT arrives; the same one every call, or -1. */
310
+ export function signalFd(): i32;
311
+ /** Block until SIGTERM or SIGINT arrives: 15 or 2, or -1 for a descriptor that is not `signalFd()`'s. */
312
+ export function readSignal(fd: i32): i32;
241
313
  /** The command line; `argv[0]` is the program path, as in C. Read-only. */
242
314
  export const argv: readonly string[];
243
315
  /** The operating system the program runs on: `"linux"`, `"darwin"`, or `"unknown"`. */
@@ -284,6 +356,18 @@ declare interface CPtr {
284
356
  readonly __nishForeignPointer: unique symbol;
285
357
  }
286
358
 
359
+ // ---- `set` on an array ----------------------------------------------------------
360
+ //
361
+ // `dst.set(src, offset)` copies all of `src` into `dst` from `offset` on, with
362
+ // `TypedArray.prototype.set`'s meaning (WP34 N2). A `u8[]` is an `Array` to
363
+ // `tsc`, and `lib.es5.d.ts` gives `Array` a `fill` of the same meaning but no
364
+ // `set`, so this is the one method added to it; `runtime/nish.mjs` installs it
365
+ // under Node. Nish admits it on an array of numbers only.
366
+
367
+ interface Array<T> {
368
+ set(source: readonly T[], offset?: number): void;
369
+ }
370
+
287
371
  // ---- What this file cannot say ----------------------------------------------
288
372
  //
289
373
  // The typed-array aliases (`Int32Array`, `Float32Array`, `Float64Array`,