@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 +29 -13
- package/docs/AI.md +259 -16
- package/docs/INSTALL.md +22 -5
- package/package.json +7 -6
- package/runtime/LICENSE-ryu +23 -0
- package/runtime/nish.d.ts +11 -0
- package/runtime/nish.h +9 -1
- package/runtime/nish.mjs +13 -0
- package/runtime/runtime.c +10 -1
- package/scripts/ci-profile.mjs +3 -2
- package/scripts/gen-pow5-tables.py +5 -0
- package/std/README.md +17 -10
- package/std/collections.ts +585 -0
- package/std/map.ts +37 -0
- package/std/threads.ts +150 -0
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
|
-
|
|
53
|
-
|
|
54
|
-
#
|
|
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`.
|
|
57
|
-
#
|
|
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
|
-
> **
|
|
67
|
-
>
|
|
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
|
|
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: `
|
|
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`, `
|
|
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
|
|
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,
|
|
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` ``.
|
|
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**: `,`
|
|
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** —
|
|
1021
|
-
|
|
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
|
|
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
|
-
**
|
|
238
|
-
|
|
239
|
-
[docs/wp12-release.md](wp12-release.md#release-procedure) step 4
|
|
240
|
-
|
|
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`
|
|
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.
|
|
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.
|
|
78
|
-
"@amritk/nish-aarch64-linux": "0.
|
|
79
|
-
"@amritk/nish-aarch64-darwin": "0.
|
|
80
|
-
"@amritk/nish-x86_64-darwin": "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
|