@amritk/nish 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -49,22 +49,19 @@ Requirements: Node.js 22.18+ and, to produce binaries, clang (LLVM 18) + lld;
49
49
  per-OS install commands are in [docs/INSTALL.md](docs/INSTALL.md).
50
50
 
51
51
  ```bash
52
- # Once this is published, the whole install is either of:
53
- # npm install -g @amritk/nish
54
- # curl -fsSL https://raw.githubusercontent.com/amritk/nish/main/install.sh | sh
52
+ npm install -g @amritk/nish # or per project: npm i -D @amritk/nish
53
+ npx @amritk/nish --version # or run it without installing
54
+ # or, with no Node at all:
55
+ curl -fsSL https://raw.githubusercontent.com/amritk/nish/main/install.sh | sh
55
56
  # `nish` on the registry is an unrelated package from 2014, so this one is
56
- # scoped; the command it installs is still `nish`. Nothing is published yet,
57
- # so take it from a release -- the npm tarball, plus the prebuilt native
58
- # compiler for your machine, which is what the scoped install would fetch.
59
- base=https://github.com/amritk/nish/releases/download/v0.4.0
60
- curl -LO $base/amritk-nish-0.4.0.tgz
61
- curl -LO $base/amritk-nish-x86_64-linux-0.4.0.tgz # or the row for your machine
62
- npm install -g ./amritk-nish-0.4.0.tgz ./amritk-nish-x86_64-linux-0.4.0.tgz
57
+ # scoped; the command it installs is still `nish`. npm fetches the prebuilt
58
+ # native compiler for your machine alongside it.
63
59
  ```
64
60
 
65
61
  > [!NOTE]
66
- > **Install the pair.** The main package is a launcher and carries no compiler:
67
- > installed on its own it has nothing to hand over to and says so, exiting 3.
62
+ > **From a release tarball rather than the registry, install the pair.** The
63
+ > main package is a launcher and carries no compiler: installed on its own it
64
+ > has nothing to hand over to and says so, exiting 3.
68
65
  > The same is true on a platform this project publishes no binary for — musl,
69
66
  > FreeBSD, 32-bit anything — which since 0.6.0 gets a diagnostic naming the four
70
67
  > that do rather than the TypeScript compiler under Node that used to ship
