@amritk/nish-aarch64-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.
@@ -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 (self/runtime.ts) is a fact rather than a hope. The
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 `runtime_os.c`, for the
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. `runtime_os.c` is the syscall wrappers and it had 29 bytes of its
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
- * There is no language surface here and nothing in the language calls this yet:
25
- * docs/wp29-thread-surface.md is the surface, and this is the stage under it.
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/runtime_wasm.c -o x.wasm --profile wasm
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 runtime_os.c — every one of those wraps a system call, so that surface
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
- * runtime_os.c's header comment has the reasoning; tests/run.js gates the two
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 runtime_os.c, which is why an old link line that
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/runtime_wasm.c; they are one contract and move together. */
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 runtime_wasm.c
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/runtime_os.c, the system-call half,
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 `runtime_os.c` skips explicitly — so there is nothing
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. `runtime_os.c` orders the names with
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 `runtime_os.c` answers with, so on any machine this compiler has a
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. `runtime_os.c` puts one `static nish_spawn_impl` behind both builtins
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 `runtime_os.c` reads). The value is a BigInt because that is how an `i64`
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 three translation units and is named as one: an input
7
- # <dir>/runtime.c also compiles <dir>/runtime_os.c, the half that wraps the
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
- # and <dir>/runtime_parallel.c, the half that divides a range of work across
10
- # threads. Each of those files says why they are compiled and measured apart.
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/runtime_wasm.c to the inputs when a
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 three translation units, and a caller names one: whoever passes
87
- # <dir>/runtime.c gets <dir>/runtime_os.c and <dir>/runtime_parallel.c compiled
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
- # self/compile.ts, the published package's recipe in every document and
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 runtime_os.c runtime_parallel.c; do
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
- gc=(-Wl,-dead_strip); strip_flag=(-Wl,-x) ;;
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 runtime_parallel.c, the translation unit that divides a range
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
- # runtime_parallel.c compiles to its sequential fallback because it tests
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
- `self/` ([`docs/LANGUAGE.md`](../docs/LANGUAGE.md) is the style guide).
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 `self/`
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