@amritk/nish-aarch64-darwin 0.25.0 → 0.27.2

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.
@@ -49,6 +49,7 @@
49
49
  #include <unistd.h>
50
50
  #ifndef __wasi__
51
51
  /* WASI has no processes, so `spawn.h` and `wait.h` do not exist there. */
52
+ #include <poll.h>
52
53
  #include <signal.h>
53
54
  #include <spawn.h>
54
55
  #include <sys/resource.h>
@@ -170,20 +171,9 @@ nish_str *nish_read_file(const nish_str *path) {
170
171
  return s;
171
172
  }
172
173
 
173
- /* `O_NOFOLLOW` (docs/security/runtime.md, RT-4): a symbolic link as the last
174
- component of the path is refused, as an unwritable file is, rather than
175
- followed to whatever file it names. `nish file.ts` writes `file.ll` beside
176
- its source, so a link planted at that name in a checkout would otherwise
177
- truncate any file the user can write. A link among the directories on the
178
- way is still followed.
179
-
180
- `O_CLOEXEC` for the reason the read has it (RT-12), and `EINTR` is retried
181
- as the read retries it (RT-13): a write cut short is finished, and one that
182
- fails is `cannot write`, never a silently short file.
183
-
184
- The loop is `nish_put_all`, which `writeFileBytesSync` and `writeBytes`
185
- share: all `n` bytes of `p` to `fd`, answering 0 or the negative errno of
186
- the write that failed (-EIO for one that wrote nothing). */
174
+ /* All `n` bytes of `p` to `fd`, answering 0 or the negative errno of the
175
+ write that failed (-EIO for one that wrote nothing). A write cut short is
176
+ finished and `EINTR` retried (RT-13). Every writer below shares it. */
187
177
  static int32_t nish_put_all(int fd, const char *p, uint64_t n) {
188
178
  for (uint64_t done = 0; done < n;) {
189
179
  ssize_t k = write(fd, p + done, n - done);
@@ -193,6 +183,17 @@ static int32_t nish_put_all(int fd, const char *p, uint64_t n) {
193
183
  }
194
184
  return 0;
195
185
  }
186
+
187
+ /* `O_NOFOLLOW` (docs/security/runtime.md, RT-4): a symbolic link as the last
188
+ component of the path is refused, as an unwritable file is, rather than
189
+ followed to whatever file it names. `nish file.ts` writes `file.ll` beside
190
+ its source, so a link planted at that name in a checkout would otherwise
191
+ truncate any file the user can write. A link among the directories on the
192
+ way is still followed.
193
+
194
+ `O_CLOEXEC` for the reason the read has it (RT-12), and the bytes go out
195
+ through `nish_put_all`, so a write that fails is `cannot write`, never a
196
+ silently short file. */
196
197
  static void nish_put_file(const nish_str *path, const nish_str *data, int flags) {
197
198
  int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, 0644);
198
199
  if (fd < 0 || nish_put_all(fd, data->data, data->len) < 0) nish_io_fail("write ", path);
@@ -248,6 +249,73 @@ int32_t nish_write_file_bytes(const nish_str *path, const nish_array *bytes) {
248
249
  return r < 0 ? r : (int32_t)bytes->len;
249
250
  }
250
251
 
252
+ /* ---- Files and directories with an explicit mode (docs/security/runtime.md,
253
+ RT-24 to RT-26): `writeFileSyncMode`, `createFileExclusiveSync` and
254
+ `mkdirSyncMode`. Each answers a value and never exits, as the writers just
255
+ above do. A mode outside 0..0777 is -EINVAL before any system call: the
256
+ setuid, setgid and sticky bits are refused, not applied, so the mode asked
257
+ for is the mode the file has. WASI
258
+ has no file modes to apply, so there each answers -ENOSYS and creates
259
+ nothing rather than a file whose mode was not set. */
260
+
261
+ /* The two file writers' shared body: `open(p, O_WRONLY | O_CREAT |
262
+ O_NOFOLLOW | O_CLOEXEC | flags, mode)`, then `fchmod(fd, mode)` before a
263
+ byte is written, so the file is exactly `mode` whatever the umask and
264
+ whatever mode a file already there had: no reader that opens it while it is
265
+ written finds it wider (RT-24). Then the data through `nish_put_all` and a
266
+ checked `close`, as `nish_write_file_bytes` does. The open truncates
267
+ before the `fchmod`, so a file there that this process may write but does
268
+ not own is emptied and then answers -EPERM. When `flags` holds `O_EXCL`,
269
+ any failure after the create unlinks the file this call made, so a lock
270
+ whose write failed does not stay taken. */
271
+ static int32_t nish_put_file_mode(const nish_str *path, const nish_str *data, int flags, int32_t mode) {
272
+ const char *p = nish_cpath(path);
273
+ if (p != path->data || mode < 0 || mode > 0777) return -EINVAL;
274
+ if (data->len > NISH_LENGTH_MAX) return -EFBIG;
275
+ #ifdef __wasi__
276
+ (void)flags;
277
+ return -ENOSYS;
278
+ #else
279
+ int fd = open(p, O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, (mode_t)mode);
280
+ if (fd < 0) return -errno;
281
+ int32_t r = fchmod(fd, (mode_t)mode) == 0 ? 0 : -errno;
282
+ if (r == 0) r = nish_put_all(fd, data->data, data->len);
283
+ if (close(fd) != 0 && r == 0 && errno != EINTR) r = -errno;
284
+ if (r < 0 && (flags & O_EXCL)) unlink(p);
285
+ return r < 0 ? r : (int32_t)data->len;
286
+ #endif
287
+ }
288
+
289
+ /* `writeFileSyncMode(path, data, mode)`: `writeFileSync` with the mode given
290
+ and an answer instead of an exit. A link at the path is refused (-ELOOP on
291
+ Linux) and the file it names is untouched (RT-25). */
292
+ int32_t nish_write_file_mode(const nish_str *path, const nish_str *data, int32_t mode) {
293
+ return nish_put_file_mode(path, data, O_TRUNC, mode);
294
+ }
295
+
296
+ /* `createFileExclusiveSync(path, data, mode)`: `O_EXCL`, so the path is
297
+ created by this call or the call answers -EEXIST, a link there included,
298
+ dangling or not. Of N processes racing for one path exactly one wins, which
299
+ is what a lock file needs; a winner whose write then fails removes the file
300
+ and answers the error. */
301
+ int32_t nish_create_file_exclusive(const nish_str *path, const nish_str *data, int32_t mode) {
302
+ return nish_put_file_mode(path, data, O_EXCL, mode);
303
+ }
304
+
305
+ /* `mkdirSyncMode(path, mode)`: mkdir(2) with `mode`, one directory, not
306
+ recursive. A path already there is -EEXIST and its mode is left as it was.
307
+ The kernel applies the umask, so the directory is `mode & ~umask`: 0700 is
308
+ exact under any umask that leaves the owner's bits (RT-26). */
309
+ int32_t nish_mkdir_mode(const nish_str *path, int32_t mode) {
310
+ const char *p = nish_cpath(path);
311
+ if (p != path->data || mode < 0 || mode > 0777) return -EINVAL;
312
+ #ifdef __wasi__
313
+ return -ENOSYS;
314
+ #else
315
+ return mkdir(p, (mode_t)mode) == 0 ? 0 : -errno;
316
+ #endif
317
+ }
318
+
251
319
  /* `writeBytes(bytes, off, len)` (nish:io): the bytes `[off, off + len)` of a
252
320
  `u8[]` on stdout, through `nish_put_all`, so a short write is finished and `EINTR` retried. The
253
321
  emitter has range-checked `off` and `len` against `bytes->len`
@@ -290,17 +358,19 @@ _Bool nish_mkdir(const nish_str *path) {
290
358
  value rather than through a child that has already run.
291
359
 
292
360
  `out` and `err` are paths for the child's stdout and stderr, or NULL to let
293
- it inherit this process's. Every spawn builtin starts its child here, so the
361
+ it inherit this process's; `dup`, when not NULL, is three descriptors the
362
+ child takes as its stdin, stdout and stderr instead (`spawnPiped`'s pipes). Every spawn builtin starts its child here, so the
294
363
  argv vector and the redirects are written once; it is `static`, so a program
295
364
  that spawns nothing loses it to `--gc-sections`. The answer is the child's
296
365
  pid, or a negative errno: -EINVAL for an empty argv or an argument holding a
297
366
  NUL, and `posix_spawnp`'s own error otherwise (-ENOENT for a command that is
298
367
  not on PATH, on glibc). */
299
- static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, const nish_str *err) {
368
+ static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, const nish_str *err, const int *dup) {
300
369
  #ifdef __wasi__
301
370
  (void)argv;
302
371
  (void)out;
303
372
  (void)err;
373
+ (void)dup;
304
374
  return -ENOSYS;
305
375
  #else
306
376
  if (!argv->len) return -EINVAL; /* argv[0] would read past an empty array */
@@ -323,6 +393,13 @@ static int32_t nish_spawn_start(const nish_array *argv, const nish_str *out, con
323
393
  posix_spawn_file_actions_t fa;
324
394
  int failed = posix_spawn_file_actions_init(&fa);
325
395
  if (failed) return -failed;
396
+ /* A `dup2` action that could not be recorded would start the child on this
397
+ process's own stream in its place, so it fails the spawn. */
398
+ for (int k = 0; dup && k < 3 && !failed; k++) failed = posix_spawn_file_actions_adddup2(&fa, dup[k], k);
399
+ if (failed) {
400
+ posix_spawn_file_actions_destroy(&fa);
401
+ return -failed;
402
+ }
326
403
  const nish_str *to[2] = { out, err };
327
404
  for (int k = 0; k < 2; k++) {
328
405
  /* The child opens the file, not this process: a redirect that this process
@@ -348,7 +425,7 @@ static int32_t nish_exit_code(int status) {
348
425
  /* The two blocking builtins: start, then wait for that one child. A failed
349
426
  start is -1, the answer they have always given. */
350
427
  static int32_t nish_spawn_impl(const nish_array *argv, const nish_str *out, const nish_str *err) {
351
- int32_t pid = nish_spawn_start(argv, out, err);
428
+ int32_t pid = nish_spawn_start(argv, out, err, 0);
352
429
  if (pid < 0) return -1;
353
430
  #ifdef __wasi__
354
431
  return -1;
@@ -379,7 +456,160 @@ int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_st
379
456
  /* `spawn(argv)`: the child's pid, or a negative errno, without waiting. The
380
457
  child is this process's until a `waitPid` reaps it; one never reaped stays
381
458
  a zombie until the program exits, as it would in C. */
382
- int32_t nish_spawn_child(const nish_array *argv) { return nish_spawn_start(argv, 0, 0); }
459
+ int32_t nish_spawn_child(const nish_array *argv) { return nish_spawn_start(argv, 0, 0, 0); }
460
+
461
+ /* `spawnPiped(argv, stdin, stdout, stderr)`: run the child as `nish_spawn`
462
+ does, with `stdin`'s bytes on its standard input and its standard output
463
+ and error appended to `stdout` and `stderr`, and answer its status. -1 is
464
+ `nish_spawn`'s failure (an empty argv, a NUL, a child that cannot start or
465
+ be waited for, a pipe or `poll` that fails), and -2 is a stream that passed
466
+ NISH_PIPED_CAP bytes: the child is killed with SIGKILL and reaped, and the
467
+ array holds the first NISH_PIPED_CAP bytes of that stream.
468
+
469
+ The three pipes are close-on-exec, so no other child this process starts
470
+ inherits an end, and each end is moved off descriptors 0 to 2, where a
471
+ `dup2` onto itself would leave close-on-exec set and the child without
472
+ that stream. Darwin has no `pipe2`, so there an end is close-on-exec only
473
+ once `F_DUPFD_CLOEXEC` has moved it: a child another thread starts in
474
+ that window inherits it (docs/security/runtime.md, RT-30).
475
+
476
+ One `poll` loop over non-blocking ends writes stdin and reads both outputs
477
+ as each is ready, so a child that fills its stdout before it reads all its
478
+ stdin, or the reverse, cannot deadlock against this process. A child that
479
+ exits, or closes its stdin, before reading everything ends the writing
480
+ with EPIPE rather than SIGPIPE: the signal is blocked on this thread once
481
+ the child has started, the one a write raised is taken off the pending
482
+ set, and the mask is put back before the answer (docs/security/runtime.md,
483
+ RT-29). Ignoring it instead would be process-wide, and every child
484
+ started meanwhile, this one included, would inherit SIGPIPE ignored and
485
+ never die of a reader that has gone.
486
+
487
+ The bytes are written from the caller's array as they stand, and nothing
488
+ copies them: the only copies are the kernel's pipe buffer and the child.
489
+ The length is read once, so `stdout` being the same array as `stdin`
490
+ appends to it without feeding the output back in, and the pointer at each
491
+ write, because that array moves when it grows and its stdin bytes stay
492
+ its first `n`. */
493
+ #define NISH_PIPED_CAP (16u << 20)
494
+ #ifndef __wasi__
495
+ static int nish_pipe(int *p) {
496
+ #ifdef __linux__
497
+ if (pipe2(p, O_CLOEXEC)) return -1;
498
+ #else
499
+ if (pipe(p)) return -1;
500
+ #endif
501
+ for (int k = 0; k < 2; k++) {
502
+ #ifdef __linux__
503
+ if (p[k] > 2) continue;
504
+ #endif
505
+ int moved = fcntl(p[k], F_DUPFD_CLOEXEC, 3);
506
+ close(p[k]);
507
+ p[k] = moved;
508
+ if (moved < 0) return -1;
509
+ }
510
+ return 0;
511
+ }
512
+
513
+ #endif
514
+
515
+ int32_t nish_spawn_piped(const nish_array *argv, const nish_array *in, nish_array *out, nish_array *err) {
516
+ #ifdef __wasi__
517
+ (void)argv;
518
+ (void)in;
519
+ (void)out;
520
+ (void)err;
521
+ return -1;
522
+ #else
523
+ uint64_t n = in->len, sent = 0, got[3] = { 0, 0, 0 };
524
+ int p[6] = { -1, -1, -1, -1, -1, -1 };
525
+ int32_t rc = -1;
526
+ if (nish_pipe(p) || nish_pipe(p + 2) || nish_pipe(p + 4)) goto done;
527
+ int child[3] = { p[0], p[3], p[5] };
528
+ int32_t pid = nish_spawn_start(argv, 0, 0, child);
529
+ /* The child holds its ends now; this process keeps only its own. */
530
+ for (int k = 0; k < 3; k++) {
531
+ close(child[k]);
532
+ p[k ? 2 * k + 1 : 0] = -1;
533
+ }
534
+ if (pid < 0) goto done;
535
+ sigset_t pipe_sig, mask, pending;
536
+ sigemptyset(&pipe_sig);
537
+ sigaddset(&pipe_sig, SIGPIPE);
538
+ pthread_sigmask(SIG_BLOCK, &pipe_sig, &mask);
539
+ sigpending(&pending);
540
+ int queued = sigismember(&pending, SIGPIPE);
541
+ struct pollfd f[3] = { { p[1], POLLOUT, 0 }, { p[2], POLLIN, 0 }, { p[4], POLLIN, 0 } };
542
+ p[1] = p[2] = p[4] = -1;
543
+ /* A blocking end could stall the loop on one stream while the child fills
544
+ another, so an end that stays blocking fails the run: the loop is skipped
545
+ and the child killed. */
546
+ rc = 0;
547
+ for (int k = 0; k < 3; k++)
548
+ if (fcntl(f[k].fd, F_SETFL, O_NONBLOCK) < 0) rc = -1;
549
+ if (!n) {
550
+ close(f[0].fd);
551
+ f[0].fd = -1;
552
+ }
553
+ nish_array *to[3] = { 0, out, err };
554
+ while (!rc && (f[0].fd >= 0 || f[1].fd >= 0 || f[2].fd >= 0)) {
555
+ if (poll(f, 3, -1) < 0) {
556
+ if (errno != EINTR) rc = -1;
557
+ continue;
558
+ }
559
+ if (f[0].revents) {
560
+ ssize_t k = write(f[0].fd, in->data + sent, n - sent);
561
+ if (k > 0) sent += k;
562
+ if (sent == n || (k < 0 && errno != EINTR && errno != EAGAIN)) {
563
+ close(f[0].fd);
564
+ f[0].fd = -1;
565
+ }
566
+ }
567
+ for (int s = 1; s < 3 && !rc; s++) {
568
+ if (!f[s].revents) continue;
569
+ nish_array *a = to[s];
570
+ /* Grown as `push` grows it, so the array keeps the shape every other
571
+ array has, and an ASan build tracks the block the same way. */
572
+ if (a->len == a->cap) nish_array_grow(a, 1);
573
+ /* One byte past the cap at most, which is how a stream that ends at
574
+ exactly the cap is told from one that goes on. */
575
+ uint64_t room = a->cap - a->len, left = NISH_PIPED_CAP + 1 - got[s];
576
+ ssize_t k = read(f[s].fd, a->data + a->len, room < left ? room : left);
577
+ if (k > 0) {
578
+ a->len += k;
579
+ got[s] += k;
580
+ if (got[s] > NISH_PIPED_CAP) {
581
+ a->len--;
582
+ rc = -2;
583
+ }
584
+ } else if (!k || (errno != EINTR && errno != EAGAIN)) {
585
+ close(f[s].fd);
586
+ f[s].fd = -1;
587
+ }
588
+ }
589
+ }
590
+ sigpending(&pending);
591
+ if (!queued && sigismember(&pending, SIGPIPE)) {
592
+ int sig;
593
+ sigwait(&pipe_sig, &sig);
594
+ }
595
+ pthread_sigmask(SIG_SETMASK, &mask, 0);
596
+ if (rc) kill(pid, SIGKILL);
597
+ for (int k = 0; k < 3; k++)
598
+ if (f[k].fd >= 0) close(f[k].fd);
599
+ int status;
600
+ while (waitpid(pid, &status, 0) < 0) {
601
+ if (errno != EINTR) {
602
+ rc = -1;
603
+ break;
604
+ }
605
+ }
606
+ if (!rc) rc = nish_exit_code(status);
607
+ done:
608
+ for (int k = 0; k < 6; k++)
609
+ if (p[k] >= 0) close(p[k]);
610
+ return rc;
611
+ #endif
612
+ }
383
613
 
384
614
  /* `kill(pid, sig)`: 0, or a negative errno. Only the four signals a program
385
615
  asks a child to stop or report with are sent -- 0 (is it there), SIGINT,
@@ -545,15 +775,20 @@ int64_t nish_total_memory(void) {
545
775
  }
546
776
 
547
777
  /* `setNice(n)`: this process's nice value set to `n`, 0 or a negative errno.
548
- The kernel clamps `n` to its range (-20 to 19), so only lowering the value
549
- below what the process has can fail: -EACCES (Linux, Darwin) or -EPERM
550
- without the privilege to raise a priority. Linux keeps the value per thread,
551
- so it is the calling thread's and every thread started after it. */
778
+ Lower-only, even as root: `n` takes 0..19 (-EINVAL outside it, with no system
779
+ call), and a value below the current one is -EACCES (Linux, Darwin) without
780
+ a `setpriority` call, so the priority never rises. Linux keeps the value per
781
+ thread, so it is the calling thread's and every thread started after it. */
552
782
  int32_t nish_set_nice(int32_t n) {
553
783
  #ifdef __wasi__
554
784
  (void)n;
555
785
  return -ENOSYS;
556
786
  #else
787
+ if (n < 0 || n > 19) return -EINVAL;
788
+ errno = 0;
789
+ int current = getpriority(PRIO_PROCESS, 0);
790
+ if (current == -1 && errno != 0) return -errno;
791
+ if (n < current) return -EACCES;
557
792
  return setpriority(PRIO_PROCESS, 0, n) == 0 ? 0 : -errno;
558
793
  #endif
559
794
  }
@@ -1,5 +1,6 @@
1
1
  /* Nish runtime, the vector half: byte searches that read sixteen or thirty-two
2
- * bytes at a time (WP38 S1, docs/wp38-simd.md §3.1).
2
+ * bytes at a time (WP38 S1, docs/wp38-simd.md §3.1), and the SSE4.1 rounding
3
+ * behind `Math.floor`, `Math.ceil` and `Math.trunc` (#943, at the end).
3
4
  *
4
5
  * A translation unit of its own for the reason runtime-host.c is one: each file
5
6
  * carries its own measured `.text*` ceiling in tests/runtime-budgets.js, and
@@ -176,10 +177,17 @@ static int nish_cpu_has_avx2(void) {
176
177
  `base` never reads the CPU's feature bits at all. */
177
178
  enum { NISH_PATH_SCALAR, NISH_PATH_BASE, NISH_PATH_AVX2 };
178
179
 
179
- static int nish_simd_choose(void) {
180
+ /* The path `NISH_SIMD` pins, or -1 when it pins none. */
181
+ static int nish_simd_pin(void) {
180
182
  const char *pin = getenv("NISH_SIMD");
181
183
  if (pin != NULL && strcmp(pin, "scalar") == 0) return NISH_PATH_SCALAR;
182
184
  if (pin != NULL && strcmp(pin, "base") == 0) return NISH_PATH_BASE;
185
+ return -1;
186
+ }
187
+
188
+ static int nish_simd_choose(void) {
189
+ int pin = nish_simd_pin();
190
+ if (pin >= 0) return pin;
183
191
  #if defined(NISH_SIMD_SSE2)
184
192
  if (nish_cpu_has_avx2()) return NISH_PATH_AVX2;
185
193
  #endif
@@ -425,3 +433,56 @@ int64_t nish_regex_scan(const nish_str *s, const nish_array *masks, int64_t dept
425
433
  int64_t from) {
426
434
  return __atomic_load_n(&nish_scan, __ATOMIC_RELAXED)(s, masks, depth, off, width, from);
427
435
  }
436
+
437
+ /* `Math.floor`, `Math.ceil` and `Math.trunc`, and the floor `Math.round` takes
438
+ (#943), the contract in nish.h. SSE4.1's `roundsd` answers each in one
439
+ instruction, but the x86-64 baseline is SSE2, where LLVM lowers
440
+ `llvm.floor.f64` to a call of libm's `floor` through the GOT. So a module
441
+ for x86-64 calls these instead (`roundingCallee`, src/emit-builtins.ts) and
442
+ registers a constructor that hands `nish_round_init` libm's three routines;
443
+ it asks the CPU once, before `main`, and keeps them only when the answer is
444
+ no. The runtime names no libm symbol itself, so a link without `-lm` (a C
445
+ host, a program that never rounds) still resolves: the module that rounds
446
+ brings the reference, as it did before. A null slot is the instruction and
447
+ is never written after the constructor, so on a CPU without SSE4.1 each
448
+ call is an indirect jump to libm, the path a module took before.
449
+ `NISH_SIMD=scalar` keeps libm, the forced fallback a test runs. `base` does
450
+ not: the instruction gate counts under it, and glibc's own `floor` takes
451
+ `roundsd` there as well, so the count is of the instruction both call.
452
+ Built for a machine that has SSE4.1 already (`--cpu x86-64-v3`), or for any
453
+ other, each is the builtin, which is the one instruction there, and the
454
+ constructor keeps nothing. The immediate is the rounding mode, 1 down, 2 up,
455
+ 3 toward zero, plus 8, which keeps the precision exception quiet as libm's
456
+ `floor` does. */
457
+ #if defined(NISH_SIMD_SSE2) && !defined(__SSE4_1__)
458
+ static double (*nish_round_libm[3])(double);
459
+
460
+ void nish_round_init(double (*floor_fn)(double), double (*ceil_fn)(double), double (*trunc_fn)(double)) {
461
+ unsigned a, b, c, d;
462
+ if (nish_simd_pin() != NISH_PATH_SCALAR && __get_cpuid(1, &a, &b, &c, &d) && (c & bit_SSE4_1)) return;
463
+ nish_round_libm[0] = floor_fn;
464
+ nish_round_libm[1] = ceil_fn;
465
+ nish_round_libm[2] = trunc_fn;
466
+ }
467
+
468
+ #define NISH_ROUNDING(name, base, slot, mode) \
469
+ double name(double x) { \
470
+ double (*libm)(double) = nish_round_libm[slot]; \
471
+ if (libm) return libm(x); \
472
+ __asm__("roundsd $" #mode ", %0, %0" : "+x"(x)); \
473
+ return x; \
474
+ }
475
+ #else
476
+ void nish_round_init(double (*floor_fn)(double), double (*ceil_fn)(double), double (*trunc_fn)(double)) {
477
+ (void)floor_fn;
478
+ (void)ceil_fn;
479
+ (void)trunc_fn;
480
+ }
481
+
482
+ #define NISH_ROUNDING(name, base, slot, mode) \
483
+ double name(double x) { return __builtin_##base(x); }
484
+ #endif
485
+
486
+ NISH_ROUNDING(nish_floor, floor, 0, 9)
487
+ NISH_ROUNDING(nish_ceil, ceil, 1, 10)
488
+ NISH_ROUNDING(nish_trunc, trunc, 2, 11)
@@ -128,3 +128,21 @@ void nish_exit(int32_t code) { (void)code; __builtin_trap(); }
128
128
  void nish_panic(const void *s) { (void)s; __builtin_trap(); }
129
129
  /* Output has nowhere to go either. */
130
130
  void nish_write(const void *s, int32_t fd, _Bool newline) { (void)s; (void)fd; (void)newline; }
131
+
132
+ /* ---- Rounding (#943): IR compiled for the host calls these and registers a
133
+ * constructor that passes `nish_round_init` libm's three routines
134
+ * (runtime-simd.c has the native ones). On wasm32 each is the one
135
+ * instruction, f64.floor, f64.ceil or f64.trunc, and there is no CPU to ask.
136
+ * The link is freestanding, so the libm names the constructor takes are
137
+ * defined here too, as the same instructions. */
138
+ void nish_round_init(double (*floor_fn)(double), double (*ceil_fn)(double), double (*trunc_fn)(double)) {
139
+ (void)floor_fn;
140
+ (void)ceil_fn;
141
+ (void)trunc_fn;
142
+ }
143
+ double nish_floor(double x) { return __builtin_floor(x); }
144
+ double nish_ceil(double x) { return __builtin_ceil(x); }
145
+ double nish_trunc(double x) { return __builtin_trunc(x); }
146
+ double floor(double x) { return __builtin_floor(x); }
147
+ double ceil(double x) { return __builtin_ceil(x); }
148
+ double trunc(double x) { return __builtin_trunc(x); }
package/scripts/build.sh CHANGED
@@ -10,7 +10,8 @@
10
10
  # threads, <dir>/runtime-cross.c, the copies that carry a string or an array
11
11
  # from one thread's arena to another's, <dir>/runtime-host.c, the wall clock, entropy, file times and
12
12
  # signals, <dir>/runtime-net.c, the sockets of `nish:net`,
13
- # <dir>/runtime-simd.c, the byte searches that read a vector at a time,
13
+ # <dir>/runtime-simd.c, the byte searches that read a vector at a time and
14
+ # the SSE4.1 rounding of `Math.floor` and its kin,
14
15
  # <dir>/runtime-aes.c, AES-GCM on the CPU's AES instructions,
15
16
  # <dir>/runtime-term.c, stdin and the terminal of `nish:term`, and
16
17
  # <dir>/runtime-bytes.c, the byte-class runs of `nish/bytes`. Each of those
@@ -665,6 +666,20 @@ case "$profile" in
665
666
  if [ "$bytes_wanted" = 1 ] && [ "$bytes_named" = 0 ]; then
666
667
  inputs+=("$(cd "$(dirname "$0")/../runtime" && pwd)/runtime-bytes.c")
667
668
  fi
669
+ # IR compiled for the host rounds through runtime-simd.c's `nish_floor` and its
670
+ # kin behind the constructor `nish_round_init` (#943). runtime-wasm.c has their
671
+ # wasm32 bodies, and it is added the same way when such IR does not name it.
672
+ wasm_named=0
673
+ round_wanted=0
674
+ for i in ${inputs[@]+"${inputs[@]}"}; do
675
+ case "$i" in
676
+ */runtime-wasm.c|runtime-wasm.c) wasm_named=1 ;;
677
+ *.ll) if grep -q '@nish_round_init' "$i"; then round_wanted=1; fi ;;
678
+ esac
679
+ done
680
+ if [ "$round_wanted" = 1 ] && [ "$wasm_named" = 0 ]; then
681
+ inputs+=("$(cd "$(dirname "$0")/../runtime" && pwd)/runtime-wasm.c")
682
+ fi
668
683
  # -mbulk-memory lowers llvm.memset/memcpy (`new Array<T>(n)`, `push` growth) to the
669
684
  # memory.fill/memory.copy instructions instead of libc calls the freestanding link lacks.
670
685
  "$CC" -Wno-override-module --target=wasm32-unknown-unknown -Oz -nostdlib -mbulk-memory \
package/std/README.md CHANGED
@@ -225,6 +225,7 @@ reads one, and it still takes its randomness as an argument.
225
225
  | [`net/qpack.ts`](./net/qpack.ts) | QPACK, HTTP/3's field compression, with a dynamic table of capacity 0: `QpackEncoder` and `QpackDecoder`, one per connection. The encoder writes a field section that starts `00 00` (Required Insert Count 0, Base 0) with `beginSection`, then one field line per `encodeField(out, name, off, len, value, off, len, neverIndexed, huffman)`: an Indexed Field Line when the 99-entry static table holds the field, else a literal with the name by static index when it can, each string Huffman-coded when that is shorter and `huffman` is set, and a never-indexed field always a literal with the N bit. The decoder answers a section's fields as windows into its own `bytes` (`nameStart`, `nameLength`, `valueStart`, `valueLength`, `neverIndexed`, `count`), its arrays reused from section to section. It answers QPACK_DECOMPRESSION_FAILED, and stays spent, for a truncated section, an integer past 2^62 − 1, a non-zero Required Insert Count, a negative Base, any dynamic-table reference, a static index past 98 and bad Huffman; a section past `maxFieldSectionSize` answers `QPACK_SECTION_TOO_LARGE`, which is not fatal. `receiveEncoderStream` accepts Set Dynamic Table Capacity 0 and answers QPACK_ENCODER_STREAM_ERROR for any other capacity, Insert or Duplicate; the encoder's `receiveDecoderStream` accepts Stream Cancellations, split anywhere, and answers QPACK_DECODER_STREAM_ERROR for a Section Acknowledgment or Insert Count Increment; `reason` names the rule. `qpackPushStreamCancellation` is the one instruction it may send. The Huffman code is `nish/net/hpack`'s. A window outside its buffer, a negative limit and a stream ID outside 0 to 2^62 − 1 panic | RFC 9204 Appendix B.1, every Appendix A entry by its index, B.2 to B.5's instructions refused, and a request matched byte for byte with pylsqpack's encoding |
226
226
  | [`net/http1.ts`](./net/http1.ts) | `Http1Parser`, an incremental HTTP/1.1 request parser: `feed(buf, off, len)` any slice, then `next()` answers `HTTP1_NEED_MORE`, `HTTP1_HEAD` (`method`, `target`, `minor`, lowercased `names` beside `values`, `header(name)`, `contentLength` or `chunked`, `keepAlive`, `upgrade`), `HTTP1_BODY` (a window onto its buffer, de-chunked), `HTTP1_END`, or one of three final events: `HTTP1_UPGRADE` (`upgradeBytes()` and `declineUpgrade()`), `HTTP1_CLOSED` and `HTTP1_ERROR` with the `status` to answer. It refuses with 400 every request-smuggling shape — `Content-Length` with `Transfer-Encoding`, a repeated or non-digit `Content-Length`, an obs-fold, a bare LF, whitespace before a colon, a malformed chunk size or terminator — and with 413, 414, 431, 501 and 505 what those mean. The head is also kept as spans in a buffer reused from request to request (`head`, `spans`, `methodIs`, `targetIs`, `headerIndex`, `headerIs`, `upgradeIs`), so a parser with `keepText` off allocates nothing per request; `maxChunk` caps each `HTTP1_BODY`, and `restart()` readies it for the next connection. The writer is `http1ResponseHead(status, reason, names, values, bodyLength)`, which adds the framing field itself and refuses (`null`) a CR, LF or other control character anywhere a caller's string lands, and `http1Chunk` and `http1LastChunk` for a chunked body; `http1WriteResponseHead` and `http1WriteChunk` write the same bytes into a caller's buffer at an offset, answering `HTTP1_NO_ROOM` or `HTTP1_REFUSED`, and the first takes a date's bytes to send as a `Date` field on a final response, unless the caller's fields hold one | RFC 9112 §2–§7 and §9.3, RFC 9110 §5, §6.2, §7.2 and §7.8; a corpus fed at every split point (`tests/link/net_http1`) |
227
227
  | [`net/http1-client.ts`](./net/http1-client.ts) | The client side of HTTP/1.1, sans-IO. The writer is `http1RequestHead(method, target, names, values, bodyLength)`, `Host` first and the framing field added for `bodyLength` (`Content-Length`, `HTTP1_CHUNKED` or `HTTP1_NO_BODY`), and `http1WriteRequestHead` into a caller's buffer, answering `HTTP1_NO_ROOM` or `HTTP1_REFUSED` with nothing written; it refuses a method that is not a token, a target that is not a form the method may send, a CR, LF or control byte in a value, not exactly one `Host`, the caller's own framing fields and a `Connection: upgrade`. `Http1ResponseParser(maxStatusLine, maxHeaderBytes, maxHeaders, maxBody, maxTrailerBytes)` is told what was asked (`expect(head, connect)`), fed any slice, and `next()` answers `HTTP1_INFORMATIONAL`, `HTTP1_HEAD` (`status`, `reason`, `minor`, lowercased `names` beside `values`, `header(name)`, `contentLength`), `HTTP1_BODY` (a window, de-chunked), `HTTP1_END` with `keepAlive()`, or the final `HTTP1_UPGRADE` (a 2xx to CONNECT, `tunnelBytes()`), `HTTP1_CLOSED` and `HTTP1_ERROR`; `finish()` reports the close that ends a body read until close, and anywhere else is `HTTP1_E_TRUNCATED`. Every smuggling shape is refused with its own `HTTP1_E_*` code, as is a 101, more than `HTTP1_MAX_INFORMATIONAL` interim responses and a chunk-size line over `HTTP1_MAX_CHUNK_LINE`. Record: [`docs/security/http1-client.md`](../docs/security/http1-client.md) | RFC 9112 §4–§7 and §9.3, RFC 9110 §9.3.6 and §15.2; a corpus fed whole, cut at every byte and a byte at a time, every refusal and limit at its edge, and the server parser reading what the writer writes (`tests/link/net_http1_client`, `_f64`, `_feed_window`) |
228
+ | [`net/http-client.ts`](./net/http-client.ts) | A blocking HTTP/1.1 client for command-line programs: `httpFetch(request, options)` looks the host up, connects, sends an `HttpRequest` (`method`, `url`, `header(name, value)`, `body`) and answers an `HttpResponse` (`status`, `reason`, `url`, `header(name)`) once its head is in, its body read through `body.read` or `body.readAll(limit)`; null on a failure. `HttpClient(options)` does the same and keeps one connection alive between fetches, sending an idempotent request once more when a kept connection turns out to be gone, and says why a fetch failed in `error` (an `HTTP_E_*` code, `httpErrorName`) and `errorText`. `HttpOptions` bounds every wait (`connectTimeoutMs`, `readTimeoutMs`, `totalTimeoutMs`) and every body (`maxBodyBytes`), follows redirects only when asked (`followRedirects`, `maxRedirects`, `crossOriginRedirects`, never https to http, credentials dropped across origins), and reads `http_proxy`, `https_proxy` and `no_proxy` when `useEnvProxy` is set (`HTTP_PROXY` ignored under CGI; https through `CONNECT`). Every byte goes through `nish/net/http1-client`. Native only. Record: [`docs/security/http-client.md`](../docs/security/http-client.md) | RFC 9110 §9.2.2 and §15.4, RFC 9112; a loopback server and proxy in the test itself: keep-alive, retries, redirects, proxies, timeouts and body caps (`tests/link/net_http_fetch`) |
228
229
  | [`net/tls/ticket.ts`](./net/tls/ticket.ts) | Session tickets a server seals for itself (RFC 8446 §4.6.1): `tlsTicketSeal` and `tlsTicketOpen` seal a `TlsTicketState` (suite, issue time, lifetime, chain start, server name, resumption PSK) under ChaCha20-Poly1305 with a borrowed `Secret<u8[]>` key, as key id ‖ nonce ‖ ciphertext ‖ tag, and wipe the plaintext on every path. `TlsTicketKeyInfo`, `tlsTicketKeyDraw` and `tlsTicketKeyDue` are a key's public record, its draw and when to replace it: a key seals for 12 h of awake time and 2^30 tickets, and opens for 14 h. `TlsTicketStrikes` is the bounded strike register (§8.1) that makes a ticket single-use, failing closed when full | `tests/link/net_tls_ticket`: every bound at its instant and 1 ns past it, a flipped bit in each region, three key rotations, replay and a full register |
229
230
  | [`net/websocket.ts`](./net/websocket.ts) | `WsDecoder(expectMasked, maxMessage)`, the same `feed` and `next()` over WebSocket frames: `WS_MESSAGE` (fragments joined, text checked as UTF-8 fragment by fragment), `WS_PING`, `WS_PONG`, and the final `WS_CLOSE` and `WS_ERROR`, whose `closeCode` is the 1002, 1007 or 1009 to close with. `websocketFrame` encodes a frame with the mask key the caller supplies (`websocketWriteFrame` into a caller's buffer, `websocketFrameSize` its length), `reset()` readies a decoder for the next connection, `websocketClosePayload` a close frame's code and reason, and `websocketIsUtf8` checks a window. The handshake is `websocketRequestKey(parser)` over an `Http1Parser`'s head, `websocketAcceptKey`, `websocketKeyIsValid` and `websocketUpgradeResponse`, the 101 | RFC 6455 §1.3's accept key, the §5.7 example frames byte for byte, §5.5's control-frame rules, §7.4's close codes |
230
231
  | [`net/http1-server.ts`](./net/http1-server.ts) | The server side of HTTP/1.1 (WP34 H1): `Http1Connection`, sans-IO, drives an `Http1Parser` — `feed`, then `next()` answers `H1_REQUEST` (the parser's head), `H1_BODY` (at most `chunkSize` bytes, either framing), `H1_END`, `H1_WRITE` (a held-back write may go again), `H1_WS_MESSAGE`, `H1_WS_CLOSE` or the final `H1_ERROR`; the program answers with `respond(status, names, values, bodyLength)`, `write` and `end`, or `acceptWebSocket(protocol)` and then `sendFrame` and `closeWebSocket`. Keep-alive and pipelining one exchange at a time, a response with a length or chunked (unframed to an HTTP/1.0 client), `Connection` and the framing decided by the server, every parser refusal answered with its status and a close, pings and closes answered on a WebSocket. Two slot pools for a program that owns its loop: `Http1Server` on plain TCP and `Http1TlsServer` on `nish/net/tls-tcp` with ALPN `http/1.1` or none (`accept`, `readable` / `writable`, `next(slot)`, `flush(slot)`, `close`, `expire(now)` for the idle timeout and the deadline for a whole head, `headTimeout`, against a client that trickles its head). Both build a slot's connection on the first accept into it rather than at start-up. `Http1Server` reads a socket until a short read rather than until it would block, takes a free slot off a stack of released slots or the next never-used one rather than scanning for one, and answers `rearm(slot, wants)`, whether the interest changed and needs a `pollModify` at all: a keep-alive request is one `recv` and one `send`. The slots borrow their output from one `Http1OutputPool` while they write and give it back once sent, so a pool's memory follows the connections the socket is holding back, not its size: an idle slot holds no output buffer. Every final response carries a `Date` from `HttpDate`, which reads `Date.now()` and formats at most once a second, shared by a pool's slots; `pin(unixMs)` fixes it for a test, and a program's own `date` field replaces it. Every cap is an `Http1Config` number fixed at start-up, and a warmed slot allocates nothing per request (`docs/security/http1.md`). Native only, as `nish:net` is | RFC 9112 §6, §7, §9; RFC 9110 §9.3.2, §15; RFC 6455 §4.2, §5.5. Nish clients over loopback against both carriers, requests cut at every byte, 2 MiB bodies each way, pipelining, a WebSocket echo, the arena flat over two hundred requests, the date formatted once a second, and every refusal (`tests/link/net_http1_server`); `curl` against its `serve` mode |
@@ -239,7 +240,7 @@ reads one, and it still takes its randomness as an argument.
239
240
  | [`net/webtransport.ts`](./net/webtransport.ts) | `WebTransport`, the server side of WebTransport over HTTP/3 as draft-ietf-webtrans-http3-02 has it and Chrome and `wtransport` 0.7 negotiate it, a layer over one `Http3Connection` whose `Http3Config.webtransportSessions` turns it on (extended CONNECT, SETTINGS_H3_DATAGRAM and the draft's two WebTransport settings): `next()` answers `WT_SESSION` (the program `accept`s or `refuse`s it), `WT_DATAGRAM`, `WT_STREAM`, `WT_STREAM_DATA`, `WT_STREAM_END`, `WT_STREAM_RESET`, `WT_STREAM_STOPPED`, `WT_WRITABLE`, `WT_DRAIN` and `WT_CLOSED`, and passes a plain request's `H3_*` events through; `sendDatagram` (the quarter stream ID, then the payload), `openStream`, `write`, `resetStream`, `stopSending`, `close` (CLOSE_WEBTRANSPORT_SESSION) and `drain`. Streams that arrive before their session wait, bounded; every cap is fixed at start-up, and a warm session allocates nothing per datagram or stream (`docs/security/webtransport.md`) | RFC 9220, RFC 9297 and draft-02: a Nish client over QUIC — sessions, datagrams at the size limits, streams both ways, close and drain from either side, every refusal — and one exchange recorded from a `wtransport` 0.7 client replayed byte for byte (`tests/link/net_webtransport`) |
240
241
  | [`net/http-compress.ts`](./net/http-compress.ts) | Content coding for HTTP responses: `httpAcceptEncoding(buf, start, end, offered)` reads an `Accept-Encoding` value (q-values, `*`, `identity`, `x-gzip`) into `HTTP_CODING_GZIP`, `HTTP_CODING_DEFLATE`, `HTTP_CODING_IDENTITY` or `HTTP_CODING_NONE`, allocating nothing; `HttpCompressor(level, minLength, maxLength)` gzips a body into its own buffer when it is worth it (-1 to send it as it is) and builds the response fields (`addFields`: `Content-Encoding: gzip` and, while `vary`, `Vary: Accept-Encoding`). On HTTP/1.1, `Http1Connection.acceptEncoding()` and `respondEncoded(status, names, values, body, off, len, enc, coding)` answer a request in one call with the compressed `Content-Length`, or nothing at all (`H1_AGAIN`, `H1_INVALID`). Opt-in; BREACH is the program's to avoid (`docs/security/compress.md`) | RFC 9110 §8.4 and §12.5.3: the negotiation table, and sans-IO responses gunzipped back (`tests/link/std_http_compress`) |
241
242
  | [`net/router.ts`](./net/router.ts) | `Router`, a method and path router that knows no carrier and allocates nothing per match: `add(method, pattern)` answers the route's index (0, 1, 2 in the order added) or a refusal — `ROUTER_BAD_PATTERN`, `ROUTER_BAD_METHOD`, `ROUTER_TOO_LONG`, `ROUTER_AMBIGUOUS` (`*` not last), `ROUTER_DUPLICATE` (the same shape and method, whatever the parameter names) or `ROUTER_FULL`, against `RouterLimits`; `match(method, mStart, mEnd, target, tStart, tEnd, params)` answers the index, `ROUTER_NOT_FOUND` or `ROUTER_METHOD_NOT_ALLOWED`, writing each `:name` and a final `*` as a `[start, end)` pair into the caller's `params` (`ROUTER_TOO_MANY_PARAMS` when it is short). Literal before parameter before `*`, segment by segment, backtracking; a lookup enters each node at most once. The query is left unparsed and nothing is decoded: `routerDecode` decodes one parameter (`ROUTER_BAD_ESCAPE`, `ROUTER_NO_ROOM`) (`docs/security/router.md`) | Every refusal code, precedence and backtracking, 405 against 404, `%2F` kept in one segment, the query, `*` and authority-form targets, a target deeper than any route, the lookup bound, params past storage, and the arena flat over matches (`tests/link/net_router`, `net_router_f64`, five `net_router_negative_*`) |
242
- | [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first, to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`) |
243
+ | [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindStartBinary` for a Bind whose result columns come in binary (read in place by `binaryInt`, `binaryBool`, `binaryFloat` and `PgBinary`'s `timestamp` and `iso`, and a one-dimensional `int2[]`, `int4[]` or `int8[]` by `PgBinaryArray`), `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first (read with `binaryInt` when `PgBatchConfig.binary` is set, so the batch's rows come in binary too), to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`) |
243
244
 
244
245
  **How a carrier drives `TlsServer`.** The server speaks in handshake
245
246
  *messages*, tagged by level: `TLS_LEVEL_INITIAL` (cleartext — ClientHello,
@@ -496,7 +497,7 @@ have, and all three have since landed as builtins:
496
497
  | Was missing | Now |
497
498
  | --- | --- |
498
499
  | a directory listing | `readdirSync(path): string[] \| null`, sorted by bytes, because there is no `sort` for a caller to reach for |
499
- | a child's **output** — `spawnSync` answers a status and nothing else | `spawnSyncTo(argv, stdoutPath, stderrPath)`, each stream to a file, an empty path inheriting |
500
+ | a child's **output** — `spawnSync` answers a status and nothing else | `spawnSyncTo(argv, stdoutPath, stderrPath)`, each stream to a file, an empty path inheriting; `spawnPiped(argv, stdin, stdout, stderr)`, stdin from a `u8[]` and both outputs into `u8[]`s, for a secret the child reads on stdin |
500
501
  | a clock | `monotonicNanos(): i64` |
501
502
 
502
503
  So the driver exists, in the language, and it is