@amritk/nish-x86_64-linux 0.20.0 → 0.22.1
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/INSTALL.md +52 -10
- package/bin/nish +0 -0
- package/docs/AI.md +2777 -0
- package/package.json +2 -1
- package/runtime/browser.d.ts +6 -0
- package/runtime/browser.mjs +77 -0
- package/runtime/core/arrays.d.ts +36 -0
- package/runtime/core/arrays.mjs +153 -0
- package/runtime/core/checks.mjs +20 -0
- package/runtime/core/host.d.ts +80 -0
- package/runtime/core/host.mjs +325 -0
- package/runtime/core/numbers.d.ts +66 -0
- package/runtime/core/numbers.mjs +343 -0
- package/runtime/core/result.d.ts +45 -0
- package/runtime/core/result.mjs +140 -0
- package/runtime/core/strings.d.ts +14 -0
- package/runtime/core/strings.mjs +169 -0
- package/runtime/core.d.ts +9 -0
- package/runtime/core.mjs +55 -0
- package/runtime/host.d.ts +6 -0
- package/runtime/nish.d.ts +46 -2
- package/runtime/nish.h +75 -2
- package/runtime/nish.mjs +23 -3
- package/runtime/node.mjs +386 -0
- package/runtime/runtime-bytes.c +401 -0
- package/runtime/runtime-cross.c +1 -1
- package/runtime/runtime-host.c +1 -1
- package/runtime/runtime-net.c +12 -1
- package/runtime/runtime-os.c +1 -1
- package/runtime/runtime-parallel.c +1 -1
- package/runtime/runtime-simd.c +231 -7
- package/runtime/runtime-term.c +504 -0
- package/runtime/runtime-wasm.c +0 -11
- package/runtime/runtime.c +190 -6
- package/runtime/shim.mjs +6 -985
- package/runtime/sqlite/LICENSE.md +85 -0
- package/runtime/sqlite/README.md +45 -0
- package/runtime/sqlite/nish-sqlite.c +359 -0
- package/runtime/sqlite/sqlite3.c +271131 -0
- package/runtime/sqlite/sqlite3.h +14447 -0
- package/scripts/build.sh +311 -59
- package/std/LICENSE-fast-float +23 -0
- package/std/LICENSE-fdlibm +8 -0
- package/std/README.md +32 -12
- package/std/base64.ts +116 -0
- package/std/bytes.ts +256 -0
- package/std/cli.ts +344 -0
- package/std/collections.ts +6 -5
- package/std/crypto/LICENSE-crypt_blowfish +26 -0
- package/std/crypto/base64url.ts +125 -0
- package/std/crypto/bcrypt.ts +1557 -0
- package/std/crypto/hmac.ts +138 -2
- package/std/crypto/x509.ts +11 -122
- package/std/json-tokenizer.ts +2998 -57
- package/std/json-value.ts +22 -3
- package/std/json.ts +87 -8
- package/std/math.ts +608 -0
- package/std/net/http1-server.ts +282 -40
- package/std/net/http1.ts +40 -21
- package/std/net/http3-server.ts +50 -12
- package/std/net/postgres.ts +2294 -0
- package/std/net/prefork.ts +23 -3
- package/std/net/quic-listener.ts +20 -4
- package/std/random.ts +66 -4
- package/std/regex.ts +4274 -0
- package/std/sqlite.ts +391 -0
- package/std/term-io.ts +978 -0
- package/std/term-pixels.ts +874 -0
- package/std/term-probe.ts +459 -0
- package/std/term-rate.ts +993 -0
- package/std/term.ts +2453 -0
- package/std/text.ts +90 -7
- package/std/yaml-lines.ts +141 -0
- package/std/yaml.ts +4675 -0
package/docs/AI.md
ADDED
|
@@ -0,0 +1,2777 @@
|
|
|
1
|
+
# Writing Nish: the rules card
|
|
2
|
+
|
|
3
|
+
This is the **whole language, stated as rules, for a model that has to write
|
|
4
|
+
Nish and get it accepted on the first try**. It is a digest, not the authority:
|
|
5
|
+
[`LANGUAGE.md`](LANGUAGE.md) is normative and settles every disagreement, and
|
|
6
|
+
[`IR_COOKBOOK.md`](IR_COOKBOOK.md) shows the exact IR each construct lowers to.
|
|
7
|
+
This page exists because LANGUAGE.md is 2,300 lines written to be *read*, and
|
|
8
|
+
what an author needs is the rules in one pass with the traps first.
|
|
9
|
+
|
|
10
|
+
> Working **on the compiler** rather than **in the language**? That is a
|
|
11
|
+
> different document: [`AGENTS.md`](../AGENTS.md) and [`.claude/`](../.claude/).
|
|
12
|
+
|
|
13
|
+
Nish is a strictly static subset of TypeScript compiled ahead of time to LLVM
|
|
14
|
+
IR. Every Nish program is legal TypeScript *syntax* — it is parsed by the
|
|
15
|
+
official TypeScript parser — and with
|
|
16
|
+
[`runtime/nish.d.ts`](../runtime/nish.d.ts) on the include path it type-checks
|
|
17
|
+
under `tsc --strict` too. That is the trap this page exists for: **your
|
|
18
|
+
TypeScript instincts produce programs that parse, that `tsc` accepts, and that
|
|
19
|
+
`nish` rejects.** The subset is much smaller than it looks.
|
|
20
|
+
|
|
21
|
+
Contents: [The loop](#the-loop-compile-read-the-code-fix) ·
|
|
22
|
+
[A whole program](#a-whole-program) ·
|
|
23
|
+
[What you must unlearn](#what-you-must-unlearn) · [Types](#types) ·
|
|
24
|
+
[Failure](#failure-result-not-exceptions) · [Absence](#absence-nullable-types) ·
|
|
25
|
+
[Declarations](#declarations) · [Generics](#generic-functions) ·
|
|
26
|
+
[Calling C](#calling-c) · [Statements](#statements) ·
|
|
27
|
+
[Expressions](#expressions) · [The builtins, in full](#the-builtins-in-full) ·
|
|
28
|
+
[Memory](#memory) · [Recipes](#recipes-for-what-is-missing) ·
|
|
29
|
+
[Before you say it compiles](#before-you-say-it-compiles)
|
|
30
|
+
|
|
31
|
+
## The loop: compile, read the code, fix
|
|
32
|
+
|
|
33
|
+
Do not reason about whether a program is legal. Ask the compiler — it is fast,
|
|
34
|
+
and it is the only authority.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
nish program.ts --json # one JSON object per diagnostic, on stdout
|
|
38
|
+
nish --fix program.ts # apply every machine-applicable fix, then report what is left
|
|
39
|
+
nish program.ts -o out.ll # emit LLVM IR
|
|
40
|
+
nish program.ts --link prog # build a native binary (needs clang)
|
|
41
|
+
nish run program.ts a b # build into a cache, then run it with `a b`
|
|
42
|
+
# (the C runtime is cached too: ~/.cache/nish/runtime)
|
|
43
|
+
nish --help # the full flag list, stdout, exit 0; plain text
|
|
44
|
+
# under an agent (NISH_AGENT=1, CLAUDECODE, GEMINI_CLI)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`--json` is the surface to automate against. One flat object per line on
|
|
48
|
+
stdout, nothing on stderr, every field 1-based with `endLine`/`endColumn`
|
|
49
|
+
exclusive:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{"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)"}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- **Key on `code`, never on `message`.** A code is a promise: `NL2249` means
|
|
56
|
+
the same rule next release. The prose may improve; the code may not.
|
|
57
|
+
- **`severity`** is `"error"`, `"performance"`, `"portability"` or
|
|
58
|
+
`"deprecation"`. A warning of any kind never changes the exit code — it is
|
|
59
|
+
advice, not a rejection. Portability warnings, the sites where the program's
|
|
60
|
+
TypeScript reading answers differently, print only under
|
|
61
|
+
`--warn-portability`; a deprecation warning names a call a later release
|
|
62
|
+
takes away and what to write instead, and always prints.
|
|
63
|
+
- **Bands**: `NL1xxx` the Phase 0 forbidden-syntax sweep, `NL2xxx` the checker,
|
|
64
|
+
`NL3xxx` the driver and modules, `NL4xxx` the interop sidecars, `NL7xxx`
|
|
65
|
+
deprecation, `NL8xxx` portability, `NL9xxx` performance, `NL0001` syntax, `NL0002` toolchain, `NL0003` internal,
|
|
66
|
+
`NL0000` a diagnostic with no rule yet.
|
|
67
|
+
- **Exit codes**: `0` ok, `1` the program was rejected, `2` usage, `3` the C
|
|
68
|
+
toolchain is unusable, `70` an internal compiler error — that last one is a
|
|
69
|
+
bug in `nish`, not in your program, and is worth reporting. `nish run`
|
|
70
|
+
answers the same codes until the program starts, and the program's own
|
|
71
|
+
status after that.
|
|
72
|
+
- Every failure is a `--json` object, toolchain and internal errors included,
|
|
73
|
+
so you never have to parse stderr to find out why a run failed.
|
|
74
|
+
|
|
75
|
+
Some diagnostics carry a **`fix`**: the edits that make the line compile,
|
|
76
|
+
when the rewrite is mechanical and keeps the program's meaning. It comes last,
|
|
77
|
+
and each edit is placed exactly as the diagnostic is — `text` replaces the span,
|
|
78
|
+
and a span whose start equals its end is an insertion:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{"file":"p.ts","line":2,"column":10,"endLine":2,"endColumn":16,"severity":"error","code":"NL1047","message":"Loose equality is forbidden; use === / !==","fix":[{"line":2,"column":12,"endLine":2,"endColumn":14,"text":"==="}]}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A diagnostic with no safe rewrite has no `fix` key at all. `nish --fix
|
|
85
|
+
program.ts` applies every fix to the files named on its command line (never to
|
|
86
|
+
an import from `std/` or `node_modules`), recompiles, and repeats until nothing
|
|
87
|
+
changes or five rounds pass; then it reports what is left exactly as a plain
|
|
88
|
+
run would, `--json` included, and exits with that run's code. It writes no IR,
|
|
89
|
+
so it refuses `-o` and `--link`. It rewrites your files in place, with no
|
|
90
|
+
backup, so run it on a committed or backed-up tree. Run it first, then read
|
|
91
|
+
what remains. A warning can carry a fix too, and `--fix` applies it only when the program
|
|
92
|
+
compiles and the warning would be printed: `NL9007`'s inserts the guard that
|
|
93
|
+
proves an index in range (not under `--no-warn-performance`), and `NL7002`'s
|
|
94
|
+
migrates a site off `--unchecked-indexing` (below).
|
|
95
|
+
|
|
96
|
+
When it compiles, **check the native build against the program's TypeScript
|
|
97
|
+
reading**. Run the project's tests natively; where it has none, write the
|
|
98
|
+
program back out and run both:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
nish program.ts --emit-ts out/ # the program as TypeScript, plus out/entry.ts
|
|
102
|
+
node out/entry.ts # Node 22.18 or newer; compare with the binary
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The two must print the same and exit the same, a panic included: `--emit-ts`
|
|
106
|
+
keeps the overflow and bounds checks the native build makes, but not a ranged
|
|
107
|
+
integer's range (`integer<0, 255>` is a plain `number`). Where they differ,
|
|
108
|
+
the difference is a row of [the user guide's table](guide/using-nish/to-typescript.md),
|
|
109
|
+
or a bug in `nish` worth reporting. A program on `nish:net`, the signal
|
|
110
|
+
descriptor or a C declaration (NL4010, NL4011), a compound assignment whose
|
|
111
|
+
index would run twice (NL4012) or a generic whose element type decides its
|
|
112
|
+
code (NL4013, NL4014) is refused and has no TypeScript to run.
|
|
113
|
+
|
|
114
|
+
Two more surfaces worth knowing: `nish --emit-ast f.ts` prints what was parsed
|
|
115
|
+
and `nish --emit-checked f.ts` prints the side tables the emitter reads. Both
|
|
116
|
+
are one node per line, which is to say diffable.
|
|
117
|
+
|
|
118
|
+
## A whole program
|
|
119
|
+
|
|
120
|
+
```ts nish:ok
|
|
121
|
+
const gcd = (a: i32, b: i32): i32 => {
|
|
122
|
+
let x = a;
|
|
123
|
+
let y = b;
|
|
124
|
+
while (y !== 0) {
|
|
125
|
+
const t = y;
|
|
126
|
+
y = x % y;
|
|
127
|
+
x = t;
|
|
128
|
+
}
|
|
129
|
+
return x;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const main = (): i32 => {
|
|
133
|
+
console.log(`gcd = ${gcd(1071, 462)}`);
|
|
134
|
+
return 0;
|
|
135
|
+
};
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
That is the shape: top-level `const` arrows, an exported `main`, explicit types
|
|
139
|
+
everywhere, a template literal instead of `String(n)`.
|
|
140
|
+
|
|
141
|
+
- A module is one `.ts` file. At the top level it may hold **only**
|
|
142
|
+
declarations: `const` arrows (functions), `class`, `interface`, module
|
|
143
|
+
`const`s, `type` aliases, numeric `enum`s, and `import`s. There is no
|
|
144
|
+
top-level code, so there is no initialisation order to think about.
|
|
145
|
+
- `export const main` lives in the entry module only, takes no parameters, and
|
|
146
|
+
returns `void` or an `i32`-lowered `number` — the process exit code. Under
|
|
147
|
+
`--number-mode f64`, declare it `main(): i32`.
|
|
148
|
+
- `number` is `i32` by default and `f64` under `--number-mode f64`. Write the
|
|
149
|
+
explicit width (`i32`, `f64`) when you mean one; it reads the same in both
|
|
150
|
+
modes.
|
|
151
|
+
|
|
152
|
+
## What you must unlearn
|
|
153
|
+
|
|
154
|
+
Every row is something a TypeScript-trained model writes by reflex and Nish
|
|
155
|
+
rejects. This table is the highest-value part of the page.
|
|
156
|
+
|
|
157
|
+
| Your reflex | What happens | Write instead |
|
|
158
|
+
| --- | --- | --- |
|
|
159
|
+
| `if (xs.length)`, `!xs.length`, `if (s.length && ok)` | `Condition must be boolean … (Nish has no truthiness)` / `` Unsupported unary operator `!` on i32 `` / `` Operator `&&` requires boolean operands, got i32 and boolean `` | `if (xs.length !== 0)`, `xs.length === 0`, `if (s.length !== 0 && ok)`; `nish --fix` writes each where it is the test `tsc` reads (an operand of `&&` or `\|\|` only where the whole is a condition or under `!`) |
|
|
160
|
+
| `x == y` | `Loose equality is forbidden` | `x === y` |
|
|
161
|
+
| `throw new Error(m)` | `` `throw` is forbidden `` | `return Err(m)`, or `panic(m)` to end the process |
|
|
162
|
+
| `try { … } catch { … }` | `` `try`/`catch`/`finally` is forbidden `` | `Result<T, E>` and `isErr()` |
|
|
163
|
+
| `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) |
|
|
164
|
+
| `s.toUpperCase()`, `s.split()`, `s.trim()`, `s.replace()` | `` Unknown method … on string `` | index bytes with `charCodeAt` / `substring` |
|
|
165
|
+
| `a?.b`, `a ?? b` | `?.` only in a chain that ends in `??`; `??` only on a `T \| null`, a chain or `m.get(k)` | `a?.b ?? d`, `a ?? d`, or `if (a !== null)` first |
|
|
166
|
+
| `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 |
|
|
167
|
+
| `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 |
|
|
168
|
+
| `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 |
|
|
169
|
+
| `any`, `unknown` | forbidden | name the real type |
|
|
170
|
+
| `undefined` | forbidden, except `x === undefined` on a `Map.get` result | `null`, with a `T \| null` type |
|
|
171
|
+
| `let total = 0` at the top level | `` Top-level `let` is not supported `` | a module `const`, or a local |
|
|
172
|
+
| `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` |
|
|
173
|
+
| 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) |
|
|
174
|
+
| `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 |
|
|
175
|
+
| `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 |
|
|
176
|
+
| `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 |
|
|
177
|
+
| `<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` |
|
|
178
|
+
| `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 |
|
|
179
|
+
| `async` / `await` / `Promise` | forbidden (no event loop) | the I/O builtins are synchronous |
|
|
180
|
+
| `namespace N { }`, `declare global { }`, `@decorator` | forbidden | one module per file; a plain function in place of a decorator |
|
|
181
|
+
| `keyof T`, `<T = i32>` (a default type argument) | `` Unsupported type `keyof T` (Phase 1 supports …) `` / `a default type argument … is not supported` | name the type; a type argument is always inferred |
|
|
182
|
+
| `class B extends A` | `` `extends` is not supported: Nish has no inheritance `` | repeat the fields and `implements` an interface |
|
|
183
|
+
| `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 |
|
|
184
|
+
| `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 |
|
|
185
|
+
| `function f(): void;` (an overload), `export default function ()` | `Functions must have a body` / `Functions must be named` | one named function, with its body |
|
|
186
|
+
| a method in an `interface` | `` Interface `I` cannot declare methods (interfaces describe layout only) `` | fields only; a top-level function over the interface |
|
|
187
|
+
| `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 |
|
|
188
|
+
| `{ 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 |
|
|
189
|
+
| `{ ...base, x: 1 }`, `{ [k]: v }`, `[k]: i32` in a class or interface | `Object spread is forbidden in Nish (set each field by name)` / `Computed property names are forbidden in Nish (member names are fixed at compile time)` | every field written by name (`{ x: 1, y: base.y }`); a `Map` for a keyed table |
|
|
190
|
+
| `type Pair = { a: i32 }` (inline object type) | `` Unsupported type `{ a: i32 }` `` | declare an `interface` |
|
|
191
|
+
| `A \| B` unions of anything but tagged classes and interfaces | `` Union types other than `T \| null` are forbidden `` | a [discriminated union](#discriminated-unions): a tag field first in each variant |
|
|
192
|
+
| `String(n)`, `n.toString()` | `` Unknown function `String` `` / `` Unknown method `toString` on i32 `` | `` `${n}` `` |
|
|
193
|
+
| `xs.length = 0` | `` Cannot assign to `length` of i32[] (array length is read-only; use `push`) `` | build a new array |
|
|
194
|
+
| `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"`) |
|
|
195
|
+
| `export default f` | `` `export default` / `export =` are not supported; use a named `export function` `` | `export const f = …` |
|
|
196
|
+
| `export { f }`, `export * from "./m"`, `export default function f` | `` `export { ... }` / `export * from` are not supported; put `export` on the function declaration itself `` / `` `export default` is not supported; use a named `export function` `` | `export` on the declaration itself |
|
|
197
|
+
| `import d from "./m"`, `import * as m from "./m"`, `import "./m"`, `import type { T } from "./m"` | `` Default imports are not supported; use `import { d } from "./m"` `` / `` Namespace imports (`import * as m`) are not supported; import functions by name `` / `` Side-effect imports (`import "./m"`) are not supported; modules have no top-level code `` / `Type-only imports are not supported` | `import { d, T } from "./m"`, naming what you use |
|
|
198
|
+
| `total += xs[i]` in a loop, with `i` from somewhere the compiler cannot bound | it compiles, with an `NL9007` performance warning: `` `i` is not proven to be in range for `xs` here, so this access keeps its bounds check `` | a guard before the statement, `if (!(i >= 0 && i < toI32(xs.length))) { panic("index out of range") }`, which proves both ends in either number mode; `nish --fix` inserts it where it changes nothing the program does |
|
|
199
|
+
| `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 |
|
|
200
|
+
| `JSON.parse`, `RegExp`, `/re/` literals, `Promise` | unknown / forbidden | none of these are built in: `regexCompile(pattern, flags)` from `nish/regex` for a pattern, `jsonDecode` from `nish/json-typed` for JSON; restructure around `Promise` |
|
|
201
|
+
| `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) |
|
|
202
|
+
| `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) { … }` |
|
|
203
|
+
| `let x = 5; x = "s"` | `Cannot initialize …` | types never change and never convert implicitly |
|
|
204
|
+
|
|
205
|
+
A worked pair. This is the single most common rejection:
|
|
206
|
+
|
|
207
|
+
```ts nish:err-body NL2188
|
|
208
|
+
const xs = [1, 2, 3];
|
|
209
|
+
if (xs.length) {
|
|
210
|
+
console.log("non-empty");
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
```ts nish:ok-body
|
|
215
|
+
const xs = [1, 2, 3];
|
|
216
|
+
if (xs.length !== 0) {
|
|
217
|
+
console.log("non-empty");
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Types
|
|
222
|
+
|
|
223
|
+
Every type is exactly one LLVM first-class type. No boxing, no runtime tag, no
|
|
224
|
+
structural subtyping, **no implicit conversion of any kind**: two values are
|
|
225
|
+
compatible only when their types are identical.
|
|
226
|
+
|
|
227
|
+
| Type | Is | Notes |
|
|
228
|
+
| --- | --- | --- |
|
|
229
|
+
| `number` | `i32`, or `f64` under `--number-mode f64` | the mode's default width |
|
|
230
|
+
| `i32`, `i64` | signed integers | overflow **panics** (exit 1) unless proven to fit; `wrappingAdd`/`wrappingSub`/`wrappingMul` from `nish:unsafe` wrap |
|
|
231
|
+
| `u8`, `u16`, `u32`, `u64` | unsigned integers | arithmetic **always** wraps; `>>` is logical |
|
|
232
|
+
| `f32`, `f64` | IEEE-754 | `f32` is never the lowering of `number` |
|
|
233
|
+
| `boolean` | `i1` | no truthiness anywhere |
|
|
234
|
+
| `string` | immutable UTF-8 bytes | `length` is the **byte** length |
|
|
235
|
+
| `T[]`, `Array<T>` | one element type, bounds-checked | `readonly T[]` refuses every write |
|
|
236
|
+
| `Int32Array`, `Float32Array`, `Float64Array`, `BigInt64Array` | aliases of `i32[]`, `f32[]`, `f64[]`, `i64[]` | not distinct types; fixed length, so no `push` or `pop` (NL2415): declare `f64[]` to grow one |
|
|
237
|
+
| `class C`, `interface I` | a struct, fields in declaration order | no header, no vtable |
|
|
238
|
+
| `enum K` | a **distinct** type represented as `i32` | never interchangeable with `i32` |
|
|
239
|
+
| `K.M` | an enum member type: the one member `K.M`, as `i32` | holds only that member; a read is a `K` |
|
|
240
|
+
| `T \| null` | `T` a class, interface, array, or string | a scalar can never be nullable |
|
|
241
|
+
| `A \| B` of tagged variants | a pointer to the variant; its first field is the tag | [Discriminated unions](#discriminated-unions); only the tag is read until a test of it narrows |
|
|
242
|
+
| `Result<T, E>` | the only way to report failure | `E` may not be `void` |
|
|
243
|
+
| `void` | return type only | |
|
|
244
|
+
|
|
245
|
+
**An unchecked index and a defined wrap are `nish:unsafe` imports**, written at
|
|
246
|
+
the site: `uncheckedGet(xs, i)` and `uncheckedSet(xs, i, v)` on an array of
|
|
247
|
+
numbers with an `i32` index (out of range is undefined behaviour), and
|
|
248
|
+
`wrappingAdd` / `wrappingSub` / `wrappingMul` on two `i32`s or two `i64`s.
|
|
249
|
+
The sixth export, `readCString`, copies a C string out of a `CPtr`
|
|
250
|
+
([Calling C](#calling-c)). Calling one without the import is NL2456. `--unchecked-indexing` and
|
|
251
|
+
`--wrapping` are deprecated (NL9014, NL9015) and reach only your own package,
|
|
252
|
+
never a dependency or `nish/`. To migrate off `--unchecked-indexing`, run
|
|
253
|
+
`nish --unchecked-indexing --fix` on your files: each index the flag leaves
|
|
254
|
+
unchecked is a `deprecation` warning of its own (NL7002), and where the site
|
|
255
|
+
is a number array with an `i32` index its `fix` rewrites it to `uncheckedGet`
|
|
256
|
+
or `uncheckedSet` and adds the import. A site with no rewrite keeps its
|
|
257
|
+
warning; dropping the flag gives it back its bounds check.
|
|
258
|
+
|
|
259
|
+
```ts nish:ok
|
|
260
|
+
import { uncheckedGet, wrappingAdd, wrappingMul } from "nish:unsafe";
|
|
261
|
+
|
|
262
|
+
export const hash = (bytes: u8[]): i32 => {
|
|
263
|
+
let h: i32 = 17;
|
|
264
|
+
for (let i: i32 = 0; i < toI32(bytes.length); i++) {
|
|
265
|
+
h = wrappingAdd(wrappingMul(h, 31), toI32(uncheckedGet(bytes, i)));
|
|
266
|
+
}
|
|
267
|
+
return h;
|
|
268
|
+
};
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Mixing widths is an error, always.** `f32 + f64`, `i64 + number`, even
|
|
272
|
+
`u32 + i32` (same LLVM type, different signedness) are rejected. Convert
|
|
273
|
+
explicitly: `toI32` `toI64` `toU8` `toU16` `toU32` `toU64` `toF32` `toF64`.
|
|
274
|
+
|
|
275
|
+
**Signed overflow panics.** `+ - *`, unary `-`, `++`, `--` and `op=` on an
|
|
276
|
+
`i32` or `i64` stop the program with `attempt to add with overflow` (or
|
|
277
|
+
`subtract`, `multiply`, `negate`) and exit 1 when the result leaves the type;
|
|
278
|
+
it is never a silent wrap and never undefined. The compiler drops the check
|
|
279
|
+
where it can prove the result fits — a loop counter under `i < n`, a byte
|
|
280
|
+
widened with `toI32`, an `integer<Lo, Hi>`, a value masked with `& c`. Arithmetic
|
|
281
|
+
that is *meant* to wrap — a hash, a PRNG, a checksum — goes in `u32` or `u64`,
|
|
282
|
+
which always wrap and are never checked; convert back with `toI32` (free), or
|
|
283
|
+
call `wrappingAdd` / `wrappingSub` / `wrappingMul` at the site. The deprecated
|
|
284
|
+
`--wrapping` makes every signed operation of your own package wrap instead.
|
|
285
|
+
|
|
286
|
+
```ts nish:ok
|
|
287
|
+
// FNV-1a: the multiply overflows on purpose, so the state is a `u32`.
|
|
288
|
+
export const fnv1a = (bytes: u8[]): u32 => {
|
|
289
|
+
let h: u32 = 2166136261;
|
|
290
|
+
for (const b of bytes) {
|
|
291
|
+
h = (h ^ toU32(b)) * 16777619;
|
|
292
|
+
}
|
|
293
|
+
return h;
|
|
294
|
+
};
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
```ts nish:err NL2231
|
|
298
|
+
const bad = (a: i32, b: i64): i64 => {
|
|
299
|
+
return a + b;
|
|
300
|
+
};
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
```ts nish:ok
|
|
304
|
+
const good = (a: i32, b: i64): i64 => {
|
|
305
|
+
return toI64(a) + b;
|
|
306
|
+
};
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**`integer<Lo, Hi>` is an `i32` that stays in `[Lo, Hi]`.** The bounds are two
|
|
310
|
+
integer literals (a sign allowed) inside `i32`, and it is an `i32` everywhere
|
|
311
|
+
the machine can see. Putting a value *into* one is checked: an `i32` or
|
|
312
|
+
another range is compared once and the program panics (exit 1) outside the
|
|
313
|
+
range, a literal outside it is a compile error, and a literal inside it or a
|
|
314
|
+
narrower range costs nothing. So does a value a loop condition or a guard
|
|
315
|
+
already bounds (`b` below), and `toI32` of a `u8`; a check left inside a loop
|
|
316
|
+
is a performance warning (NL9013) naming the guard that removes it. A range
|
|
317
|
+
bounds an index too: `integer<0, 255>`, like `u8`, needs only a length guard of
|
|
318
|
+
256. Taking one *out* is free: every operator reads
|
|
319
|
+
it as `i32`, a `const` keeps the range and a `let` widens to `i32`. A `u8` is
|
|
320
|
+
not an `integer<0, 255>` (convert with `toI32`), and a `declare function` may
|
|
321
|
+
not mention a range. Put the range on the value you use, not on a loop
|
|
322
|
+
counter, which has to leave the range to end the loop.
|
|
323
|
+
|
|
324
|
+
```ts nish:ok
|
|
325
|
+
const getByte = (buf: u8[], i: integer<0, 255>): u8 => buf[i];
|
|
326
|
+
|
|
327
|
+
const sumAll = (buf: u8[]): i32 => {
|
|
328
|
+
let sum = 0;
|
|
329
|
+
for (let i = 0; i < 256 && i < buf.length; i++) {
|
|
330
|
+
const b: integer<0, 255> = i;
|
|
331
|
+
sum = sum + toI32(getByte(buf, b));
|
|
332
|
+
}
|
|
333
|
+
return sum;
|
|
334
|
+
};
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
```ts nish:err-body NL2384
|
|
338
|
+
const digit: integer<0, 9> = 12;
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Declaring a type alias, enum, class, interface or function named `integer` is
|
|
342
|
+
refused, because the name is the type. A value named `integer` (a local or a
|
|
343
|
+
module constant) is fine.
|
|
344
|
+
|
|
345
|
+
```ts nish:err NL2332
|
|
346
|
+
class integer {
|
|
347
|
+
value: i32 = 0;
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Numeric literals take their type from the immediate context
|
|
352
|
+
|
|
353
|
+
A literal is the mode's default (`i32`, or `f64` in f64 mode) **unless its
|
|
354
|
+
immediate context demands another numeric type**, and the contexts that do are
|
|
355
|
+
a closed list: an annotated initializer, a `return`, an argument to a user
|
|
356
|
+
*function* or to a constructor, the other operand of a binary operator whose
|
|
357
|
+
type is already known, `Math.min` / `Math.max`, the f64-only `Math.*` and
|
|
358
|
+
`toF64` / `toF32`, `process.exit`, `Number`, an element of an array literal
|
|
359
|
+
that itself has a `T[]` context, a class field's literal initializer, a field
|
|
360
|
+
or element assignment target, a ternary arm taking the conditional's own
|
|
361
|
+
context, and `push` / `indexOf` through a receiver that is a **plain name**.
|
|
362
|
+
|
|
363
|
+
Everything else leaves the literal at the default, however obvious the intent.
|
|
364
|
+
The three that catch you out: an **object-literal property**, a `Result`
|
|
365
|
+
payload, and a **method's** argument.
|
|
366
|
+
|
|
367
|
+
```ts nish:err NL2269
|
|
368
|
+
interface Pixel { r: u8; g: u8; b: u8; }
|
|
369
|
+
|
|
370
|
+
export const main = (): i32 => {
|
|
371
|
+
const p: Pixel = { r: 255, g: 0, b: 0 };
|
|
372
|
+
console.log(`${p.r}`);
|
|
373
|
+
return 0;
|
|
374
|
+
};
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```ts nish:ok
|
|
378
|
+
interface Pixel { r: u8; g: u8; b: u8; }
|
|
379
|
+
|
|
380
|
+
export const main = (): i32 => {
|
|
381
|
+
const p: Pixel = { r: toU8(255), g: toU8(0), b: toU8(0) };
|
|
382
|
+
console.log(`${p.r}`);
|
|
383
|
+
return 0;
|
|
384
|
+
};
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
The same rule refuses `const b: u8 = 1 + 2` — that is a sum of two `i32`
|
|
388
|
+
literals, not a literal in a `u8` context. Name the value first, or convert.
|
|
389
|
+
|
|
390
|
+
## Failure: `Result`, not exceptions
|
|
391
|
+
|
|
392
|
+
There is no `throw`, no `try`, and no unwinding anywhere in the language. A
|
|
393
|
+
function that can fail says so in its return type.
|
|
394
|
+
|
|
395
|
+
```ts nish:ok
|
|
396
|
+
const half = (n: i32): Result<i32, string> => {
|
|
397
|
+
if (n % 2 !== 0) {
|
|
398
|
+
return Err("odd");
|
|
399
|
+
}
|
|
400
|
+
return Ok(n / 2);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const quarter = (n: i32): Result<i32, string> => {
|
|
404
|
+
const h = half(n).orReturn(); // Rust's `?`: propagates the error
|
|
405
|
+
return half(h);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
export const main = (): i32 => {
|
|
409
|
+
const outcome = quarter(8);
|
|
410
|
+
if (outcome.isErr()) {
|
|
411
|
+
console.error(outcome.error);
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
414
|
+
console.log(`${outcome.value}`);
|
|
415
|
+
return 0;
|
|
416
|
+
};
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The surface: `Ok(v)` / `Ok()`, `Err(e)`, `r.isOk()`, `r.isErr()`, `r.ok`,
|
|
420
|
+
`r.value`, `r.error`, `r.orReturn()`, `r.unwrapOr(d)`, `r.expect(msg)`. There
|
|
421
|
+
is deliberately no `unwrap()` — `expect` insists on a reason — and no
|
|
422
|
+
`map`/`andThen`, which would need function values.
|
|
423
|
+
|
|
424
|
+
**Three rules, and they are the ones that catch you out:**
|
|
425
|
+
|
|
426
|
+
1. **A `Result` cannot be dropped.** A call answering one may not stand as an
|
|
427
|
+
expression statement, and a local holding one must be read at least once.
|
|
428
|
+
Passing it on — as an argument, as a `return` — counts as reading it.
|
|
429
|
+
2. **The error arm comes first.** `r.value` is legal only where the checker
|
|
430
|
+
proved `isOk()`; `r.error` only where it proved `isErr()`. There is no
|
|
431
|
+
spelling that reaches the success payload without deciding what happens to
|
|
432
|
+
the failure.
|
|
433
|
+
3. **Propagation is contagious.** `r.orReturn()` is legal only inside a
|
|
434
|
+
function that itself returns a `Result` whose error arm accepts `E`. There
|
|
435
|
+
is no implicit error conversion — convert by hand with
|
|
436
|
+
`if (r.isErr()) { return Err(…); }`.
|
|
437
|
+
|
|
438
|
+
```ts nish:err NL2044
|
|
439
|
+
const half = (n: i32): Result<i32, string> => {
|
|
440
|
+
return Ok(n / 2);
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
export const main = (): i32 => {
|
|
444
|
+
const r = half(8);
|
|
445
|
+
console.log(`${r.value}`);
|
|
446
|
+
return 0;
|
|
447
|
+
};
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Narrowing follows the same engine as `T | null` below: it applies to a
|
|
451
|
+
**variable**, never a property path; it ends at any assignment to that
|
|
452
|
+
variable; and it is dropped before a loop that assigns it. `if (r.isOk()) A
|
|
453
|
+
else B` narrows in `A`, and after the `if` when `B` cannot fall through.
|
|
454
|
+
The proof stays with `r`: `const y = r`, `c ? r : q` and `[r]` are plain
|
|
455
|
+
`Result`s, so test `y` itself before `y.value`.
|
|
456
|
+
|
|
457
|
+
`panic(message)` is the other ending: message to stderr, exit 1. It is for an
|
|
458
|
+
invariant that cannot hold, not for a failure a caller should handle. It
|
|
459
|
+
terminates control flow, so a non-`void` function may end with it.
|
|
460
|
+
|
|
461
|
+
## Absence: nullable types
|
|
462
|
+
|
|
463
|
+
`T | null` is available for `T` a class, interface, array, or string — never a
|
|
464
|
+
scalar, which has no null value. It is the same pointer with `null` as one more
|
|
465
|
+
value, so nothing is boxed.
|
|
466
|
+
|
|
467
|
+
```ts nish:ok
|
|
468
|
+
class Node {
|
|
469
|
+
value: i32;
|
|
470
|
+
next: Node | null = null;
|
|
471
|
+
constructor(value: i32) {
|
|
472
|
+
this.value = value;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const total = (head: Node | null): i32 => {
|
|
477
|
+
let sum = 0;
|
|
478
|
+
let cur = head;
|
|
479
|
+
while (cur !== null) {
|
|
480
|
+
sum = sum + cur.value;
|
|
481
|
+
cur = cur.next;
|
|
482
|
+
}
|
|
483
|
+
return sum;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
export const main = (): i32 => {
|
|
487
|
+
const a = new Node(1);
|
|
488
|
+
a.next = new Node(2);
|
|
489
|
+
console.log(`${total(a)}`);
|
|
490
|
+
return 0;
|
|
491
|
+
};
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
- Reading anything off an un-narrowed nullable is an error: narrow with
|
|
495
|
+
`!== null` first, or read it through a `?.` chain that ends in `??`.
|
|
496
|
+
- `a ?? d` defaults a nullable: it is `a` where that is not `null`, `d` where
|
|
497
|
+
it is, and its type is `T` when `d` is a `T`, so it needs no narrowing. It
|
|
498
|
+
reads `a` once, so a property path works: `cur = cur.next ?? cur`. `d` that
|
|
499
|
+
may be `null` keeps the `T | null`. An element read (`xs[i] ?? d`) is
|
|
500
|
+
refused, because Node answers `d` past the end where Nish panics. `??` also
|
|
501
|
+
defaults a `Map.get` result (see [Map and Set](#map-and-set)).
|
|
502
|
+
|
|
503
|
+
```ts nish:ok
|
|
504
|
+
class Node {
|
|
505
|
+
value: i32;
|
|
506
|
+
next: Node | null = null;
|
|
507
|
+
constructor(value: i32) {
|
|
508
|
+
this.value = value;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const label = (name: string | null): string => name ?? "anonymous";
|
|
513
|
+
|
|
514
|
+
export const main = (): i32 => {
|
|
515
|
+
const a = new Node(1);
|
|
516
|
+
const after = a.next ?? a; // `Node`, no narrowing needed
|
|
517
|
+
console.log(`${label(null)} ${after.value}`); // anonymous 1
|
|
518
|
+
return 0;
|
|
519
|
+
};
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
```ts nish:err-body NL2489
|
|
523
|
+
const names = new Array<string | null>(2);
|
|
524
|
+
console.log(names[2] ?? "none");
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
- `a?.b?.c ?? d` reads each link only where the receiver before its `?.` is
|
|
528
|
+
not `null`, and is `d` where one is. A `?.` is allowed **only in a chain
|
|
529
|
+
that is the left operand of `??`** (NL1062): `const x = a?.b`, `f(a?.b)` and
|
|
530
|
+
`` `${a?.b}` `` are refused, because on its own a chain answers
|
|
531
|
+
`undefined`. The last link may be any type, a number included
|
|
532
|
+
(`o?.next?.id ?? -1` is an `i32`), and a method call is a link
|
|
533
|
+
(`c?.greeting() ?? ""`). Only a `?.` skips: `a?.next.v` with a nullable
|
|
534
|
+
`next` needs `a?.next?.v`. `?.` on something that cannot be `null` is
|
|
535
|
+
NL2490; `?.[i]` and `f?.()` are refused.
|
|
536
|
+
|
|
537
|
+
```ts nish:ok
|
|
538
|
+
class Order {
|
|
539
|
+
id: i32;
|
|
540
|
+
next: Order | null = null;
|
|
541
|
+
constructor(id: i32) {
|
|
542
|
+
this.id = id;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const nextId = (o: Order | null): i32 => o?.next?.id ?? -1;
|
|
547
|
+
|
|
548
|
+
export const main = (): i32 => {
|
|
549
|
+
const a = new Order(1);
|
|
550
|
+
a.next = new Order(2);
|
|
551
|
+
console.log(`${nextId(a)} ${nextId(null)}`); // 2 -1
|
|
552
|
+
return 0;
|
|
553
|
+
};
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
```ts nish:err-body NL1062
|
|
557
|
+
const s: string | null = "x";
|
|
558
|
+
const n = s?.length;
|
|
559
|
+
```
|
|
560
|
+
- Narrowing applies to a **local or parameter**, never a property path. `if
|
|
561
|
+
(n.next !== null) n.next.v` is rejected — copy into a local first.
|
|
562
|
+
- A narrowing ends at any assignment to the variable, including one in an
|
|
563
|
+
earlier operand of the same `&&` / `||` chain, and is dropped before a loop
|
|
564
|
+
whose body, condition or update assigns it.
|
|
565
|
+
- Two nullables cannot be compared with each other; compare each with `null`.
|
|
566
|
+
- `new Array<T | null>(n)` is allowed: the zero fill *is* `null`.
|
|
567
|
+
|
|
568
|
+
## Declarations
|
|
569
|
+
|
|
570
|
+
### Functions
|
|
571
|
+
|
|
572
|
+
```ts nish:ok
|
|
573
|
+
const add = (a: i32, b: i32): i32 => {
|
|
574
|
+
return a + b;
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const double = (n: i32): i32 => n * 2; // a concise body is that one `return`
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
- A function is a **module-level `const` bound to an arrow**. `let` is
|
|
581
|
+
rejected; annotating the `const` is rejected (the annotation would be a
|
|
582
|
+
function type, which only a parameter may have). The `function` keyword is
|
|
583
|
+
accepted as the legacy spelling and compiles to identical IR.
|
|
584
|
+
- **Every parameter and the return type must be annotated.**
|
|
585
|
+
- **A function is not a value.** `const alias = double` is
|
|
586
|
+
`` Unknown identifier `double` ``. A function may be *passed* only for a
|
|
587
|
+
function-typed parameter, where it is resolved at compile time — see
|
|
588
|
+
[Function parameters](#function-parameters) — and never stored, returned or
|
|
589
|
+
held in a field, a local or an array.
|
|
590
|
+
- **Parameters are immutable**: `p = 1`, `p++`, `p += 1` are all rejected. Copy
|
|
591
|
+
into a `let` first.
|
|
592
|
+
- A non-`void` function must return on every path.
|
|
593
|
+
- Rejected: generators, `async`, destructured / rest / optional / default
|
|
594
|
+
parameters, overloads, nested function declarations. Type parameters *are*
|
|
595
|
+
allowed — see [Generic functions](#generic-functions).
|
|
596
|
+
|
|
597
|
+
### Generic functions
|
|
598
|
+
|
|
599
|
+
A function may declare type parameters, and **each instantiation is compiled to
|
|
600
|
+
its own specialised function** — no boxing, no dictionary, no runtime type
|
|
601
|
+
information. The IR is what somebody would have written by hand at that type.
|
|
602
|
+
|
|
603
|
+
```ts nish:ok
|
|
604
|
+
const identity = <T>(x: T): T => x;
|
|
605
|
+
|
|
606
|
+
export const main = (): i32 => {
|
|
607
|
+
console.log(identity(7));
|
|
608
|
+
console.log(identity("hi"));
|
|
609
|
+
return 0;
|
|
610
|
+
};
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
- **The type argument is inferred from the arguments and is never written at a
|
|
614
|
+
call site.** `identity<i32>(7)` is
|
|
615
|
+
`Type arguments are not written at a call site in Nish`. Inference matches the
|
|
616
|
+
shape of each declared parameter against its argument — `T` against `i32`,
|
|
617
|
+
`T[]` against `i32[]`, `Result<T, string>` against `Result<i32, string>` —
|
|
618
|
+
left to right, first binding wins.
|
|
619
|
+
- **A type parameter that appears in no parameter is an error at the
|
|
620
|
+
declaration**, because it could never be inferred.
|
|
621
|
+
- **Every other rule still applies inside.** A type parameter is not a hole to
|
|
622
|
+
smuggle something through: each instantiation is checked with `T` bound to a
|
|
623
|
+
concrete type.
|
|
624
|
+
- **A template may be exported and instantiated from another module.** Each
|
|
625
|
+
instantiation is defined once, in the module that declares the template, and
|
|
626
|
+
`declare`d everywhere else — so one imported name becomes one symbol per
|
|
627
|
+
distinct type-argument tuple, and two modules asking for the same tuple share
|
|
628
|
+
the one definition. A type argument may be a class the declaring module has
|
|
629
|
+
never heard of. A template that is not exported cannot be imported, and an
|
|
630
|
+
imported one may be renamed (`import { identity as id }`) without moving the
|
|
631
|
+
symbol. A type-argument list on an imported name that is *not* a template is
|
|
632
|
+
`` `Point` in `./lib` takes no type arguments ``.
|
|
633
|
+
- **A type parameter has no members unless it is constrained.** Passing a `T`
|
|
634
|
+
on, returning it, storing it and comparing it are fine; `p.x`, `p.x = 1` and
|
|
635
|
+
`p.m()` through a `T` are `` Cannot read `x` of `T`: an unconstrained type
|
|
636
|
+
parameter has no members ``, even when every call passes a type with an `x`.
|
|
637
|
+
It is where the value came from that counts — a local copied from a `T`, an
|
|
638
|
+
element of a `T[]` and a narrowed `T | null` are all a `T` — never its type.
|
|
639
|
+
|
|
640
|
+
```ts nish:err NL2329
|
|
641
|
+
interface Point { x: i32; y: i32; }
|
|
642
|
+
|
|
643
|
+
const getX = <T>(p: T): i32 => p.x;
|
|
644
|
+
|
|
645
|
+
export const main = (): i32 => {
|
|
646
|
+
const p: Point = { x: 1, y: 2 };
|
|
647
|
+
return getX(p);
|
|
648
|
+
};
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
- **`<T extends Shape>` gives `T` exactly `Shape`'s members**: its fields, and
|
|
652
|
+
its methods when `Shape` is a class. A constraint is a declared class or
|
|
653
|
+
interface (or `Container<i32>`, with its arguments written out) — never a
|
|
654
|
+
scalar, `string`, an array or another type parameter. Each instantiation still
|
|
655
|
+
reads its own struct directly, with no vtable.
|
|
656
|
+
- **A type argument must satisfy the constraint**: be `Shape` itself, or a
|
|
657
|
+
class that declares `implements Shape`. Matching field names are not enough.
|
|
658
|
+
A class constraint is satisfied by that class alone. The refusal is at the
|
|
659
|
+
call: `` `T` of `areaOf` requires `T extends Shape`, and `i32` does not
|
|
660
|
+
implement it ``.
|
|
661
|
+
- **A constraint is a declaration, not a name.** A `Shape` your module declares
|
|
662
|
+
is not the `Shape` another module's `<T extends Shape>` names, even with the
|
|
663
|
+
same fields, so implementing your own is refused the same way; import the
|
|
664
|
+
template's `Shape` instead.
|
|
665
|
+
|
|
666
|
+
```ts nish:ok
|
|
667
|
+
interface Shape { area: i32; }
|
|
668
|
+
|
|
669
|
+
class Circle implements Shape {
|
|
670
|
+
area: i32;
|
|
671
|
+
radius: i32;
|
|
672
|
+
constructor(r: i32) { this.area = 3 * r * r; this.radius = r; }
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const areaOf = <T extends Shape>(s: T): i32 => s.area; // s.radius would be refused
|
|
676
|
+
|
|
677
|
+
export const main = (): i32 => areaOf(new Circle(2)) - 12;
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
- **Each type parameter is named once**: `<T, T>` is
|
|
681
|
+
`` Duplicate type parameter `T` ``, on a function, method, class or
|
|
682
|
+
interface, and in a template nothing calls too.
|
|
683
|
+
- **Not supported yet**: a default type argument (`<T = string>`) and type
|
|
684
|
+
parameters on a **constructor**, each with its own message, and a generic
|
|
685
|
+
**type alias**, which is only the syntax error `` expected `=`, found `<` ``.
|
|
686
|
+
A generic `main` is refused. There are no multiple bounds (`T extends A & B`)
|
|
687
|
+
and no bound that mentions another parameter.
|
|
688
|
+
- **`$` may not appear in a function, class or interface name** — it is what
|
|
689
|
+
separates a generic's name from its type arguments in the emitted symbol.
|
|
690
|
+
- **Read `identity<i32>` in a message, `identity$i32` in the IR.** Diagnostics,
|
|
691
|
+
`--emit-checked` and the `-g` debug names spell an instantiation as written
|
|
692
|
+
(`identity<Box<i32>>`, `Box<i32>.get`); symbols and `%struct` names are
|
|
693
|
+
mangled (`@identity$$Box$i32`, `%struct.Box$i32`).
|
|
694
|
+
- **A host calls an exported instantiation as `nish_gen_…`**: `identity<i32>`
|
|
695
|
+
is `nish_gen_identity_i32` in the C header, the `.d.ts`, the wasm loader and
|
|
696
|
+
the N-API addon, and `identity<i32[]>` is `nish_gen_identity_arr_i32`. There
|
|
697
|
+
is no `identity` to call — only the instantiations the program makes.
|
|
698
|
+
- **With a sidecar flag, an exported generic must be instantiated.** Nothing
|
|
699
|
+
instantiated means no symbol, so `--emit-header`, `--emit-dts` and
|
|
700
|
+
`--emit-napi` refuse it (NL4007) rather than leave it out; call it somewhere,
|
|
701
|
+
or do not export it. The same flags refuse two names that are one C
|
|
702
|
+
identifier, such as a method `Point.shifted` and a function `Point_shifted`
|
|
703
|
+
(NL4008).
|
|
704
|
+
|
|
705
|
+
```ts nish:err NL4007 --emit-dts build/docs-ai/nl4007.d.ts
|
|
706
|
+
export const identity = <T>(x: T): T => x; // exported, never called
|
|
707
|
+
|
|
708
|
+
export const main = (): i32 => 0;
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Generic classes and interfaces
|
|
712
|
+
|
|
713
|
+
A `class` or an `interface` may take type parameters too, and an instantiation
|
|
714
|
+
is **an ordinary struct**: `Box<i32>` is `%struct.Box$i32` with `Box`'s fields
|
|
715
|
+
at `T = i32`, and everything the language does to a struct works on it.
|
|
716
|
+
|
|
717
|
+
```ts nish:ok
|
|
718
|
+
class Box<T> {
|
|
719
|
+
value: T;
|
|
720
|
+
constructor(v: T) { this.value = v; }
|
|
721
|
+
get(): T { return this.value; }
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
interface Pair<A, B> { first: A; second: B; }
|
|
725
|
+
|
|
726
|
+
export const main = (): i32 => {
|
|
727
|
+
const b = new Box<i32>(7);
|
|
728
|
+
const p: Pair<i32, string> = { first: 1, second: "hi" };
|
|
729
|
+
console.log(p.second);
|
|
730
|
+
return b.get();
|
|
731
|
+
};
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
- **Write the type arguments out.** `Box` on its own is not a type — there is
|
|
735
|
+
no layout until `T` is bound — so it is `` `Box` is generic: it must be
|
|
736
|
+
written with its type arguments ``. The two positions that take them are an
|
|
737
|
+
annotation (`const b: Box<i32>`) and `new` (`new Box<i32>(7)`); a *call*
|
|
738
|
+
infers instead. A class that is not generic takes none.
|
|
739
|
+
- **A type argument may be anything a type may be**, including another
|
|
740
|
+
instantiation: `Box<Point>`, `Box<Box<i32>>`, `Box<i32[]>`,
|
|
741
|
+
`Box<Result<i32, string>>`.
|
|
742
|
+
- **`implements` may name an instantiation**: `class Box<T> implements
|
|
743
|
+
Container<T>` is checked per instantiation by the usual field-prefix rule.
|
|
744
|
+
- **A field may not grow its own type argument.** `class Nest<T> { inner:
|
|
745
|
+
Nest<T[]> | null }` is `` Monomorphising `Nest` would not terminate ``;
|
|
746
|
+
`Nest<T>` and a type that mentions no parameter are both fine.
|
|
747
|
+
- **A type parameter takes no type arguments of its own.** `T` is whatever the
|
|
748
|
+
instantiation bound it to, so `const y: T<i32>` is
|
|
749
|
+
`` Unsupported type reference `T<i32>` ``.
|
|
750
|
+
- **A template obeys every rule a declared class obeys**: `declare class
|
|
751
|
+
Box<T>`, `export default class Box<T>`, `abstract class Box<T>` and
|
|
752
|
+
`interface Box<T> extends Base` are refused in the words their non-generic
|
|
753
|
+
spellings are refused in.
|
|
754
|
+
- **A template claims its name.** `class Box<T>` is a declaration of `Box`, so
|
|
755
|
+
a function, constant, alias, enum or second class of that name is refused
|
|
756
|
+
whichever was written first: `` `Box` is already declared in this module ``,
|
|
757
|
+
or `` Duplicate declaration of `Box` `` for a second class.
|
|
758
|
+
- **Two modules may not both declare a generic class of one name** once both
|
|
759
|
+
instantiate it: `%struct.Box$i32` is program-wide, so
|
|
760
|
+
`` Generic class `Holder` is also declared in helper.ts ``.
|
|
761
|
+
- **A generic class or interface may be imported**, and the rule is a generic
|
|
762
|
+
function's: `%struct.Box$i32` and every `@Box$i32.*` symbol are defined once,
|
|
763
|
+
by the module that declares `Box<T>`, and `declare`d by every module that
|
|
764
|
+
holds one. Unlike a declared class it may be renamed on import, because the
|
|
765
|
+
name that crosses the ABI is the template's. Named without its type arguments
|
|
766
|
+
it is `` `Crate` is generic: it must be written with its type arguments ``.
|
|
767
|
+
- **A class or interface may constrain its parameters** — `class Holder<T
|
|
768
|
+
extends Shape>` — by the rules a function's follow: its methods may read
|
|
769
|
+
`this.item.area` through a field of type `T`, and `new Holder<Point>` or an
|
|
770
|
+
annotation `Holder<Point>` is refused unless `Point` satisfies `Shape`.
|
|
771
|
+
|
|
772
|
+
### Generic methods
|
|
773
|
+
|
|
774
|
+
A method — of a generic class or not — may declare its own type parameters.
|
|
775
|
+
They are inferred from the arguments like a generic function's, and each
|
|
776
|
+
(receiver, method type arguments) pair is its own `define`:
|
|
777
|
+
`@Chooser.pick$i32`, `@Box$i32.keep$str`.
|
|
778
|
+
|
|
779
|
+
```ts nish:ok
|
|
780
|
+
class Chooser {
|
|
781
|
+
flip: boolean = false;
|
|
782
|
+
pick<T>(a: T, b: T): T { return this.flip ? b : a; }
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
class Box<T> {
|
|
786
|
+
value: T;
|
|
787
|
+
constructor(v: T) { this.value = v; }
|
|
788
|
+
keep<U>(other: U): T { return this.value; }
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export const main = (): i32 => {
|
|
792
|
+
const c = new Chooser();
|
|
793
|
+
const word: string = c.pick("left", "right");
|
|
794
|
+
console.log(word);
|
|
795
|
+
return c.pick(0, 1) + new Box<i32>(7).keep("seven") - 7;
|
|
796
|
+
};
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
- **Never write the type arguments at the call**: `c.pick<i32>(1, 2)` is
|
|
800
|
+
`` Type arguments are not written at a call site ``. A method type parameter
|
|
801
|
+
that no parameter mentions is `` Cannot infer `U` for `Holder.make` ``.
|
|
802
|
+
- **Do not reuse a class parameter's name**: `class Box<T> { map<T>(…) }` is
|
|
803
|
+
`` Type parameter `T` of `Box.map` shadows `Box`'s own `T` `` (NL2331) —
|
|
804
|
+
call the method's `U`.
|
|
805
|
+
- **Constrain it like a function's** — `apply<U extends Shape>(u: U)` reads
|
|
806
|
+
`u.area` — but the constraint may not mention a type parameter, the class's
|
|
807
|
+
included.
|
|
808
|
+
- **A constructor takes none**; its class's are written after `new`.
|
|
809
|
+
|
|
810
|
+
```ts nish:err NL2331
|
|
811
|
+
class Box<T> {
|
|
812
|
+
value: T;
|
|
813
|
+
constructor(v: T) { this.value = v; }
|
|
814
|
+
map<T>(other: T): T { return other; } // shadows Box's T: call it U
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export const main = (): i32 => new Box<i32>(1).value;
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### Function parameters
|
|
821
|
+
|
|
822
|
+
A parameter of a top-level function may have a **function type**. Its argument
|
|
823
|
+
is a top-level function named at the call, or an arrow written right there, and
|
|
824
|
+
each one is compiled into its **own copy** of the function, with a direct call
|
|
825
|
+
where the parameter is called — there is no function pointer at run time.
|
|
826
|
+
|
|
827
|
+
```ts nish:ok
|
|
828
|
+
const map = <T, U>(xs: T[], f: (x: T) => U): U[] => {
|
|
829
|
+
const out: U[] = [];
|
|
830
|
+
for (const x of xs) {
|
|
831
|
+
out.push(f(x));
|
|
832
|
+
}
|
|
833
|
+
return out;
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
const fold = <T>(xs: T[], f: (acc: T, x: T) => T, identity: T): T => {
|
|
837
|
+
let acc = identity;
|
|
838
|
+
for (const x of xs) {
|
|
839
|
+
acc = f(acc, x);
|
|
840
|
+
}
|
|
841
|
+
return acc;
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
const add = (a: i32, b: i32): i32 => a + b;
|
|
845
|
+
|
|
846
|
+
export const main = (): i32 => {
|
|
847
|
+
const doubled = map([1, 2, 3], (n) => n * 2); // U is bound from the body
|
|
848
|
+
return fold(doubled, add, 0) - 12; // 0
|
|
849
|
+
};
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
- **Call it, or pass it on to another function parameter. Nothing else**:
|
|
853
|
+
`const g = f`, `return f` and `[f]` are
|
|
854
|
+
`` `f` is a function parameter and can only be called or passed on as a function argument ``.
|
|
855
|
+
- **An arrow captures nothing.** It sees its own parameters and top-level
|
|
856
|
+
names; reading a local of the function around it is
|
|
857
|
+
`` The arrow reads `k`, which belongs to the function it is written in ``.
|
|
858
|
+
Pass the value in as another argument of the template instead.
|
|
859
|
+
- **The argument's type must match exactly**: `(x: i32) => i64` for a
|
|
860
|
+
`(x: i32) => i32` parameter is refused. A generic function cannot be passed
|
|
861
|
+
by name — write `(x) => identity(x)`.
|
|
862
|
+
- **Annotate what cannot be inferred**: an arrow parameter whose type no value
|
|
863
|
+
argument binds, and the return type of a block-bodied arrow whose result is a
|
|
864
|
+
type parameter.
|
|
865
|
+
- **Only a top-level function** may have one: not a method, a constructor, a
|
|
866
|
+
field, a local, a return type or an alias.
|
|
867
|
+
|
|
868
|
+
```ts nish:err NL2335
|
|
869
|
+
const apply = (f: (x: i32) => i32, x: i32): i32 => f(x);
|
|
870
|
+
|
|
871
|
+
export const main = (): i32 => {
|
|
872
|
+
const k = 3;
|
|
873
|
+
return apply((x) => x + k, 1); // captures `k`: pass it to `apply` instead
|
|
874
|
+
};
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
### Data parallelism: `nish/threads`
|
|
878
|
+
|
|
879
|
+
`parallelMapInto(src, dst, f)` writes `f(src[i])` into `dst[i]`, and
|
|
880
|
+
`parallelReduce(src, f, identity)` folds `src` with `f` — each on as many
|
|
881
|
+
threads as the array is long enough for. `f` is a [function parameter](#function-parameters).
|
|
882
|
+
Importing the module compiles the program with `--threads`; there is nothing
|
|
883
|
+
else to switch on and no thread count to choose. `parallelMap(src, f)` and
|
|
884
|
+
`parallelFrom(n, f)` answer a new array instead (`f(src[i])`, `f(i)`), under
|
|
885
|
+
the same rules for `f`.
|
|
886
|
+
|
|
887
|
+
```ts nish:ok
|
|
888
|
+
import { parallelMapInto, parallelReduce } from "nish/threads";
|
|
889
|
+
|
|
890
|
+
const square = (x: f64): f64 => x * x;
|
|
891
|
+
|
|
892
|
+
export const main = (): i32 => {
|
|
893
|
+
const src: f64[] = [1.0, 2.0, 3.0];
|
|
894
|
+
const dst: f64[] = [0.0, 0.0, 0.0]; // at least src.length, or it panics
|
|
895
|
+
parallelMapInto(src, dst, square);
|
|
896
|
+
const total = parallelReduce(dst, (a, b) => a + b, 0.0); // 14, the same bits on any core count
|
|
897
|
+
return toI32(total);
|
|
898
|
+
};
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
```ts nish:ok
|
|
902
|
+
import { parallelFrom, parallelMap } from "nish/threads";
|
|
903
|
+
|
|
904
|
+
export const main = (): i32 => {
|
|
905
|
+
const names = parallelFrom(3, (i) => `w${i}`); // each string copied out at the join
|
|
906
|
+
const lens = parallelMap(names, (n) => n.length); // 2 2 2
|
|
907
|
+
console.log(`${names[2]} ${lens[0]}`); // w2 2
|
|
908
|
+
return 0;
|
|
909
|
+
};
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
- **`f` writes nothing its caller can see.** No field or element store
|
|
913
|
+
through its argument and no `console.log` — each is refused at the call,
|
|
914
|
+
naming the write (NL2345): answer the value and write after the call. An
|
|
915
|
+
arrow may read the `const` locals and parameters around it, never write
|
|
916
|
+
them (NL2503: answer the value from the body instead, and fold the answers with `parallelReduce`,
|
|
917
|
+
`const sumTotal = parallelReduce(xs, (a, b) => a + b, 0)`, or read them from the array the call
|
|
918
|
+
gives back, `const kValues = parallelMap(xs, (x): i32 => x)`)
|
|
919
|
+
and never read a `let` (NL2500: snapshot it first, `const kNow = k`). Reading is fine; so are a bounds check, an integer
|
|
920
|
+
division and `panic`.
|
|
921
|
+
- **What `f` allocates is freed after every element.** A string, array or
|
|
922
|
+
object built on the way to the result compiles, with performance warning
|
|
923
|
+
NL9012: each element pays for building it and for the release. Storing an
|
|
924
|
+
allocation into another object (NL2352) and touching `Arena` (NL2351) are
|
|
925
|
+
refused, because either would make that release unsound.
|
|
926
|
+
- **A map's result is a number, a `boolean`, an enum, a `string` or an array
|
|
927
|
+
of numbers, booleans or enums**: a string or a scalar array is copied out of
|
|
928
|
+
the worker's arena at the join. An array of strings, a nested array, a class,
|
|
929
|
+
a `Map` or a `Set` is refused (NL2348): answer a string or a number per
|
|
930
|
+
element and build the structure after the call. **A reduce's `T` stays a number, a
|
|
931
|
+
`boolean` or an enum**: map strings to numbers first.
|
|
932
|
+
- **`dst` may not be reachable from an element of `src`**: mapping `Row[]` into
|
|
933
|
+
`f64[]` is refused when `Row` holds an `f64[]`, because it could be `dst`.
|
|
934
|
+
- **A reduce's `f` is associative and `identity` is its identity** — `0` for
|
|
935
|
+
`+`, `1` for `*`. The array is folded in fixed blocks and the blocks combined
|
|
936
|
+
in order, so `-` or a wrong identity is refused when `f` is an arrow of one
|
|
937
|
+
operator; a named function is trusted.
|
|
938
|
+
- A short array runs on the calling thread at the cost of the loop, so there
|
|
939
|
+
is no reason to guard a call by size. How short is sized from what `f`
|
|
940
|
+
costs: a body estimated at 3, such as `(x) => x + 1`, is divided only past
|
|
941
|
+
about 700,000 elements, a cheaper one later, one with a loop far sooner.
|
|
942
|
+
|
|
943
|
+
```ts nish:err NL2350
|
|
944
|
+
import { parallelReduce } from "nish/threads";
|
|
945
|
+
|
|
946
|
+
export const main = (): i32 => parallelReduce([1, 2, 3], (a, b) => a + b, 1); // `+` folds from 0
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### Scoped tasks: `using s = scope()`
|
|
950
|
+
|
|
951
|
+
Different functions at once, one thread each: open a scope with `using`, give
|
|
952
|
+
it tasks with `spawn(entry, arg, dst, at)`, and every task has run and stored
|
|
953
|
+
`entry(arg)` into `dst[at]` when the block ends.
|
|
954
|
+
|
|
955
|
+
```ts nish:ok
|
|
956
|
+
import { scope } from "nish/threads";
|
|
957
|
+
|
|
958
|
+
const sumOf = (xs: f64[]): f64 => xs[0] + xs[1];
|
|
959
|
+
const square = (n: i32): i32 => n * n;
|
|
960
|
+
|
|
961
|
+
export const main = (): i32 => {
|
|
962
|
+
const xs: f64[] = [1.5, 2.5];
|
|
963
|
+
const sums: f64[] = [0.0];
|
|
964
|
+
const squares: i32[] = [0];
|
|
965
|
+
{
|
|
966
|
+
using s = scope();
|
|
967
|
+
s.spawn(sumOf, xs, sums, 0);
|
|
968
|
+
s.spawn(square, 12, squares, 0);
|
|
969
|
+
} // joined here, on every exit
|
|
970
|
+
console.log(`${sums[0]} ${squares[0]}`); // 4 144
|
|
971
|
+
return 0;
|
|
972
|
+
};
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
- **The tasks run when the block ends**, together, and each answer is stored
|
|
976
|
+
after the last one finishes. So read a destination after the block. A
|
|
977
|
+
destination is a `const` bound to a fresh array (`[0, 0]`, `new Array`) that
|
|
978
|
+
you only index; never pass it on. Between the first `spawn` and the block's
|
|
979
|
+
end, don't read a destination, and don't write memory a task could read (a
|
|
980
|
+
store into an argument, a call that writes through its argument).
|
|
981
|
+
- **`using` takes only `scope()`**, a `Mutex`'s `lock()` (below), or `arena()`
|
|
982
|
+
(under [Memory](#memory)), and `scope()` only comes from `using`
|
|
983
|
+
(NL2388): `using s = scope()`.
|
|
984
|
+
The scope is only ever the receiver of a `spawn` statement: never pass it,
|
|
985
|
+
store it or return it.
|
|
986
|
+
- **The task is a top-level function named in the `spawn`, or an arrow written
|
|
987
|
+
there.** `s.spawn(entry, arg)` with no destination runs an entry that
|
|
988
|
+
answers `void`; `s.spawn(() => work(a, b))` runs an arrow that reads the
|
|
989
|
+
`const` locals and parameters around it (never a `let` or `this` (NL2506):
|
|
990
|
+
bind a `const` first, `const kNow = k`) and answers `void`. An entry that
|
|
991
|
+
answers a value has a destination (NL2511): `const answer = new Array<i32>(1)`
|
|
992
|
+
above the scope and `s.spawn(entry, arg, answer, 0)`. Either follows the
|
|
993
|
+
data-parallel rules: it writes nothing anybody else can see but what it
|
|
994
|
+
writes through a `Mutex`'s guard or sends on a `Channel` (below), answers a number,
|
|
995
|
+
a `boolean`, an enum, a `string` or an array of numbers, booleans or enums
|
|
996
|
+
(copied out of its arena at the join), and leaves `Arena` alone. A task cannot open a scope
|
|
997
|
+
of its own or call `parallelMapInto`.
|
|
998
|
+
- **Any argument will do** — an array, an object — because nothing writes
|
|
999
|
+
memory while the tasks run. There is no thread count: one task, one thread.
|
|
1000
|
+
|
|
1001
|
+
```ts nish:err NL2388
|
|
1002
|
+
import { scope } from "nish/threads";
|
|
1003
|
+
|
|
1004
|
+
const one = (n: i32): i32 => n + 1;
|
|
1005
|
+
|
|
1006
|
+
export const main = (): i32 => {
|
|
1007
|
+
const out: i32[] = [0];
|
|
1008
|
+
const s = scope(); // a scope must be introduced by `using`
|
|
1009
|
+
s.spawn(one, 1, out, 0);
|
|
1010
|
+
return out[0];
|
|
1011
|
+
};
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
### Sharing a value between tasks: `Mutex<T>`
|
|
1015
|
+
|
|
1016
|
+
When tasks must update one value, put it in a `Mutex` and reach it only through
|
|
1017
|
+
`using g = m.lock()`: the lock is held until that block ends, on every exit.
|
|
1018
|
+
|
|
1019
|
+
```ts nish:ok
|
|
1020
|
+
import { Mutex, scope } from "nish/threads";
|
|
1021
|
+
|
|
1022
|
+
class Tally {
|
|
1023
|
+
bins: i32[];
|
|
1024
|
+
constructor() {
|
|
1025
|
+
this.bins = [0, 0, 0, 0];
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
class Shard {
|
|
1030
|
+
xs: i32[];
|
|
1031
|
+
tally: Mutex<Tally>;
|
|
1032
|
+
constructor(xs: i32[], tally: Mutex<Tally>) {
|
|
1033
|
+
this.xs = xs;
|
|
1034
|
+
this.tally = tally;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const binAll = (s: Shard): i32 => {
|
|
1039
|
+
for (const x of s.xs) {
|
|
1040
|
+
const bin: i32 = x & 3;
|
|
1041
|
+
using g = s.tally.lock(); // held to the end of this pass
|
|
1042
|
+
g.value.bins[bin] = g.value.bins[bin] + 1;
|
|
1043
|
+
}
|
|
1044
|
+
return 0;
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
export const main = (): i32 => {
|
|
1048
|
+
const tally = new Mutex<Tally>(new Tally()); // a fresh value: `new` or a literal
|
|
1049
|
+
const done: i32[] = [0, 0];
|
|
1050
|
+
{
|
|
1051
|
+
using s = scope();
|
|
1052
|
+
s.spawn(binAll, new Shard([1, 2, 3], tally), done, 0);
|
|
1053
|
+
s.spawn(binAll, new Shard([4, 5, 6], tally), done, 1);
|
|
1054
|
+
}
|
|
1055
|
+
using g = tally.lock(); // after the join: lock freely
|
|
1056
|
+
console.log(`${g.value.bins[1]}`); // 2
|
|
1057
|
+
return 0;
|
|
1058
|
+
};
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
- **Only `g.value`, and only as the base of a field or element access.** Read
|
|
1062
|
+
a number, a `boolean`, an enum or a `string` through it, or store one into a
|
|
1063
|
+
field or an element that already exists (a string is copied each way) (for a `Mutex<i32>`, `g.value = g.value + 1`).
|
|
1064
|
+
Don't bind, store, return or walk `g` itself (NL2466), nor bind, store, return
|
|
1065
|
+
or walk `g.value` or an array it holds (NL2467); store only a scalar or a
|
|
1066
|
+
`string` into it (NL2468), never `null` (NL2537), and call no method
|
|
1067
|
+
through it (no `push`).
|
|
1068
|
+
- **A guard goes only to a helper**: a top-level function named at the call,
|
|
1069
|
+
whose parameter is `MutexGuard<T>`, once per call — `note(g, x)` with
|
|
1070
|
+
`const note = (g: MutexGuard<Stats>, x: i32): void => { … }`. The helper is
|
|
1071
|
+
held to the block's rules. A method, a constructor or a generic is refused
|
|
1072
|
+
(NL2536): `this` or `T` would be a way to keep the guard.
|
|
1073
|
+
- **Make the data fresh all the way down**: `new Mutex<Hist>(new Hist())`, not
|
|
1074
|
+
`new Hist(bins)` with a `bins` you still hold.
|
|
1075
|
+
- **`lock()` only as `using g = m.lock()`** (NL2465), one lock per `using`. The lock is
|
|
1076
|
+
released at every exit of the block, and at a `return` after the returned
|
|
1077
|
+
value is computed, so `return g.value.n` reads under the lock.
|
|
1078
|
+
- **One lock at a time, and no waiting while it is held**: inside the guard's
|
|
1079
|
+
block, directly or through any call or `new`, no second `lock()`, no
|
|
1080
|
+
`scope()` or `spawn`, no `parallelMapInto` or `parallelReduce`. So a program
|
|
1081
|
+
cannot deadlock.
|
|
1082
|
+
- **Only a scope's tasks and the thread that opened it lock**: the function
|
|
1083
|
+
given to `parallelMapInto` or `parallelReduce` takes no lock, even through a
|
|
1084
|
+
call (NL2474).
|
|
1085
|
+
- **A `Mutex` reaches a task as its argument or a field of its argument's
|
|
1086
|
+
class** — not in an array. From the statement holding a scope's first
|
|
1087
|
+
`spawn` to its block's end the parent doesn't lock what the tasks share,
|
|
1088
|
+
even through a call; before and after, it may.
|
|
1089
|
+
- **Make the updates commute exactly** (an integer sum or count, a histogram;
|
|
1090
|
+
not an `f64` sum): the critical sections run in the scheduler's order
|
|
1091
|
+
natively and in spawn order under Node.
|
|
1092
|
+
|
|
1093
|
+
```ts nish:err NL2467
|
|
1094
|
+
import { Mutex } from "nish/threads";
|
|
1095
|
+
|
|
1096
|
+
class Counter {
|
|
1097
|
+
n: i32 = 0;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export const main = (): i32 => {
|
|
1101
|
+
const m = new Mutex<Counter>(new Counter());
|
|
1102
|
+
using g = m.lock();
|
|
1103
|
+
const c = g.value; // what the guard reaches would outlive the lock
|
|
1104
|
+
return c.n;
|
|
1105
|
+
};
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
```ts nish:ok
|
|
1109
|
+
import { Mutex, MutexGuard } from "nish/threads";
|
|
1110
|
+
|
|
1111
|
+
class Stats {
|
|
1112
|
+
count: i32 = 0;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const bump = (g: MutexGuard<Stats>): void => {
|
|
1116
|
+
g.value.count = g.value.count + 1; // a helper: the block's rules, the block's lock
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
export const main = (): i32 => {
|
|
1120
|
+
const m = new Mutex<Stats>(new Stats());
|
|
1121
|
+
using g = m.lock();
|
|
1122
|
+
bump(g);
|
|
1123
|
+
return g.value.count - 1; // 0
|
|
1124
|
+
};
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
### Counters without a lock: `AtomicI32` and `AtomicI64`
|
|
1128
|
+
|
|
1129
|
+
A shared counter, minimum, maximum or flag is an atomic: `new AtomicI32(0)`,
|
|
1130
|
+
`new AtomicI64(toI64(0))`, updated from any task or parallel `f` by one
|
|
1131
|
+
indivisible method call.
|
|
1132
|
+
|
|
1133
|
+
```ts nish:ok
|
|
1134
|
+
import { AtomicI32, parallelMapInto } from "nish/threads";
|
|
1135
|
+
|
|
1136
|
+
export const main = (): i32 => {
|
|
1137
|
+
const hits = new AtomicI32(0);
|
|
1138
|
+
const top = new AtomicI32(0);
|
|
1139
|
+
const xs: i32[] = [3, 9, 4];
|
|
1140
|
+
const ys: i32[] = [0, 0, 0];
|
|
1141
|
+
parallelMapInto(xs, ys, (x) => hits.add(1) + top.max(x)); // each answers the old value
|
|
1142
|
+
console.log(`${hits.load()} ${top.load()}`); // 3 9
|
|
1143
|
+
return 0;
|
|
1144
|
+
};
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
- **Only its methods**: `load`, `store`, `add`, `sub`, `min`, `max`,
|
|
1148
|
+
`exchange`, `compareExchange(expected, desired)`; each update answers the
|
|
1149
|
+
value held just before it. No field to read.
|
|
1150
|
+
- **Not inside a scope's block after the first `spawn`** (NL2513): read it
|
|
1151
|
+
before, or after the block. Two scopes of one `using` share no atomic
|
|
1152
|
+
(NL2516): give each scope a block of its own, `{ using s = scope(); … }`
|
|
1153
|
+
then `{ using t = scope(); … }`.
|
|
1154
|
+
- **Make the updates commute** — `add`, `sub`, `min`, `max` — or native and
|
|
1155
|
+
Node differ.
|
|
1156
|
+
- **Add once per batch**: count into a local array, then one `add` per bin.
|
|
1157
|
+
One `add` per value makes the cores wait on each other.
|
|
1158
|
+
- **`AtomicI64` takes `i64` values**, exact under Node within ±2^53.
|
|
1159
|
+
|
|
1160
|
+
```ts nish:err NL2513
|
|
1161
|
+
import { AtomicI32, scope } from "nish/threads";
|
|
1162
|
+
|
|
1163
|
+
const bump = (a: AtomicI32): i32 => a.add(1);
|
|
1164
|
+
|
|
1165
|
+
export const main = (): i32 => {
|
|
1166
|
+
const a = new AtomicI32(0);
|
|
1167
|
+
const done: i32[] = [0];
|
|
1168
|
+
let seen: i32 = 0;
|
|
1169
|
+
{
|
|
1170
|
+
using s = scope();
|
|
1171
|
+
s.spawn(bump, a, done, 0);
|
|
1172
|
+
seen = a.load(); // the task may not have run yet
|
|
1173
|
+
}
|
|
1174
|
+
return seen;
|
|
1175
|
+
};
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
### Passing values between tasks: `Channel<T>`
|
|
1179
|
+
|
|
1180
|
+
When one task produces values and another consumes them, give both a
|
|
1181
|
+
`Channel`: the producers `send`, and the consumer walks the channel with
|
|
1182
|
+
`for...of`, which waits for values and ends once every producer has returned.
|
|
1183
|
+
Nobody closes a channel.
|
|
1184
|
+
|
|
1185
|
+
```ts nish:ok
|
|
1186
|
+
import { Channel, scope } from "nish/threads";
|
|
1187
|
+
|
|
1188
|
+
class Pipe {
|
|
1189
|
+
ch: Channel<i32>;
|
|
1190
|
+
n: i32;
|
|
1191
|
+
constructor(ch: Channel<i32>, n: i32) {
|
|
1192
|
+
this.ch = ch;
|
|
1193
|
+
this.n = n;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const produce = (p: Pipe): i32 => {
|
|
1198
|
+
for (let i: i32 = 1; i <= p.n; i++) {
|
|
1199
|
+
p.ch.send(i); // never waits
|
|
1200
|
+
}
|
|
1201
|
+
return p.n;
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
const consume = (p: Pipe): i32 => {
|
|
1205
|
+
let sum: i32 = 0;
|
|
1206
|
+
for (const x of p.ch) { // ends once every sender has returned
|
|
1207
|
+
sum = sum + x;
|
|
1208
|
+
}
|
|
1209
|
+
return sum;
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
export const main = (): i32 => {
|
|
1213
|
+
const sent: i32[] = [0, 0];
|
|
1214
|
+
const sums: i32[] = [0];
|
|
1215
|
+
const ch = new Channel<i32>(); // a `const`, right before its scope
|
|
1216
|
+
{
|
|
1217
|
+
using s = scope();
|
|
1218
|
+
s.spawn(produce, new Pipe(ch, 10), sent, 0); // every sender first
|
|
1219
|
+
s.spawn(produce, new Pipe(ch, 20), sent, 1);
|
|
1220
|
+
s.spawn(consume, new Pipe(ch, 0), sums, 0); // then the one receiver
|
|
1221
|
+
}
|
|
1222
|
+
console.log(`${sums[0]}`); // 265
|
|
1223
|
+
return 0;
|
|
1224
|
+
};
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
- **It carries a number, a `boolean`, an enum or a `string`** (copied in by
|
|
1228
|
+
`send`, out by the receive), never an array or an object.
|
|
1229
|
+
- **Declare it as a `const` right before its scope** — in the block that holds
|
|
1230
|
+
`using s = scope()`, or in the block around the scope's block — and hand it
|
|
1231
|
+
to tasks only as the task's argument or a field of the object you build for
|
|
1232
|
+
it in the `spawn` (`new Pipe(ch, 10)`). Don't bind it to another name, store
|
|
1233
|
+
it, return it, or name any member but `send`. Passing it to a function
|
|
1234
|
+
whose parameter is declared `Channel<T>` is fine; pushing it onto an array,
|
|
1235
|
+
putting it in a `Map`, or passing it as a generic's `T` is not. A `spawn`
|
|
1236
|
+
hands a task only a channel the function itself declared, never one the
|
|
1237
|
+
function was handed, and no task gets one channel in two fields. Hold it
|
|
1238
|
+
only as `Channel<T>` (no `| null`, no `Channel<T>[]`, no `Map` of them), and
|
|
1239
|
+
use it only in a top-level function's body, never inside an arrow
|
|
1240
|
+
(NL2484): write the task as a module function,
|
|
1241
|
+
`const sendAll = (ch: Channel<i32>): void => { … }`, and spawn that.
|
|
1242
|
+
`new Channel<i32>()` takes no capacity: it is unbounded.
|
|
1243
|
+
- **One scope, one receiver.** Only one scope's tasks use a channel. Its one
|
|
1244
|
+
receiver is either a single task spawned outside any loop, or the parent
|
|
1245
|
+
after the scope's block; never both, never two. The parent sends only before
|
|
1246
|
+
the scope's block, and receives nothing inside it.
|
|
1247
|
+
- **Senders before the receiver.** Spawn every task that may send before the
|
|
1248
|
+
task that receives (NL2481: `s.spawn(produce, …)` above
|
|
1249
|
+
`s.spawn(consume, …)`), and never let one task both send and receive on the
|
|
1250
|
+
same channel. So channels cannot deadlock.
|
|
1251
|
+
- **No receive while a lock is held**, even through a call. A `send` under a
|
|
1252
|
+
guard is fine.
|
|
1253
|
+
- **Print what the interleaving cannot change**: two senders' values arrive in
|
|
1254
|
+
the scheduler's order natively and in spawn order under Node, so a count or an
|
|
1255
|
+
integer sum prints the same both ways.
|
|
1256
|
+
|
|
1257
|
+
```ts nish:err NL2481
|
|
1258
|
+
import { Channel, scope } from "nish/threads";
|
|
1259
|
+
|
|
1260
|
+
const one = (ch: Channel<i32>): i32 => {
|
|
1261
|
+
ch.send(1);
|
|
1262
|
+
return 1;
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
const drain = (ch: Channel<i32>): i32 => {
|
|
1266
|
+
let sum: i32 = 0;
|
|
1267
|
+
for (const x of ch) {
|
|
1268
|
+
sum = sum + x;
|
|
1269
|
+
}
|
|
1270
|
+
return sum;
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
export const main = (): i32 => {
|
|
1274
|
+
const out: i32[] = [0, 0];
|
|
1275
|
+
const ch = new Channel<i32>();
|
|
1276
|
+
{
|
|
1277
|
+
using s = scope();
|
|
1278
|
+
s.spawn(drain, ch, out, 0); // the receiver first: under Node it finds nothing
|
|
1279
|
+
s.spawn(one, ch, out, 1);
|
|
1280
|
+
}
|
|
1281
|
+
return out[0];
|
|
1282
|
+
};
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
### What crosses between threads, and `nish --fix`
|
|
1286
|
+
|
|
1287
|
+
- **Crosses, by copy:** a number, a `boolean`, an enum, a `string`, an array
|
|
1288
|
+
of numbers, booleans or enums — a task's answer, a `parallelMap` element, a
|
|
1289
|
+
`Channel<string>` value, a string stored through a guard. Each thread moves
|
|
1290
|
+
the `parallelMap` elements it answers into blocks of its own, and the caller
|
|
1291
|
+
takes the blocks whole at the join; a task's answer under 64 KB is copied in
|
|
1292
|
+
by the joining thread. On one thread every element still pays the move, so
|
|
1293
|
+
cheap work that answers strings can be slower there than sequential.
|
|
1294
|
+
- **Doesn't cross:** a class value, a nested array, a `string[]`, a `Map`, a
|
|
1295
|
+
`Set`. Answer what it is built from, and build it after the join.
|
|
1296
|
+
- **`nish --fix` writes the usual repair**: `const` to `using` for a scope or a
|
|
1297
|
+
guard (NL2388, NL2465), a destination for a number or `boolean` answer
|
|
1298
|
+
(NL2511), a `const kNow = k` snapshot of a `let` an arrow reads (NL2506,
|
|
1299
|
+
NL2500), and a channel arrow hoisted to a top-level `const` (NL2484). It
|
|
1300
|
+
leaves to you a `spawn` in a loop, a `let` another argument names or a `for`
|
|
1301
|
+
header declares, and a channel arrow that captures or leaves a parameter's or
|
|
1302
|
+
its result's type out.
|
|
1303
|
+
- **Every thread refusal ends with the code to write**, in your names where
|
|
1304
|
+
the compiler has them; the guide's
|
|
1305
|
+
[refusal table](guide/using-nish/threads.md#refused-and-what-to-write-instead)
|
|
1306
|
+
pairs each code with an example that compiles.
|
|
1307
|
+
|
|
1308
|
+
```ts nish:err NL2506
|
|
1309
|
+
import { scope } from "nish/threads";
|
|
1310
|
+
|
|
1311
|
+
export const main = (): i32 => {
|
|
1312
|
+
let k: i32 = 3;
|
|
1313
|
+
const out: i32[] = [0];
|
|
1314
|
+
{
|
|
1315
|
+
using s = scope();
|
|
1316
|
+
s.spawn((n: i32): i32 => n + k, 1, out, 0); // --fix: `const kNow = k;` above, the arrow reads `kNow`
|
|
1317
|
+
}
|
|
1318
|
+
return out[0];
|
|
1319
|
+
};
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
### Calling C
|
|
1323
|
+
|
|
1324
|
+
`declare function name(params): T;` declares a C function this program calls but
|
|
1325
|
+
does not define. The symbol is the identifier, unmangled, in any package
|
|
1326
|
+
(a package's own functions are prefixed, its declarations are not), and the
|
|
1327
|
+
call is an ordinary call. Two declarations of one name must have the same
|
|
1328
|
+
types.
|
|
1329
|
+
|
|
1330
|
+
```ts nish:ok
|
|
1331
|
+
declare function abs(n: i32): i32;
|
|
1332
|
+
|
|
1333
|
+
export const main = (): i32 => {
|
|
1334
|
+
console.log(`${abs(-5)}`);
|
|
1335
|
+
return 0;
|
|
1336
|
+
};
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
- **A parameter is a scalar, a `CPtr`, a borrowed `string` or a `u8[]` buffer
|
|
1340
|
+
with its count; a return is a scalar, a `CPtr` or a copied `string`.** The
|
|
1341
|
+
scalars are the integer and float widths, `boolean` and `void`. Any other
|
|
1342
|
+
array, a class or an interface is rejected, in either position.
|
|
1343
|
+
- **A run of `u8[]` / `readonly u8[]` parameters is followed by exactly one
|
|
1344
|
+
`u64`**, the byte count C may touch in each (`memcpy(dst: u8[], src:
|
|
1345
|
+
readonly u8[], n: u64)`). `readonly` is C's `const`. A run without its count
|
|
1346
|
+
is NL2549, a count that is not `u64` NL2550, `i32[]` NL2551, `u8[] | null`
|
|
1347
|
+
NL2552. Declare `fwrite` as `(buf: readonly u8[], n: u64, one: u64, f: CPtr)`
|
|
1348
|
+
and pass `1` for `one`. A call passes the data pointer and first checks the
|
|
1349
|
+
count against every buffer of the run: a larger count is an `ffi-length`
|
|
1350
|
+
panic, and only `toU64(buf.length)` of the buffer itself is proved. C
|
|
1351
|
+
borrows the bytes for the call and may write a `u8[]`; a spawned task may
|
|
1352
|
+
not make the call.
|
|
1353
|
+
|
|
1354
|
+
```ts nish:err NL2549
|
|
1355
|
+
declare function SHA256(d: readonly u8[], n: u64, md: u8[]): CPtr;
|
|
1356
|
+
|
|
1357
|
+
export const main = (): i32 => {
|
|
1358
|
+
return 0;
|
|
1359
|
+
};
|
|
1360
|
+
```
|
|
1361
|
+
|
|
1362
|
+
- **A `string` parameter is lent for the call.** C gets a NUL-terminated
|
|
1363
|
+
`const char *` that it may read until it returns, and may neither keep nor
|
|
1364
|
+
write; a C function that does either is declared to take a `CPtr` instead.
|
|
1365
|
+
`string | null` passes `NULL` for `null`. A string with a NUL byte in it
|
|
1366
|
+
panics (`ffi-nul`) before the call, because C would read only up to it; a
|
|
1367
|
+
literal with no NUL is not checked.
|
|
1368
|
+
|
|
1369
|
+
```ts nish:ok
|
|
1370
|
+
declare function puts(s: string): i32;
|
|
1371
|
+
|
|
1372
|
+
export const main = (): i32 => {
|
|
1373
|
+
puts("hello");
|
|
1374
|
+
return 0;
|
|
1375
|
+
};
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
- **A `string` result is copied out of C's `const char *`** into the arena at
|
|
1379
|
+
the call, so it lives like any other string and C may reuse its buffer.
|
|
1380
|
+
`string | null` maps `NULL` to `null`; a `string` result that comes back
|
|
1381
|
+
`NULL` panics (`ffi-null`). The copy never frees: a result the caller must
|
|
1382
|
+
`free` (`strdup`) is declared `CPtr | null` instead.
|
|
1383
|
+
|
|
1384
|
+
```ts nish:ok
|
|
1385
|
+
declare function strerror(errnum: i32): string;
|
|
1386
|
+
declare function getenv(name: string): string | null;
|
|
1387
|
+
|
|
1388
|
+
export const main = (): i32 => {
|
|
1389
|
+
console.log(strerror(2));
|
|
1390
|
+
const home = getenv("HOME");
|
|
1391
|
+
console.log(home === null ? "unset" : "set");
|
|
1392
|
+
return 0;
|
|
1393
|
+
};
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
- **A buffer C allocated is read with `readCString` from `nish:unsafe`, then
|
|
1397
|
+
freed.** `readCString(p: CPtr): string` copies the NUL-terminated bytes at
|
|
1398
|
+
`p` into a new string; a dangling or unterminated `p` is undefined
|
|
1399
|
+
behaviour, which is why it needs the import. It takes a `CPtr`, so narrow a
|
|
1400
|
+
`CPtr | null` with `!== null` first.
|
|
1401
|
+
- **A `CPtr | null` parameter takes the literal `null` or a narrowed pointer.**
|
|
1402
|
+
`realpath(path, null)` passes `NULL`; a nullable pointer held in a variable
|
|
1403
|
+
is NL2539, so C is never handed a `NULL` by accident.
|
|
1404
|
+
|
|
1405
|
+
```ts nish:ok
|
|
1406
|
+
import { readCString } from "nish:unsafe";
|
|
1407
|
+
|
|
1408
|
+
declare function realpath(path: string, resolved: CPtr | null): CPtr | null;
|
|
1409
|
+
declare function free(p: CPtr): void;
|
|
1410
|
+
|
|
1411
|
+
export const main = (): i32 => {
|
|
1412
|
+
const p = realpath(".", null);
|
|
1413
|
+
if (p === null) {
|
|
1414
|
+
return 1;
|
|
1415
|
+
}
|
|
1416
|
+
const dir = readCString(p);
|
|
1417
|
+
free(p);
|
|
1418
|
+
console.log(dir.startsWith("/") ? "absolute" : "relative");
|
|
1419
|
+
return 0;
|
|
1420
|
+
};
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
- **No body, and it cannot be `export`ed.**
|
|
1424
|
+
- **A class with a `CPtr` field is a handle class: open one with `using`.**
|
|
1425
|
+
`using f = new File(p)` closes `f` once at every exit of its block (falling
|
|
1426
|
+
off the end, `return`, `break`, `continue`, `orReturn()`), so never call the
|
|
1427
|
+
close yourself. The class is checked: one `readonly` `CPtr` or
|
|
1428
|
+
`CPtr | null` field written once by the constructor's foreign call, read only
|
|
1429
|
+
as `this.f` in its own methods, and a `[Symbol.dispose]` that is exactly the
|
|
1430
|
+
close. A handle is never copied, stored, returned or captured: it is called
|
|
1431
|
+
or lent to a parameter of its type, and `new` of one appears only as a
|
|
1432
|
+
`using` initialiser. A panic or `process.exit` does not close it, so
|
|
1433
|
+
`fflush` a written `FILE *` before code that may panic. The rules:
|
|
1434
|
+
`LANGUAGE.md` → "A handle class".
|
|
1435
|
+
- **It costs the caller its attributes.** Nothing about a body the compiler
|
|
1436
|
+
cannot see is provable, so a function that calls C loses `readnone` and
|
|
1437
|
+
`willreturn`, and so does everything above it in the call graph. A program
|
|
1438
|
+
that calls C is no longer one the compiler can reason about end to end — that
|
|
1439
|
+
trade *is* the feature.
|
|
1440
|
+
|
|
1441
|
+
### Module constants
|
|
1442
|
+
|
|
1443
|
+
```ts nish:ok
|
|
1444
|
+
const TOKEN_NAME: i32 = 1;
|
|
1445
|
+
const TOKEN_END: i32 = TOKEN_NAME + 1;
|
|
1446
|
+
export const PROMPT: string = "> ";
|
|
1447
|
+
```
|
|
1448
|
+
|
|
1449
|
+
A top-level `const` names a value the compiler already knows. **No symbol is
|
|
1450
|
+
emitted and no initialiser runs** — every use lowers to the value itself.
|
|
1451
|
+
|
|
1452
|
+
- The **type annotation is required**, and so is an initialiser.
|
|
1453
|
+
- The type must be `i32`, `i64`, `f64`, `number`, `boolean`, or `string`. An
|
|
1454
|
+
array, class or interface constant would need an allocation, and there is no
|
|
1455
|
+
code to run it.
|
|
1456
|
+
- The initialiser is literals, other constants (declared anywhere, or
|
|
1457
|
+
imported), and arithmetic over them. A call, a `new`, a local — anything else
|
|
1458
|
+
— is rejected.
|
|
1459
|
+
- Folding uses the language's own arithmetic, so an overflowing fold is an
|
|
1460
|
+
error rather than a wrap, and `1 / 0` in a constant is refused at compile
|
|
1461
|
+
time.
|
|
1462
|
+
|
|
1463
|
+
### Classes and interfaces
|
|
1464
|
+
|
|
1465
|
+
```ts nish:ok
|
|
1466
|
+
interface Shape { width: i32; height: i32; }
|
|
1467
|
+
|
|
1468
|
+
class Rect implements Shape {
|
|
1469
|
+
width: i32;
|
|
1470
|
+
height: i32;
|
|
1471
|
+
label: string = "r";
|
|
1472
|
+
|
|
1473
|
+
constructor(width: i32, height: i32) {
|
|
1474
|
+
this.width = width;
|
|
1475
|
+
this.height = height;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
area(): i32 {
|
|
1479
|
+
return this.width * this.height;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const describe = (s: Shape): i32 => s.width + s.height;
|
|
1484
|
+
|
|
1485
|
+
export const main = (): i32 => {
|
|
1486
|
+
const r = new Rect(3, 4);
|
|
1487
|
+
console.log(`${r.area()} ${describe(r)}`);
|
|
1488
|
+
return 0;
|
|
1489
|
+
};
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
- Fields need annotations; an initializer must be a **literal** of the field's
|
|
1493
|
+
type. Layout is declaration order, exactly as clang lays out the same C
|
|
1494
|
+
struct.
|
|
1495
|
+
- At most one constructor, no parameter properties, no overloads. **Definite
|
|
1496
|
+
assignment** is checked syntactically: after the constructor returns every
|
|
1497
|
+
field holds a value, and reading `this.f` before every field is assigned is
|
|
1498
|
+
an error.
|
|
1499
|
+
- Methods need a body and an explicit return type. `readonly` fields may be
|
|
1500
|
+
assigned only as `this.f = v` in their own class's constructor.
|
|
1501
|
+
- `===` / `!==` on two values of one class is pointer identity; `<` and friends
|
|
1502
|
+
are rejected.
|
|
1503
|
+
- **No inheritance.** `extends` and `super` are rejected in every spelling.
|
|
1504
|
+
- An **interface** is a struct with fields only — no methods, no `extends`, no
|
|
1505
|
+
`new`.
|
|
1506
|
+
- **`class C implements I` requires `I`'s fields to be `C`'s first fields**, in
|
|
1507
|
+
order, with identical types; `C` may declare more after them. A `C` then
|
|
1508
|
+
converts to an `I` wherever one is expected. **This is the only widening in
|
|
1509
|
+
the language**, and nothing converts back — no downcast, no `instanceof`.
|
|
1510
|
+
|
|
1511
|
+
### An array of records is one contiguous block
|
|
1512
|
+
|
|
1513
|
+
`Point[]` where `Point` is a class or interface stores the records themselves,
|
|
1514
|
+
back to back — not an array of pointers. So `ps[i]` is an *interior* pointer,
|
|
1515
|
+
and `push` may move the whole block to grow it. **Holding an element across a
|
|
1516
|
+
`push` on the same array is therefore refused**, because the write would land
|
|
1517
|
+
in the old storage:
|
|
1518
|
+
|
|
1519
|
+
```ts nish:err NL2290
|
|
1520
|
+
interface Point { x: i32; }
|
|
1521
|
+
|
|
1522
|
+
export const main = (): i32 => {
|
|
1523
|
+
const ps: Point[] = [{ x: 1 }];
|
|
1524
|
+
const p = ps[0];
|
|
1525
|
+
ps.push({ x: 2 });
|
|
1526
|
+
p.x = 3;
|
|
1527
|
+
return 0;
|
|
1528
|
+
};
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
Index again after the `push` instead of holding the element across it:
|
|
1532
|
+
|
|
1533
|
+
```ts nish:ok
|
|
1534
|
+
interface Point { x: i32; }
|
|
1535
|
+
|
|
1536
|
+
export const main = (): i32 => {
|
|
1537
|
+
const ps: Point[] = [{ x: 1 }];
|
|
1538
|
+
ps.push({ x: 2 });
|
|
1539
|
+
ps[0].x = 3;
|
|
1540
|
+
console.log(`${ps[0].x}`);
|
|
1541
|
+
return 0;
|
|
1542
|
+
};
|
|
1543
|
+
```
|
|
1544
|
+
|
|
1545
|
+
### Object literals
|
|
1546
|
+
|
|
1547
|
+
An object literal takes its type from context — a variable annotation, the
|
|
1548
|
+
enclosing function's return type, the parameter it is passed to, the field it
|
|
1549
|
+
is assigned to, or a field of an enclosing literal. It must set every field
|
|
1550
|
+
exactly once, with plain identifier keys.
|
|
1551
|
+
|
|
1552
|
+
```ts nish:ok
|
|
1553
|
+
interface Pair { first: i32; second: i32; }
|
|
1554
|
+
|
|
1555
|
+
const swap = (p: Pair): Pair => ({ first: p.second, second: p.first });
|
|
1556
|
+
|
|
1557
|
+
export const main = (): i32 => {
|
|
1558
|
+
const p: Pair = { first: 1, second: 2 };
|
|
1559
|
+
console.log(`${swap(p).first}`);
|
|
1560
|
+
return 0;
|
|
1561
|
+
};
|
|
1562
|
+
```
|
|
1563
|
+
|
|
1564
|
+
The context reaches a nested literal and a `null`, and stops there: a numeric
|
|
1565
|
+
literal in a property keeps the mode's default type (above), and `[]` has no
|
|
1566
|
+
element type at all in that position.
|
|
1567
|
+
|
|
1568
|
+
A class with methods is made only by `new`: a literal typed as one would be a
|
|
1569
|
+
plain object under Node, without the methods (NL2556). A class with fields
|
|
1570
|
+
alone, and an interface, still take a literal.
|
|
1571
|
+
|
|
1572
|
+
```ts nish:err NL2556
|
|
1573
|
+
class Counter {
|
|
1574
|
+
n: i32;
|
|
1575
|
+
constructor(n: i32) {
|
|
1576
|
+
this.n = n;
|
|
1577
|
+
}
|
|
1578
|
+
get(): i32 {
|
|
1579
|
+
return this.n;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
export const main = (): i32 => {
|
|
1584
|
+
const c: Counter = { n: 5 }; // write `new Counter(5)`
|
|
1585
|
+
return c.get();
|
|
1586
|
+
};
|
|
1587
|
+
```
|
|
1588
|
+
|
|
1589
|
+
### Type aliases and enums
|
|
1590
|
+
|
|
1591
|
+
```ts nish:ok
|
|
1592
|
+
type Byte = u8;
|
|
1593
|
+
type Bytes = Byte[];
|
|
1594
|
+
|
|
1595
|
+
enum Kind {
|
|
1596
|
+
If = 1,
|
|
1597
|
+
While = 2,
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const name = (k: Kind): string => {
|
|
1601
|
+
switch (k) {
|
|
1602
|
+
case Kind.If:
|
|
1603
|
+
return "if";
|
|
1604
|
+
default:
|
|
1605
|
+
return "while";
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
|
|
1609
|
+
export const main = (): i32 => {
|
|
1610
|
+
console.log(name(Kind.If));
|
|
1611
|
+
return 0;
|
|
1612
|
+
};
|
|
1613
|
+
```
|
|
1614
|
+
|
|
1615
|
+
- A `type` alias is a second name for an existing type, **not a type of its
|
|
1616
|
+
own** — the same program with and without its aliases emits byte-identical
|
|
1617
|
+
IR. A generic *alias* is still forbidden even though a generic function,
|
|
1618
|
+
class and interface are not; built-in names may not be aliased.
|
|
1619
|
+
- A numeric `enum` is a **distinct type** represented as `i32`, and that is the
|
|
1620
|
+
point: `Kind` and `i32` never convert in either direction. Members must be
|
|
1621
|
+
numeric literals. `===` and `!==` are the **only** operators — no arithmetic,
|
|
1622
|
+
no bitwise, so **bit flags stay `i32` module constants**.
|
|
1623
|
+
- **A member is also a type.** `Kind.While` as an annotation is a slot that
|
|
1624
|
+
holds exactly that member — the shape of a tag field that says which case
|
|
1625
|
+
its object is. Only the member's own constant (or a value already of that
|
|
1626
|
+
member type) enters it; any other member, or a `Kind` value, is NL2519. A
|
|
1627
|
+
member type converts to its enum, and a read of one *is* the enum, so it
|
|
1628
|
+
compares, switches and passes as a `Kind`. The IR is the enum's `i32`. A
|
|
1629
|
+
generic call takes the member's constant where the member type is wanted
|
|
1630
|
+
(`fold(xs, Kind.Array)` with `xs: Kind.Array[]`), parenthesised or not.
|
|
1631
|
+
It binds a type parameter only from the arguments, so `id(Kind.Array)`
|
|
1632
|
+
into a `Kind.Array` slot, and `apply(take, Kind.Array)` with
|
|
1633
|
+
`take: (x: Kind.Array) => Kind.Array`, bind `T` to `Kind` and are refused:
|
|
1634
|
+
pass a value already of the member type instead. An unannotated `let`
|
|
1635
|
+
keeps the member type of a value already of it, and widens the member's
|
|
1636
|
+
constant to `Kind`, as `tsc` does; so do a ternary, an array literal, `??`
|
|
1637
|
+
and an arrow's result (`[a]` is a `Kind.Array[]`, and so is
|
|
1638
|
+
`map([a], (k) => k)`; `(k) => Kind.Array` answers `Kind`). Arms or elements
|
|
1639
|
+
that differ where one is a member type or an array of one (`c ? a : b`,
|
|
1640
|
+
`c ? a : k`, `[a, Kind.Array]`, `[[a], [Kind.Array]]`) are refused:
|
|
1641
|
+
annotate the variable.
|
|
1642
|
+
`new Array<Kind.Array>(n)` zero-fills, so it is NL2520 unless the member is
|
|
1643
|
+
0; build such an array with `[]` and `push`. A `parallelMap`,
|
|
1644
|
+
`parallelFrom` or `parallelReduce` body answering one is NL2521: answer
|
|
1645
|
+
`Kind` instead.
|
|
1646
|
+
|
|
1647
|
+
```ts nish:ok
|
|
1648
|
+
enum Kind {
|
|
1649
|
+
Scalar,
|
|
1650
|
+
Array,
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
class ArrayNode {
|
|
1654
|
+
readonly kind: Kind.Array = Kind.Array;
|
|
1655
|
+
size: i32 = 0;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
const isArray = (k: Kind): boolean => k === Kind.Array;
|
|
1659
|
+
|
|
1660
|
+
export const main = (): i32 => {
|
|
1661
|
+
const n = new ArrayNode();
|
|
1662
|
+
return isArray(n.kind) ? 0 : 1;
|
|
1663
|
+
};
|
|
1664
|
+
```
|
|
1665
|
+
|
|
1666
|
+
```ts nish:err NL2519
|
|
1667
|
+
enum Kind {
|
|
1668
|
+
Scalar,
|
|
1669
|
+
Array,
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
const pick = (k: Kind): Kind.Array => k; // a `Kind` could be any member
|
|
1673
|
+
|
|
1674
|
+
export const main = (): i32 => (pick(Kind.Array) === Kind.Array ? 0 : 1);
|
|
1675
|
+
```
|
|
1676
|
+
|
|
1677
|
+
- **Both can be exported.** `export enum Kind` is imported like a function,
|
|
1678
|
+
`import { Kind } from "./kinds"` (renamed with `as` if you like), and in the
|
|
1679
|
+
importer it *is* the exporter's `Kind`: the members fold to the exporter's
|
|
1680
|
+
integers and a `Kind` value crosses a call as itself. A `Kind` a second
|
|
1681
|
+
module declares for itself is a different type, however alike the two are.
|
|
1682
|
+
`export type Conn = Socket | null` is imported the same way and is, in the
|
|
1683
|
+
importer, the type it names; its right-hand side is resolved in the module
|
|
1684
|
+
that wrote it, so the importer need not import `Socket`. A module does not
|
|
1685
|
+
re-export an enum or alias it imported.
|
|
1686
|
+
- Both are top-level only.
|
|
1687
|
+
|
|
1688
|
+
### Discriminated unions
|
|
1689
|
+
|
|
1690
|
+
```ts nish:ok
|
|
1691
|
+
enum Kind {
|
|
1692
|
+
Circle,
|
|
1693
|
+
Square,
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
class Circle {
|
|
1697
|
+
readonly kind: Kind.Circle = Kind.Circle;
|
|
1698
|
+
r: i32 = 3;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
class Square {
|
|
1702
|
+
readonly kind: Kind.Square = Kind.Square;
|
|
1703
|
+
side: i32 = 4;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
type Shape = Circle | Square;
|
|
1707
|
+
|
|
1708
|
+
const pick = (round: boolean): Shape => (round ? new Circle() : new Square());
|
|
1709
|
+
|
|
1710
|
+
export const main = (): i32 => {
|
|
1711
|
+
const shapes: Shape[] = [pick(true), new Square()];
|
|
1712
|
+
let circles = 0;
|
|
1713
|
+
let area = 0;
|
|
1714
|
+
for (const s of shapes) {
|
|
1715
|
+
if (s.kind === Kind.Circle) {
|
|
1716
|
+
circles = circles + 1; // `s` is a `Circle` here
|
|
1717
|
+
area = area + 3 * s.r * s.r;
|
|
1718
|
+
} else {
|
|
1719
|
+
area = area + s.side * s.side; // and a `Square` here
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return circles === 1 && area === 43 ? 0 : 1;
|
|
1723
|
+
};
|
|
1724
|
+
```
|
|
1725
|
+
|
|
1726
|
+
- A **variant** is a class or interface, not generic, whose **first field is
|
|
1727
|
+
its tag**, of one enum member type: `readonly kind: Kind.Circle =
|
|
1728
|
+
Kind.Circle`. All variants of a union take their tags from one enum, under
|
|
1729
|
+
one field name, with different members; a union names at least two. Each
|
|
1730
|
+
broken rule has its code (NL2522–NL2527); a tag that is not `readonly` is
|
|
1731
|
+
NL2535, one typed as the whole enum (`readonly kind: Kind`) NL2559. Any
|
|
1732
|
+
other union is still NL1053.
|
|
1733
|
+
- A union is a local, parameter, field, return, array element, or nullable
|
|
1734
|
+
(`Shape | null`). An alias may name another union, which is flattened. A
|
|
1735
|
+
variant, or a smaller union, converts into it wherever the union is
|
|
1736
|
+
expected — argument, `return`, `push`, an annotated array literal, a
|
|
1737
|
+
ternary arm or `??` default whose other side is the union. A class that
|
|
1738
|
+
implements an interface variant converts to that interface first. A union
|
|
1739
|
+
crosses modules but not a sidecar (NL4009).
|
|
1740
|
+
- **Test the tag to read a variant.** Before a test, the tag is the only
|
|
1741
|
+
member (NL2528 for anything else, even a field every variant has). A test of
|
|
1742
|
+
a local's or parameter's tag narrows it as `tsc` does: `if`/`else`, a
|
|
1743
|
+
ternary, `&&`, `||`, `!==`, an early `return`/`break`/`continue`, each
|
|
1744
|
+
`case` of a `switch (s.kind)`, and composed with `s !== null &&`. Two or
|
|
1745
|
+
more variants left still give only the tag; an assignment drops the
|
|
1746
|
+
narrowing. Where `a || b` held (or `a && b` failed), the variable is what
|
|
1747
|
+
either side proved, `Circle | Square` after two tag tests. A test reads
|
|
1748
|
+
through an assignment: `(t = next()).kind === Kind.Square` narrows `t`. Past an `if`, the
|
|
1749
|
+
variable is what the arms that fall through left it (an `else if` chain
|
|
1750
|
+
that returns leaves the variant no test named), including what a test nested
|
|
1751
|
+
in each arm proved: two arms that each return when `s.kind === Kind.A` leave
|
|
1752
|
+
`s` not a `VA`; a `switch`'s exits join the same way. A path does not narrow the
|
|
1753
|
+
union (`h.s.kind` then `h.s.r` is NL2558): copy it into a local and test
|
|
1754
|
+
that. The tag read through a path (`h.k`, `this.k`, `h.s.kind`,
|
|
1755
|
+
`xs[0].kind`, `xs[i].kind`) does stay narrowed for later comparisons, until a store to
|
|
1756
|
+
the path, a prefix of it or its root. An index `i` is part of the path when
|
|
1757
|
+
it holds one value: a parameter, a `const` (`const i = 0` makes `xs[i]` the
|
|
1758
|
+
path `xs[0]`, and so does `const j = i` after it), a module constant
|
|
1759
|
+
(`const IDX: i32 = 0` makes `xs[IDX]` the path `xs[0]`), or a `let` nothing
|
|
1760
|
+
assigns; through a `let` that is assigned
|
|
1761
|
+
or stepped, `xs[i]` is not narrowed, and `xs[j] = t` drops nothing. A read where every variant was excluded is
|
|
1762
|
+
NL2557.
|
|
1763
|
+
- The tag reads as the enum: `const k: Kind = s.kind` — annotate it,
|
|
1764
|
+
`const k = s.kind` is NL2530. `k` holds only the variants' members. A
|
|
1765
|
+
union is read-only, even for a field every variant has (NL2529). Comparing
|
|
1766
|
+
the tag (or such a `k`) with a member it cannot hold — one no variant has,
|
|
1767
|
+
or one a test already excluded — is NL2531, a `case` for one NL2532.
|
|
1768
|
+
- A `switch` whose subject reads a variant's tag field (`s.kind`,
|
|
1769
|
+
`h.shape.kind`, `xs[i].kind`, the receiver typed as the union) and whose
|
|
1770
|
+
labels name every variant, with no `default`, is complete: if every
|
|
1771
|
+
clause returns, it ends a function that returns a value, and its unmatched
|
|
1772
|
+
case is `unreachable` in the IR. A statement after it is unreachable code
|
|
1773
|
+
when every clause returns; a clause's `break` reaches it.
|
|
1774
|
+
With a variant unnamed it is the missing-return error, as TS2366. A
|
|
1775
|
+
`switch (k)` on an enum variable (a parameter, or `const k: Kind = s.kind`,
|
|
1776
|
+
`let` or `const`), or on an enum field assigned from a tag
|
|
1777
|
+
(`b.k = s.kind`), is never complete, whatever its labels
|
|
1778
|
+
(C may pass an enum any `i32`): give it a `default` or return after it.
|
|
1779
|
+
A path's tag a test narrowed (`h.s.kind` after
|
|
1780
|
+
`if (h.s.kind === Kind.A) return`) needs every tag of the whole union; a
|
|
1781
|
+
narrowed union local is complete with the variants left to it.
|
|
1782
|
+
- An enum variable, a parameter `k: Kind` as much as such a tag local, narrows
|
|
1783
|
+
on `k === Kind.A` / `!==` in the same forms as a union, `switch (k)`
|
|
1784
|
+
included: after `if (k === Kind.A) return`, `k === Kind.A` is NL2531.
|
|
1785
|
+
Narrowed to one member, `let j = k` is a `Kind.A`; to several,
|
|
1786
|
+
`const j = k` is NL2530 (annotate it `Kind`). `s.kind === k` narrows `s`
|
|
1787
|
+
to the variants of `k`'s members where they are equal. An enum local an
|
|
1788
|
+
`if`, ternary, loop or `switch` stores into holds, after it, what it held
|
|
1789
|
+
or any member stored; after a `switch`, what its exits left (each `break`,
|
|
1790
|
+
the last clause's end, and the unmatched case when there is no `default`);
|
|
1791
|
+
after a loop, what its condition leaves false at the head, joined until it
|
|
1792
|
+
no longer changes, and each `break`, as `tsc` reads it — a variable the
|
|
1793
|
+
loop only tests included, and an `if` or `switch` inside the loop narrowing
|
|
1794
|
+
as it does outside; a `for` update is read after the body. A loop holding
|
|
1795
|
+
a path (`h.k`), a store inside an expression (an argument, an initializer,
|
|
1796
|
+
a chain's inner `=`), a store of a field or method call read through an
|
|
1797
|
+
element, a call or a receiver the loop declares (`gs[0].b`,
|
|
1798
|
+
`makeG().b`), of a `let` the loop declares or of a `const` it declares
|
|
1799
|
+
from a tracked variable (a swap's `const t: Kind = a`), a ternary arm
|
|
1800
|
+
reading the stored variable where it holds no member
|
|
1801
|
+
(`k = k === Kind.A ? Kind.B : k`), or a type of more than 29 members
|
|
1802
|
+
falls back to every member the body stored
|
|
1803
|
+
anywhere or the whole type, which accepts a few comparisons `tsc` refuses
|
|
1804
|
+
(TS2367; LANGUAGE.md, "Accepted here, refused by `tsc`").
|
|
1805
|
+
- **Refused by design:** union type arguments (`Box<Shape>`,
|
|
1806
|
+
`Map<K, Shape>`, `Result<Shape, E>`, a generic call binding `T` to a
|
|
1807
|
+
union: NL2533), unions as `Map`/`Set` keys, covariant arrays (`Circle[]`
|
|
1808
|
+
is not `Shape[]`), and inferred unions (an unannotated ternary or array of
|
|
1809
|
+
two variants).
|
|
1810
|
+
- **Declare what you store.** A store of a value that is only some of the
|
|
1811
|
+
variants into a union slot is NL2534, because `tsc` would narrow the slot
|
|
1812
|
+
from there on. Declare the slot as the variant, or store a value of the
|
|
1813
|
+
whole union (a call returning `Shape`):
|
|
1814
|
+
|
|
1815
|
+
```ts nish:err NL2534
|
|
1816
|
+
enum Kind {
|
|
1817
|
+
Circle,
|
|
1818
|
+
Square,
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
class Circle {
|
|
1822
|
+
readonly kind: Kind.Circle = Kind.Circle;
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
class Square {
|
|
1826
|
+
readonly kind: Kind.Square = Kind.Square;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
type Shape = Circle | Square;
|
|
1830
|
+
|
|
1831
|
+
export const main = (): i32 => {
|
|
1832
|
+
const s: Shape = new Circle(); // tsc treats `s` as a Circle from here
|
|
1833
|
+
return s.kind === Kind.Circle ? 0 : 1;
|
|
1834
|
+
};
|
|
1835
|
+
```
|
|
1836
|
+
|
|
1837
|
+
### Modules
|
|
1838
|
+
|
|
1839
|
+
- `export` goes on `const` (function or constant), `class`, `interface`,
|
|
1840
|
+
`enum` and `type` declarations. No `export default`, no `export { … }`, no
|
|
1841
|
+
`export *`, no `export =`: each is refused by its own rule, once.
|
|
1842
|
+
- The only import form is a **named import**:
|
|
1843
|
+
`import { square, cube as pow3 } from "./math"`. `.ts` is optional. Default
|
|
1844
|
+
imports, namespace imports, side-effect imports, type-only imports,
|
|
1845
|
+
`import x = require(…)`, import attributes (`with { … }`) and
|
|
1846
|
+
`import defer` are all rejected, each by its own rule and before anything
|
|
1847
|
+
is loaded. `type`, `as` and `from` are still names: `import { type } from`
|
|
1848
|
+
imports a function called `type`.
|
|
1849
|
+
- A specifier is a relative path (`./x`, `../x`), a builtin module (`nish:fs`),
|
|
1850
|
+
a standard-library module (`nish/text`, `nish/pair`), or a **package name** (`hash`,
|
|
1851
|
+
`@scope/hash`, with a subpath after it if you want one).
|
|
1852
|
+
- **A package is resolved through `node_modules` and compiled from source.**
|
|
1853
|
+
The package's `package.json` must offer the file under the `nish` export
|
|
1854
|
+
condition — `{"exports": {".": {"nish": "./src/index.ts"}}}` — and the mode
|
|
1855
|
+
gets a spelling of its own, `nish-i32` / `nish-f64`, for source that is only
|
|
1856
|
+
correct under one `--number-mode`. The mode-qualified condition wins wherever
|
|
1857
|
+
the package declares it, so a manifest carrying both never compiles the wrong
|
|
1858
|
+
one of the two. A package without that condition is
|
|
1859
|
+
`` Package `lodash` has no Nish entry point ``, which is what an ordinary npm
|
|
1860
|
+
package gets: there is nothing to compile in a `.js` file. One that offers
|
|
1861
|
+
only the other mode is `` supports Nish in f64 mode only ``, one whose
|
|
1862
|
+
`"engines": {"nish": ">=X.Y.Z"}` is above this compiler `` needs a newer
|
|
1863
|
+
compiler ``, and a `package.json` that is not JSON names its line and
|
|
1864
|
+
column — each with its own `--json` code (`NL3017`–`NL3021`).
|
|
1865
|
+
- Functions and enums may be renamed on import; classes and interfaces may
|
|
1866
|
+
not — the type name is part of the ABI.
|
|
1867
|
+
- Import cycles are allowed; a shared dependency is compiled once.
|
|
1868
|
+
- **A function name is unique across the whole program**, exported or not.
|
|
1869
|
+
- An `export`ed function is an external C-ABI symbol; every other function gets
|
|
1870
|
+
`internal` linkage so LLVM may inline or drop it.
|
|
1871
|
+
- **So an exported function is not named after a C library function**
|
|
1872
|
+
(`write`, `read`, `free`, `exit`, `strlen`, `sin`, …, the list in
|
|
1873
|
+
`src/c-symbols.ts`) and does not begin with `_`, `napi_` or `node_api_`: its symbol would
|
|
1874
|
+
replace the C one in the linked program, in a native, a wasm or an N-API
|
|
1875
|
+
build alike. `` `write` is the C library's `write`: an exported function of the program's own package keeps its name as its symbol, so the references the C library and the runtime make to that name would reach this function instead; rename it ``
|
|
1876
|
+
(NL3037). Name it `writeLine`, or leave it private: a private function with
|
|
1877
|
+
such a name gets a symbol ending in `$`.
|
|
1878
|
+
- **Nor, in a wasm build, `memory`**: a wasm `--target`, `--profile wasi` or
|
|
1879
|
+
`--emit-dts` refuses it, because the `--profile wasm` module exports its
|
|
1880
|
+
linear memory under that name; wasi reserves it too, so a program that
|
|
1881
|
+
builds for wasi also builds as a freestanding wasm module. `` `memory` is the name a wasm module exports its linear memory under: an exported function of the program's own package keeps its name as its symbol, so the module would export two things named `memory` and no host could load it; rename it ``
|
|
1882
|
+
(NL3038). A native build keeps it.
|
|
1883
|
+
|
|
1884
|
+
## Statements
|
|
1885
|
+
|
|
1886
|
+
- **Conditions must be `boolean`.** There is no truthiness, in `if`, `while`,
|
|
1887
|
+
`for`, the ternary, `&&` or `||`.
|
|
1888
|
+
- `let` / `const` need an initializer and take their type from it; an
|
|
1889
|
+
annotation must match **exactly**. `const` freezes the binding, not the
|
|
1890
|
+
contents: `xs[0] = 1` and `xs.push(1)` on a `const xs` are fine.
|
|
1891
|
+
- `var` is forbidden. Destructuring is not supported.
|
|
1892
|
+
- **Semicolons are optional**, where TypeScript would insert one: at a line
|
|
1893
|
+
break, before `}` and at the end of the file. It is TypeScript's rule, so a
|
|
1894
|
+
line starting with `(` or `[` continues the one before it, and `return`
|
|
1895
|
+
followed by a line break returns nothing. Here, the value left on the next
|
|
1896
|
+
line is an unreachable-code error rather than a silent bug.
|
|
1897
|
+
- `for (init; cond; update)` with every clause optional, and
|
|
1898
|
+
`for (const x of xs)` over an **array** only — `x` gets the element type and
|
|
1899
|
+
must not be annotated. The array's `length` is re-read each iteration, so a
|
|
1900
|
+
`push` inside the body extends the loop. `for...in`, `for await` and
|
|
1901
|
+
`for (x of xs)` over an existing `x` are each refused by name.
|
|
1902
|
+
- `switch` takes an **integer or enum** discriminant; every `case` label is an
|
|
1903
|
+
integer constant expression of that type. **There is no implicit
|
|
1904
|
+
fallthrough** — a clause with statements ends in `break`, `return`,
|
|
1905
|
+
`continue` or `process.exit`, unless it is the last. An *empty* clause does
|
|
1906
|
+
fall through, which is how `case 1: case 2:` shares a body. A clause cannot
|
|
1907
|
+
declare a variable directly; wrap its body in a block.
|
|
1908
|
+
- `break` / `continue` are unlabelled only; labeled statements are forbidden.
|
|
1909
|
+
- `process.exit(code)` and `panic(msg)` **terminate control flow**, so a
|
|
1910
|
+
non-`void` function may end with either.
|
|
1911
|
+
- **Unreachable code is an error**, not a warning: anything after a `return`,
|
|
1912
|
+
`break`, `continue`, `process.exit`, an `if` whose branches all return, or an
|
|
1913
|
+
infinite loop.
|
|
1914
|
+
|
|
1915
|
+
```ts nish:err NL2166
|
|
1916
|
+
const classify = (n: i32): string => {
|
|
1917
|
+
switch (n) {
|
|
1918
|
+
case 0:
|
|
1919
|
+
console.log("zero"); // no `break`: a clause with statements must end in one
|
|
1920
|
+
case 1:
|
|
1921
|
+
return "one";
|
|
1922
|
+
default:
|
|
1923
|
+
return "many";
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
|
|
1927
|
+
export const main = (): i32 => {
|
|
1928
|
+
console.log(classify(0));
|
|
1929
|
+
return 0;
|
|
1930
|
+
};
|
|
1931
|
+
```
|
|
1932
|
+
|
|
1933
|
+
## Expressions
|
|
1934
|
+
|
|
1935
|
+
- Arithmetic `+ - * / %` takes **two numbers of one type**. `+` on two strings
|
|
1936
|
+
concatenates; `string + number` is rejected (use a template literal).
|
|
1937
|
+
- **Integer division is checked**, Rust-style: a zero divisor, or `MIN / -1`,
|
|
1938
|
+
prints to stderr and exits 1. Float division is never checked.
|
|
1939
|
+
- Bitwise `& | ^ ~` and shifts `<< >> >>>` take **integers of one type**.
|
|
1940
|
+
`>>` is arithmetic on a signed type and logical on an unsigned one; the shift
|
|
1941
|
+
count is masked to the operand width.
|
|
1942
|
+
- `=== !==` on any two values of the same type: integers and booleans by value,
|
|
1943
|
+
`f64` by `fcmp` (so `x !== x` is true for `NaN`), **strings by content**,
|
|
1944
|
+
arrays and objects by **identity**.
|
|
1945
|
+
- `< <= > >=` on two numbers of one type and nothing else — not booleans, not
|
|
1946
|
+
strings, not structs.
|
|
1947
|
+
- `++` / `--` work on **numeric mutable locals only**, not fields or elements.
|
|
1948
|
+
- Compound assignment `+= -= *= /= %=` requires a numeric target (so `+=` never
|
|
1949
|
+
concatenates strings); the bitwise forms need an integer target.
|
|
1950
|
+
- **Forbidden**: `,` `in` `instanceof` `typeof` `delete` `void expr` `==`
|
|
1951
|
+
`!=` `**` unary `+`, `?.[i]` and `f?.()`, `?.` outside a chain that ends in
|
|
1952
|
+
`??`, and `??` on anything but a `T | null`, a `?.` chain or a `Map.get`
|
|
1953
|
+
result.
|
|
1954
|
+
- Element access `a[i]` needs an array and a numeric index, and is
|
|
1955
|
+
bounds-checked (negative indices fail too). String-keyed access is forbidden.
|
|
1956
|
+
The check comes off where the compiler proves the index in range — a loop
|
|
1957
|
+
condition `i < xs.length`, or `i < h.xs.length` on a field path — as long as
|
|
1958
|
+
nothing between the test and the access calls a function, stores to a field
|
|
1959
|
+
the path names, or reassigns its root.
|
|
1960
|
+
- Array literals need one element type; `[]` needs a contextual `T[]`. Holes
|
|
1961
|
+
and spread are not supported.
|
|
1962
|
+
- Template literals accept holes of type `string`, `number`, `i64`, `f64` or
|
|
1963
|
+
`boolean` — this is how you convert a number to a string.
|
|
1964
|
+
|
|
1965
|
+
## The builtins, in full
|
|
1966
|
+
|
|
1967
|
+
No import is needed; these are resolved by name, and a user function of the
|
|
1968
|
+
same bare name shadows the builtin. **This list is exhaustive** — anything not
|
|
1969
|
+
on it does not exist, and inventing a method is the most common way to write a
|
|
1970
|
+
program that does not compile.
|
|
1971
|
+
|
|
1972
|
+
**Output.** `console.log(x)` and `console.error(x)` take exactly one argument
|
|
1973
|
+
of `string | number | i64 | f64 | boolean` and are **statement position only**.
|
|
1974
|
+
`write(s)` / `writeError(s)` take a `string` and add no newline.
|
|
1975
|
+
`panic(message)` writes to stderr and exits 1.
|
|
1976
|
+
|
|
1977
|
+
**`Math`.** `sqrt` `floor` `ceil` `trunc` `sin` `cos` `exp` `log` (all `f64`
|
|
1978
|
+
argument, `f64` result), `pow(x, y)`, `round(x)`, `abs(x)` (any numeric type),
|
|
1979
|
+
`min(a, b)` / `max(a, b)` (exactly two, one type), `clz32(x)` (an `i32` or `u32` in,
|
|
1980
|
+
the leading zero bits out, 0 to 32), `random()`, and the
|
|
1981
|
+
constants `Math.PI` / `Math.E`. The f64-only ones reject an `i32`: write
|
|
1982
|
+
`Math.sqrt(toF64(n))`.
|
|
1983
|
+
|
|
1984
|
+
**Conversions.** `toI32` `toI64` `toU8` `toU16` `toU32` `toU64` `toF32`
|
|
1985
|
+
`toF64`; `f64ToBits(x)` / `bitsToF64(b)` reinterpret rather than convert;
|
|
1986
|
+
`Number(x)`, `parseInt(s)` (base 10, `i32`, no `NaN` — no digits give `0`),
|
|
1987
|
+
`parseFloat(s)`.
|
|
1988
|
+
|
|
1989
|
+
**Constant time.** `ctSelect(mask, a, b)` is `(a & mask) | (b & ~mask)` and
|
|
1990
|
+
`ctEq(a, b)` is all-ones when equal and zero otherwise, both over `u32` or
|
|
1991
|
+
`u64` only, every operand one type. The mask sits behind an optimisation
|
|
1992
|
+
barrier, so neither ever becomes a branch; select and compare on a secret with
|
|
1993
|
+
these, never with `if`, `?:`, `===` or `table[secret]`, which they cannot fix.
|
|
1994
|
+
`secureZero(bytes)` sets every byte of a `u8[]` to zero in stores the optimiser
|
|
1995
|
+
may not drop, as a statement: how a key in a plain array is cleared once used.
|
|
1996
|
+
|
|
1997
|
+
**Secrets.** Hold a key as a `Secret<T>` from `nish:secret` (`T` an integer
|
|
1998
|
+
array or a record of integer fields). It is opaque: no printing, interpolating,
|
|
1999
|
+
comparing with a value, branching, indexing, `.value`, array element or
|
|
2000
|
+
`Result` payload, a field other than a holder's `readonly` one (below), and no
|
|
2001
|
+
builtin takes one. Read it only with `expose(s, f)` or
|
|
2002
|
+
`exposeWith(s, arg, f)`, where `f` is a top-level function or an arrow that
|
|
2003
|
+
declares its return type, reaches no I/O and no C, keeps nothing of the value
|
|
2004
|
+
and returns no `Secret` — that return is what leaves. A `Secret` your function
|
|
2005
|
+
makes must be `wipe`d or returned **on every path** (each `return`, `break`,
|
|
2006
|
+
`orReturn()`), is moved by `const j = k`, and is never read after `wipe`;
|
|
2007
|
+
`secret(local)` moves the local too. `secret(v)` takes only a value nothing
|
|
2008
|
+
else holds (NL2512): a literal, a `new`, a call that makes one, or a local that
|
|
2009
|
+
is the only name for its value. A second name made before the move — `const
|
|
2010
|
+
alias = raw`, a field or element store, `push`, `Ok(raw)`, a ternary arm, a
|
|
2011
|
+
function that keeps its parameter, a data-parallel arrow that captures it — is
|
|
2012
|
+
refused with where it was made; pass a fresh value, or a copy
|
|
2013
|
+
(`const copy: u8[] = new Array<u8>(raw.length); copy.set(raw)`, then
|
|
2014
|
+
`secret(copy)`). `std/crypto`'s `p256`, `x25519` and
|
|
2015
|
+
`x509ParseP256PrivateKey` take and give keys this way.
|
|
2016
|
+
|
|
2017
|
+
```ts nish:ok
|
|
2018
|
+
import { Secret, expose, secret, wipe } from "nish:secret";
|
|
2019
|
+
|
|
2020
|
+
const key = (): u8[] => [7, 1, 9];
|
|
2021
|
+
const width = (k: u8[]): i32 => toI32(k.length);
|
|
2022
|
+
|
|
2023
|
+
export const main = (): i32 => {
|
|
2024
|
+
const k: Secret<u8[]> = secret(key());
|
|
2025
|
+
const n: i32 = expose(k, width);
|
|
2026
|
+
wipe(k);
|
|
2027
|
+
return n === 3 ? 0 : 1;
|
|
2028
|
+
};
|
|
2029
|
+
```
|
|
2030
|
+
|
|
2031
|
+
```ts nish:err NL2440
|
|
2032
|
+
import { Secret, expose, secret } from "nish:secret";
|
|
2033
|
+
|
|
2034
|
+
const key = (): u8[] => [7, 1, 9];
|
|
2035
|
+
const width = (k: u8[]): i32 => toI32(k.length);
|
|
2036
|
+
|
|
2037
|
+
export const main = (): i32 => {
|
|
2038
|
+
const k: Secret<u8[]> = secret(key());
|
|
2039
|
+
return expose(k, width); // NL2440: `k` leaves unwiped
|
|
2040
|
+
};
|
|
2041
|
+
```
|
|
2042
|
+
|
|
2043
|
+
```ts nish:err NL2512
|
|
2044
|
+
import { Secret, secret, wipe } from "nish:secret";
|
|
2045
|
+
|
|
2046
|
+
export const main = (): i32 => {
|
|
2047
|
+
const raw: u8[] = [7, 1, 9];
|
|
2048
|
+
const alias = raw; // a second name for the bytes
|
|
2049
|
+
const k: Secret<u8[]> = secret(raw); // NL2512: `raw` has a second reference
|
|
2050
|
+
wipe(k);
|
|
2051
|
+
return toI32(alias[0]);
|
|
2052
|
+
};
|
|
2053
|
+
```
|
|
2054
|
+
|
|
2055
|
+
**A key kept in an object.** A class keeps a key as long as it lives in a
|
|
2056
|
+
`readonly` field of exactly `Secret<T>` (the class not generic). Set it once,
|
|
2057
|
+
in the constructor, with `this.k = secret(fresh)`; rewrite it only in place
|
|
2058
|
+
through `exposeWith`; read `this.k` only as a call or `new` argument (`expose`,
|
|
2059
|
+
`exposeWith`, a function taking a `Secret`; `wipe` only in the holder's own `wipe()`) or a `===`/`!==` operand —
|
|
2060
|
+
never bind, return or store it. The class declares `wipe(): void`, no
|
|
2061
|
+
parameters, calling `wipe(this.k)` itself for every such field on every path
|
|
2062
|
+
(NL2540, NL2541); a field is wiped nowhere else (NL2547). A function taking a
|
|
2063
|
+
`Secret` borrows it: it neither returns it (NL2546) nor wipes it (NL2548). Nothing checks that `wipe()` is called: call it when the
|
|
2064
|
+
holder is done.
|
|
2065
|
+
|
|
2066
|
+
```ts nish:ok
|
|
2067
|
+
import { Secret, expose, secret, wipe } from "nish:secret";
|
|
2068
|
+
|
|
2069
|
+
const width = (k: u8[]): i32 => toI32(k.length);
|
|
2070
|
+
|
|
2071
|
+
class Session {
|
|
2072
|
+
readonly key: Secret<u8[]>;
|
|
2073
|
+
constructor() {
|
|
2074
|
+
this.key = secret(new Array<u8>(32));
|
|
2075
|
+
}
|
|
2076
|
+
size(): i32 {
|
|
2077
|
+
return expose(this.key, width);
|
|
2078
|
+
}
|
|
2079
|
+
wipe(): void {
|
|
2080
|
+
wipe(this.key);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
export const main = (): i32 => {
|
|
2085
|
+
const s = new Session();
|
|
2086
|
+
const n: i32 = s.size();
|
|
2087
|
+
s.wipe();
|
|
2088
|
+
return n === 32 ? 0 : 1;
|
|
2089
|
+
};
|
|
2090
|
+
```
|
|
2091
|
+
|
|
2092
|
+
```ts nish:err NL2544
|
|
2093
|
+
import { Secret, secret, wipe } from "nish:secret";
|
|
2094
|
+
|
|
2095
|
+
class Session {
|
|
2096
|
+
readonly key: Secret<u8[]>;
|
|
2097
|
+
constructor() {
|
|
2098
|
+
this.key = secret(new Array<u8>(32));
|
|
2099
|
+
}
|
|
2100
|
+
wipe(): void {
|
|
2101
|
+
wipe(this.key);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
export const main = (): i32 => {
|
|
2106
|
+
const s = new Session();
|
|
2107
|
+
const k = s.key; // NL2544: a second owner of the key
|
|
2108
|
+
s.wipe();
|
|
2109
|
+
return 0;
|
|
2110
|
+
};
|
|
2111
|
+
```
|
|
2112
|
+
|
|
2113
|
+
**Arrays.** `a.length` (read-only), `a.push(v)`, `a.pop()` (panics when empty —
|
|
2114
|
+
there is no `undefined` to return), `a.indexOf(v)`, `a.join(sep)` — **`join` is
|
|
2115
|
+
`string[]` only** — and, on an array of numbers only, `dst.set(src[, offset])`
|
|
2116
|
+
(one `memmove`; a range past the end panics) and `a.fill(v[, start[, end]])`
|
|
2117
|
+
(ends clamped and counted back from the end when negative, as in JavaScript;
|
|
2118
|
+
never panics). Both are statements. That is every array method there is. A
|
|
2119
|
+
window into a buffer is `(buf, offset, length)`; there is no view type.
|
|
2120
|
+
|
|
2121
|
+
**Strings.** `s.length` (**bytes**), `s.charCodeAt(i)` (the byte, bounds-checked),
|
|
2122
|
+
`s.substring(start[, end])`, `s.slice(start[, end])`, `s.indexOf(sub)`,
|
|
2123
|
+
`s.startsWith(sub)`, `s.endsWith(sub)`, `String.fromCharCode(c)`, and
|
|
2124
|
+
`String.fromBytes(bytes, start, end)` (a `u8[]` range as one string, one
|
|
2125
|
+
allocation; the range panics outside `0 <= start <= end <= bytes.length`; build
|
|
2126
|
+
text from bytes with it, never a `fromCharCode` per byte). That is every string
|
|
2127
|
+
method there is. `substring` **clamps** an out-of-range offset the
|
|
2128
|
+
way JavaScript does; `slice` instead **panics** unless
|
|
2129
|
+
`0 <= start <= end <= s.length`, and is the faster of the two when you have
|
|
2130
|
+
already established the range. Every offset is a **byte** offset, so cutting a multi-byte character
|
|
2131
|
+
in half is possible. To count characters rather than bytes, `codePointLength(s)`
|
|
2132
|
+
from `nish/text` counts code points natively and, for text written in the
|
|
2133
|
+
source, under Node alike; under Node a string built from bytes (`String.fromBytes`,
|
|
2134
|
+
a `\u` escape `nish/json` decodes) is one unit per byte, and counts its bytes.
|
|
2135
|
+
|
|
2136
|
+
**Process.** `process.exit(code)`, `process.argv` (a read-only `string[]`;
|
|
2137
|
+
`argv[0]` is the program path, one index earlier than Node),
|
|
2138
|
+
`process.platform`, `process.arch`.
|
|
2139
|
+
|
|
2140
|
+
**Files and the system.** `readFileSync(path)` (exits 1 on failure),
|
|
2141
|
+
`readFileSyncOrNull(path)` (`string | null`), `readFileBytesSync(path)`
|
|
2142
|
+
(`u8[] | null`, the bytes as they are on disk), `writeFileSync(path, data)`,
|
|
2143
|
+
`appendFileSync(path, data)`, `mkdirSync(path)` (one level, `boolean`),
|
|
2144
|
+
`isDirectorySync(path)`, `readdirSync(path)` (`string[] | null`, sorted by
|
|
2145
|
+
bytes, no `.`/`..`), `spawnSync(argv)`, `spawnSyncTo(argv, outPath, errPath)`,
|
|
2146
|
+
`getenv(name)` (`string | null` — unset and empty are different answers),
|
|
2147
|
+
`realpathSync(path)` (`string | null`; symbolic links resolved, absolute, and
|
|
2148
|
+
`null` when it does not resolve),
|
|
2149
|
+
`monotonicNanos()` (`i64`; elapsed time only), and, for checking a path before
|
|
2150
|
+
trusting it, `lstatOwnerModeSync(path)` (an `i64`: the owner's uid in the high
|
|
2151
|
+
32 bits, `st_mode` in the low 32, `-1` when it does not resolve; a symbolic
|
|
2152
|
+
link answers for itself), `geteuid()` (`i64`) and `isExecutableSync(path)`
|
|
2153
|
+
(`boolean`).
|
|
2154
|
+
|
|
2155
|
+
**The host.** `Date.now()` (an `f64` of whole milliseconds, the wall clock —
|
|
2156
|
+
**the only `Date` there is**: `new Date()`, `Date.parse` and the rest are
|
|
2157
|
+
refused), `crypto.getRandomValues(bytes)` (a statement over a `u8[]` only, at
|
|
2158
|
+
most 65,536 bytes a call, from the kernel's CSPRNG; more panics),
|
|
2159
|
+
`statMtimeSync(path)` (an `f64` of milliseconds, **NaN** — not `null` — when the
|
|
2160
|
+
path cannot be stat'd, so test `m !== m`), and `signalFd()` / `readSignal(fd)`:
|
|
2161
|
+
call `signalFd()` once at the top of `main`, then `readSignal(fd)` blocks until
|
|
2162
|
+
SIGTERM or SIGINT and answers 15 or 2. None of them exists on a wasm target.
|
|
2163
|
+
|
|
2164
|
+
**Children and the machine.** `spawn(argv)` starts a child like `spawnSync` but
|
|
2165
|
+
answers its pid (`i32`) at once, without waiting; `kill(pid, sig)` sends 0, 2,
|
|
2166
|
+
9 or 15 and answers 0 (any other signal is `-22`); `waitPid(pid, flags)` answers
|
|
2167
|
+
`[pid, status]` as an `i32[]` — pid `-1` is any child, flag `1` does not block,
|
|
2168
|
+
`[0, 0]` means none has exited yet, and the status is the exit code or 128 plus
|
|
2169
|
+
the signal. Each answers a **negative errno** on failure (`[-10, 0]`: no child
|
|
2170
|
+
left), so test `pid > 0` before passing a pid on: `-1` (EPERM) handed to
|
|
2171
|
+
`waitPid` means any child. After `signalFd()` a blocking `waitPid` restarts
|
|
2172
|
+
through SIGTERM, so a supervisor reaps with flag `1` on a `pollWait` tick that
|
|
2173
|
+
also watches the signal fd. None of `spawn`, `kill`, `waitPid` may be called in
|
|
2174
|
+
a task or a parallel body; the two host facts may. `availableParallelism()` (`i32`, at least 1, no `--threads` needed) and
|
|
2175
|
+
`totalMemory()` (`i64` bytes, or `-1`) size a program to the machine. None of
|
|
2176
|
+
the five exists on a wasm target, and `spawn` and `waitPid` throw under Node.
|
|
2177
|
+
|
|
2178
|
+
**Sockets** (`nish:net`, or the same names as globals). `tcpListen(host, port,
|
|
2179
|
+
backlog)`, `tcpAccept(fd, peer)`, `netRead(fd, buf, off, len)`,
|
|
2180
|
+
`netWrite(fd, buf, off, len)`, `netShutdown(fd, how)`, `netClose(fd)`,
|
|
2181
|
+
`netLocalPort(fd)` and `netAddress(out, host, port)`. Every one answers an
|
|
2182
|
+
`i32`: **a negative number is a failure, not an exception** — `-11` means "would
|
|
2183
|
+
block, call again", `-32` a peer that has gone (never SIGPIPE), `-98` the port
|
|
2184
|
+
is in use, `-111` refused, `-22` a bad argument. Every socket is non-blocking,
|
|
2185
|
+
and there is no `async` and no callback: your `main` is the loop. A client calls
|
|
2186
|
+
`tcpConnect(addr)`, which answers the descriptor while it is still connecting;
|
|
2187
|
+
wait for it to be writable, then `connectResult(fd)` is 0 or the failure. Buffers
|
|
2188
|
+
are `u8[]` only; an address is 18 bytes of one (IPv4 as `::ffff:a.b.c.d`, then
|
|
2189
|
+
the port big-endian); hosts are numeric literals, because there is no DNS.
|
|
2190
|
+
`netRead` / `netWrite` panic on a range outside the buffer.
|
|
2191
|
+
`tcpListenFlags(host, port, backlog, flags)` is `tcpListen` with flag 1
|
|
2192
|
+
`SO_REUSEPORT`, so several listeners, in one process or many, share a port (Linux
|
|
2193
|
+
spreads the connections), and flag 2 `TCP_NODELAY` on every socket `tcpAccept`
|
|
2194
|
+
takes from it; flags add, and any other bit is `-22`. UDP is
|
|
2195
|
+
`udpBind(host, port, flags)` (flag 1 `SO_REUSEPORT`, flag 2 GRO),
|
|
2196
|
+
`udpSendTo(fd, buf, off, len, to, segment, ecn)` and
|
|
2197
|
+
`udpRecvFrom(fd, buf, off, len, from, meta)`: a `segment` above 0 sends one
|
|
2198
|
+
buffer as many datagrams of that size, `meta` is an `i32[]` of at least 2 that
|
|
2199
|
+
receives the GRO segment size (0 when nothing was coalesced) and the ECN bits,
|
|
2200
|
+
and an empty datagram answers 0. GSO, GRO and ECN are Linux's: on Darwin they
|
|
2201
|
+
answer `-95`. To wait for whichever descriptor is ready, `pollCreate()` makes a
|
|
2202
|
+
loop, `pollAdd(loop, fd, events, token)` / `pollModify(...)` watch `fd` for
|
|
2203
|
+
`events` (1 readable, 2 writable) under a `token` you choose, and
|
|
2204
|
+
`pollRemove(loop, fd)` stops. `pollWait(loop, ready, timeoutMs)` waits (forever
|
|
2205
|
+
for `-1`), fills `ready: i32[]` with pairs — `ready[2k]` a token,
|
|
2206
|
+
`ready[2k + 1]` its events, 4 added for a hang-up or error — and answers how
|
|
2207
|
+
many, **0 on a timeout or a signal**. It is level-triggered, so what you leave
|
|
2208
|
+
unread is reported again. Add `signalFd()` to the loop to wake on SIGTERM. None
|
|
2209
|
+
exists on a wasm target, and under Node each throws.
|
|
2210
|
+
|
|
2211
|
+
**Arena.** `Arena.mark()`, `Arena.used()` and `using a = arena()` — see below.
|
|
2212
|
+
`Arena.release(m)` and `Arena.reset()` are deprecated (NL7001).
|
|
2213
|
+
|
|
2214
|
+
### Map and Set
|
|
2215
|
+
|
|
2216
|
+
`Map<K, V>` and `Set<T>` are global, as in JavaScript: no import, insertion
|
|
2217
|
+
order, SameValueZero keys (`-0` is `+0`, `NaN` finds `NaN`). What they have is
|
|
2218
|
+
exactly `size` (a read-only `number`), `get(k)`, `set(k, v)` / `add(x)` (both
|
|
2219
|
+
answer the receiver, so they chain), `has(k)`, `delete(k)` (answers whether it
|
|
2220
|
+
was there) and `clear()`, and `keys()` / `values()` as the iterable of a
|
|
2221
|
+
`for...of` and nowhere else. `entries` and `forEach` are refused by name.
|
|
2222
|
+
|
|
2223
|
+
```ts nish:ok-body
|
|
2224
|
+
const seen = new Set<string>();
|
|
2225
|
+
const counts: Map<string, i32> = new Map();
|
|
2226
|
+
seen.add("a").add("b").add("a");
|
|
2227
|
+
counts.set("a", 1).set("b", 2);
|
|
2228
|
+
counts.delete("b");
|
|
2229
|
+
console.log(`${seen.size} ${counts.size} ${seen.has("b")}`);
|
|
2230
|
+
```
|
|
2231
|
+
|
|
2232
|
+
- Write the type arguments on `new`, or annotate the declaration and write
|
|
2233
|
+
`new Map()`. `new Map(entries)` and `new Set(array)` are refused: start empty
|
|
2234
|
+
and `set` / `add` in a loop.
|
|
2235
|
+
- A key is a string, any number type, a `boolean`, an enum or a class instance
|
|
2236
|
+
(by identity). An interface, an array, a nullable type or a `Result` is not a
|
|
2237
|
+
key; a value is anything but `void` or an interface.
|
|
2238
|
+
- A module that declares its own `Map` or `Set` keeps it, but then no other
|
|
2239
|
+
module of the program may name the global one.
|
|
2240
|
+
|
|
2241
|
+
`m.get(k)` is `V | undefined`, as under `tsc`, and it is read in exactly one of
|
|
2242
|
+
three ways: a default with `??`, a `const` tested with `!== undefined` (or
|
|
2243
|
+
`=== undefined` and an early `return`), or a bare test. Each is one probe.
|
|
2244
|
+
|
|
2245
|
+
```ts nish:ok-body
|
|
2246
|
+
const counts = new Map<string, i32>();
|
|
2247
|
+
for (const w of ["a", "b", "a"]) {
|
|
2248
|
+
counts.set(w, (counts.get(w) ?? 0) + 1);
|
|
2249
|
+
}
|
|
2250
|
+
const a = counts.get("a");
|
|
2251
|
+
if (a !== undefined) {
|
|
2252
|
+
console.log(`${a} ${counts.get("z") === undefined}`); // 2 true
|
|
2253
|
+
}
|
|
2254
|
+
```
|
|
2255
|
+
|
|
2256
|
+
- Anywhere else — a `let`, an argument, a `return`, a field or element, a
|
|
2257
|
+
template hole, an arithmetic operand, the default of another `??`, a `const`
|
|
2258
|
+
annotated `i32` — the maybe is refused with a message naming the place.
|
|
2259
|
+
Default it with `??` first: `m.get(a) ?? (m.get(b) ?? 0)`.
|
|
2260
|
+
- The only spelling of the type is `const a: i32 | undefined = m.get(k)`.
|
|
2261
|
+
- `??` does not mix with `||` or `&&` without parentheses, and its default must
|
|
2262
|
+
be the value type. For a `Map<K, Node | null>` it replaces a stored `null`
|
|
2263
|
+
too, but the result is still `Node | null`.
|
|
2264
|
+
|
|
2265
|
+
```ts nish:err-body NL2361
|
|
2266
|
+
const m = new Map<string, i32>();
|
|
2267
|
+
let v = m.get("a");
|
|
2268
|
+
```
|
|
2269
|
+
|
|
2270
|
+
Walk a map with `for (const k of m.keys())` or `for (const v of m.values())`,
|
|
2271
|
+
and a set with `for (const x of s)`: insertion order, deleted keys skipped. You
|
|
2272
|
+
may `set`, `delete` or `clear` the table inside the walk and get exactly what
|
|
2273
|
+
JavaScript gives (a new key is visited later in the same walk; a key deleted
|
|
2274
|
+
before it is reached is not).
|
|
2275
|
+
|
|
2276
|
+
```ts nish:ok-body
|
|
2277
|
+
const ages = new Map<string, i32>();
|
|
2278
|
+
ages.set("ada", 36).set("alan", 41);
|
|
2279
|
+
ages.delete("alan");
|
|
2280
|
+
ages.set("grace", 85);
|
|
2281
|
+
let total = 0;
|
|
2282
|
+
for (const name of ages.keys()) {
|
|
2283
|
+
console.log(name); // ada, then grace
|
|
2284
|
+
}
|
|
2285
|
+
for (const age of ages.values()) {
|
|
2286
|
+
total += age;
|
|
2287
|
+
}
|
|
2288
|
+
console.log(`${total}`); // 121
|
|
2289
|
+
```
|
|
2290
|
+
|
|
2291
|
+
- An iterator is not a value: `keys()` / `values()` stored, passed, returned or
|
|
2292
|
+
spread are refused. Pass the map itself and walk it where it is used.
|
|
2293
|
+
- `for (const e of m)` over a `Map` itself is refused, because it would need
|
|
2294
|
+
`[key, value]` destructuring: walk `m.keys()` and call `m.get(k)`.
|
|
2295
|
+
|
|
2296
|
+
```ts nish:err-body NL2375
|
|
2297
|
+
const m = new Map<string, i32>();
|
|
2298
|
+
for (const e of m) {
|
|
2299
|
+
}
|
|
2300
|
+
```
|
|
2301
|
+
|
|
2302
|
+
Asking about one key twice costs one probe when the second call writes
|
|
2303
|
+
through the first: `m.set(k, E)` whose `E` reads `m.get(k)` or `m.has(k)`
|
|
2304
|
+
once, and `if (m.has(k)) { m.set(k, E); … }`, `if (!m.has(k)) { m.set(k, E); … }`
|
|
2305
|
+
or `if (!s.has(x)) { s.add(x); … }` with the write first in the branch. The
|
|
2306
|
+
receiver and key must be a local, a parameter or a `this.<field>` path (the key
|
|
2307
|
+
may be a literal), spelled the same in both calls, and nothing in between may
|
|
2308
|
+
call anything, allocate (`new`, a literal array or object, a template, a
|
|
2309
|
+
string `+`) or assign. Anything else still compiles, as one probe per call.
|
|
2310
|
+
`nish/map` adds `getOrInsert(m, k, v)` — the value of `k`, or `v` after
|
|
2311
|
+
inserting it — and `reserve(m, n)`, which sizes the table for `n` entries; both
|
|
2312
|
+
are one module with the program and run unchanged under Node.
|
|
2313
|
+
|
|
2314
|
+
```ts nish:ok
|
|
2315
|
+
import { getOrInsert, reserve } from "nish/map";
|
|
2316
|
+
|
|
2317
|
+
export const main = (): i32 => {
|
|
2318
|
+
const words = ["to", "be", "or", "not", "to", "be"];
|
|
2319
|
+
const counts = new Map<string, i32>();
|
|
2320
|
+
reserve(counts, words.length);
|
|
2321
|
+
for (const w of words) {
|
|
2322
|
+
counts.set(w, (counts.get(w) ?? 0) + 1); // one probe per word
|
|
2323
|
+
}
|
|
2324
|
+
const first = new Map<string, i32>();
|
|
2325
|
+
for (let i = 0; i < words.length; i++) {
|
|
2326
|
+
getOrInsert(first, words[i], i); // one probe; inserts only the first time
|
|
2327
|
+
}
|
|
2328
|
+
console.log(`${counts.get("to") ?? 0} ${first.get("be") ?? -1}`); // 2 1
|
|
2329
|
+
return 0;
|
|
2330
|
+
};
|
|
2331
|
+
```
|
|
2332
|
+
|
|
2333
|
+
### Typed JSON: `nish/json-typed`
|
|
2334
|
+
|
|
2335
|
+
`jsonDecode(d, out)` reads one JSON object into a class and `jsonEncode(w,
|
|
2336
|
+
value)` writes one; the compiler writes the code at each call from the class's
|
|
2337
|
+
fields. A field is `i32`, `u32`, `i64`, `u64`, `f64`, `boolean`, `string`, a
|
|
2338
|
+
numeric enum, a class, an array of one of those, a `Map<string, V>` of any of
|
|
2339
|
+
those, or a `string`, class, array or map `| null`; anything else is refused at
|
|
2340
|
+
the call (NL2485), a `Map` with non-`string` keys and a `Set` included, and so
|
|
2341
|
+
is a value that is not a class (NL2486). Decoding is strict: every field
|
|
2342
|
+
required, each key once, an unknown key an error unless `d.ignoreUnknown`, and
|
|
2343
|
+
every outcome a sticky negative code in `d.failed`, never a panic. An enum is
|
|
2344
|
+
its number, and a number that is no member's value is `JSON_DECODE_TYPE`; a
|
|
2345
|
+
`u64` is exact to 2^64 - 1, and negative is `JSON_DECODE_RANGE`; a map is
|
|
2346
|
+
emptied and filled in the input's key order, a repeated key an error, and
|
|
2347
|
+
more keys over all the maps of one decode than `d.maxMapKeys` (4,096 unless
|
|
2348
|
+
set) `JSON_DECODE_MAP_LIMIT`. A class
|
|
2349
|
+
the decoder makes (an array's element, a `| null` field's object, a map's
|
|
2350
|
+
value) has a constructor with no parameters (NL2488). A class's options are
|
|
2351
|
+
one `static readonly json = { rename: { field: "key" }, optional: ["field"] }`
|
|
2352
|
+
literal, the only `static` a class may have: `rename` gives a field its key on
|
|
2353
|
+
the wire, and a missing `optional` key sets its field to the initializer, so
|
|
2354
|
+
an optional field has one (NL2498). Each misuse is its own code where the class
|
|
2355
|
+
is declared: not `static readonly` or on a generic class (NL2492), not a
|
|
2356
|
+
literal (NL2493), an unknown option (NL2494), a name that is no field (NL2495),
|
|
2357
|
+
a rename to a non-string (NL2496) or to a key that is not valid UTF-8
|
|
2358
|
+
(NL2499), two fields with one key (NL2497). A
|
|
2359
|
+
`JsonNumber` field keeps a number's text byte for byte (`text`, `i64`, `u64`,
|
|
2360
|
+
`f64`, `setText`).
|
|
2361
|
+
|
|
2362
|
+
```ts nish:ok
|
|
2363
|
+
import { JsonWriter } from "nish/json";
|
|
2364
|
+
import { JsonLimits } from "nish/json-tokenizer";
|
|
2365
|
+
import { JsonDecoder, jsonDecode, jsonEncode } from "nish/json-typed";
|
|
2366
|
+
|
|
2367
|
+
class Point {
|
|
2368
|
+
x: i32 = 0;
|
|
2369
|
+
y: i32 = 0;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
export const main = (): i32 => {
|
|
2373
|
+
const text = `{"x":3,"y":4}`;
|
|
2374
|
+
const body = new Array<u8>(toI32(text.length));
|
|
2375
|
+
for (let i: i32 = 0; i < toI32(body.length) && i < toI32(text.length); i += 1) {
|
|
2376
|
+
body[i] = toU8(text.charCodeAt(i));
|
|
2377
|
+
}
|
|
2378
|
+
const p = new Point();
|
|
2379
|
+
const code = jsonDecode(new JsonDecoder(body, 0, toI32(body.length), new JsonLimits()), p);
|
|
2380
|
+
const w = new JsonWriter(new Array<u8>(64), 0);
|
|
2381
|
+
jsonEncode(w, p); // {"x":3,"y":4}
|
|
2382
|
+
return code; // 0, or a JSON_DECODE_* / JSON_TOK_ERR_* code
|
|
2383
|
+
};
|
|
2384
|
+
```
|
|
2385
|
+
|
|
2386
|
+
```ts nish:err NL2485
|
|
2387
|
+
import { JsonWriter } from "nish/json";
|
|
2388
|
+
import { jsonEncode } from "nish/json-typed";
|
|
2389
|
+
|
|
2390
|
+
class Reading {
|
|
2391
|
+
value: f32 = 0; // no JSON reading: use f64
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
export const main = (): i32 => {
|
|
2395
|
+
const w = new JsonWriter(new Array<u8>(16), 0);
|
|
2396
|
+
jsonEncode(w, new Reading());
|
|
2397
|
+
return w.failed;
|
|
2398
|
+
};
|
|
2399
|
+
```
|
|
2400
|
+
|
|
2401
|
+
```ts nish:ok
|
|
2402
|
+
import { JsonWriter } from "nish/json";
|
|
2403
|
+
import { JsonDecoder, JsonNumber, jsonDecode, jsonEncode } from "nish/json-typed";
|
|
2404
|
+
|
|
2405
|
+
class Order {
|
|
2406
|
+
static readonly json = { rename: { orderId: "order_id" }, optional: ["note"] };
|
|
2407
|
+
orderId: i32 = 0;
|
|
2408
|
+
note: string = ""; // missing key: back to ""
|
|
2409
|
+
total: JsonNumber; // any number, kept as written
|
|
2410
|
+
constructor() {
|
|
2411
|
+
this.total = new JsonNumber();
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
export const readOrder = (d: JsonDecoder, o: Order): i32 => jsonDecode(d, o);
|
|
2416
|
+
|
|
2417
|
+
export const writeOrder = (w: JsonWriter, o: Order): void => {
|
|
2418
|
+
jsonEncode(w, o); // {"order_id":..,"note":..,"total":..}
|
|
2419
|
+
};
|
|
2420
|
+
```
|
|
2421
|
+
|
|
2422
|
+
```ts nish:err NL2495
|
|
2423
|
+
import { JsonWriter } from "nish/json";
|
|
2424
|
+
import { jsonEncode } from "nish/json-typed";
|
|
2425
|
+
|
|
2426
|
+
class Order {
|
|
2427
|
+
static readonly json = { optional: ["notes"] }; // no field `notes`
|
|
2428
|
+
note: string = "";
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
export const main = (): i32 => {
|
|
2432
|
+
const w = new JsonWriter(new Array<u8>(16), 0);
|
|
2433
|
+
jsonEncode(w, new Order());
|
|
2434
|
+
return w.failed;
|
|
2435
|
+
};
|
|
2436
|
+
```
|
|
2437
|
+
|
|
2438
|
+
```ts nish:err NL2485
|
|
2439
|
+
import { JsonWriter } from "nish/json";
|
|
2440
|
+
import { jsonEncode } from "nish/json-typed";
|
|
2441
|
+
|
|
2442
|
+
class Index {
|
|
2443
|
+
byId: Map<i32, string>; // a JSON object's keys are strings
|
|
2444
|
+
constructor() {
|
|
2445
|
+
this.byId = new Map<i32, string>();
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
export const main = (): i32 => {
|
|
2450
|
+
const w = new JsonWriter(new Array<u8>(16), 0);
|
|
2451
|
+
jsonEncode(w, new Index());
|
|
2452
|
+
return w.failed;
|
|
2453
|
+
};
|
|
2454
|
+
```
|
|
2455
|
+
|
|
2456
|
+
## Memory
|
|
2457
|
+
|
|
2458
|
+
There is **no garbage collector and no `free`**, and nothing about this changes
|
|
2459
|
+
what a program computes — only where memory lives. You do not have to manage
|
|
2460
|
+
it, and mostly you should not try:
|
|
2461
|
+
|
|
2462
|
+
1. **Stack allocation** for a `new`, object literal, array literal or
|
|
2463
|
+
`new Array<T>(literal)` whose value provably does not outlive its function.
|
|
2464
|
+
2. **Automatic arena scopes** for a function whose temporaries all die with it:
|
|
2465
|
+
a mark on entry, a release before every `ret`.
|
|
2466
|
+
3. **A reclaim at the call site** for a function that returns a string.
|
|
2467
|
+
4. **Explicit control** with `using a = arena()`, for code that manages
|
|
2468
|
+
batches itself.
|
|
2469
|
+
|
|
2470
|
+
**To free a batch, wrap it in a block with `using a = arena()`**: everything
|
|
2471
|
+
the block allocates after that line is released when the block ends, on every
|
|
2472
|
+
exit, and the compiler refuses the block if anything allocated in it could
|
|
2473
|
+
outlive it — so allocate what must outlive the block before it, and let only
|
|
2474
|
+
numbers, booleans and enums out. `arena()` is only ever a `using` initialiser,
|
|
2475
|
+
and `a` is never read.
|
|
2476
|
+
|
|
2477
|
+
```ts nish:ok-body
|
|
2478
|
+
const rows: string[] = ["a", "bb"];
|
|
2479
|
+
let total = 0;
|
|
2480
|
+
for (const row of rows) {
|
|
2481
|
+
using a = arena();
|
|
2482
|
+
const line = `${row}: ${row.length}`;
|
|
2483
|
+
total = total + line.length;
|
|
2484
|
+
}
|
|
2485
|
+
```
|
|
2486
|
+
|
|
2487
|
+
**Do not hand an outer local a new allocation on every pass.**
|
|
2488
|
+
`let last = ""` above a loop whose body runs ``last = `item ${i}` `` keeps
|
|
2489
|
+
every pass's string, because nothing frees a value a later pass replaced:
|
|
2490
|
+
10,000,000 passes peaked at 392 MB. The compiler warns (NL9016). Declare the
|
|
2491
|
+
value with `const` inside the loop when only the pass reads it, or keep
|
|
2492
|
+
numbers across passes and build the value once after the loop. A server, or
|
|
2493
|
+
anything that runs for a long time, should compile with `--deny-retention`,
|
|
2494
|
+
which makes this warning and the three like it errors.
|
|
2495
|
+
|
|
2496
|
+
Everything else is bumped from the arena, which is released when `main`
|
|
2497
|
+
returns. **Do not write `Arena.release` or `Arena.reset`**: releasing while
|
|
2498
|
+
anything allocated after the mark is still referenced is undefined behaviour
|
|
2499
|
+
the compiler does not check, so both are deprecated, and each call compiles
|
|
2500
|
+
with a `deprecation` warning (NL7001) pointing to `using a = arena()`.
|
|
2501
|
+
|
|
2502
|
+
## Recipes for what is missing
|
|
2503
|
+
|
|
2504
|
+
**Iterate and transform** — there is no `map`, and no function to pass it:
|
|
2505
|
+
|
|
2506
|
+
```ts nish:ok-body
|
|
2507
|
+
const xs: i32[] = [1, 2, 3];
|
|
2508
|
+
const doubled: i32[] = [];
|
|
2509
|
+
for (const x of xs) {
|
|
2510
|
+
doubled.push(x * 2);
|
|
2511
|
+
}
|
|
2512
|
+
console.log(`${doubled.length}`);
|
|
2513
|
+
```
|
|
2514
|
+
|
|
2515
|
+
**Convert a number to a string** — there is no `toString` and no `String()`:
|
|
2516
|
+
|
|
2517
|
+
```ts nish:ok-body
|
|
2518
|
+
const n = 42;
|
|
2519
|
+
const s = `${n}`;
|
|
2520
|
+
console.log(s);
|
|
2521
|
+
```
|
|
2522
|
+
|
|
2523
|
+
**Return two values** — there are no tuples: declare an interface.
|
|
2524
|
+
|
|
2525
|
+
```ts nish:ok
|
|
2526
|
+
interface Split { head: string; rest: string; }
|
|
2527
|
+
|
|
2528
|
+
const cut = (s: string, at: i32): Split => ({
|
|
2529
|
+
head: s.substring(0, at),
|
|
2530
|
+
rest: s.substring(at),
|
|
2531
|
+
});
|
|
2532
|
+
|
|
2533
|
+
export const main = (): i32 => {
|
|
2534
|
+
console.log(cut("hello", 2).rest);
|
|
2535
|
+
return 0;
|
|
2536
|
+
};
|
|
2537
|
+
```
|
|
2538
|
+
|
|
2539
|
+
When the two values need no names of their own, `import { Pair } from "nish/pair"`
|
|
2540
|
+
and return `Pair<A, B>`: an object literal `{ first, second }` at the return,
|
|
2541
|
+
exactly as above. It is for returning two values, not storing them — a struct
|
|
2542
|
+
kept in an array or a field wants named fields.
|
|
2543
|
+
|
|
2544
|
+
**A map or a set** — the global `Map` and `Set` ([Map and Set](#map-and-set)).
|
|
2545
|
+
Read a value with `m.get(k) ?? d`, and keep the keys in an array beside it when
|
|
2546
|
+
you need to walk them, since there is no iteration yet.
|
|
2547
|
+
|
|
2548
|
+
**Find the next byte of a few** — `indexOfAny(text, bytes, from)` from
|
|
2549
|
+
`nish/text` answers the first index at or after `from` whose byte is one of
|
|
2550
|
+
`bytes`, or -1: the scan for the next `"` or `\\` in a JSON string, the next
|
|
2551
|
+
delimiter in a line. `bytes` holds 1 to 16 ASCII bytes and anything else
|
|
2552
|
+
panics; `from` is clamped to `[0, text.length]`. Natively it reads 16 or 32
|
|
2553
|
+
bytes at a time, so prefer it to a `charCodeAt` loop over a long text — and
|
|
2554
|
+
never call it on secret data, because its time says where the match is.
|
|
2555
|
+
(No block: a program importing `nish/text` writes two `.ll` files;
|
|
2556
|
+
`tests/link/std_text_index_of_any` is its program.)
|
|
2557
|
+
|
|
2558
|
+
**Match a pattern** — there is no `RegExp` and no `/re/` literal:
|
|
2559
|
+
`regexCompile(pattern, flags)` from `nish/regex` answers a
|
|
2560
|
+
`Result<Regex, string>`, the error text naming the byte. The syntax is
|
|
2561
|
+
JavaScript's with the `u` flag's strictness (a stray `{` or `]` is an
|
|
2562
|
+
error), the flags are `i`, `m` and `s`, and backreferences and lookaround are
|
|
2563
|
+
refused, because matching never backtracks: time is linear in the input for
|
|
2564
|
+
every pattern. `test`, `exec(text, from, m)` into a reused `RegexMatch`,
|
|
2565
|
+
`find`, `findAll`, `count`, `replace`, `replaceAll` (`$&`, `$1`,
|
|
2566
|
+
`$<name>`) and `split`; offsets are UTF-8 bytes and `\w`, `\d`, `\b` and `i`
|
|
2567
|
+
are ASCII. A `Regex` keeps its scratch memory, so compile once outside the
|
|
2568
|
+
loop and use it from one thread. (No block, as for `nish/text`;
|
|
2569
|
+
`tests/link/std_regex` is its program, and the user guide's [standard-library
|
|
2570
|
+
page](guide/using-nish/standard-library.md#nishregex) its walk-through.)
|
|
2571
|
+
|
|
2572
|
+
**Decode a URL or read a query** — there is no `decodeURIComponent` and no
|
|
2573
|
+
`URLSearchParams`: `nish/net/url` has both readings. `urlDecode(s, flags)` and
|
|
2574
|
+
`urlQueryGet(target, name)` answer a `string | null`; the byte API
|
|
2575
|
+
(`percentDecode`, `UrlPathSegments`, `UrlQuery`, `urlQueryFind`) reads a
|
|
2576
|
+
window of a `u8[]` and allocates nothing. Decoding is strict by default — a
|
|
2577
|
+
malformed escape or `%00` is refused, `+` is kept — and `URL_DECODE_FORM`,
|
|
2578
|
+
`URL_DECODE_LENIENT` and `URL_DECODE_ALLOW_NUL` add `URLSearchParams`'s
|
|
2579
|
+
repairs, short of its U+FFFD for invalid UTF-8. `percentDecode` answers the
|
|
2580
|
+
length or a negative `URL_DECODE_ERR_*` code — test it before reading `out`,
|
|
2581
|
+
since a peer's `%zz` makes it negative and `urlQueryStart` answers -1 for no
|
|
2582
|
+
`?` — and `percentDecodeErrorAt` the offset an error is at; a query yields at
|
|
2583
|
+
most 100 pairs. (No block, as for `nish/text`; `tests/link/std_url` is its
|
|
2584
|
+
program, and the user guide's [standard-library
|
|
2585
|
+
page](guide/using-nish/standard-library.md#nishneturl) its walk-through.)
|
|
2586
|
+
|
|
2587
|
+
**Draw to the whole terminal** (a game, a dashboard) — there is no curses and
|
|
2588
|
+
no `process.stdout.on("resize")`: `runTerminal(options, state, update, render)`
|
|
2589
|
+
from `nish/term-io` opens the terminal, probes what it can draw, steps
|
|
2590
|
+
`update(state, input)` at `options.stepsPerSecond` and calls
|
|
2591
|
+
`render(state, frame)` only when the link to the terminal has room, then
|
|
2592
|
+
leaves the terminal as it was, even on a panic. Draw cells on `frame.screen`
|
|
2593
|
+
or pixels on `frame.framebuffer` (sized by `options.framebufferWidth` and
|
|
2594
|
+
`framebufferHeight`) and never pick a backend: kitty graphics, Sixel, half
|
|
2595
|
+
blocks or ASCII is chosen from the probe and the measured link. It answers
|
|
2596
|
+
`Err` with one sentence on a pipe, so test the `Result`; it reaches `tty`.
|
|
2597
|
+
`NISH_TERM_GRAPHICS` and `NISH_TERM_FPS` override what it picked. (No block,
|
|
2598
|
+
as for `nish/text`; `examples/term-probe.ts` is its program, and the user
|
|
2599
|
+
guide's [terminal page](guide/using-nish/terminal.md) its walk-through.)
|
|
2600
|
+
|
|
2601
|
+
**Shared behaviour over several shapes** — there is no inheritance and no
|
|
2602
|
+
dispatch. Give each class the interface's fields **first**, `implements` it,
|
|
2603
|
+
and write a free function over the interface:
|
|
2604
|
+
|
|
2605
|
+
```ts nish:ok
|
|
2606
|
+
interface Sized { width: i32; height: i32; }
|
|
2607
|
+
|
|
2608
|
+
class Photo implements Sized {
|
|
2609
|
+
width: i32;
|
|
2610
|
+
height: i32;
|
|
2611
|
+
path: string = "";
|
|
2612
|
+
constructor(width: i32, height: i32) {
|
|
2613
|
+
this.width = width;
|
|
2614
|
+
this.height = height;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
const area = (s: Sized): i32 => s.width * s.height;
|
|
2619
|
+
|
|
2620
|
+
export const main = (): i32 => {
|
|
2621
|
+
console.log(`${area(new Photo(3, 4))}`);
|
|
2622
|
+
return 0;
|
|
2623
|
+
};
|
|
2624
|
+
```
|
|
2625
|
+
|
|
2626
|
+
**Optional parameters** — there are none. Write two functions with different
|
|
2627
|
+
names, or take the value and document the sentinel.
|
|
2628
|
+
|
|
2629
|
+
**A calendar, regex, JSON, threads of your own** — not in the language (the
|
|
2630
|
+
wall clock is `Date.now()`, and that is all of `Date`). Say so rather than
|
|
2631
|
+
emitting code that cannot compile.
|
|
2632
|
+
|
|
2633
|
+
### Proving it cannot panic
|
|
2634
|
+
|
|
2635
|
+
Under `--deny-panics`, or for a module the root `package.json` lists in
|
|
2636
|
+
`"nish": { "noPanic": [...] }`, every place the program could stop with a panic
|
|
2637
|
+
is an error (NL2457) until a proof removes it; out of memory is the one kind
|
|
2638
|
+
allowed. Each error names its fix, and there are five:
|
|
2639
|
+
|
|
2640
|
+
- **Guard** what the checker cannot prove: `if (i >= 0 && i < xs.length)` before
|
|
2641
|
+
`xs[i]` (`if (toU32(i) < toU32(xs.length))` for a `u8`, `u16` or `u32` index,
|
|
2642
|
+
whose type is the lower end; `toI32(xs.length)` for the bound under
|
|
2643
|
+
`--number-mode f64`; an `i64`, `u64` or float index — `number` under
|
|
2644
|
+
`--number-mode f64` — has no guard, so index with an `i32` or a `u32`),
|
|
2645
|
+
`if (d !== 0 && d !== -1)` before `a / d` (a
|
|
2646
|
+
constant divisor needs nothing), `if (xs.length > 0)` before `xs.pop()`,
|
|
2647
|
+
`if (n >= 0 && n <= 9)` before `n` enters `integer<0, 9>`.
|
|
2648
|
+
- **Bound a signed `+ - *`, or say it wraps**: an `i32`/`i64` operation the
|
|
2649
|
+
checker cannot prove fits is an `overflow` site; bound its operands (a loop
|
|
2650
|
+
below a length, a guard on the value), compute in `u32`/`u64`, or write
|
|
2651
|
+
`wrappingAdd`, `wrappingSub` or `wrappingMul` from `nish:unsafe`.
|
|
2652
|
+
- **Swap `expect` for `orReturn` or `unwrapOr`**, which hand the error on or
|
|
2653
|
+
supply a value instead of stopping.
|
|
2654
|
+
- **Swap `readFileSync` for `readFileSyncOrNull`**, which answers `null` where
|
|
2655
|
+
the other exits.
|
|
2656
|
+
- **Read the list**: `nish file.ts --emit-panics panics.json` writes every site
|
|
2657
|
+
per function, proven or not, and the `"panics"` of `--emit-capabilities` the
|
|
2658
|
+
same.
|
|
2659
|
+
|
|
2660
|
+
```ts nish:ok --deny-panics
|
|
2661
|
+
const at = (xs: i32[], i: i32): i32 => {
|
|
2662
|
+
if (i >= 0 && i < xs.length) {
|
|
2663
|
+
return xs[i];
|
|
2664
|
+
}
|
|
2665
|
+
return 0;
|
|
2666
|
+
};
|
|
2667
|
+
|
|
2668
|
+
const byte = (xs: i32[], i: u8): i32 => {
|
|
2669
|
+
if (toU32(i) < toU32(xs.length)) {
|
|
2670
|
+
return xs[i];
|
|
2671
|
+
}
|
|
2672
|
+
return 0;
|
|
2673
|
+
};
|
|
2674
|
+
|
|
2675
|
+
const ratio = (a: i32, d: i32): i32 => {
|
|
2676
|
+
if (d !== 0 && d !== -1) {
|
|
2677
|
+
return a / d;
|
|
2678
|
+
}
|
|
2679
|
+
return a / 2;
|
|
2680
|
+
};
|
|
2681
|
+
|
|
2682
|
+
const size = (path: string): i32 => {
|
|
2683
|
+
const text = readFileSyncOrNull(path);
|
|
2684
|
+
return text === null ? -1 : text.length;
|
|
2685
|
+
};
|
|
2686
|
+
|
|
2687
|
+
export const main = (): i32 => {
|
|
2688
|
+
console.log(`${at([4, 5], 1)} ${byte([4, 5], 0)} ${ratio(9, 3)} ${size("missing.txt")}`);
|
|
2689
|
+
return 0;
|
|
2690
|
+
};
|
|
2691
|
+
```
|
|
2692
|
+
|
|
2693
|
+
The same function without its guard is refused, and the error names the guard:
|
|
2694
|
+
|
|
2695
|
+
```ts nish:err NL2457 --deny-panics
|
|
2696
|
+
export const at = (xs: i32[], i: i32): i32 => xs[i];
|
|
2697
|
+
```
|
|
2698
|
+
|
|
2699
|
+
A call into the standard library or a dependency that may panic is refused at
|
|
2700
|
+
the call (NL2458), because only its own author can prove it: call a function
|
|
2701
|
+
that cannot panic, or make the call from a module outside the scope.
|
|
2702
|
+
`--unchecked-indexing` is not a proof, and an index it leaves unchecked is
|
|
2703
|
+
still refused. `uncheckedGet` and `uncheckedSet` are not proofs either, but
|
|
2704
|
+
they cannot panic, and their `nish:unsafe` import is the module's visible
|
|
2705
|
+
opt-in to an index out of range being undefined behaviour, so the scope allows
|
|
2706
|
+
them (as `"allowed": true` sites) and a call into them is never refused:
|
|
2707
|
+
|
|
2708
|
+
```ts nish:ok --deny-panics
|
|
2709
|
+
import { uncheckedGet } from "nish:unsafe";
|
|
2710
|
+
|
|
2711
|
+
export const first = (xs: i32[]): i32 => uncheckedGet(xs, 0);
|
|
2712
|
+
```
|
|
2713
|
+
|
|
2714
|
+
Without the import the call is refused (NL2456), scope or not:
|
|
2715
|
+
|
|
2716
|
+
```ts nish:err NL2456 --deny-panics
|
|
2717
|
+
export const first = (xs: i32[]): i32 => uncheckedGet(xs, 0);
|
|
2718
|
+
```
|
|
2719
|
+
|
|
2720
|
+
### Holding it to its capabilities
|
|
2721
|
+
|
|
2722
|
+
A package that must not touch the network says so in `package.json`,
|
|
2723
|
+
`"nish": { "capabilities": { "deny": ["net"] } }`, and `--deny net` or an
|
|
2724
|
+
allowlist such as `--allow fs.read,exit` says the same on the command line.
|
|
2725
|
+
The `"nish"` field holds `noPanic` and `capabilities` and nothing else, each
|
|
2726
|
+
written once; anything more is refused (NL3036) rather than skipped. A
|
|
2727
|
+
program whose `main` — or, in a library or a header build, any export —
|
|
2728
|
+
reaches more is refused (NL2459), spanned at the first call and naming the
|
|
2729
|
+
whole chain to the builtin; nothing changes in a program
|
|
2730
|
+
that keeps to it ([LANGUAGE.md](LANGUAGE.md#the-capability-policy)). Under an
|
|
2731
|
+
allowlist `exit` is a capability like the others, and `unsafe` can be denied
|
|
2732
|
+
but never allowed: importing `nish:unsafe` is the opt-in.
|
|
2733
|
+
|
|
2734
|
+
```ts nish:err NL2459 --deny fs.read
|
|
2735
|
+
const settings = (): string => readFileSync("settings.txt");
|
|
2736
|
+
|
|
2737
|
+
export const main = (): i32 => settings().length;
|
|
2738
|
+
```
|
|
2739
|
+
|
|
2740
|
+
## Before you say it compiles
|
|
2741
|
+
|
|
2742
|
+
Run it. `nish file.ts --json` is one command and it is the only proof.
|
|
2743
|
+
|
|
2744
|
+
1. Did you **run the compiler**? Nothing else counts.
|
|
2745
|
+
2. Are all conditions `boolean` — no `if (x)` on a number, string or array?
|
|
2746
|
+
3. Did you invent a method? Check it against
|
|
2747
|
+
[the builtins list](#the-builtins-in-full).
|
|
2748
|
+
4. Is every parameter, return type, field and module constant annotated?
|
|
2749
|
+
5. Does every numeric type match exactly — no `i32` meeting `i64`, no literal
|
|
2750
|
+
in an object-literal property or a method argument expecting a width?
|
|
2751
|
+
6. Is every `Result` read, and is every `.value` behind an `isOk()`?
|
|
2752
|
+
7. Is every nullable narrowed with `!== null` before it is touched?
|
|
2753
|
+
8. Did you use `throw`, `try`, `any`, `undefined` or `??` (outside a `T | null`, a chain or `Map.get`), `?.` without a `??`, a cast, a
|
|
2754
|
+
stored or returned callback, a generic *alias*, or `extends`?
|
|
2755
|
+
9. Does it reach only what you meant it to? `nish file.ts --emit-capabilities
|
|
2756
|
+
caps.json` lists every capability each function can reach — files, processes,
|
|
2757
|
+
the network, the environment, the clock, entropy, signals, `exit`, C calls —
|
|
2758
|
+
with the call chain that reaches it, and `nish run --capabilities file.ts`
|
|
2759
|
+
prints the one-line summary ([LANGUAGE.md](LANGUAGE.md#capabilities)).
|
|
2760
|
+
A package that must not touch the network says so in `package.json`
|
|
2761
|
+
([Holding it to its capabilities](#holding-it-to-its-capabilities)).
|
|
2762
|
+
10. Must it never panic? Compile with `--deny-panics` (or list the module in
|
|
2763
|
+
`package.json`'s `"nish": { "noPanic": [...] }`): every index, division,
|
|
2764
|
+
signed `+ - *`, `pop`, range entry, `expect` and exiting call it cannot
|
|
2765
|
+
prove is an error
|
|
2766
|
+
naming the guard ([Proving it cannot panic](#proving-it-cannot-panic)); an
|
|
2767
|
+
`uncheckedGet` or `uncheckedSet` behind its `nish:unsafe` import is allowed.
|
|
2768
|
+
11. If you are adding to this repository: `npm run check` and the touched
|
|
2769
|
+
`node tests/run.js` sections green locally, every CI `test` shard green
|
|
2770
|
+
([CLAUDE.md](../CLAUDE.md#definition-of-done)), and a new construct ships a
|
|
2771
|
+
golden `.ll`, an `llvm-as` pass, a native round trip, a negative test, its
|
|
2772
|
+
`LANGUAGE.md` rule and cookbook entry, and a conventional commit message,
|
|
2773
|
+
which is its changelog entry.
|
|
2774
|
+
|
|
2775
|
+
Every `ts nish:ok` and `ts nish:err` block on this page is compiled by
|
|
2776
|
+
`npm test`, so an example that has gone stale is a failing test rather than a
|
|
2777
|
+
program that no longer works.
|