@amritk/nish-x86_64-linux 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/INSTALL.md +13 -13
- package/bin/nish +0 -0
- package/package.json +1 -1
- 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/build.sh +19 -13
- 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
|
@@ -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
|
package/runtime/shim.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The differential runner (tests/differential/run.js) rewrites an Nish
|
|
5
5
|
* program into plain JavaScript and runs it under Node with this module as
|
|
6
6
|
* `__nish`. Every helper here reproduces the *runtime* semantics the compiled
|
|
7
|
-
* binary has (runtime/runtime.c and runtime/
|
|
7
|
+
* binary has (runtime/runtime.c and runtime/runtime-os.c, the system-call half,
|
|
8
8
|
* plus the intrinsics in docs/wp7-runtime.md) where JavaScript's own semantics
|
|
9
9
|
* differ:
|
|
10
10
|
*
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* The rewrite rules that call these helpers are listed in docs/wp13-differential.md.
|
|
43
43
|
*/
|
|
44
44
|
import child_process from "node:child_process";
|
|
45
|
+
import { webcrypto } from "node:crypto";
|
|
45
46
|
import fs from "node:fs";
|
|
46
47
|
import os from "node:os";
|
|
47
48
|
|
|
@@ -91,6 +92,37 @@ export function bitsToF64(b) {
|
|
|
91
92
|
return BITS.getFloat64(0);
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* `ctSelect` / `ctEq` (WP34 N6). A `u32` is a `number` here and a `u64` a
|
|
97
|
+
* BigInt, so the operands' kind picks the width, and a mix of the two — which
|
|
98
|
+
* the native checker refuses, and which a `u64` written as a bare literal is
|
|
99
|
+
* under an unrewritten run — throws the `TypeError` BigInt arithmetic throws
|
|
100
|
+
* rather than comparing a number with a BigInt and answering zero. JavaScript's
|
|
101
|
+
* `&` reads a `number` as a signed 32-bit integer, so each answer is put back in
|
|
102
|
+
* range with `>>> 0` or `asUintN(64, ...)`. These branch: only the native
|
|
103
|
+
* lowering promises constant time.
|
|
104
|
+
*/
|
|
105
|
+
function ctWide(name, first, second, third) {
|
|
106
|
+
const wide = typeof first === "bigint";
|
|
107
|
+
if ((typeof second === "bigint") !== wide || (typeof third === "bigint") !== wide) {
|
|
108
|
+
throw new TypeError(`${name}: cannot mix a u64 (BigInt) with a u32 (number)`);
|
|
109
|
+
}
|
|
110
|
+
return wide;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const U64_ONES = (1n << 64n) - 1n;
|
|
114
|
+
|
|
115
|
+
export function ctSelect(mask, a, b) {
|
|
116
|
+
if (ctWide("ctSelect", mask, a, b)) return wrapU64((a & mask) | (b & ~mask));
|
|
117
|
+
return ((a & mask) | (b & ~mask)) >>> 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function ctEq(a, b) {
|
|
121
|
+
// `ctEq` has two operands, so the second one stands in for the third.
|
|
122
|
+
if (ctWide("ctEq", a, b, b)) return wrapU64(a ^ b) === 0n ? U64_ONES : 0n;
|
|
123
|
+
return (a ^ b) === 0 ? 0xffffffff : 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
94
126
|
/** Wrap a BigInt to the i64 range: every i64 `+ - * /` and unary minus goes through here. */
|
|
95
127
|
export function wrapI64(x) {
|
|
96
128
|
return BigInt.asIntN(64, x);
|
|
@@ -401,6 +433,28 @@ export function updIdx(a, i, f) {
|
|
|
401
433
|
return v;
|
|
402
434
|
}
|
|
403
435
|
|
|
436
|
+
/**
|
|
437
|
+
* `dst.set(src, offset)` (WP34 N2): `TypedArray.prototype.set`'s copy on the
|
|
438
|
+
* plain array a `u8[]` is here. The source is copied first, so a self-copy or
|
|
439
|
+
* an overlapping one reads what was there before, as `memmove` does natively;
|
|
440
|
+
* a range past the end fails with the native panic and its words, where a
|
|
441
|
+
* typed array would throw a `RangeError` for the same offsets.
|
|
442
|
+
*/
|
|
443
|
+
export function arraySet(dst, src, offset) {
|
|
444
|
+
// `ToIntegerOrInfinity`: NaN is 0, as `llvm.fptosi.sat` makes it natively.
|
|
445
|
+
// `Math.trunc` rather than `toIndex`, which converts a bigint: an `i64` or
|
|
446
|
+
// `u64` offset is a bigint here, and it throws the `TypeError` the typed
|
|
447
|
+
// array and `Array.prototype.fill` throw for one, instead of being rounded
|
|
448
|
+
// to the nearest double past 2^53 (docs/RUN_UNDER_NODE.md).
|
|
449
|
+
const at = offset === undefined ? 0 : Math.trunc(offset) || 0;
|
|
450
|
+
const end = at + src.length;
|
|
451
|
+
if (!(at >= 0 && end <= dst.length)) panicSlice(at, end, dst.length);
|
|
452
|
+
// Two plain arrays overlap only when they are one array, which is the one
|
|
453
|
+
// case that must read the source before writing it.
|
|
454
|
+
const from = src === dst ? src.slice() : src;
|
|
455
|
+
for (let i = 0; i < from.length; i++) dst[at + i] = from[i];
|
|
456
|
+
}
|
|
457
|
+
|
|
404
458
|
/** `new Array<T>(n)`: `n` zero-filled elements (`0`, `0n`, or `false`). */
|
|
405
459
|
export function newArray(n, zero) {
|
|
406
460
|
return new Array(toIndex(n)).fill(zero);
|
|
@@ -434,6 +488,15 @@ export function readFileSyncOrNull(path) {
|
|
|
434
488
|
}
|
|
435
489
|
}
|
|
436
490
|
|
|
491
|
+
/** `readFileBytesSync(path)` (WP34 N2): the bytes as a plain array of numbers, or null. */
|
|
492
|
+
export function readFileBytesSync(path) {
|
|
493
|
+
try {
|
|
494
|
+
return Array.from(fs.readFileSync(path));
|
|
495
|
+
} catch {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
437
500
|
export function writeFileSync(path, data) {
|
|
438
501
|
try {
|
|
439
502
|
fs.writeFileSync(path, data, "utf8");
|
|
@@ -486,10 +549,10 @@ export function isDirectorySync(path) {
|
|
|
486
549
|
* the directory cannot be read. Node throws where the runtime answers a value,
|
|
487
550
|
* so the `catch` is what makes the two agree, and a directory that exists and
|
|
488
551
|
* is empty answers an empty array on both sides. Node's readdir never yields
|
|
489
|
-
* `.` or `..` — the pair `
|
|
552
|
+
* `.` or `..` — the pair `runtime-os.c` skips explicitly — so there is nothing
|
|
490
553
|
* to filter out here.
|
|
491
554
|
*
|
|
492
|
-
* The sort is the semantic point. `
|
|
555
|
+
* The sort is the semantic point. `runtime-os.c` orders the names with
|
|
493
556
|
* `strcmp`, which compares UTF-8 bytes, and `Array#sort` compares UTF-16 code
|
|
494
557
|
* units. The two agree on ASCII names and part company above the BMP, where a
|
|
495
558
|
* surrogate pair sorts below `U+E000`..`U+FFFF` in UTF-16 and above them in
|
|
@@ -526,7 +589,7 @@ export function realpathSync(path) {
|
|
|
526
589
|
|
|
527
590
|
/**
|
|
528
591
|
* `process.platform` / `process.arch` (WP14 §7a). Node's spellings are the
|
|
529
|
-
* ones `
|
|
592
|
+
* ones `runtime-os.c` answers with, so on any machine this compiler has a
|
|
530
593
|
* triple for the two runtimes give the same string; elsewhere the native build
|
|
531
594
|
* says `unknown` where Node names the platform, which is the one place they
|
|
532
595
|
* part.
|
|
@@ -553,7 +616,7 @@ export function getenv(name) {
|
|
|
553
616
|
* `spawnSync(argv)` and `spawnSyncTo(argv, out, err)` (WP14 D4), which are one
|
|
554
617
|
* run with its streams answered differently: the child's exit status, 128 + n
|
|
555
618
|
* when signal n killed it, -1 for an empty vector or a program that would not
|
|
556
|
-
* start. `
|
|
619
|
+
* start. `runtime-os.c` puts one `static nish_spawn_impl` behind both builtins
|
|
557
620
|
* for the same reason this module puts one function behind both helpers — the
|
|
558
621
|
* argument vector, the wait and the signal convention are written once and
|
|
559
622
|
* cannot drift between the two.
|
|
@@ -606,7 +669,7 @@ export function spawnSyncTo(argv, out, err) {
|
|
|
606
669
|
/**
|
|
607
670
|
* `monotonicNanos()`: `process.hrtime.bigint()`, a monotonic clock in
|
|
608
671
|
* nanoseconds (`CLOCK_MONOTONIC` on every platform that has it, which is the
|
|
609
|
-
* one `
|
|
672
|
+
* one `runtime-os.c` reads). The value is a BigInt because that is how an `i64`
|
|
610
673
|
* is held on this side.
|
|
611
674
|
*
|
|
612
675
|
* No rewrite can make a *reading* agree with a native run: both origins are
|
|
@@ -618,6 +681,65 @@ export function monotonicNanos() {
|
|
|
618
681
|
return process.hrtime.bigint();
|
|
619
682
|
}
|
|
620
683
|
|
|
684
|
+
// ---- The host (WP34 N3) ------------------------------------------------------
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* `statMtimeSync(path)`: Node's `mtimeMs` for the path, or NaN when it cannot
|
|
688
|
+
* be stat'd, which is the native answer too. `mtimeMs` is the same arithmetic
|
|
689
|
+
* `runtime-host.c` does, so the two print the same digits, fraction and all.
|
|
690
|
+
*/
|
|
691
|
+
export function statMtimeSync(path) {
|
|
692
|
+
const st = fs.statSync(path, { throwIfNoEntry: false });
|
|
693
|
+
if (st === undefined) {
|
|
694
|
+
return Number.NaN;
|
|
695
|
+
}
|
|
696
|
+
return st.mtimeMs;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Node's own fill, taken before `runtime/nish.mjs` puts the one below in its
|
|
701
|
+
* place on the same object: `webcrypto` is the global `crypto`.
|
|
702
|
+
*/
|
|
703
|
+
const webRandom = webcrypto.getRandomValues.bind(webcrypto);
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* `crypto.getRandomValues(bytes)` for the plain array a `u8[]` is here. Node's
|
|
707
|
+
* own takes only a typed array, so the bytes are drawn into one and copied
|
|
708
|
+
* across. More than 65,536 fails with the native panic and its words, where
|
|
709
|
+
* Node would throw a `QuotaExceededError`: the exit status is 1 either way. A
|
|
710
|
+
* typed array goes straight through.
|
|
711
|
+
*/
|
|
712
|
+
export function getRandomValues(bytes) {
|
|
713
|
+
if (!Array.isArray(bytes)) {
|
|
714
|
+
return webRandom(bytes);
|
|
715
|
+
}
|
|
716
|
+
if (bytes.length > 65536) {
|
|
717
|
+
panic(`crypto.getRandomValues: ${bytes.length} bytes asked for, and one call fills at most 65536`);
|
|
718
|
+
}
|
|
719
|
+
const drawn = webRandom(new Uint8Array(bytes.length));
|
|
720
|
+
for (let i = 0; i < drawn.length; i++) {
|
|
721
|
+
bytes[i] = drawn[i];
|
|
722
|
+
}
|
|
723
|
+
return bytes;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* `signalFd()` and `readSignal(fd)` have no faithful reading under Node, and
|
|
728
|
+
* these say so rather than answer something else. Node delivers a signal to
|
|
729
|
+
* its event loop (`process.on("SIGTERM")`), and a blocking read keeps the loop
|
|
730
|
+
* from ever running, so no synchronous function here can learn that one
|
|
731
|
+
* arrived. docs/wp33-round-trip.md §3.5 has the row and the translation.
|
|
732
|
+
*/
|
|
733
|
+
export function signalFd() {
|
|
734
|
+
throw new Error(
|
|
735
|
+
"signalFd has no synchronous reading under Node: a signal reaches the event loop, which a blocking readSignal never returns to (docs/wp33-round-trip.md)"
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export function readSignal() {
|
|
740
|
+
return signalFd();
|
|
741
|
+
}
|
|
742
|
+
|
|
621
743
|
/** `process.argv`: index 0 is the program (the script here, the executable natively), then the arguments. */
|
|
622
744
|
export function argv() {
|
|
623
745
|
return process.argv.slice(1);
|
package/scripts/build.sh
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
#
|
|
4
4
|
# scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm]
|
|
5
5
|
#
|
|
6
|
-
# The C runtime is
|
|
7
|
-
# <dir>/runtime.c also compiles <dir>/
|
|
6
|
+
# The C runtime is four translation units and is named as one: an input
|
|
7
|
+
# <dir>/runtime.c also compiles <dir>/runtime-os.c, the half that wraps the
|
|
8
8
|
# system calls (files, directories, subprocesses, the environment, the clock),
|
|
9
|
-
#
|
|
10
|
-
# threads
|
|
9
|
+
# <dir>/runtime-parallel.c, the half that divides a range of work across
|
|
10
|
+
# threads, and <dir>/runtime-host.c, the wall clock, entropy, file times and
|
|
11
|
+
# signals. Each of those files says why they are compiled and measured apart.
|
|
11
12
|
#
|
|
12
13
|
# Profiles:
|
|
13
14
|
# debug clang defaults: no optimisation, symbols kept. The "before" number.
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
# size -Oz + LTO + section GC + strip + no unwind tables. Rust
|
|
16
17
|
# `opt-level="z"`, `panic="abort"`, `strip=true` equivalent.
|
|
17
18
|
# wasm wasm32 freestanding module exporting every non-internal function;
|
|
18
|
-
# load it from Node. Add runtime/
|
|
19
|
+
# load it from Node. Add runtime/runtime-wasm.c to the inputs when a
|
|
19
20
|
# function uses arrays (the arena and the array cold paths, no libc);
|
|
20
21
|
# strings and I/O still need a WASI runtime and are not available.
|
|
21
22
|
# wasm wasm32 freestanding module exporting every non-internal function
|
|
@@ -83,13 +84,13 @@ done
|
|
|
83
84
|
[ ${#inputs[@]} -gt 0 ] || { echo "error: no input files" >&2; exit 2; }
|
|
84
85
|
[ -n "$out" ] || { echo "error: -o <out> is required" >&2; exit 2; }
|
|
85
86
|
|
|
86
|
-
# The runtime is
|
|
87
|
-
# <dir>/runtime.c gets <dir>/
|
|
88
|
-
# beside it. They were one file until the operating-system half was split out
|
|
87
|
+
# The runtime is four translation units, and a caller names one: whoever passes
|
|
88
|
+
# <dir>/runtime.c gets <dir>/runtime-os.c, <dir>/runtime-parallel.c and
|
|
89
|
+
# <dir>/runtime-host.c compiled beside it. They were one file until the operating-system half was split out
|
|
89
90
|
# for its own size budget, and the parallel half followed for the same reason
|
|
90
91
|
# (each file's header comment says why), and a link line is where those splits
|
|
91
92
|
# would otherwise leak: `nish --link` builds its command line in
|
|
92
|
-
#
|
|
93
|
+
# src/compile.ts, the published package's recipe in every document and
|
|
93
94
|
# README names runtime.c, and a user's own clang line does too. Pairing them
|
|
94
95
|
# here keeps every one of those correct, and keeps "the runtime" one thing to
|
|
95
96
|
# name from the outside. A caller that names one itself is left alone, because
|
|
@@ -97,7 +98,7 @@ done
|
|
|
97
98
|
for i in ${inputs[@]+"${inputs[@]}"}; do
|
|
98
99
|
case "$i" in
|
|
99
100
|
*/runtime.c|runtime.c)
|
|
100
|
-
for half in
|
|
101
|
+
for half in runtime-os.c runtime-parallel.c runtime-host.c; do
|
|
101
102
|
side="${i%runtime.c}$half"
|
|
102
103
|
have=0
|
|
103
104
|
for j in "${inputs[@]}"; do
|
|
@@ -133,8 +134,12 @@ case "$(uname -s)" in
|
|
|
133
134
|
# of one input to one output path, and differing on a link to another. That
|
|
134
135
|
# is what scripts/bootstrap.sh links every comparable stage at one path for,
|
|
135
136
|
# and it is why `stage3 == stage2` holds on Mach-O with the load command in.
|
|
136
|
-
|
|
137
|
+
# shellcheck disable=SC2054 # -Wl,<flag> is one argument: the comma is the linker's
|
|
138
|
+
gc=(-Wl,-dead_strip)
|
|
139
|
+
# shellcheck disable=SC2054
|
|
140
|
+
strip_flag=(-Wl,-x) ;;
|
|
137
141
|
*)
|
|
142
|
+
# shellcheck disable=SC2054 # -Wl,<flag> is one argument: the comma is the linker's
|
|
138
143
|
gc=(-Wl,--gc-sections -Wl,--as-needed -Wl,-O2 -Wl,--build-id=none); strip_flag=(-s)
|
|
139
144
|
elf=(-fno-plt)
|
|
140
145
|
# GNU ld needs the gold plugin for LTO; prefer lld when clang can find it.
|
|
@@ -158,7 +163,7 @@ if [ "$debug" = 1 ]; then common+=(-g); strip_flag=(); fi
|
|
|
158
163
|
# own command line instead of using `common`; it is empty on every ordinary
|
|
159
164
|
# build, hence the bash 3.2 expansion spelling explained below.
|
|
160
165
|
#
|
|
161
|
-
# -pthread is for
|
|
166
|
+
# -pthread is for runtime-parallel.c, the translation unit that divides a range
|
|
162
167
|
# of work across threads: it is compiled in either configuration and only spawns
|
|
163
168
|
# under this macro, so this is the build where the flag has to be on the command
|
|
164
169
|
# line. On a current glibc the library half is already inside libc and the link
|
|
@@ -167,7 +172,7 @@ if [ "$debug" = 1 ]; then common+=(-g); strip_flag=(); fi
|
|
|
167
172
|
#
|
|
168
173
|
# It is deliberately in `common` and not in `tls`: `tls` is the wasm and wasi
|
|
169
174
|
# command lines, which have no threads to link against, and where
|
|
170
|
-
#
|
|
175
|
+
# runtime-parallel.c compiles to its sequential fallback because it tests
|
|
171
176
|
# __wasi__ and __wasm__ as well as the macro.
|
|
172
177
|
tls=()
|
|
173
178
|
if [ "$threads" = 1 ]; then
|
|
@@ -265,6 +270,7 @@ case "$profile" in
|
|
|
265
270
|
shared=(-shared -fPIC)
|
|
266
271
|
# macOS: the napi_* symbols come from the node binary at load time, so the
|
|
267
272
|
# linker must not insist on resolving them. ELF shared objects allow this.
|
|
273
|
+
# shellcheck disable=SC2054 # -Wl,<flag> is one argument: the comma is the linker's
|
|
268
274
|
case "$(uname -s)" in Darwin) shared+=(-Wl,-undefined,dynamic_lookup) ;; esac
|
|
269
275
|
# runtime/nish.h is the public ABI header the generated shim includes.
|
|
270
276
|
runtime_inc="$(cd "$(dirname "$0")/../runtime" && pwd)"
|
package/std/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Nish modules written in Nish, for Nish programs to import. There is no magic
|
|
|
4
4
|
here and — with three exceptions, `threads.ts`, `collections.ts` and `map.ts` —
|
|
5
5
|
nothing the compiler knows about: a module in this directory is an ordinary Nish source file, compiled as part of
|
|
6
6
|
whatever program imports it, and subject to the same rules as `examples/` or
|
|
7
|
-
`
|
|
7
|
+
`src/` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md) is the style guide).
|
|
8
8
|
|
|
9
9
|
| Module | What it is |
|
|
10
10
|
| --- | --- |
|
|
@@ -16,6 +16,46 @@ whatever program imports it, and subject to the same rules as `examples/` or
|
|
|
16
16
|
| [`map.ts`](./map.ts) | `reserve(m, n)` and `getOrInsert(m, k, v)` for the global `Map`. Their bodies are the meaning, and what runs under Node: `reserve` does nothing, and `getOrInsert` is a `get`, and a `set` of `v` when the key was missing. Natively the compiler lowers every call in place — `reserve` to the table's `reserveSlots`, which grows the buckets once so that `n` entries fit without a rebuild, and `getOrInsert` to one `probe` and a `valueAt` or an `insertAt` through its answer — so, like `collections.ts`, it writes no `.ll` of its own ([`docs/wp32-map.md`](../docs/wp32-map.md) §9.2, [`docs/LANGUAGE.md`](../docs/LANGUAGE.md#map-and-set)) |
|
|
17
17
|
| [`threads.ts`](./threads.ts) | `parallelMapInto(src, dst, f)` and `parallelReduce(src, f, identity)`: a function over every element of an array, on as many threads as the length is worth. Its bodies are the sequential meaning, which is what runs under Node; the compiler recognises the two templates by module and name, lowers the one loop in each onto `nish_parallel_range`, holds the function to the rules that make that safe, and compiles an importing program with `--threads` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md#data-parallelism-nishthreads)). `tests/link/par_*` are its programs |
|
|
18
18
|
|
|
19
|
+
## `nish/crypto` — the primitives under TLS 1.3
|
|
20
|
+
|
|
21
|
+
The first lanes of [WP34](../docs/wp34-hosting-cs.md) §5: K1's hashes, MACs and
|
|
22
|
+
key derivation, and K4's key exchange, in pure Nish (decision S1), each module
|
|
23
|
+
imported by its own specifier. Every one is written from its specification
|
|
24
|
+
rather than ported, and reproduces that specification's published vectors in
|
|
25
|
+
its `tests/link/crypto_*` programs. The performance gate compiles every module
|
|
26
|
+
with no diagnostics under both `--number-mode i32` and `f64`, and the hashes,
|
|
27
|
+
HMAC, HKDF and X25519 also run their vectors in `f64` (`crypto_*_f64`).
|
|
28
|
+
|
|
29
|
+
| Module | What it is | Reproduces |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| [`crypto/sha256.ts`](./crypto/sha256.ts) | `sha256(data)`, and `Sha256`, a streaming hasher: `update(buf, off, len)` over a window of a `u8[]`, `copy()` for the hash of a prefix while the original keeps going, and `digest()`, a fresh 32-byte array. `SHA256_SIZE` and `SHA256_BLOCK` | FIPS 180-4 §6.2 |
|
|
32
|
+
| [`crypto/sha512.ts`](./crypto/sha512.ts) | SHA-512 and SHA-384 on one compression function: `sha512` and `sha384`, and the streaming `Sha512` and `Sha384` with `Sha256`'s three methods; digests of 64 and 48 bytes. `SHA512_SIZE`, `SHA384_SIZE` and `SHA512_BLOCK` | FIPS 180-4 §6.4, §6.5 |
|
|
33
|
+
| [`crypto/hmac.ts`](./crypto/hmac.ts) | `hmacSha256` and `hmacSha384`, the streaming `HmacSha256` and `HmacSha384` (keyed in the constructor, then `update` and `digest`), and `hmacSha256Verify` / `hmacSha384Verify`, which compare a received tag with `timingSafeEqual` | RFC 2104, RFC 4231 §4 |
|
|
34
|
+
| [`crypto/hkdf.ts`](./crypto/hkdf.ts) | `hkdfExtractSha256` / `hkdfExtractSha384` (an empty salt is HashLen zeros) and `hkdfExpandSha256` / `hkdfExpandSha384`, which answer `null` for a length below zero or above 255 × HashLen. TLS 1.3's HKDF-Expand-Label is not here; it belongs with TLS | RFC 5869 §2, Appendix A |
|
|
35
|
+
| [`crypto/ct.ts`](./crypto/ct.ts) | `timingSafeEqual(a, b)`, which reads every byte whatever it holds, and `timingSafeEqualAt(a, aOff, b, bOff, len)` over two windows, which answers `false` for a window outside its array. Two lengths that differ answer `false` at once, because a length is public | — |
|
|
36
|
+
| [`crypto/base64url.ts`](./crypto/base64url.ts) | `base64urlEncode(data)` and `base64urlDecode(text)`, unpadded. Decoding is strict, so every byte string has one spelling: a `=`, a character outside the alphabet, a length of 1 mod 4 or nonzero unused low bits answer `null` | RFC 4648 §5, §10 |
|
|
37
|
+
| [`crypto/x25519.ts`](./crypto/x25519.ts) | `x25519(scalar, u)` and `x25519Base(scalar)`, on ten 25.5-bit limbs in `i64`. Either answers `null` unless its arguments are `X25519_SIZE` (32) bytes; the scalar is clamped on a copy | RFC 7748 §5.2, §6.1 |
|
|
38
|
+
|
|
39
|
+
Three rules hold across the modules:
|
|
40
|
+
|
|
41
|
+
- **A digest ends the computation.** After `digest()` on a hasher or an HMAC, a
|
|
42
|
+
further `update` or `digest` panics rather than answering a hash over the
|
|
43
|
+
padding, and so does a window outside its buffer. `Sha256.copy()` on a
|
|
44
|
+
digested hasher panics too; `Sha512.copy()` and `Sha384.copy()` answer a copy
|
|
45
|
+
that is itself spent, so any `update` or `digest` on it panics. Either way,
|
|
46
|
+
copy *before* `digest` when the computation has to go on.
|
|
47
|
+
- **An all-zero X25519 result is returned, not refused.** It is what a
|
|
48
|
+
low-order `u` gives, and RFC 7748 §6.1 leaves the check to the protocol; TLS
|
|
49
|
+
1.3 (WP34 T1) makes it. A key exchange outside TLS has to make it itself.
|
|
50
|
+
- **Constant time by construction, not yet by proof.** No module branches on,
|
|
51
|
+
or indexes by, a secret: comparisons OR the differences into one word and
|
|
52
|
+
test it once, the ladder swaps with a mask and always runs 255 steps, and
|
|
53
|
+
base64url maps characters by arithmetic on range masks rather than a table.
|
|
54
|
+
Every branch is on a length, a loop counter or a bit position. What checks
|
|
55
|
+
that the machine code kept that shape is WP34 N6 — the `ctSelect` / `ctEq`
|
|
56
|
+
builtins behind an optimisation barrier, and a disassembly check — and it is
|
|
57
|
+
not built yet, so this is the discipline and not a verified property.
|
|
58
|
+
|
|
19
59
|
## How a program imports it
|
|
20
60
|
|
|
21
61
|
By its package specifier:
|
|
@@ -130,7 +170,7 @@ that are *not* this package.
|
|
|
130
170
|
stdout. `tests/link/std_text_f64` is the same corpus under
|
|
131
171
|
`--number-mode f64`, which is where a module that spelled its widths and forgot
|
|
132
172
|
a `toI32` is caught.
|
|
133
|
-
- **`std/` is not on the compiler's dependency list.** Nothing in `
|
|
173
|
+
- **`std/` is not on the compiler's dependency list.** Nothing in `src/`
|
|
134
174
|
imports it, and nothing should: the compiler is the thing that has to
|
|
135
175
|
build before the library means anything.
|
|
136
176
|
|