@amritk/nish 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -23
- package/bin/launcher.js +45 -41
- package/bin/packaging.js +39 -33
- package/docs/AI.md +157 -35
- package/docs/INSTALL.md +13 -13
- package/llms.txt +1 -1
- package/package.json +9 -6
- package/runtime/nish.d.ts +85 -1
- package/runtime/nish.h +62 -11
- package/runtime/nish.mjs +38 -3
- package/runtime/runtime-host.c +178 -0
- package/runtime/{runtime_os.c → runtime-os.c} +16 -1
- package/runtime/{runtime_parallel.c → runtime-parallel.c} +112 -4
- package/runtime/{runtime_wasm.c → runtime-wasm.c} +6 -1
- package/runtime/runtime.c +5 -5
- package/runtime/shim.mjs +128 -6
- package/scripts/bootstrap.sh +29 -29
- package/scripts/build.sh +19 -13
- package/scripts/changelog-gen.mjs +260 -192
- package/scripts/ci-profile.mjs +80 -68
- package/scripts/codes-registry.js +25 -13
- package/scripts/gen-diagnostic-codes.mjs +124 -85
- package/scripts/nish-compiler.sh +2 -0
- package/scripts/platform-package.mjs +26 -26
- package/scripts/postinstall.mjs +46 -36
- package/scripts/size-report.sh +3 -3
- package/scripts/smoke.sh +1 -1
- package/std/README.md +42 -2
- package/std/collections.ts +194 -188
- 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/std/json.ts +136 -136
- package/std/map.ts +9 -7
- package/std/pair.ts +2 -2
- package/std/testing.ts +67 -67
- package/std/text.ts +54 -54
- package/std/threads.ts +126 -38
package/runtime/nish.h
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
*
|
|
3
3
|
* Include this from C drivers, N-API shims, or any other host that links the C
|
|
4
4
|
* runtime next to compiled Nish modules. Everything here is a contract shared
|
|
5
|
-
* with `
|
|
5
|
+
* with `src/runtime.ts` (the IR side) and the implementation, which is
|
|
6
6
|
* two translation units: `runtime/runtime.c` holds the core every program
|
|
7
7
|
* touches — the arena, strings, arrays, number formatting, the panics — and
|
|
8
|
-
* `runtime/
|
|
8
|
+
* `runtime/runtime-os.c` holds everything that wraps a system call: the file
|
|
9
9
|
* functions, the directory and subprocess calls, `nish_getenv`, the monotonic
|
|
10
10
|
* clock, `nish_platform` / `nish_arch`. They are separate so that each carries
|
|
11
11
|
* its own measured code-size ceiling (docs/wp7-runtime.md, "Runtime additions
|
|
@@ -41,7 +41,7 @@ extern "C" {
|
|
|
41
41
|
* ELF refuses to link that against a non-TLS definition. `scripts/build.sh
|
|
42
42
|
* --threads` passes the macro to every input, which is how `nish --threads
|
|
43
43
|
* --link` keeps the two halves in step. The same definition is in
|
|
44
|
-
* runtime/runtime.c and runtime/
|
|
44
|
+
* runtime/runtime.c and runtime/runtime-wasm.c. */
|
|
45
45
|
#ifdef NISH_THREADS
|
|
46
46
|
#define NISH_TLS _Thread_local
|
|
47
47
|
#else
|
|
@@ -73,7 +73,7 @@ typedef struct nish_str {
|
|
|
73
73
|
* `off` and `cap` are `uint64_t` and not `size_t` because that `i64` is what
|
|
74
74
|
* the inlined fast path bumps and compares on every target: under wasm32 a
|
|
75
75
|
* `size_t` pair would put them at bytes 4 and 8 while compiled code reads 8
|
|
76
|
-
* and 16. runtime.c and
|
|
76
|
+
* and 16. runtime.c and runtime-wasm.c static-assert these offsets. */
|
|
77
77
|
struct nish_arena {
|
|
78
78
|
char *buf;
|
|
79
79
|
uint64_t off;
|
|
@@ -140,7 +140,7 @@ nish_str *nish_str_from_u64(uint64_t v);
|
|
|
140
140
|
|
|
141
141
|
/* Process and file I/O (WP7). `nish_exit` never returns; the file functions
|
|
142
142
|
* print a message to stderr and exit(1) on a fatal error. Everything from here
|
|
143
|
-
* to the end of the clock section below is implemented in
|
|
143
|
+
* to the end of the clock section below is implemented in runtime-os.c, with
|
|
144
144
|
* `nish_random` and `nish_argv_init` the exceptions: neither asks the operating
|
|
145
145
|
* system anything (the clock only seeds the first, and the entry point hands the
|
|
146
146
|
* second its arguments), so both stay with the core. `nish_random` keeps one seed
|
|
@@ -181,11 +181,11 @@ void nish_append_file(const nish_str *path, const nish_str *data);
|
|
|
181
181
|
* A returned array lives in the arena (valid until the next reset/release):
|
|
182
182
|
* copy `len` elements out of `data` before recycling.
|
|
183
183
|
*
|
|
184
|
-
* An array field stored inside its object (`
|
|
184
|
+
* An array field stored inside its object (`src/inline-arrays.ts`) is this
|
|
185
185
|
* header followed by its `K` slots, `struct { nish_array h; T slots[K]; }`
|
|
186
|
-
* with `h.data == (char *)slots` and `h.cap == K`; `
|
|
187
|
-
* `ARRAY_TYPE` and `
|
|
188
|
-
* bytes, and `tests/layout/
|
|
186
|
+
* with `h.data == (char *)slots` and `h.cap == K`; `src/runtime.ts`'s
|
|
187
|
+
* `ARRAY_TYPE` and `src/structs.ts`'s `INLINE_HEADER_BYTES` are the same 24
|
|
188
|
+
* bytes, and `tests/layout/inline-array.c` holds the three to it. It only
|
|
189
189
|
* happens where no header, `.d.ts` or N-API shim describes the class, so a
|
|
190
190
|
* host that includes a generated header never meets one. */
|
|
191
191
|
typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
|
|
@@ -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
|
|
@@ -260,7 +266,7 @@ int32_t nish_spawn(const nish_array *argv);
|
|
|
260
266
|
* paths must differ: each is opened separately with its own offset, so naming
|
|
261
267
|
* one file twice makes the streams overwrite each other instead of
|
|
262
268
|
* interleaving; capture them apart and concatenate to merge them. One `static`
|
|
263
|
-
* implementation in
|
|
269
|
+
* implementation in runtime-os.c backs both spawn builtins, which is what keeps
|
|
264
270
|
* the argument vector, the wait and the signal convention written once. */
|
|
265
271
|
int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_str *err);
|
|
266
272
|
|
|
@@ -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),
|
|
@@ -316,7 +347,7 @@ double nish_parse_number(const nish_str *s, int32_t mode);
|
|
|
316
347
|
/* Checked integer division (Rust semantics): the failed-check path. */
|
|
317
348
|
void nish_panic_div(bool by_zero);
|
|
318
349
|
|
|
319
|
-
/* ---- Parallel work (WP20 T1 / wp29 stage P1), runtime/
|
|
350
|
+
/* ---- Parallel work (WP20 T1 / wp29 stage P1), runtime/runtime-parallel.c ----
|
|
320
351
|
*
|
|
321
352
|
* One region of work, divided. `nish_parallel_range` calls `body(lo, hi, ctx)`
|
|
322
353
|
* once per chunk of a partition of `[0, len)`: contiguous chunks, at most one
|
|
@@ -341,6 +372,26 @@ typedef void (*nish_par_body)(int64_t lo, int64_t hi, void *ctx);
|
|
|
341
372
|
int64_t nish_cpu_count(void);
|
|
342
373
|
void nish_parallel_range(nish_par_body body, void *ctx, int64_t len, int64_t grain);
|
|
343
374
|
|
|
375
|
+
/* ---- A scope's tasks (wp29 stage P2), runtime/runtime-parallel.c ----
|
|
376
|
+
*
|
|
377
|
+
* `nish_scope_spawn` files one task under `scope`, an address the caller keeps
|
|
378
|
+
* alive until it joins: it copies `size` bytes of `payload` and keeps them with
|
|
379
|
+
* `run` and `finish`, and runs nothing yet. `nish_scope_join` takes every task
|
|
380
|
+
* filed under `scope`, runs `run(payload)` for each -- the first on the calling
|
|
381
|
+
* thread and each other on a thread of its own, which frees its arena before it
|
|
382
|
+
* exits -- waits for all of them, and then calls `finish(payload)` for each on
|
|
383
|
+
* the calling thread, in the order they were filed. Without `-DNISH_THREADS`,
|
|
384
|
+
* or when a thread cannot be created, a task runs on the calling thread, so
|
|
385
|
+
* running short of threads costs speed and never an answer.
|
|
386
|
+
*
|
|
387
|
+
* The preconditions are the language's (docs/LANGUAGE.md, "Scoped tasks"):
|
|
388
|
+
* `run` writes nothing another task or the caller can see, its result is a
|
|
389
|
+
* scalar it writes into its own payload, and only `finish`, on the calling
|
|
390
|
+
* thread, stores into memory the caller owns. */
|
|
391
|
+
typedef void (*nish_task_fn)(void *payload);
|
|
392
|
+
void nish_scope_spawn(void *scope, nish_task_fn run, nish_task_fn finish, const void *payload, int64_t size);
|
|
393
|
+
void nish_scope_join(void *scope);
|
|
394
|
+
|
|
344
395
|
#ifdef __cplusplus
|
|
345
396
|
}
|
|
346
397
|
#endif
|
package/runtime/nish.mjs
CHANGED
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
* so is every string offset. ASCII agrees; nothing else does.
|
|
29
29
|
* - **`a[i]` is unchecked**: out of range is `undefined` here and an exit-1
|
|
30
30
|
* panic natively. Only a program that indexes out of range can tell.
|
|
31
|
-
* - **
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* - **A record put into an array is shared here** and copied natively, and
|
|
32
|
+
* `slice`, `new Array<T>(n)` and the typed-array names each differ too;
|
|
33
|
+
* the document lists them.
|
|
34
34
|
* - **`orReturn()` does not propagate.** It throws a marker the rewriter's
|
|
35
35
|
* `try`/`catch` turns into an early `return`; unmodified there is no
|
|
36
36
|
* `catch`, so it escapes. `Ok`/`Err`/`isOk`/`isErr`/`value`/`error`/
|
|
@@ -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
|
|
@@ -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);
|
|
@@ -319,7 +334,7 @@ nish_str *nish_realpath(const nish_str *path) {
|
|
|
319
334
|
this file is compiled — a cross build compiles the runtime for the target,
|
|
320
335
|
so the answer is the target's — which is why each is a string in constant
|
|
321
336
|
data handed back by address: no allocation and no load, and `readnone` on
|
|
322
|
-
the declaration (
|
|
337
|
+
the declaration (src/runtime.ts) is a fact rather than a hope. The
|
|
323
338
|
spellings are Node's, so a program reads the same answer from this runtime
|
|
324
339
|
and from `runtime/shim.mjs`; anything neither branch names is "unknown",
|
|
325
340
|
which is what `--target host` then refuses. Contracts: nish.h.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* Nish runtime, the parallel half: how one range of work becomes several
|
|
2
2
|
* threads, and nothing else.
|
|
3
3
|
*
|
|
4
|
-
* A third translation unit rather than a third of `
|
|
4
|
+
* A third translation unit rather than a third of `runtime-os.c`, for the
|
|
5
5
|
* reason that file's header gives for being a second one: a budget should mean
|
|
6
|
-
* one thing. `
|
|
6
|
+
* one thing. `runtime-os.c` is the syscall wrappers and it had 29 bytes of its
|
|
7
7
|
* ceiling left; `pthread_create` plus a partitioner does not fit in 29 bytes,
|
|
8
8
|
* and raising the syscall half's ceiling to make room would move the number a
|
|
9
9
|
* reader sees for "the operating-system surface" for a reason that has nothing
|
|
@@ -21,11 +21,16 @@
|
|
|
21
21
|
* function exists. The thread-local arena the divided case needs is WP20 T0 and
|
|
22
22
|
* arrives with the same macro.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* docs/wp29-thread-surface.md is the surface this is the stage under: P1's
|
|
25
|
+
* `parallelMapInto` and `parallelReduce` divide a range here, and P2's scopes
|
|
26
|
+
* run their tasks here (`nish_scope_spawn`, `nish_scope_join`).
|
|
26
27
|
*/
|
|
27
28
|
#include "nish.h"
|
|
28
29
|
|
|
30
|
+
#include <stdlib.h>
|
|
31
|
+
#include <string.h>
|
|
32
|
+
#include <unistd.h>
|
|
33
|
+
|
|
29
34
|
#if defined(NISH_THREADS) && !defined(__wasi__) && !defined(__wasm__)
|
|
30
35
|
#define NISH_PAR_REAL 1
|
|
31
36
|
#include <pthread.h>
|
|
@@ -154,3 +159,106 @@ void nish_parallel_range(nish_par_body body, void *ctx, int64_t len, int64_t gra
|
|
|
154
159
|
body(0, len, ctx);
|
|
155
160
|
#endif
|
|
156
161
|
}
|
|
162
|
+
|
|
163
|
+
/* ---- A scope's tasks (wp29 P2) ----
|
|
164
|
+
*
|
|
165
|
+
* A task is filed when it is spawned and run when its scope joins, which is
|
|
166
|
+
* what keeps a scope race-free without a borrow checker: while the tasks run,
|
|
167
|
+
* the thread that opened the scope is inside the join and runs nothing else,
|
|
168
|
+
* the tasks write nothing but their own payloads (the language's rule), and
|
|
169
|
+
* every store into the caller's memory is a `finish` made on the caller's
|
|
170
|
+
* thread after the last task has finished.
|
|
171
|
+
*
|
|
172
|
+
* The filed tasks are one list per thread, newest first, and each carries the
|
|
173
|
+
* scope it belongs to. Only the thread that opened a scope files into it or
|
|
174
|
+
* joins it, so the list needs no lock, and scopes may nest: a join takes its
|
|
175
|
+
* own scope's tasks out of the list and leaves the rest. */
|
|
176
|
+
typedef struct nish_task {
|
|
177
|
+
struct nish_task *next;
|
|
178
|
+
void *scope;
|
|
179
|
+
nish_task_fn run;
|
|
180
|
+
nish_task_fn finish;
|
|
181
|
+
#ifdef NISH_PAR_REAL
|
|
182
|
+
pthread_t th;
|
|
183
|
+
int started;
|
|
184
|
+
#endif
|
|
185
|
+
/* The copied payload, aligned for any field it holds. */
|
|
186
|
+
uint64_t payload[];
|
|
187
|
+
} nish_task;
|
|
188
|
+
|
|
189
|
+
static NISH_TLS nish_task *nish_tasks = 0;
|
|
190
|
+
|
|
191
|
+
void nish_scope_spawn(void *scope, nish_task_fn run, nish_task_fn finish, const void *payload, int64_t size) {
|
|
192
|
+
nish_task *t = (nish_task *)malloc(sizeof(nish_task) + (size_t)size);
|
|
193
|
+
/* Out of memory is the end of the program, as it is for every other
|
|
194
|
+
* allocation the runtime makes: running the task here instead would store
|
|
195
|
+
* its answer ahead of tasks filed before it, and a later spawn into the same
|
|
196
|
+
* slot has to be the one that stays. */
|
|
197
|
+
if (!t) {
|
|
198
|
+
(void)!write(2, "nish: out of memory\n", 20);
|
|
199
|
+
_exit(1);
|
|
200
|
+
}
|
|
201
|
+
t->scope = scope;
|
|
202
|
+
t->run = run;
|
|
203
|
+
t->finish = finish;
|
|
204
|
+
memcpy(t->payload, payload, (size_t)size);
|
|
205
|
+
t->next = nish_tasks;
|
|
206
|
+
nish_tasks = t;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#ifdef NISH_PAR_REAL
|
|
210
|
+
static void *nish_task_worker(void *p) {
|
|
211
|
+
nish_task *t = (nish_task *)p;
|
|
212
|
+
/* A task is one thread's work, as a chunk is, so a region inside it runs on
|
|
213
|
+
* this thread rather than multiplying the threads. */
|
|
214
|
+
nish_par_depth = 1;
|
|
215
|
+
t->run(t->payload);
|
|
216
|
+
nish_free_arena(); /* as `nish_par_worker`: this thread's arena, and only it */
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
#endif
|
|
220
|
+
|
|
221
|
+
void nish_scope_join(void *scope) {
|
|
222
|
+
/* This scope's tasks, oldest first: the list is newest first, and moving each
|
|
223
|
+
* one to the front of `mine` reverses it. */
|
|
224
|
+
nish_task *mine = 0;
|
|
225
|
+
nish_task **link = &nish_tasks;
|
|
226
|
+
while (*link) {
|
|
227
|
+
nish_task *t = *link;
|
|
228
|
+
if (t->scope == scope) {
|
|
229
|
+
*link = t->next;
|
|
230
|
+
t->next = mine;
|
|
231
|
+
mine = t;
|
|
232
|
+
} else {
|
|
233
|
+
link = &t->next;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (!mine) return;
|
|
237
|
+
#ifdef NISH_PAR_REAL
|
|
238
|
+
/* Every task but the first on a thread of its own, and the first on this
|
|
239
|
+
* one, so N tasks cost N-1 spawns and this thread is not idle. A task inside
|
|
240
|
+
* a region or another task's thread runs here, as a nested region does. */
|
|
241
|
+
for (nish_task *t = mine->next; t; t = t->next) {
|
|
242
|
+
t->started = nish_par_depth == 0 && pthread_create(&t->th, 0, nish_task_worker, t) == 0;
|
|
243
|
+
}
|
|
244
|
+
nish_par_depth++;
|
|
245
|
+
mine->run(mine->payload);
|
|
246
|
+
for (nish_task *t = mine->next; t; t = t->next) {
|
|
247
|
+
if (!t->started) t->run(t->payload);
|
|
248
|
+
}
|
|
249
|
+
nish_par_depth--;
|
|
250
|
+
for (nish_task *t = mine->next; t; t = t->next) {
|
|
251
|
+
if (t->started) pthread_join(t->th, 0);
|
|
252
|
+
}
|
|
253
|
+
#else
|
|
254
|
+
for (nish_task *t = mine; t; t = t->next) t->run(t->payload);
|
|
255
|
+
#endif
|
|
256
|
+
/* The stores, in the order the tasks were spawned, so two tasks with one
|
|
257
|
+
* destination leave the later one's answer there, as they would in sequence. */
|
|
258
|
+
while (mine) {
|
|
259
|
+
nish_task *t = mine;
|
|
260
|
+
mine = t->next;
|
|
261
|
+
t->finish(t->payload);
|
|
262
|
+
free(t);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Nish runtime for the freestanding wasm profile (WP8): the arena and the
|
|
2
2
|
* array cold paths, without libc. Link it next to the module when a function
|
|
3
3
|
* takes or returns an array:
|
|
4
|
-
* scripts/build.sh x.ll runtime/
|
|
4
|
+
* scripts/build.sh x.ll runtime/runtime-wasm.c -o x.wasm --profile wasm
|
|
5
5
|
*
|
|
6
6
|
* Linear memory past `__heap_base` (the linker's end-of-data symbol) is one
|
|
7
7
|
* arena chunk that grows with `memory.grow`; there is no chunk list because
|
|
@@ -97,3 +97,8 @@ void nish_panic_index(uint64_t idx, uint64_t len) { (void)idx; (void)len; __buil
|
|
|
97
97
|
void nish_panic_slice(int64_t s, int64_t e, int64_t len) { (void)s; (void)e; (void)len; __builtin_trap(); }
|
|
98
98
|
void nish_panic_div(_Bool by_zero) { (void)by_zero; __builtin_trap(); }
|
|
99
99
|
void nish_exit(int32_t code) { (void)code; __builtin_trap(); }
|
|
100
|
+
/* A panic's message (`panic`, `expect`, a failed range entry) has no stderr to
|
|
101
|
+
* go to here; the `nish_exit` that follows it is the trap. An exported function
|
|
102
|
+
* with a ranged parameter checks it on entry (WP31 §9), so this is what lets
|
|
103
|
+
* such a module link; the loader throws a RangeError before the call reaches it. */
|
|
104
|
+
void nish_write(const void *s, int32_t fd, _Bool newline) { (void)s; (void)fd; (void)newline; }
|
package/runtime/runtime.c
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* it is also the half with the tighter size budget: the arena, strings, number
|
|
5
5
|
* formatting, the array cold paths, `process.argv`, `Math.random`, and the two
|
|
6
6
|
* panics. Files, directories, subprocesses, the environment and the clock are
|
|
7
|
-
* in
|
|
7
|
+
* in runtime-os.c — every one of those wraps a system call, so that surface
|
|
8
8
|
* grows as the language reaches further into the operating system, and a
|
|
9
9
|
* program that reaches nowhere should not pay for it or be measured with it.
|
|
10
|
-
*
|
|
10
|
+
* runtime-os.c's header comment has the reasoning; tests/run.js gates the two
|
|
11
11
|
* `.text*` budgets separately and docs/wp7-runtime.md records both.
|
|
12
12
|
*
|
|
13
|
-
* Nothing here calls into
|
|
13
|
+
* Nothing here calls into runtime-os.c, which is why an old link line that
|
|
14
14
|
* names runtime.c alone still builds a program that uses none of that surface.
|
|
15
15
|
* The other direction does happen: `nish_readdir` allocates through
|
|
16
16
|
* `nish_alloc_struct` and `nish_str_new`, so those calls no longer inline into
|
|
@@ -50,7 +50,7 @@ int __main_argc_argv(int argc, char **argv) { return nish_c_main(argc, argv); }
|
|
|
50
50
|
definition. Off by default so the ordinary build pays nothing.
|
|
51
51
|
|
|
52
52
|
The same definition is in runtime/nish.h (for a host that includes it) and
|
|
53
|
-
runtime/
|
|
53
|
+
runtime/runtime-wasm.c; they are one contract and move together. */
|
|
54
54
|
#ifdef NISH_THREADS
|
|
55
55
|
#define NISH_TLS _Thread_local
|
|
56
56
|
#else
|
|
@@ -59,7 +59,7 @@ int __main_argc_argv(int argc, char **argv) { return nish_c_main(argc, argv); }
|
|
|
59
59
|
|
|
60
60
|
/* ---- Arena: %struct.nish_arena = type { i8*, i64, i64, i8* }
|
|
61
61
|
|
|
62
|
-
The widths are fixed rather than `size_t` for the reason
|
|
62
|
+
The widths are fixed rather than `size_t` for the reason runtime-wasm.c
|
|
63
63
|
gives for its own copy: every compiled function inlines the bump allocator
|
|
64
64
|
and reads these fields directly, so the IR's `i64` is what `off` and `cap`
|
|
65
65
|
have to be on every target, not just the 64-bit ones. With `size_t` they
|