@amritk/nish 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/docs/AI.md +63 -26
- package/llms.txt +1 -1
- package/package.json +5 -5
- package/runtime/nish.d.ts +65 -0
- package/runtime/nish.h +31 -0
- package/runtime/nish.mjs +35 -0
- package/runtime/runtime-host.c +178 -0
- package/runtime/runtime-os.c +15 -0
- package/runtime/shim.mjs +122 -0
- package/scripts/build.sh +8 -7
- package/scripts/gen-diagnostic-codes.mjs +47 -25
- package/std/README.md +40 -0
- package/std/crypto/base64url.ts +145 -0
- package/std/crypto/ct.ts +64 -0
- package/std/crypto/hkdf.ts +118 -0
- package/std/crypto/hmac.ts +155 -0
- package/std/crypto/sha256.ts +444 -0
- package/std/crypto/sha512.ts +510 -0
- package/std/crypto/x25519.ts +494 -0
package/README.md
CHANGED
|
@@ -306,6 +306,8 @@ nish run [options] <file.ts> [args ...]
|
|
|
306
306
|
language spawns a thread yet)
|
|
307
307
|
--no-warn-performance do not report the `performance` diagnostics (they are on by default,
|
|
308
308
|
print on stderr, and never change the exit code)
|
|
309
|
+
--warn-portability report the `portability` diagnostics: where the program's TypeScript
|
|
310
|
+
reading answers differently (off by default; never changes the exit code)
|
|
309
311
|
-g emit DWARF debug info (!dbg locations, variables); kept by --link
|
|
310
312
|
--json print diagnostics as one JSON object per line on stdout (no excerpt)
|
|
311
313
|
--emit-ast print the syntax tree of every module to stdout instead of IR
|
package/docs/AI.md
CHANGED
|
@@ -46,16 +46,18 @@ stdout, nothing on stderr, every field 1-based with `endLine`/`endColumn`
|
|
|
46
46
|
exclusive:
|
|
47
47
|
|
|
48
48
|
```json
|
|
49
|
-
{"file":"p.ts","line":3,"column":17,"endLine":3,"endColumn":20,"severity":"error","code":"NL2249","message":"Unknown method `map` on i32[] (supported: push, pop, indexOf, join)"}
|
|
49
|
+
{"file":"p.ts","line":3,"column":17,"endLine":3,"endColumn":20,"severity":"error","code":"NL2249","message":"Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill)"}
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
- **Key on `code`, never on `message`.** A code is a promise: `NL2249` means
|
|
53
53
|
the same rule next release. The prose may improve; the code may not.
|
|
54
|
-
- **`severity`** is `"error"` or `"
|
|
55
|
-
changes the exit code — it is advice, not a rejection.
|
|
54
|
+
- **`severity`** is `"error"`, `"performance"` or `"portability"`. A warning
|
|
55
|
+
of either kind never changes the exit code — it is advice, not a rejection.
|
|
56
|
+
Portability warnings, the sites where the program's TypeScript reading
|
|
57
|
+
answers differently, print only under `--warn-portability`.
|
|
56
58
|
- **Bands**: `NL1xxx` the Phase 0 forbidden-syntax sweep, `NL2xxx` the checker,
|
|
57
|
-
`NL3xxx` the driver and modules, `NL4xxx` the interop sidecars, `
|
|
58
|
-
performance, `NL0001` syntax, `NL0002` toolchain, `NL0003` internal,
|
|
59
|
+
`NL3xxx` the driver and modules, `NL4xxx` the interop sidecars, `NL8xxx`
|
|
60
|
+
portability, `NL9xxx` performance, `NL0001` syntax, `NL0002` toolchain, `NL0003` internal,
|
|
59
61
|
`NL0000` a diagnostic with no rule yet.
|
|
60
62
|
- **Exit codes**: `0` ok, `1` the program was rejected, `2` usage, `3` the C
|
|
61
63
|
toolchain is unusable, `70` an internal compiler error — that last one is a
|
|
@@ -114,31 +116,39 @@ rejects. This table is the highest-value part of the page.
|
|
|
114
116
|
| `x == y` | `Loose equality is forbidden` | `x === y` |
|
|
115
117
|
| `throw new Error(m)` | `` `throw` is forbidden `` | `return Err(m)`, or `panic(m)` to end the process |
|
|
116
118
|
| `try { … } catch { … }` | `` `try`/`catch`/`finally` is forbidden `` | `Result<T, E>` and `isErr()` |
|
|
117
|
-
| `xs.map(f)`, `.filter`, `.reduce`, `.forEach`, `.slice`, `.sort`, `.shift` | `` Unknown method `map` on i32[] (supported: push, pop, indexOf, join) `` | a `for` loop, or a top-level `map(xs, f)` with a [function parameter](#function-parameters) |
|
|
119
|
+
| `xs.map(f)`, `.filter`, `.reduce`, `.forEach`, `.slice`, `.sort`, `.shift` | `` Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill) `` | a `for` loop, or a top-level `map(xs, f)` with a [function parameter](#function-parameters) |
|
|
118
120
|
| `s.toUpperCase()`, `s.split()`, `s.trim()`, `s.replace()` | `` Unknown method … on string `` | index bytes with `charCodeAt` / `substring` |
|
|
119
121
|
| `a?.b`, `a ?? b` | forbidden, except `m.get(k) ?? d` | `if (a !== null)` first |
|
|
120
|
-
| `x as T`, `<T>x`, `x
|
|
122
|
+
| `x as T`, `<T>x`, `x satisfies T` | `Unsupported expression in Phase 1: AsExpression` (to `any` or `unknown`: `` Type assertion to `any` is forbidden ``); `x!` is a syntax error | there are no casts; `implements` is the only widening |
|
|
123
|
+
| `typeof x`, `x instanceof C`, `"k" in o`, `delete o.k`, `void 0`, `a, b` | each refused by name (`` `typeof` is forbidden ``, …) | there is no runtime type information: a value's type is its declared one |
|
|
124
|
+
| `a ** b`, `[...xs]`, `[1, , 2]` | `` Unsupported binary operator `**` `` / `Spread in array literals is not supported` / `Holes in array literals are not supported` | `Math.pow` on `f64`, a loop, every element written out |
|
|
121
125
|
| `any`, `unknown` | forbidden | name the real type |
|
|
122
126
|
| `undefined` | forbidden, except `x === undefined` on a `Map.get` result | `null`, with a `T \| null` type |
|
|
123
127
|
| `let total = 0` at the top level | `` Top-level `let` is not supported `` | a module `const`, or a local |
|
|
128
|
+
| `for (const k in o)` | `` `for...in` is forbidden `` | `for (const x of xs)` over an array, or `for (const k of m.keys())` over a `Map` |
|
|
124
129
|
| a callback stored or returned: `const cb = (n: i32) => n`, a field `cb: (n: i32) => i32` | `An arrow may only be written as the argument for a function-typed parameter` / `` … is a function type, which may only annotate a parameter of a top-level function `` | there are **no function values**; a callback the call names is a [function parameter](#function-parameters) |
|
|
125
|
-
| `type Pair<T>` (a generic alias) |
|
|
130
|
+
| `type Pair<T>` (a generic alias) | `Generic type parameters are forbidden on a type alias` | a generic **function**, **class**, **interface** and **method** all work — see below; an alias renames a type that already exists, so it has nothing to specialise |
|
|
126
131
|
| `constructor<T>(x: T)` | a syntax error: `a constructor cannot have type parameters` | put the parameter on the class (`class Box<T>`), or on a method |
|
|
127
132
|
| `h.get<i32>(7)` (type argument at a method call) | `Type arguments are not written at a call site in Nish` | `h.get(7)` — a generic method infers like a generic function |
|
|
128
133
|
| `<T>(p: T) => p.x` (a member of a type parameter) | `` Cannot read `x` of `T`: an unconstrained type parameter has no members `` | `<T extends Point>(p: T) => p.x`, where `Point` is a class or interface that declares `x` |
|
|
129
134
|
| `identity<i32>(7)` (type argument at a call) | `Type arguments are not written at a call site in Nish` | `identity(7)` — `T` is inferred from the arguments |
|
|
130
135
|
| `async` / `await` / `Promise` | forbidden (no event loop) | the I/O builtins are synchronous |
|
|
136
|
+
| `namespace N { }`, `declare global { }`, `@decorator` | forbidden | one module per file; a plain function in place of a decorator |
|
|
137
|
+
| `keyof T`, `<T = i32>` (a default type argument) | `` Unsupported type `keyof T` `` / `a default type argument … is not supported` | name the type; a type argument is always inferred |
|
|
131
138
|
| `class B extends A` | `` `extends` is not supported: Nish has no inheritance `` | repeat the fields and `implements` an interface |
|
|
132
|
-
| `static` members,
|
|
139
|
+
| `static` members, `get x()` / `set x(v)` | `` … `static` members are not supported `` / `` Getters and setters are not supported in class `C` (use a method) `` | module `const`s and plain methods |
|
|
140
|
+
| `const [a, b] = xs`, `({ x }: Point) =>`, `(n = 1)`, `(n?: i32)`, `(...ns: i32[])` | `Destructuring is not supported` / `Destructured parameters are not supported` / `Optional/default parameters are not supported` / `Rest parameters are not supported` | one name per binding, every parameter passed; an array for a variable count |
|
|
141
|
+
| `function f(): void;` (an overload), `export default function ()` | `Functions must have a body` / `Functions must be named` | one named function, with its body |
|
|
142
|
+
| a method in an `interface` | `` Interface `I` cannot declare methods (interfaces describe layout only) `` | fields only; a top-level function over the interface |
|
|
143
|
+
| `abstract class`, `declare class`, `constructor(public x: i32)`, `interface B extends A`, `[k: string]: T` | `Abstract classes are not supported` / `` `declare class` is not supported `` / `Parameter properties … are not supported; declare the field and assign it` / ``Interface inheritance (`extends`) is not supported; list every field`` / `Index signatures are not supported …` | a plain class with its fields declared and assigned in the constructor; every field listed; a `Map` for a keyed table |
|
|
144
|
+
| `{ m() { … } }`, `{ "a": 1 }`, `const C = class { }` | ``Unsupported object literal member: MethodDeclaration (only `key: value`)`` / `Object literal keys must be plain identifiers` / `Classes must be named` | fields with identifier keys, a top-level function over the interface, a named top-level class |
|
|
133
145
|
| `type Pair = { a: i32 }` (inline object type) | `` Unsupported type `{ a: i32 }` `` | declare an `interface` |
|
|
134
146
|
| `A \| B` unions | `` Union types other than `T \| null` are forbidden `` | one type, or an `interface` prefix |
|
|
135
147
|
| `String(n)`, `n.toString()` | `` Unknown function `String` `` / `` Unknown method `toString` on i32 `` | `` `${n}` `` |
|
|
136
148
|
| `xs.length = 0` | `` Cannot assign to `length` of i32[] (array length is read-only; use `push`) `` | build a new array |
|
|
137
|
-
| `for (const k in o)` | `Unsupported statement in Phase 1: ForInStatement` | `for (const x of xs)` over an array |
|
|
138
149
|
| `import { readFileSync } from "fs"` | `` Cannot find package `fs` `` — a bare specifier is a **package name**, so it is looked for in `node_modules`; one that is installed but has no `nish` condition is `` Package `fs` has no Nish entry point `` | `readFileSync` is a global; no import needed (or `import { readFileSync } from "nish:fs"`) |
|
|
139
150
|
| `export default f` | `` `export default` / `export =` are not supported `` | `export const f = …` |
|
|
140
|
-
| `
|
|
141
|
-
| `new Date()`, `Date.now()` | `` Unknown builtin `Date.now` `` | `monotonicNanos()` for elapsed time; there is no wall clock and no calendar |
|
|
151
|
+
| `new Date()` | `` `new Date()` is refused: Nish has no `Date` object; the one `Date` member is `Date.now()`, the wall clock in milliseconds `` | `Date.now()` for the wall clock, `monotonicNanos()` for elapsed time; there is no calendar |
|
|
142
152
|
| `JSON.parse`, `RegExp`, `Promise` | unknown / forbidden | none of these exist; write them or restructure |
|
|
143
153
|
| `for (const [k, v] of m)`, `m.forEach(...)`, `new Map(entries)` | each refused by name | `Map` and `Set` exist, with `size`, `get`, `set` / `add`, `has`, `delete`, `clear`, and `keys()` / `values()` in a `for...of` — see [Map and Set](#map-and-set) |
|
|
144
154
|
| `let v = m.get(k)`, `f(m.get(k))`, `m.get(k) + 1` | `` `m.get(k)` is `i32 \| undefined` and cannot be held in a `let` `` (and one message per place) | `m.get(k) ?? 0`, or `const v = m.get(k); if (v !== undefined) { … }` |
|
|
@@ -996,15 +1006,21 @@ export const main = (): i32 => {
|
|
|
996
1006
|
point: `Kind` and `i32` never convert in either direction. Members must be
|
|
997
1007
|
numeric literals. `===` and `!==` are the **only** operators — no arithmetic,
|
|
998
1008
|
no bitwise, so **bit flags stay `i32` module constants**.
|
|
999
|
-
- **
|
|
1000
|
-
|
|
1001
|
-
|
|
1009
|
+
- **Both can be exported.** `export enum Kind` is imported like a function,
|
|
1010
|
+
`import { Kind } from "./kinds"` (renamed with `as` if you like), and in the
|
|
1011
|
+
importer it *is* the exporter's `Kind`: the members fold to the exporter's
|
|
1012
|
+
integers and a `Kind` value crosses a call as itself. A `Kind` a second
|
|
1013
|
+
module declares for itself is a different type, however alike the two are.
|
|
1014
|
+
`export type Conn = Socket | null` is imported the same way and is, in the
|
|
1015
|
+
importer, the type it names; its right-hand side is resolved in the module
|
|
1016
|
+
that wrote it, so the importer need not import `Socket`. A module does not
|
|
1017
|
+
re-export an enum or alias it imported.
|
|
1002
1018
|
- Both are top-level only.
|
|
1003
1019
|
|
|
1004
1020
|
### Modules
|
|
1005
1021
|
|
|
1006
|
-
- `export` goes on `const` (function or constant), `class`,
|
|
1007
|
-
declarations. No `export default`, no `export { … }`, no `export *`.
|
|
1022
|
+
- `export` goes on `const` (function or constant), `class`, `interface`,
|
|
1023
|
+
`enum` and `type` declarations. No `export default`, no `export { … }`, no `export *`.
|
|
1008
1024
|
- The only import form is a **named import**:
|
|
1009
1025
|
`import { square, cube as pow3 } from "./math"`. `.ts` is optional. Default
|
|
1010
1026
|
imports, namespace imports, side-effect imports and type-only imports are all
|
|
@@ -1025,8 +1041,8 @@ export const main = (): i32 => {
|
|
|
1025
1041
|
`"engines": {"nish": ">=X.Y.Z"}` is above this compiler `` needs a newer
|
|
1026
1042
|
compiler ``, and a `package.json` that is not JSON names its line and
|
|
1027
1043
|
column — each with its own `--json` code (`NL3017`–`NL3021`).
|
|
1028
|
-
- Functions may be renamed on import; classes and interfaces may
|
|
1029
|
-
name is part of the ABI.
|
|
1044
|
+
- Functions and enums may be renamed on import; classes and interfaces may
|
|
1045
|
+
not — the type name is part of the ABI.
|
|
1030
1046
|
- Import cycles are allowed; a shared dependency is compiled once.
|
|
1031
1047
|
- **A function name is unique across the whole program**, exported or not.
|
|
1032
1048
|
- An `export`ed function is an external C-ABI symbol; every other function gets
|
|
@@ -1048,7 +1064,8 @@ export const main = (): i32 => {
|
|
|
1048
1064
|
- `for (init; cond; update)` with every clause optional, and
|
|
1049
1065
|
`for (const x of xs)` over an **array** only — `x` gets the element type and
|
|
1050
1066
|
must not be annotated. The array's `length` is re-read each iteration, so a
|
|
1051
|
-
`push` inside the body extends the loop.
|
|
1067
|
+
`push` inside the body extends the loop. `for...in`, `for await` and
|
|
1068
|
+
`for (x of xs)` over an existing `x` are each refused by name.
|
|
1052
1069
|
- `switch` takes an **integer or enum** discriminant; every `case` label is an
|
|
1053
1070
|
integer constant expression of that type. **There is no implicit
|
|
1054
1071
|
fallthrough** — a clause with statements ends in `break`, `return`,
|
|
@@ -1133,9 +1150,19 @@ constants `Math.PI` / `Math.E`. The f64-only ones reject an `i32`: write
|
|
|
1133
1150
|
`Number(x)`, `parseInt(s)` (base 10, `i32`, no `NaN` — no digits give `0`),
|
|
1134
1151
|
`parseFloat(s)`.
|
|
1135
1152
|
|
|
1153
|
+
**Constant time.** `ctSelect(mask, a, b)` is `(a & mask) | (b & ~mask)` and
|
|
1154
|
+
`ctEq(a, b)` is all-ones when equal and zero otherwise, both over `u32` or
|
|
1155
|
+
`u64` only, every operand one type. The mask sits behind an optimisation
|
|
1156
|
+
barrier, so neither ever becomes a branch; select and compare on a secret with
|
|
1157
|
+
these, never with `if`, `?:`, `===` or `table[secret]`, which they cannot fix.
|
|
1158
|
+
|
|
1136
1159
|
**Arrays.** `a.length` (read-only), `a.push(v)`, `a.pop()` (panics when empty —
|
|
1137
1160
|
there is no `undefined` to return), `a.indexOf(v)`, `a.join(sep)` — **`join` is
|
|
1138
|
-
`string[]` only
|
|
1161
|
+
`string[]` only** — and, on an array of numbers only, `dst.set(src[, offset])`
|
|
1162
|
+
(one `memmove`; a range past the end panics) and `a.fill(v[, start[, end]])`
|
|
1163
|
+
(ends clamped and counted back from the end when negative, as in JavaScript;
|
|
1164
|
+
never panics). Both are statements. That is every array method there is. A
|
|
1165
|
+
window into a buffer is `(buf, offset, length)`; there is no view type.
|
|
1139
1166
|
|
|
1140
1167
|
**Strings.** `s.length` (**bytes**), `s.charCodeAt(i)` (the byte, bounds-checked),
|
|
1141
1168
|
`s.substring(start[, end])`, `s.slice(start[, end])`, `s.indexOf(sub)`,
|
|
@@ -1151,15 +1178,24 @@ in half is possible.
|
|
|
1151
1178
|
`process.platform`, `process.arch`.
|
|
1152
1179
|
|
|
1153
1180
|
**Files and the system.** `readFileSync(path)` (exits 1 on failure),
|
|
1154
|
-
`readFileSyncOrNull(path)` (`string | null`), `
|
|
1181
|
+
`readFileSyncOrNull(path)` (`string | null`), `readFileBytesSync(path)`
|
|
1182
|
+
(`u8[] | null`, the bytes as they are on disk), `writeFileSync(path, data)`,
|
|
1155
1183
|
`appendFileSync(path, data)`, `mkdirSync(path)` (one level, `boolean`),
|
|
1156
1184
|
`isDirectorySync(path)`, `readdirSync(path)` (`string[] | null`, sorted by
|
|
1157
1185
|
bytes, no `.`/`..`), `spawnSync(argv)`, `spawnSyncTo(argv, outPath, errPath)`,
|
|
1158
1186
|
`getenv(name)` (`string | null` — unset and empty are different answers),
|
|
1159
1187
|
`realpathSync(path)` (`string | null`; symbolic links resolved, absolute, and
|
|
1160
1188
|
`null` when it does not resolve),
|
|
1161
|
-
`monotonicNanos()` (`i64`; elapsed time only
|
|
1162
|
-
|
|
1189
|
+
`monotonicNanos()` (`i64`; elapsed time only).
|
|
1190
|
+
|
|
1191
|
+
**The host.** `Date.now()` (an `f64` of whole milliseconds, the wall clock —
|
|
1192
|
+
**the only `Date` there is**: `new Date()`, `Date.parse` and the rest are
|
|
1193
|
+
refused), `crypto.getRandomValues(bytes)` (a statement over a `u8[]` only, at
|
|
1194
|
+
most 65,536 bytes a call, from the kernel's CSPRNG; more panics),
|
|
1195
|
+
`statMtimeSync(path)` (an `f64` of milliseconds, **NaN** — not `null` — when the
|
|
1196
|
+
path cannot be stat'd, so test `m !== m`), and `signalFd()` / `readSignal(fd)`:
|
|
1197
|
+
call `signalFd()` once at the top of `main`, then `readSignal(fd)` blocks until
|
|
1198
|
+
SIGTERM or SIGINT and answers 15 or 2. None of them exists on a wasm target.
|
|
1163
1199
|
|
|
1164
1200
|
**Arena.** `Arena.mark()`, `Arena.release(m)`, `Arena.reset()`, `Arena.used()`
|
|
1165
1201
|
— see below.
|
|
@@ -1376,8 +1412,9 @@ export const main = (): i32 => {
|
|
|
1376
1412
|
**Optional parameters** — there are none. Write two functions with different
|
|
1377
1413
|
names, or take the value and document the sentinel.
|
|
1378
1414
|
|
|
1379
|
-
**A
|
|
1380
|
-
|
|
1415
|
+
**A calendar, regex, JSON, threads of your own** — not in the language (the
|
|
1416
|
+
wall clock is `Date.now()`, and that is all of `Date`). Say so rather than
|
|
1417
|
+
emitting code that cannot compile.
|
|
1381
1418
|
|
|
1382
1419
|
## Before you say it compiles
|
|
1383
1420
|
|
package/llms.txt
CHANGED
|
@@ -26,7 +26,7 @@ the compiler.
|
|
|
26
26
|
- [Rules card](https://raw.githubusercontent.com/amritk/nish/main/docs/AI.md): the whole language as rules, sized for one read — the traps first, then types, `Result`, nullables, declarations, statements, the complete builtin inventory, and recipes for what is missing. Ships in the npm package as `docs/AI.md`.
|
|
27
27
|
- [Language reference](https://raw.githubusercontent.com/amritk/nish/main/docs/LANGUAGE.md): normative and exhaustive. Every rule cites the test that pins it and quotes the exact rejection message. Settles any disagreement with the rules card.
|
|
28
28
|
- [Ambient declarations](https://raw.githubusercontent.com/amritk/nish/main/runtime/nish.d.ts): reference this from `tsconfig.json` and `tsc --strict`, your editor and your language server accept `Result<T, E>`, `i32` and the rest. `nish` is still the authority — `tsc` cannot see the flow rules.
|
|
29
|
-
- [Standard library](https://raw.githubusercontent.com/amritk/nish/main/std/README.md): `std/testing`, a suite a program drives to check itself and answer an exit code; `std/text`, the `split` / `trim` / `replace` the language does not have; `std/json`, the value of one field of one flat JSON object — enough to read this compiler's own `--json` output. Source rather than a built library, imported
|
|
29
|
+
- [Standard library](https://raw.githubusercontent.com/amritk/nish/main/std/README.md): `std/testing`, a suite a program drives to check itself and answer an exit code; `std/text`, the `split` / `trim` / `replace` the language does not have; `std/json`, the value of one field of one flat JSON object — enough to read this compiler's own `--json` output; `nish/crypto`, SHA-256, SHA-384 and SHA-512, HMAC, HKDF, a constant-time compare, base64url and X25519, one module each (`nish/crypto/sha256` and so on). Source rather than a built library, imported as `nish/<module>`, and shipped in the npm package under `std/`.
|
|
30
30
|
- [FAQ](https://raw.githubusercontent.com/amritk/nish/main/docs/FAQ.md): why no `any`, why `i32`, how to get JavaScript numbers, why no GC, overflow and `--wrapping`, what errors look like.
|
|
31
31
|
- [Install](https://raw.githubusercontent.com/amritk/nish/main/docs/INSTALL.md): prerequisites per OS, hello world, exit codes, troubleshooting.
|
|
32
32
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/nish",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Nish: an ahead-of-time compiler from a static subset of TypeScript to LLVM IR",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -77,10 +77,10 @@
|
|
|
77
77
|
"nish"
|
|
78
78
|
],
|
|
79
79
|
"optionalDependencies": {
|
|
80
|
-
"@amritk/nish-x86_64-linux": "0.
|
|
81
|
-
"@amritk/nish-aarch64-linux": "0.
|
|
82
|
-
"@amritk/nish-aarch64-darwin": "0.
|
|
83
|
-
"@amritk/nish-x86_64-darwin": "0.
|
|
80
|
+
"@amritk/nish-x86_64-linux": "0.14.0",
|
|
81
|
+
"@amritk/nish-aarch64-linux": "0.14.0",
|
|
82
|
+
"@amritk/nish-aarch64-darwin": "0.14.0",
|
|
83
|
+
"@amritk/nish-x86_64-darwin": "0.14.0"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@biomejs/biome": "2.5.12",
|
package/runtime/nish.d.ts
CHANGED
|
@@ -161,6 +161,18 @@ declare function toF64(x: number | boolean): f64;
|
|
|
161
161
|
declare function f64ToBits(x: f64): i64;
|
|
162
162
|
declare function bitsToF64(bits: i64): f64;
|
|
163
163
|
|
|
164
|
+
// ---- Constant time (WP34 N6) ------------------------------------------------
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* `(a & mask) | (b & ~mask)` with the mask hidden from the optimiser, so it is
|
|
168
|
+
* never turned into a branch: `a` for an all-ones mask, `b` for zero. Every
|
|
169
|
+
* operand is one type, `u32` or `u64`. Both are `number` here, so one generic
|
|
170
|
+
* declaration stands for the two.
|
|
171
|
+
*/
|
|
172
|
+
declare function ctSelect<T extends u32 | u64>(mask: T, a: T, b: T): T;
|
|
173
|
+
/** All-ones of the operands' type when `a === b`, zero otherwise, without a branch. */
|
|
174
|
+
declare function ctEq<T extends u32 | u64>(a: T, b: T): T;
|
|
175
|
+
|
|
164
176
|
// ---- Streams and files (globals: Nish has no package resolution) ---------
|
|
165
177
|
|
|
166
178
|
/** `s` to stdout with no trailing newline and no conversion. */
|
|
@@ -182,6 +194,12 @@ declare function panic(message: string): never;
|
|
|
182
194
|
declare function readFileSync(path: string): string;
|
|
183
195
|
/** The same read, answering `null` for every path the other exits over. */
|
|
184
196
|
declare function readFileSyncOrNull(path: string): string | null;
|
|
197
|
+
/**
|
|
198
|
+
* The file's bytes as they are on disk — no UTF-8 assumed, so a zero byte and
|
|
199
|
+
* a byte of 0x80 or above survive — or `null` for every path
|
|
200
|
+
* `readFileSyncOrNull` answers `null` for.
|
|
201
|
+
*/
|
|
202
|
+
declare function readFileBytesSync(path: string): u8[] | null;
|
|
185
203
|
declare function writeFileSync(path: string, data: string): void;
|
|
186
204
|
declare function appendFileSync(path: string, data: string): void;
|
|
187
205
|
/** One directory, not recursive; whether a directory is there afterwards. */
|
|
@@ -213,6 +231,34 @@ declare function getenv(name: string): string | null;
|
|
|
213
231
|
* origin is arbitrary, so only the difference between two reads is meaningful.
|
|
214
232
|
*/
|
|
215
233
|
declare function monotonicNanos(): i64;
|
|
234
|
+
/**
|
|
235
|
+
* The modification time of `path` in milliseconds since the epoch, with the
|
|
236
|
+
* sub-millisecond fraction the file system keeps (Node's `mtimeMs`), or NaN
|
|
237
|
+
* when it cannot be stat'd. Follows a symbolic link; a directory has one too.
|
|
238
|
+
*/
|
|
239
|
+
declare function statMtimeSync(path: string): f64;
|
|
240
|
+
/**
|
|
241
|
+
* A descriptor that becomes readable when SIGTERM or SIGINT arrives, whichever
|
|
242
|
+
* thread the signal lands on, made once (every call answers the same one), or -1.
|
|
243
|
+
*/
|
|
244
|
+
declare function signalFd(): i32;
|
|
245
|
+
/**
|
|
246
|
+
* Block until SIGTERM or SIGINT arrives and answer its number, 15 or 2; -1 for
|
|
247
|
+
* any `fd` that is not `signalFd()`'s. No reading under Node, which throws.
|
|
248
|
+
*/
|
|
249
|
+
declare function readSignal(fd: i32): i32;
|
|
250
|
+
|
|
251
|
+
// ---- `Date` and `crypto` (WP34 N3) -----------------------------------------------
|
|
252
|
+
//
|
|
253
|
+
// `lib.es2022` already declares `Date`, whose `now()` is Nish's one member of it,
|
|
254
|
+
// so nothing is added: `tsc` accepting `new Date()` is `tsc` not being Nish's
|
|
255
|
+
// checker. `crypto` is a Web API that `lib.es2022` leaves out, so it is
|
|
256
|
+
// declared with its one member, typed as Nish types it.
|
|
257
|
+
|
|
258
|
+
declare var crypto: {
|
|
259
|
+
/** Every byte of `bytes` from the system's CSPRNG; at most 65,536 bytes a call. */
|
|
260
|
+
getRandomValues(bytes: u8[]): void;
|
|
261
|
+
};
|
|
216
262
|
|
|
217
263
|
// ---- The builtin modules (`nish:`) ---------------------------------------------
|
|
218
264
|
//
|
|
@@ -229,6 +275,7 @@ declare function monotonicNanos(): i64;
|
|
|
229
275
|
declare module "nish:fs" {
|
|
230
276
|
export function readFileSync(path: string): string;
|
|
231
277
|
export function readFileSyncOrNull(path: string): string | null;
|
|
278
|
+
export function readFileBytesSync(path: string): u8[] | null;
|
|
232
279
|
export function writeFileSync(path: string, data: string): void;
|
|
233
280
|
export function appendFileSync(path: string, data: string): void;
|
|
234
281
|
/** `true` when the directory was created, `false` when it already existed. */
|
|
@@ -240,6 +287,8 @@ declare module "nish:fs" {
|
|
|
240
287
|
*/
|
|
241
288
|
export function readdirSync(path: string): string[] | null;
|
|
242
289
|
export function realpathSync(path: string): string | null;
|
|
290
|
+
/** Node's `mtimeMs` for the path, or NaN when it cannot be stat'd. */
|
|
291
|
+
export function statMtimeSync(path: string): f64;
|
|
243
292
|
}
|
|
244
293
|
|
|
245
294
|
declare module "nish:process" {
|
|
@@ -257,6 +306,10 @@ declare module "nish:process" {
|
|
|
257
306
|
* difference between two reads is meaningful.
|
|
258
307
|
*/
|
|
259
308
|
export function monotonicNanos(): i64;
|
|
309
|
+
/** A descriptor readable when SIGTERM or SIGINT arrives; the same one every call, or -1. */
|
|
310
|
+
export function signalFd(): i32;
|
|
311
|
+
/** Block until SIGTERM or SIGINT arrives: 15 or 2, or -1 for a descriptor that is not `signalFd()`'s. */
|
|
312
|
+
export function readSignal(fd: i32): i32;
|
|
260
313
|
/** The command line; `argv[0]` is the program path, as in C. Read-only. */
|
|
261
314
|
export const argv: readonly string[];
|
|
262
315
|
/** The operating system the program runs on: `"linux"`, `"darwin"`, or `"unknown"`. */
|
|
@@ -303,6 +356,18 @@ declare interface CPtr {
|
|
|
303
356
|
readonly __nishForeignPointer: unique symbol;
|
|
304
357
|
}
|
|
305
358
|
|
|
359
|
+
// ---- `set` on an array ----------------------------------------------------------
|
|
360
|
+
//
|
|
361
|
+
// `dst.set(src, offset)` copies all of `src` into `dst` from `offset` on, with
|
|
362
|
+
// `TypedArray.prototype.set`'s meaning (WP34 N2). A `u8[]` is an `Array` to
|
|
363
|
+
// `tsc`, and `lib.es5.d.ts` gives `Array` a `fill` of the same meaning but no
|
|
364
|
+
// `set`, so this is the one method added to it; `runtime/nish.mjs` installs it
|
|
365
|
+
// under Node. Nish admits it on an array of numbers only.
|
|
366
|
+
|
|
367
|
+
interface Array<T> {
|
|
368
|
+
set(source: readonly T[], offset?: number): void;
|
|
369
|
+
}
|
|
370
|
+
|
|
306
371
|
// ---- What this file cannot say ----------------------------------------------
|
|
307
372
|
//
|
|
308
373
|
// The typed-array aliases (`Int32Array`, `Float32Array`, `Float64Array`,
|
package/runtime/nish.h
CHANGED
|
@@ -243,6 +243,12 @@ bool nish_is_dir(const nish_str *path);
|
|
|
243
243
|
* under WASI: `fd_readdir` lists a preopened directory rather than a path, so
|
|
244
244
|
* porting this there is a different contract and not a translation. */
|
|
245
245
|
nish_array *nish_readdir(const nish_str *path);
|
|
246
|
+
/* `readFileBytesSync(path)` (WP34 N2): the file's bytes as a `u8[]` with
|
|
247
|
+
* `len == cap`, zero bytes and bytes of 0x80 and above kept as they are, or
|
|
248
|
+
* NULL for every path `nish_read_file_or_null` answers NULL for. The header
|
|
249
|
+
* and the bytes live in the arena, so copy what you keep before the next reset
|
|
250
|
+
* or release. */
|
|
251
|
+
nish_array *nish_read_file_bytes(const nish_str *path);
|
|
246
252
|
/* `spawnSync(argv)`: run element 0 of `argv` (searched on `PATH`) with `argv`
|
|
247
253
|
* as its argument vector, wait for it, and answer its exit status, or
|
|
248
254
|
* `128 + n` when signal `n` killed it. -1 when `argv` is empty, when the
|
|
@@ -306,6 +312,31 @@ const nish_str *nish_arch(void);
|
|
|
306
312
|
* region into one and measure zero. */
|
|
307
313
|
int64_t nish_monotonic_nanos(void);
|
|
308
314
|
|
|
315
|
+
/* ---- The host (WP34 N3), runtime/runtime-host.c --------------------------
|
|
316
|
+
* `Date.now()`: the wall clock, `CLOCK_REALTIME`, in whole milliseconds since
|
|
317
|
+
* the epoch, as JavaScript answers it. Unlike `nish_monotonic_nanos` it can go
|
|
318
|
+
* backwards when the clock is corrected; it is for a time a person or a
|
|
319
|
+
* certificate means, not for measuring an interval. */
|
|
320
|
+
double nish_date_now(void);
|
|
321
|
+
/* `crypto.getRandomValues(bytes)`: fills `bytes->len` bytes from the kernel's
|
|
322
|
+
* CSPRNG (`getrandom` on Linux, `getentropy` elsewhere). More than 65,536
|
|
323
|
+
* bytes, the Web API's limit for one call, and a failing entropy source both
|
|
324
|
+
* print a message and exit 1; nothing weaker is ever substituted. */
|
|
325
|
+
void nish_random_fill(nish_array *bytes);
|
|
326
|
+
/* `statMtimeSync(path)`: the modification time in milliseconds with its
|
|
327
|
+
* sub-millisecond fraction, computed as Node's `mtimeMs` is, or NaN when
|
|
328
|
+
* `stat` fails. Follows a symbolic link. */
|
|
329
|
+
double nish_stat_mtime(const nish_str *path);
|
|
330
|
+
/* `signalFd()`: a descriptor that becomes readable when SIGTERM or SIGINT
|
|
331
|
+
* arrives: the read end of a pipe that a `sigaction` handler writes each
|
|
332
|
+
* signal's number to, whichever thread the signal lands on. Nothing is
|
|
333
|
+
* blocked. Made once; every call answers the same descriptor, and -1 when it
|
|
334
|
+
* could not be made. `readSignal(fd)`: block until one arrives and
|
|
335
|
+
* answer its number (15 or 2), or -1 for any `fd` that is not that descriptor
|
|
336
|
+
* or a read that fails. */
|
|
337
|
+
int32_t nish_signal_fd(void);
|
|
338
|
+
int32_t nish_read_signal(int32_t fd);
|
|
339
|
+
|
|
309
340
|
/* String to number (WP7), ASCII whitespace only. mode 0 is `parseFloat`
|
|
310
341
|
* (longest JS decimal literal or `Infinity`, else NaN), mode 1 is `Number`
|
|
311
342
|
* (the whole string, trimmed; blank is 0; `0x` hex accepted, as in JS),
|
package/runtime/nish.mjs
CHANGED
|
@@ -84,6 +84,7 @@ provide("writeError", shim.writeError);
|
|
|
84
84
|
provide("panic", shim.panic);
|
|
85
85
|
provide("readFileSync", shim.readFileSync);
|
|
86
86
|
provide("readFileSyncOrNull", shim.readFileSyncOrNull);
|
|
87
|
+
provide("readFileBytesSync", shim.readFileBytesSync);
|
|
87
88
|
provide("writeFileSync", shim.writeFileSync);
|
|
88
89
|
provide("appendFileSync", shim.appendFileSync);
|
|
89
90
|
provide("mkdirSync", shim.mkdirSync);
|
|
@@ -93,6 +94,35 @@ provide("realpathSync", shim.realpathSync);
|
|
|
93
94
|
provide("spawnSync", shim.spawnSync);
|
|
94
95
|
provide("spawnSyncTo", shim.spawnSyncTo);
|
|
95
96
|
provide("getenv", shim.getenv);
|
|
97
|
+
provide("statMtimeSync", shim.statMtimeSync);
|
|
98
|
+
provide("signalFd", shim.signalFd);
|
|
99
|
+
provide("readSignal", shim.readSignal);
|
|
100
|
+
|
|
101
|
+
// `crypto.getRandomValues(bytes)` (WP34 N3). Node has the global, but it
|
|
102
|
+
// takes only a typed array and a `u8[]` is a plain `Array` here, so the one
|
|
103
|
+
// method is replaced on Node's own `crypto` object by one that fills a plain
|
|
104
|
+
// array too (and hands a typed array to the original).
|
|
105
|
+
Object.defineProperty(globalThis.crypto, "getRandomValues", {
|
|
106
|
+
value: shim.getRandomValues,
|
|
107
|
+
writable: true,
|
|
108
|
+
configurable: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// `dst.set(src, offset)` (WP34 N2). A `u8[]` is a plain `Array` here, which
|
|
112
|
+
// has `fill` with the typed array's meaning already but no `set`, so the one
|
|
113
|
+
// method is added to `Array.prototype` — non-enumerable, as a builtin method is,
|
|
114
|
+
// so no `for...in` sees it — unless something got there first. It copies the
|
|
115
|
+
// source before writing, as the native `memmove` does, and fails a range past
|
|
116
|
+
// the end with the native panic.
|
|
117
|
+
if (!("set" in Array.prototype)) {
|
|
118
|
+
Object.defineProperty(Array.prototype, "set", {
|
|
119
|
+
value: function set(source, offset) {
|
|
120
|
+
shim.arraySet(this, source, offset);
|
|
121
|
+
},
|
|
122
|
+
writable: true,
|
|
123
|
+
configurable: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
96
126
|
|
|
97
127
|
// The clock. `monotonicNanos()` answers an `i64`, so the value is a BigInt here
|
|
98
128
|
// as it is in the rewritten runner — and unusually for the i64 surface that is
|
|
@@ -117,6 +147,11 @@ provide("toU64", (x) => shim.convert(x, "f64", "u64"));
|
|
|
117
147
|
provide("f64ToBits", shim.f64ToBits);
|
|
118
148
|
provide("bitsToF64", shim.bitsToF64);
|
|
119
149
|
|
|
150
|
+
// Constant time (WP34 N6). Pure functions of their operands, so the answers
|
|
151
|
+
// agree with a native run; the timing does not, and is not claimed here.
|
|
152
|
+
provide("ctSelect", shim.ctSelect);
|
|
153
|
+
provide("ctEq", shim.ctEq);
|
|
154
|
+
|
|
120
155
|
// `Result`. Everything works but `orReturn`, which needs the caller's control
|
|
121
156
|
// flow and therefore the rewriter; see the header.
|
|
122
157
|
provide("Ok", shim.Ok);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* Nish runtime, the host half: the wall clock, entropy, a file's modification
|
|
2
|
+
* time and a descriptor that becomes readable when SIGTERM or SIGINT arrives
|
|
3
|
+
* (WP34 N3) — the four facts a program that runs for days asks of the machine
|
|
4
|
+
* it runs on.
|
|
5
|
+
*
|
|
6
|
+
* A translation unit of its own for the reason runtime-os.c is one: each file
|
|
7
|
+
* carries its own measured `.text*` ceiling in tests/run.js. runtime-os.c had
|
|
8
|
+
* 143 bytes of its ceiling left when these arrived, the four measure more than
|
|
9
|
+
* that, and a ceiling is not raised to make room (docs/MASTER_PLAN.md §2 has
|
|
10
|
+
* the numbers). Section GC still means a program that calls none of them pays
|
|
11
|
+
* for none of them, and scripts/build.sh pairs this file with runtime.c like
|
|
12
|
+
* the other two halves, so a link line still names one runtime.
|
|
13
|
+
*
|
|
14
|
+
* Every function here is platform code, and the two platforms differ in three
|
|
15
|
+
* places: `getrandom` on Linux and `getentropy` elsewhere, `st_mtim`
|
|
16
|
+
* against Darwin's `st_mtimespec`, and `pipe2` against `pipe` for the signal
|
|
17
|
+
* descriptor. A WASI build has none of this
|
|
18
|
+
* (the checker refuses all four under a wasm target), so there the file is
|
|
19
|
+
* empty.
|
|
20
|
+
*/
|
|
21
|
+
#if defined(__wasi__) || defined(__wasm__)
|
|
22
|
+
/* ISO C wants at least one declaration in a translation unit. */
|
|
23
|
+
typedef int nish_host_unused;
|
|
24
|
+
#else
|
|
25
|
+
#if defined(__linux__)
|
|
26
|
+
/* glibc declares `clock_gettime`, `sigaction` and `pipe2` under `-std=c11`
|
|
27
|
+
only with a feature macro, and `pipe2` only with this one. Darwin declares
|
|
28
|
+
everything by default and hides `getentropy` and the `st_mtimespec`
|
|
29
|
+
spelling behind a strict feature level, so it gets none. */
|
|
30
|
+
#define _GNU_SOURCE
|
|
31
|
+
#endif
|
|
32
|
+
#include <errno.h>
|
|
33
|
+
#include <signal.h>
|
|
34
|
+
#include <stdint.h>
|
|
35
|
+
#include <stdio.h>
|
|
36
|
+
#include <sys/stat.h>
|
|
37
|
+
#include <time.h>
|
|
38
|
+
#include <unistd.h>
|
|
39
|
+
#include <fcntl.h>
|
|
40
|
+
#include <sys/random.h>
|
|
41
|
+
|
|
42
|
+
#include "nish.h"
|
|
43
|
+
|
|
44
|
+
/* The spelling runtime.c and runtime-os.c use for their cold paths. */
|
|
45
|
+
#define NISH_COLD __attribute__((noreturn, cold, noinline))
|
|
46
|
+
|
|
47
|
+
/* `Date.now()`: `CLOCK_REALTIME` in whole milliseconds since the epoch, as
|
|
48
|
+
JavaScript answers it — the milliseconds are counted in integers and then
|
|
49
|
+
converted, so the answer is exact until the year 287,396 and a negative
|
|
50
|
+
time floors the way `Date.now` does rather than rounding towards zero. */
|
|
51
|
+
double nish_date_now(void) {
|
|
52
|
+
struct timespec ts;
|
|
53
|
+
clock_gettime(CLOCK_REALTIME, &ts);
|
|
54
|
+
return (double)((int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static NISH_COLD void nish_entropy_fail(uint64_t asked) {
|
|
58
|
+
if (asked > 65536) {
|
|
59
|
+
dprintf(2, "crypto.getRandomValues: %llu bytes asked for, and one call fills at most 65536\n",
|
|
60
|
+
(unsigned long long)asked);
|
|
61
|
+
} else {
|
|
62
|
+
dprintf(2, "crypto.getRandomValues: the system's entropy source failed\n");
|
|
63
|
+
}
|
|
64
|
+
_exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* `crypto.getRandomValues(bytes)`: every byte of `bytes` from the kernel's
|
|
68
|
+
CSPRNG, or a panic. There is no fallback to a weaker source, because a
|
|
69
|
+
program handed predictable bytes for a key or a nonce cannot tell. The Web
|
|
70
|
+
API's limit of 65,536 bytes a call is kept, so a program behaves the same
|
|
71
|
+
under Node. `getrandom` without flags blocks only until the pool is first
|
|
72
|
+
initialised at boot and can return short or be interrupted, so it loops;
|
|
73
|
+
`getentropy` fills at most 256 bytes a call and retries nothing itself. */
|
|
74
|
+
void nish_random_fill(nish_array *bytes) {
|
|
75
|
+
uint64_t n = bytes->len;
|
|
76
|
+
char *p = bytes->data;
|
|
77
|
+
if (n > 65536) nish_entropy_fail(n);
|
|
78
|
+
while (n > 0) {
|
|
79
|
+
#if defined(__linux__)
|
|
80
|
+
ssize_t got = getrandom(p, n, 0);
|
|
81
|
+
if (got < 0) {
|
|
82
|
+
if (errno == EINTR) continue;
|
|
83
|
+
nish_entropy_fail(0);
|
|
84
|
+
}
|
|
85
|
+
#else
|
|
86
|
+
size_t got = n < 256 ? n : 256;
|
|
87
|
+
if (getentropy(p, got) != 0) nish_entropy_fail(0);
|
|
88
|
+
#endif
|
|
89
|
+
p += got;
|
|
90
|
+
n -= (uint64_t)got;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/* `statMtimeSync(path)`: the modification time in milliseconds, with the
|
|
95
|
+
sub-millisecond part the file system keeps, or NaN when `stat` fails. The
|
|
96
|
+
arithmetic is Node's `mtimeMs` operation for operation — seconds times 1e3
|
|
97
|
+
plus nanoseconds over 1e6, in doubles — so the two readings print the same
|
|
98
|
+
digits. `stat` follows a symbolic link, as `fs.statSync` does. */
|
|
99
|
+
double nish_stat_mtime(const nish_str *path) {
|
|
100
|
+
struct stat st;
|
|
101
|
+
if (stat(path->data, &st) != 0) return __builtin_nan("");
|
|
102
|
+
#if defined(__APPLE__)
|
|
103
|
+
struct timespec t = st.st_mtimespec;
|
|
104
|
+
#else
|
|
105
|
+
struct timespec t = st.st_mtim;
|
|
106
|
+
#endif
|
|
107
|
+
return (double)t.tv_sec * 1e3 + (double)t.tv_nsec / 1e6;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ---- Signals: `signalFd()` and `readSignal(fd)`
|
|
111
|
+
*
|
|
112
|
+
* A descriptor rather than a handler in the language, because a handler would
|
|
113
|
+
* be a function value the language does not have, and because a descriptor is
|
|
114
|
+
* what a loop waiting in `epoll` or `poll` can wait on beside its sockets.
|
|
115
|
+
*
|
|
116
|
+
* Underneath it is a C handler writing each signal's number, one byte, to a
|
|
117
|
+
* pipe. It is not a `signalfd` on Linux, deliberately: a `signalfd` only
|
|
118
|
+
* hears a signal that is blocked in **every** thread, and a mask reaches only
|
|
119
|
+
* the calling thread, so a `scope()` task or a parallel worker already
|
|
120
|
+
* running when `signalFd()` was called would take the signal at its default
|
|
121
|
+
* action and end the process. A handler runs in whichever thread the kernel
|
|
122
|
+
* picks, so no thread's mask matters, and nothing is blocked for a child
|
|
123
|
+
* `spawnSync` starts to inherit: `exec` resets a caught signal to its
|
|
124
|
+
* default. Installing the handler also overrides a disposition the process
|
|
125
|
+
* was started with, so a signal its parent ignored is heard, as
|
|
126
|
+
* `process.on('SIGTERM')` hears it under Node.
|
|
127
|
+
*
|
|
128
|
+
* The handler is the whole of what runs in signal context, and `write` is
|
|
129
|
+
* async-signal-safe. The write end is non-blocking, so a thousand unread
|
|
130
|
+
* signals lose the newest rather than wedge the handler, and `errno` is put
|
|
131
|
+
* back for the code the signal interrupted. The descriptor is made once and
|
|
132
|
+
* every later `signalFd()` answers the same one. */
|
|
133
|
+
static int nish_signal_read_end = -1;
|
|
134
|
+
static int nish_signal_write_end = -1;
|
|
135
|
+
|
|
136
|
+
static void nish_on_signal(int sig) {
|
|
137
|
+
int saved = errno;
|
|
138
|
+
unsigned char b = (unsigned char)sig;
|
|
139
|
+
(void)!write(nish_signal_write_end, &b, 1);
|
|
140
|
+
errno = saved;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
int32_t nish_signal_fd(void) {
|
|
144
|
+
if (nish_signal_read_end >= 0) return nish_signal_read_end;
|
|
145
|
+
int p[2];
|
|
146
|
+
#if defined(__linux__)
|
|
147
|
+
/* Both ends close-on-exec from the moment they exist, so a child a sibling
|
|
148
|
+
thread spawns meanwhile cannot inherit either. */
|
|
149
|
+
if (pipe2(p, O_CLOEXEC) != 0) return -1;
|
|
150
|
+
#else
|
|
151
|
+
/* Darwin has no `pipe2`, so there is a window between `pipe` and the two
|
|
152
|
+
`fcntl`s in which a child spawned by another thread inherits the ends. */
|
|
153
|
+
if (pipe(p) != 0) return -1;
|
|
154
|
+
fcntl(p[0], F_SETFD, FD_CLOEXEC);
|
|
155
|
+
fcntl(p[1], F_SETFD, FD_CLOEXEC);
|
|
156
|
+
#endif
|
|
157
|
+
fcntl(p[1], F_SETFL, O_NONBLOCK);
|
|
158
|
+
nish_signal_write_end = p[1];
|
|
159
|
+
struct sigaction sa;
|
|
160
|
+
sa.sa_handler = nish_on_signal;
|
|
161
|
+
sa.sa_flags = SA_RESTART;
|
|
162
|
+
sigemptyset(&sa.sa_mask);
|
|
163
|
+
sigaction(SIGINT, &sa, 0);
|
|
164
|
+
sigaction(SIGTERM, &sa, 0);
|
|
165
|
+
return nish_signal_read_end = p[0];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Block until a signal's byte arrives and answer it: 15 or 2, or -1 for any
|
|
169
|
+
`fd` that is not the signal descriptor and for a read that fails. */
|
|
170
|
+
int32_t nish_read_signal(int32_t fd) {
|
|
171
|
+
if (fd != nish_signal_read_end) return -1;
|
|
172
|
+
unsigned char b;
|
|
173
|
+
ssize_t n;
|
|
174
|
+
do n = read(fd, &b, 1);
|
|
175
|
+
while (n < 0 && errno == EINTR);
|
|
176
|
+
return n == 1 ? (int32_t)b : -1;
|
|
177
|
+
}
|
|
178
|
+
#endif
|
package/runtime/runtime-os.c
CHANGED
|
@@ -102,6 +102,21 @@ nish_str *nish_read_file_or_null(const nish_str *path) {
|
|
|
102
102
|
return s;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/* `readFileBytesSync(path)` (WP34 N2): the same read, handed to the language as
|
|
106
|
+
a `u8[]`. The bytes are not copied a second time: `nish_read_file_or_null`
|
|
107
|
+
has already put them in the arena behind their 8-byte length, 8-aligned, so
|
|
108
|
+
the header points `data` there with `len == cap`, and the NUL written after
|
|
109
|
+
them is simply never indexed. Nothing on the way assumes UTF-8, so a zero
|
|
110
|
+
byte and a byte of 0x80 or above come back as they are on disk. A `push`
|
|
111
|
+
onto the result grows it into a fresh block, as it would any full array. */
|
|
112
|
+
nish_array *nish_read_file_bytes(const nish_str *path) {
|
|
113
|
+
nish_str *s = nish_read_file_or_null(path);
|
|
114
|
+
if (!s) return 0;
|
|
115
|
+
nish_array *a = nish_alloc_struct(sizeof *a);
|
|
116
|
+
*a = (nish_array){ s->len, s->len, s->data };
|
|
117
|
+
return a;
|
|
118
|
+
}
|
|
119
|
+
|
|
105
120
|
nish_str *nish_read_file(const nish_str *path) {
|
|
106
121
|
nish_str *s = nish_read_file_or_null(path);
|
|
107
122
|
if (!s) nish_io_fail("read ", path);
|