@@ -91,6 +88,7 @@ export const main = (): number => {
91
88
  nish hello.ts --link hello # writes hello.ll, then builds hello with clang -O3 -flto
92
89
  ./hello # hello from Nish
93
90
  nish hello.ts -o hello.ll # IR only
91
+ nish run hello.ts # build into a cache and run it: a script, still native
94
92
  ```
95
93
 
96
94
  The IR is readable as is. `examples/add.ts` compiles to:
@@ -278,6 +276,7 @@ buys is the output: no GC, no runtime, and the sizes under
278
276
 
279
277
  ```
280
278
  nish <entry.ts> [more.ts ...] [options]
279
+ nish run [options] <file.ts> [args ...]
281
280
  nish --version | --help
282
281
  -o, --output <file.ll> output path for a single module (default: <input>.ll)
283
282
  -o, --output <dir>/ output directory: one <dir>/<module>.ll per module
@@ -330,6 +329,22 @@ with `2`. `-g` adds a DWARF line table and variables to the IR so
330
329
  Multi-file programs: `nish examples/multi/main.ts --link build/multi && ./build/multi; echo $?`
331
330
  prints `49`.
332
331
 
332
+ **`nish run` is the scripting shape.** `nish run tool.ts a b` compiles
333
+ `tool.ts`, links it into a cache, and starts it with `a b` as its arguments.
334
+ Its stdin, stdout and stderr are the program's, and its exit status is the
335
+ program's once it starts (`128 + n` for a signal). The options before the file
336
+ are the compiler's (`--number-mode f64`, `--profile speed`, `-g`), and
337
+ everything after it is the program's, `--help` included. Nothing is
338
+ interpreted: every run compiles, which takes milliseconds, and links only when
339
+ the IR, the recipe or the runtime is new, so the first run of an edit pays for
340
+ one `--profile debug` link (the default here, and the fast one) and every run
341
+ after that starts the cached binary. The cache is `$XDG_CACHE_HOME/nish/run`,
342
+ or `~/.cache/nish/run`. Each entry is one program, so `rm -rf` of it is always
343
+ safe. `-o`, `--link`, `--target`, the `--emit-*` sidecars and
344
+ `--profile wasi` write something a run keeps to itself, so they are usage
345
+ errors there, and performance warnings are not printed, because stderr belongs
346
+ to the program.
347
+
333
348
  Without `--link`, build the IR yourself: `clang add.ll examples/main.c runtime/runtime.c runtime/runtime_os.c -o app`
334
349
  (the `overriding the module target triple` warning is harmless: the IR is
335
350
  target-neutral unless you pass `--target`; `-Wno-override-module` silences
@@ -550,4 +565,5 @@ coding agents are in [AGENTS.md](AGENTS.md) and [`.claude/`](.claude/).
550
565
 
551
566
  ## License
552
567
 
553
- MIT, see [LICENSE](LICENSE).
568
+ MIT, see [LICENSE](LICENSE). Third-party code in the repository keeps its own
569
+ licence; see [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
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
 
@@ -111,14 +114,14 @@ rejects. This table is the highest-value part of the page.
111
114
  | `x == y` | `Loose equality is forbidden` | `x === y` |
112
115
  | `throw new Error(m)` | `` `throw` is forbidden `` | `return Err(m)`, or `panic(m)` to end the process |
113
116
  | `try { … } catch { … }` | `` `try`/`catch`/`finally` is forbidden `` | `Result<T, E>` and `isErr()` |
114
- | `xs.map(f)`, `.filter`, `.reduce`, `.forEach`, `.slice`, `.sort`, `.shift` | `` Unknown method `map` on i32[] (supported: push, pop, indexOf, join) `` | a `for` loop; there are no function values to pass |
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) |
115
118
  | `s.toUpperCase()`, `s.split()`, `s.trim()`, `s.replace()` | `` Unknown method … on string `` | index bytes with `charCodeAt` / `substring` |
116
- | `a?.b`, `a ?? b` | forbidden | `if (a !== null)` first |
119
+ | `a?.b`, `a ?? b` | forbidden, except `m.get(k) ?? d` | `if (a !== null)` first |
117
120
  | `x as T`, `<T>x`, `x!` | `Unsupported expression in Phase 1: AsExpression` | there are no casts; `implements` is the only widening |
118
121
  | `any`, `unknown` | forbidden | name the real type |
119
- | `undefined` | forbidden | `null`, with a `T \| null` type |
122
+ | `undefined` | forbidden, except `x === undefined` on a `Map.get` result | `null`, with a `T \| null` type |
120
123
  | `let total = 0` at the top level | `` Top-level `let` is not supported `` | a module `const`, or a local |
121
- | a callback: `xs.forEach(f)`, `(cb: (n: i32) => i32)` | `` Unsupported type `(n: i32) => i32` `` | there are **no function values**; inline the body or write a loop |
124
+ | a callback stored or returned: `const cb = (n: i32) => n`, a field `cb: (n: i32) => i32` | `An arrow may only be written as the argument for a function-typed parameter` / `` … is a function type, which may only annotate a parameter of a top-level function `` | there are **no function values**; a callback the call names is a [function parameter](#function-parameters) |
122
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 |
123
126
  | `constructor<T>(x: T)` | a syntax error: `a constructor cannot have type parameters` | put the parameter on the class (`class Box<T>`), or on a method |
124
127
  | `h.get<i32>(7)` (type argument at a method call) | `Type arguments are not written at a call site in Nish` | `h.get(7)` — a generic method infers like a generic function |
@@ -136,7 +139,9 @@ rejects. This table is the highest-value part of the page.
136
139
  | `export default f` | `` `export default` / `export =` are not supported `` | `export const f = …` |
137
140
  | `export type T = …`, `export enum K` | cannot be exported | declare the alias/enum in each module that needs it |
138
141
  | `new Date()`, `Date.now()` | `` Unknown builtin `Date.now` `` | `monotonicNanos()` for elapsed time; there is no wall clock and no calendar |
139
- | `JSON.parse`, `RegExp`, `Map`, `Set`, `Promise` | unknown / forbidden | none of these exist; write them or restructure |
142
+ | `JSON.parse`, `RegExp`, `Promise` | unknown / forbidden | none of these exist; write them or restructure |
143
+ | `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
+ | `let v = m.get(k)`, `f(m.get(k))`, `m.get(k) + 1` | `` `m.get(k)` is `i32 \| undefined` and cannot be held in a `let` `` (and one message per place) | `m.get(k) ?? 0`, or `const v = m.get(k); if (v !== undefined) { … }` |
140
145
  | `let x = 5; x = "s"` | `Cannot initialize …` | types never change and never convert implicitly |
141
146
 
142
147
  A worked pair. This is the single most common rejection:
@@ -351,7 +356,8 @@ export const main = (): i32 => {
351
356
  ```
352
357
 
353
358
  - Reading anything off an un-narrowed nullable is an error: narrow with
354
- `!== null` first. `?.` and `??` are forbidden outright.
359
+ `!== null` first. `?.` is forbidden outright, and so is `??`, except after
360
+ `Map.get` (see [Map and Set](#map-and-set)).
355
361
  - Narrowing applies to a **local or parameter**, never a property path. `if
356
362
  (n.next !== null) n.next.v` is rejected — copy into a local first.
357
363
  - A narrowing ends at any assignment to the variable, and is dropped before a
@@ -373,11 +379,14 @@ const double = (n: i32): i32 => n * 2; // a concise body is that one `return`
373
379
 
374
380
  - A function is a **module-level `const` bound to an arrow**. `let` is
375
381
  rejected; annotating the `const` is rejected (the annotation would be a
376
- function type, and those are forbidden). The `function` keyword is accepted
377
- as the legacy spelling and compiles to identical IR.
382
+ function type, which only a parameter may have). The `function` keyword is
383
+ accepted as the legacy spelling and compiles to identical IR.
378
384
  - **Every parameter and the return type must be annotated.**
379
385
  - **A function is not a value.** `const alias = double` is
380
- `` Unknown identifier `double` ``. No callbacks, no function types, ever.
386
+ `` Unknown identifier `double` ``. A function may be *passed* only for a
387
+ function-typed parameter, where it is resolved at compile time — see
388
+ [Function parameters](#function-parameters) — and never stored, returned or
389
+ held in a field, a local or an array.
381
390
  - **Parameters are immutable**: `p = 1`, `p++`, `p += 1` are all rejected. Copy
382
391
  into a `let` first.
383
392
  - A non-`void` function must return on every path.
@@ -605,6 +614,115 @@ class Box<T> {
605
614
  export const main = (): i32 => new Box<i32>(1).value;
606
615
  ```
607
616
 
617
+ ### Function parameters
618
+
619
+ A parameter of a top-level function may have a **function type**. Its argument
620
+ is a top-level function named at the call, or an arrow written right there, and
621
+ each one is compiled into its **own copy** of the function, with a direct call
622
+ where the parameter is called — there is no function pointer at run time.
623
+
624
+ ```ts nish:ok
625
+ const map = <T, U>(xs: T[], f: (x: T) => U): U[] => {
626
+ const out: U[] = [];
627
+ for (const x of xs) {
628
+ out.push(f(x));
629
+ }
630
+ return out;
631
+ };
632
+
633
+ const fold = <T>(xs: T[], f: (acc: T, x: T) => T, identity: T): T => {
634
+ let acc = identity;
635
+ for (const x of xs) {
636
+ acc = f(acc, x);
637
+ }
638
+ return acc;
639
+ };
640
+
641
+ const add = (a: i32, b: i32): i32 => a + b;
642
+
643
+ export const main = (): i32 => {
644
+ const doubled = map([1, 2, 3], (n) => n * 2); // U is bound from the body
645
+ return fold(doubled, add, 0) - 12; // 0
646
+ };
647
+ ```
648
+
649
+ - **Call it, or pass it on to another function parameter. Nothing else**:
650
+ `const g = f`, `return f` and `[f]` are
651
+ `` `f` is a function parameter and can only be called or passed on as a function argument ``.
652
+ - **An arrow captures nothing.** It sees its own parameters and top-level
653
+ names; reading a local of the function around it is
654
+ `` The arrow reads `k`, which belongs to the function it is written in ``.
655
+ Pass the value in as another argument of the template instead.
656
+ - **The argument's type must match exactly**: `(x: i32) => i64` for a
657
+ `(x: i32) => i32` parameter is refused. A generic function cannot be passed
658
+ by name — write `(x) => identity(x)`.
659
+ - **Annotate what cannot be inferred**: an arrow parameter whose type no value
660
+ argument binds, and the return type of a block-bodied arrow whose result is a
661
+ type parameter.
662
+ - **Only a top-level function** may have one: not a method, a constructor, a
663
+ field, a local, a return type or an alias.
664
+
665
+ ```ts nish:err NL2335
666
+ const apply = (f: (x: i32) => i32, x: i32): i32 => f(x);
667
+
668
+ export const main = (): i32 => {
669
+ const k = 3;
670
+ return apply((x) => x + k, 1); // captures `k`: pass it to `apply` instead
671
+ };
672
+ ```
673
+
674
+ ### Data parallelism: `nish/threads`
675
+
676
+ `parallelMapInto(src, dst, f)` writes `f(src[i])` into `dst[i]`, and
677
+ `parallelReduce(src, f, identity)` folds `src` with `f` — each on as many
678
+ threads as the array is long enough for. `f` is a [function parameter](#function-parameters).
679
+ Importing the module compiles the program with `--threads`; there is nothing
680
+ else to switch on and no thread count to choose. (A plain block: a program
681
+ importing `nish/threads` writes two `.ll` files, so this one is compiled by
682
+ `tests/link/par_map` and `tests/link/par_reduce` instead.)
683
+
684
+ ```ts
685
+ import { parallelMapInto, parallelReduce } from "nish/threads";
686
+
687
+ const square = (x: f64): f64 => x * x;
688
+
689
+ export const main = (): i32 => {
690
+ const src: f64[] = [1.0, 2.0, 3.0];
691
+ const dst: f64[] = [0.0, 0.0, 0.0]; // at least src.length, or it panics
692
+ parallelMapInto(src, dst, square);
693
+ const total = parallelReduce(dst, (a, b) => a + b, 0.0); // 14, the same bits on any core count
694
+ return toI32(total);
695
+ };
696
+ ```
697
+
698
+ - **`f` writes nothing its caller can see.** No field or element store
699
+ through its argument and no `console.log` — each is refused at the call,
700
+ naming the write. Reading is fine; so are a bounds check, an integer
701
+ division and `panic`.
702
+ - **What `f` allocates is freed after every element.** A string, array or
703
+ object built on the way to the result compiles, with performance warning
704
+ NL9012: each element pays for building it and for the release. Storing an
705
+ allocation into another object (NL2352) and touching `Arena` (NL2351) are
706
+ refused, because either would make that release unsound.
707
+ - **The result is a number, a `boolean` or an enum.** A string or an object
708
+ made on another thread would be freed with it.
709
+ - **`dst` may not be reachable from an element of `src`**: mapping `Row[]` into
710
+ `f64[]` is refused when `Row` holds an `f64[]`, because it could be `dst`.
711
+ - **A reduce's `f` is associative and `identity` is its identity** — `0` for
712
+ `+`, `1` for `*`. The array is folded in fixed blocks and the blocks combined
713
+ in order, so `-` or a wrong identity is refused when `f` is an arrow of one
714
+ operator; a named function is trusted.
715
+ - A short array runs on the calling thread at the cost of the loop, so there
716
+ is no reason to guard a call by size. How short is sized from what `f`
717
+ costs: a cheap body is divided only past about a million elements, one with
718
+ a loop far sooner.
719
+
720
+ ```ts nish:err NL2350
721
+ import { parallelReduce } from "nish/threads";
722
+
723
+ export const main = (): i32 => parallelReduce([1, 2, 3], (a, b) => a + b, 1); // `+` folds from 0
724
+ ```
725
+
608
726
  ### Calling C
609
727
 
610
728
  `declare function name(params): T;` declares a C function this program calls but
@@ -837,6 +955,11 @@ export const main = (): i32 => {
837
955
  annotation must match **exactly**. `const` freezes the binding, not the
838
956
  contents: `xs[0] = 1` and `xs.push(1)` on a `const xs` are fine.
839
957
  - `var` is forbidden. Destructuring is not supported.
958
+ - **Semicolons are optional**, where TypeScript would insert one: at a line
959
+ break, before `}` and at the end of the file. It is TypeScript's rule, so a
960
+ line starting with `(` or `[` continues the one before it, and `return`
961
+ followed by a line break returns nothing. Here, the value left on the next
962
+ line is an unreachable-code error rather than a silent bug.
840
963
  - `for (init; cond; update)` with every clause optional, and
841
964
  `for (const x of xs)` over an **array** only — `x` gets the element type and
842
965
  must not be annotated. The array's `length` is re-read each iteration, so a
@@ -889,8 +1012,8 @@ export const main = (): i32 => {
889
1012
  - `++` / `--` work on **numeric mutable locals only**, not fields or elements.
890
1013
  - Compound assignment `+= -= *= /= %=` requires a numeric target (so `+=` never
891
1014
  concatenates strings); the bitwise forms need an integer target.
892
- - **Forbidden**: `,` `??` `?.` `in` `instanceof` `typeof` `delete` `void expr`
893
- `==` `!=` `**` unary `+`.
1015
+ - **Forbidden**: `,` `?.` `in` `instanceof` `typeof` `delete` `void expr`
1016
+ `==` `!=` `**` unary `+`, and `??` on anything but a `Map.get` result.
894
1017
  - Element access `a[i]` needs an array and a numeric index, and is
895
1018
  bounds-checked (negative indices fail too). String-keyed access is forbidden.
896
1019
  The check comes off where the compiler proves the index in range — a loop
@@ -956,6 +1079,125 @@ bytes, no `.`/`..`), `spawnSync(argv)`, `spawnSyncTo(argv, outPath, errPath)`,
956
1079
  **Arena.** `Arena.mark()`, `Arena.release(m)`, `Arena.reset()`, `Arena.used()`
957
1080
  — see below.
958
1081
 
1082
+ ### Map and Set
1083
+
1084
+ `Map<K, V>` and `Set<T>` are global, as in JavaScript: no import, insertion
1085
+ order, SameValueZero keys (`-0` is `+0`, `NaN` finds `NaN`). What they have is
1086
+ exactly `size` (a read-only `number`), `get(k)`, `set(k, v)` / `add(x)` (both
1087
+ answer the receiver, so they chain), `has(k)`, `delete(k)` (answers whether it
1088
+ was there) and `clear()`, and `keys()` / `values()` as the iterable of a
1089
+ `for...of` and nowhere else. `entries` and `forEach` are refused by name.
1090
+
1091
+ ```ts nish:ok-body
1092
+ const seen = new Set<string>();
1093
+ const counts: Map<string, i32> = new Map();
1094
+ seen.add("a").add("b").add("a");
1095
+ counts.set("a", 1).set("b", 2);
1096
+ counts.delete("b");
1097
+ console.log(`${seen.size} ${counts.size} ${seen.has("b")}`);
1098
+ ```
1099
+
1100
+ - Write the type arguments on `new`, or annotate the declaration and write
1101
+ `new Map()`. `new Map(entries)` and `new Set(array)` are refused: start empty
1102
+ and `set` / `add` in a loop.
1103
+ - A key is a string, any number type, a `boolean`, an enum or a class instance
1104
+ (by identity). An interface, an array, a nullable type or a `Result` is not a
1105
+ key; a value is anything but `void` or an interface.
1106
+ - A module that declares its own `Map` or `Set` keeps it, but then no other
1107
+ module of the program may name the global one.
1108
+
1109
+ `m.get(k)` is `V | undefined`, as under `tsc`, and it is read in exactly one of
1110
+ three ways: a default with `??`, a `const` tested with `!== undefined` (or
1111
+ `=== undefined` and an early `return`), or a bare test. Each is one probe.
1112
+
1113
+ ```ts nish:ok-body
1114
+ const counts = new Map<string, i32>();
1115
+ for (const w of ["a", "b", "a"]) {
1116
+ counts.set(w, (counts.get(w) ?? 0) + 1);
1117
+ }
1118
+ const a = counts.get("a");
1119
+ if (a !== undefined) {
1120
+ console.log(`${a} ${counts.get("z") === undefined}`); // 2 true
1121
+ }
1122
+ ```
1123
+
1124
+ - Anywhere else — a `let`, an argument, a `return`, a field or element, a
1125
+ template hole, an arithmetic operand, the default of another `??`, a `const`
1126
+ annotated `i32` — the maybe is refused with a message naming the place.
1127
+ Default it with `??` first: `m.get(a) ?? (m.get(b) ?? 0)`.
1128
+ - The only spelling of the type is `const a: i32 | undefined = m.get(k)`.
1129
+ - `??` does not mix with `||` or `&&` without parentheses, and its default must
1130
+ be the value type. For a `Map<K, Node | null>` it replaces a stored `null`
1131
+ too, but the result is still `Node | null`.
1132
+
1133
+ ```ts nish:err-body NL2361
1134
+ const m = new Map<string, i32>();
1135
+ let v = m.get("a");
1136
+ ```
1137
+
1138
+ Walk a map with `for (const k of m.keys())` or `for (const v of m.values())`,
1139
+ and a set with `for (const x of s)`: insertion order, deleted keys skipped. You
1140
+ may `set`, `delete` or `clear` the table inside the walk and get exactly what
1141
+ JavaScript gives (a new key is visited later in the same walk; a key deleted
1142
+ before it is reached is not).
1143
+
1144
+ ```ts nish:ok-body
1145
+ const ages = new Map<string, i32>();
1146
+ ages.set("ada", 36).set("alan", 41);
1147
+ ages.delete("alan");
1148
+ ages.set("grace", 85);
1149
+ let total = 0;
1150
+ for (const name of ages.keys()) {
1151
+ console.log(name); // ada, then grace
1152
+ }
1153
+ for (const age of ages.values()) {
1154
+ total += age;
1155
+ }
1156
+ console.log(`${total}`); // 121
1157
+ ```
1158
+
1159
+ - An iterator is not a value: `keys()` / `values()` stored, passed, returned or
1160
+ spread are refused. Pass the map itself and walk it where it is used.
1161
+ - `for (const e of m)` over a `Map` itself is refused, because it would need
1162
+ `[key, value]` destructuring: walk `m.keys()` and call `m.get(k)`.
1163
+
1164
+ ```ts nish:err-body NL2375
1165
+ const m = new Map<string, i32>();
1166
+ for (const e of m) {
1167
+ }
1168
+ ```
1169
+
1170
+ Asking about one key twice costs one probe when the second call writes
1171
+ through the first: `m.set(k, E)` whose `E` reads `m.get(k)` or `m.has(k)`
1172
+ once, and `if (m.has(k)) { m.set(k, E); … }`, `if (!m.has(k)) { m.set(k, E); … }`
1173
+ or `if (!s.has(x)) { s.add(x); … }` with the write first in the branch. The
1174
+ receiver and key must be a local, a parameter or a `this.<field>` path (the key
1175
+ may be a literal), spelled the same in both calls, and nothing in between may
1176
+ call anything, allocate (`new`, a literal array or object, a template, a
1177
+ string `+`) or assign. Anything else still compiles, as one probe per call.
1178
+ `nish/map` adds `getOrInsert(m, k, v)` — the value of `k`, or `v` after
1179
+ inserting it — and `reserve(m, n)`, which sizes the table for `n` entries; both
1180
+ are one module with the program and run unchanged under Node.
1181
+
1182
+ ```ts nish:ok
1183
+ import { getOrInsert, reserve } from "nish/map";
1184
+
1185
+ export const main = (): i32 => {
1186
+ const words = ["to", "be", "or", "not", "to", "be"];
1187
+ const counts = new Map<string, i32>();
1188
+ reserve(counts, words.length);
1189
+ for (const w of words) {
1190
+ counts.set(w, (counts.get(w) ?? 0) + 1); // one probe per word
1191
+ }
1192
+ const first = new Map<string, i32>();
1193
+ for (let i = 0; i < words.length; i++) {
1194
+ getOrInsert(first, words[i], i); // one probe; inserts only the first time
1195
+ }
1196
+ console.log(`${counts.get("to") ?? 0} ${first.get("be") ?? -1}`); // 2 1
1197
+ return 0;
1198
+ };
1199
+ ```
1200
+
959
1201
  ## Memory
960
1202
 
961
1203
  There is **no garbage collector and no `free`**, and nothing about this changes
@@ -1017,8 +1259,9 @@ and return `Pair<A, B>`: an object literal `{ first, second }` at the return,
1017
1259
  exactly as above. It is for returning two values, not storing them — a struct
1018
1260
  kept in an array or a field wants named fields.
1019
1261
 
1020
- **A map or a set** — neither exists. Use parallel arrays with a linear scan, or
1021
- an array indexed by a small integer key, and write the lookup out.
1262
+ **A map or a set** — the global `Map` and `Set` ([Map and Set](#map-and-set)).
1263
+ Read a value with `m.get(k) ?? d`, and keep the keys in an array beside it when
1264
+ you need to walk them, since there is no iteration yet.
1022
1265
 
1023
1266
  **Shared behaviour over several shapes** — there is no inheritance and no
1024
1267
  dispatch. Give each class the interface's fields **first**, `implements` it,
@@ -1064,8 +1307,8 @@ Run it. `nish file.ts --json` is one command and it is the only proof.
1064
1307
  in an object-literal property or a method argument expecting a width?
1065
1308
  6. Is every `Result` read, and is every `.value` behind an `isOk()`?
1066
1309
  7. Is every nullable narrowed with `!== null` before it is touched?
1067
- 8. Did you use `throw`, `try`, `any`, `undefined`, `?.`, `??`, a cast, a
1068
- callback, a generic *alias*, or `extends`?
1310
+ 8. Did you use `throw`, `try`, `any`, `undefined` or `??` (outside `Map.get`), `?.`, a cast, a
1311
+ stored or returned callback, a generic *alias*, or `extends`?
1069
1312
  9. If you are adding to this repository: `npm run check` and `npm test` green,
1070
1313
  and a new construct ships a golden `.ll`, an `llvm-as` pass, a native round
1071
1314
  trip, a negative test, its `LANGUAGE.md` rule and cookbook entry, and a
package/docs/INSTALL.md CHANGED
@@ -234,12 +234,13 @@ routes are
234
234
  If you need one of these to be a first-class install, that is the conversation
235
235
  to have on the issue tracker rather than a workaround to discover here.
236
236
 
237
- **Nothing is published to the registry yet.** The installer is built and
238
- tested, and what is left is a person publishing a release under it, which
239
- [docs/wp12-release.md](wp12-release.md#release-procedure) step 4 is about.
240
- Until that happens, install from a release.
237
+ **On the registry from 0.10.0.** Every release publishes the main package and
238
+ its platform packages to npm from `release.yml`, by trusted publishing
239
+ ([docs/wp12-release.md](wp12-release.md#release-procedure) step 4), so
240
+ `npm install -g @amritk/nish` is the whole install. The rest of this section is
241
+ for a machine that cannot reach the registry, or a release before 0.10.0.
241
242
 
242
- From a release tarball on GitHub — the same package `npm publish` would upload,
243
+ From a release tarball on GitHub — the same package `npm publish` uploads,
243
244
  carried by the release instead of the registry. The `Release` workflow attaches
244
245
  the npm tarball to the release it builds for a `v*` tag. `npm pack` names it
245
246
  after `package.json#name`, so it is `amritk-nish-<version>.tgz` from 0.4.0 on
@@ -445,6 +446,21 @@ Other build profiles: `--profile size` (smallest binary), `--profile debug`
445
446
  (no optimisation, symbols kept). Run `nish --help` for every flag, and
446
447
  see the [README](../README.md) for the language subset.
447
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
+
448
464
  ## 4. Exit codes
449
465
 
450
466
  | Code | Meaning |
@@ -453,6 +469,7 @@ see the [README](../README.md) for the language subset.
453
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 |
454
470
  | 2 | usage error: unknown flag, missing argument, no input files |
455
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 |
456
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 |
457
474
 
458
475
  ## Troubleshooting
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish",
3
- "version": "0.10.0",
3
+ "version": "0.12.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,7 @@
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 --advisory && biome check --formatter-enabled=false .",
49
50
  "format": "biome format --write .",
50
51
  "prepublishOnly": "npm run check && npm test",
51
52
  "changelog": "node scripts/changelog-gen.mjs"
@@ -74,10 +75,10 @@
74
75
  "nish"
75
76
  ],
76
77
  "optionalDependencies": {
77
- "@amritk/nish-x86_64-linux": "0.10.0",
78
- "@amritk/nish-aarch64-linux": "0.10.0",
79
- "@amritk/nish-aarch64-darwin": "0.10.0",
80
- "@amritk/nish-x86_64-darwin": "0.10.0"
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"
81
82
  },
82
83
  "devDependencies": {
83
84
  "@biomejs/biome": "2.5.12",
@@ -0,0 +1,23 @@
1
+ Boost Software License - Version 1.0 - August 17th, 2003
2
+
3
+ Permission is hereby granted, free of charge, to any person or organization
4
+ obtaining a copy of the software and accompanying documentation covered by
5
+ this license (the "Software") to use, reproduce, display, distribute,
6
+ execute, and transmit the Software, and to prepare derivative works of the
7
+ Software, and to permit third-parties to whom the Software is furnished to
8
+ do so, all subject to the following:
9
+
10
+ The copyright notices in the Software and this entire statement, including
11
+ the above license grant, this restriction and the following disclaimer,
12
+ must be included in all copies of the Software, in whole or in part, and
13
+ all derivative works of the Software, unless such copies or derivative
14
+ works are solely in the form of machine-executable object code generated by
15
+ a source language processor.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
20
+ SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
21
+ FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
22
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23
+ DEALINGS IN THE SOFTWARE.
package/runtime/nish.d.ts CHANGED
@@ -25,6 +25,17 @@
25
25
  * So a program that `tsc` accepts may still be rejected by `nish`; the
26
26
  * reverse should never happen, and a case where it does is a bug in this file.
27
27
  * `docs/LANGUAGE.md` is the normative description.
28
+ *
29
+ * **`Map` and `Set` are not declared here**, although `nish` has them as
30
+ * globals: the `"lib": ["ES2022"]` a program is checked against already
31
+ * declares JavaScript's, and a second declaration would clash with it.
32
+ * `nish`'s are that surface less what it defers — no `entries`, no `forEach`,
33
+ * no `for...of` over a `Map` itself — with `keys()` and `values()` admitted
34
+ * only as the iterable of a `for...of`, and `get`'s `V | undefined` only as a
35
+ * `const`'s initialiser, the left of `??` or an operand of `=== undefined`,
36
+ * none of which `tsc` can know (docs/LANGUAGE.md -> `Map` and `Set`).
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.
28
39
  */
29
40
 
30
41
  // ---- Numeric widths (docs/LANGUAGE.md -> Types) ------------------------------
package/runtime/nish.h CHANGED
@@ -179,7 +179,15 @@ void nish_append_file(const nish_str *path, const nish_str *data);
179
179
  * `push` that grows it copies the elements into the arena and leaves your
180
180
  * buffer behind; the callee must not retain the pointer beyond the call.
181
181
  * A returned array lives in the arena (valid until the next reset/release):
182
- * copy `len` elements out of `data` before recycling. */
182
+ * copy `len` elements out of `data` before recycling.
183
+ *
184
+ * An array field stored inside its object (`self/inline_arrays.ts`) is this
185
+ * header followed by its `K` slots, `struct { nish_array h; T slots[K]; }`
186
+ * with `h.data == (char *)slots` and `h.cap == K`; `self/runtime.ts`'s
187
+ * `ARRAY_TYPE` and `self/structs.ts`'s `INLINE_HEADER_BYTES` are the same 24
188
+ * bytes, and `tests/layout/inline_array.c` holds the three to it. It only
189
+ * happens where no header, `.d.ts` or N-API shim describes the class, so a
190
+ * host that includes a generated header never meets one. */
183
191
  typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
184
192
  /* A fresh arena array of `len` uninitialised elements (`len == cap`), for a
185
193
  * host that wants the runtime to own the storage (the wasm loader does).
package/runtime/nish.mjs CHANGED
@@ -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
+ });
package/runtime/runtime.c CHANGED
@@ -277,6 +277,13 @@ nish_str *nish_str_from_u64(uint64_t v) { return str_from_digits(v, 0); }
277
277
 
278
278
  /* ---- Shortest round-trip digits (Ryu)
279
279
 
280
+ Adapted from Ryu (https://github.com/ulfjack/ryu), ryu/d2s.c and
281
+ ryu/common.h. Copyright 2018 Ulf Adams. Used under the Boost Software
282
+ License, Version 1.0; see runtime/LICENSE-ryu. The code from here to the end
283
+ of `nish_shortest_digits` follows upstream's `d2d()` and its helpers, which
284
+ is why it keeps their names, constants and comments; the two tables are
285
+ regenerated by scripts/gen-pow5-tables.py rather than copied.
286
+
280
287
  `String(x)` in JavaScript prints the *fewest* digits that read back as the
281
288
  same double, and the language promises that spelling. Reaching it by asking
282
289
  snprintf for k digits and strtod whether they round-trip -- which is what
@@ -1079,7 +1086,9 @@ nish_str *nish_str_from_f64(double v) {
1079
1086
  return nish_str_new(out, o - out);
1080
1087
  }
1081
1088
 
1082
- /* ---- Math.random: xorshift64*, seeded lazily from time and pid.
1089
+ /* ---- Math.random: xorshift64*, seeded lazily from time and pid. The shifts
1090
+ and the multiplier are Marsaglia's and Vigna's published constants, which
1091
+ are public domain.
1083
1092
  Thread-local under -DNISH_THREADS (WP20 T0 §3.2): a shared seed word is a
1084
1093
  race, and a per-thread one also makes each worker's stream its own rather
1085
1094
  than an interleaving of everyone's. Two threads that start in the same