@amritk/nish-x86_64-linux 0.17.0 → 0.20.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.
Files changed (61) hide show
  1. package/INSTALL.md +43 -25
  2. package/bin/nish +0 -0
  3. package/package.json +2 -2
  4. package/runtime/nish.d.ts +55 -5
  5. package/runtime/nish.h +231 -20
  6. package/runtime/nish.mjs +16 -0
  7. package/runtime/runtime-aes.c +490 -0
  8. package/runtime/runtime-cross.c +366 -0
  9. package/runtime/runtime-host.c +7 -6
  10. package/runtime/runtime-net.c +93 -34
  11. package/runtime/runtime-os.c +213 -15
  12. package/runtime/runtime-parallel.c +590 -107
  13. package/runtime/runtime-simd.c +203 -0
  14. package/runtime/runtime-wasm.c +16 -3
  15. package/runtime/runtime.c +89 -23
  16. package/runtime/shim.mjs +71 -1
  17. package/scripts/build.sh +225 -20
  18. package/std/README.md +86 -55
  19. package/std/bytes.ts +163 -0
  20. package/std/crypto/LICENSE-libsecp256k1 +19 -0
  21. package/std/crypto/aes.ts +238 -76
  22. package/std/crypto/chacha20poly1305.ts +43 -9
  23. package/std/crypto/ed25519.ts +840 -0
  24. package/std/crypto/hkdf.ts +304 -12
  25. package/std/crypto/hmac.ts +245 -53
  26. package/std/crypto/p256.ts +1376 -42
  27. package/std/crypto/x25519.ts +55 -37
  28. package/std/crypto/x509-random.ts +71 -0
  29. package/std/crypto/x509.ts +9 -9
  30. package/std/json-tokenizer.ts +1296 -0
  31. package/std/json-typed.ts +1014 -0
  32. package/std/json-value.ts +1163 -0
  33. package/std/json.ts +1412 -70
  34. package/std/net/buffer-pool.ts +151 -0
  35. package/std/net/hpack.ts +416 -112
  36. package/std/net/http-fields.ts +93 -0
  37. package/std/net/http1-server.ts +417 -55
  38. package/std/net/http1.ts +31 -6
  39. package/std/net/http2-tls.ts +112 -24
  40. package/std/net/http2.ts +333 -75
  41. package/std/net/http3-server.ts +231 -45
  42. package/std/net/http3.ts +187 -28
  43. package/std/net/prefork.ts +412 -0
  44. package/std/net/quic-conn-params.ts +86 -16
  45. package/std/net/quic-listener.ts +576 -219
  46. package/std/net/quic-packet.ts +443 -58
  47. package/std/net/quic-recovery.ts +361 -30
  48. package/std/net/quic-stream.ts +434 -28
  49. package/std/net/quic.ts +922 -206
  50. package/std/net/router.ts +676 -0
  51. package/std/net/tls/codec.ts +426 -27
  52. package/std/net/tls/record-server.ts +478 -132
  53. package/std/net/tls/record.ts +133 -42
  54. package/std/net/tls/schedule.ts +464 -55
  55. package/std/net/tls/ticket.ts +610 -0
  56. package/std/net/tls-tcp.ts +204 -57
  57. package/std/net/tls.ts +1132 -305
  58. package/std/net/url.ts +571 -0
  59. package/std/random.ts +94 -0
  60. package/std/text.ts +77 -0
  61. package/std/threads.ts +438 -14
@@ -0,0 +1,203 @@
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).
3
+ *
4
+ * A translation unit of its own for the reason runtime-host.c is one: each file
5
+ * carries its own measured `.text*` ceiling in tests/runtime-budgets.js, and
6
+ * runtime.c had no room left when this arrived. Section GC means a program that
7
+ * never calls a kernel links none of this file, and scripts/build.sh pairs it
8
+ * with runtime.c like the other halves, so a link line still names one runtime.
9
+ *
10
+ * Every kernel has up to three paths. The scalar one runs anywhere. The base
11
+ * one uses the vectors every machine of the target has -- SSE2 on x86-64, NEON
12
+ * on AArch64, simd128 on wasm only when built with `-msimd128`, which is not
13
+ * the default (wp38 §5) -- so it needs no question asked of the CPU. The AVX2
14
+ * one is compiled for that extension alone with `target("avx2")`, so the binary
15
+ * stays a baseline binary and only a machine that has it ever runs it.
16
+ *
17
+ * Which path runs is decided on a kernel's first call, never at program start.
18
+ * The kernel is called through a pointer that starts at its resolver; the
19
+ * resolver asks the CPU, stores the path it picked and calls it, and every
20
+ * later call goes straight to the stored path. `NISH_SIMD` pins the choice:
21
+ * `scalar` and `base` name their paths, and `avx2`, any other value, or no
22
+ * value at all mean the best this machine has. A machine without AVX2 that is
23
+ * asked for it gets the base path, because running it would fault. The
24
+ * instruction gate sets `NISH_SIMD=base` (bench/run.mjs) so that a count does
25
+ * not depend on the host it was measured on.
26
+ */
27
+ #include <stdint.h>
28
+ #include <stdlib.h>
29
+ #include <string.h>
30
+
31
+ #include "nish.h"
32
+
33
+ #if defined(__x86_64__) || defined(__i386__)
34
+ #if defined(__SSE2__)
35
+ #include <cpuid.h>
36
+ #include <immintrin.h>
37
+ #define NISH_SIMD_SSE2 1
38
+ #endif
39
+ #elif defined(__ARM_NEON)
40
+ #include <arm_neon.h>
41
+ #define NISH_SIMD_NEON 1
42
+ #elif defined(__wasm_simd128__)
43
+ #include <wasm_simd128.h>
44
+ #define NISH_SIMD_WASM 1
45
+ #endif
46
+
47
+ /* One path of `nish_str_index_of_any`: the contract in nish.h. */
48
+ typedef int64_t (*nish_any_path)(const nish_str *s, const nish_str *set, int64_t from);
49
+
50
+ /* The scalar path, and the tail of the vector ones. A 256-bit membership
51
+ table answers each byte with one load and one shift whatever the size of
52
+ the set, so this path is also total: an empty set finds nothing and a set
53
+ longer than sixteen bytes is answered correctly, which is why the vector
54
+ paths hand any set outside 1..16 bytes to it rather than overrun their
55
+ sixteen splatted registers. */
56
+ static int64_t nish_any_scalar(const nish_str *s, const nish_str *set, int64_t from) {
57
+ uint32_t in[8] = {0};
58
+ for (uint64_t k = 0; k < set->len; k++) {
59
+ uint8_t b = (uint8_t)set->data[k];
60
+ in[b >> 5] |= 1u << (b & 31);
61
+ }
62
+ for (uint64_t i = (uint64_t)from; i < s->len; i++) {
63
+ uint8_t b = (uint8_t)s->data[i];
64
+ if (in[b >> 5] >> (b & 31) & 1) return (int64_t)i;
65
+ }
66
+ return -1;
67
+ }
68
+
69
+ /* The vector paths compare each block against every byte of the set and OR
70
+ the answers, rather than classify each byte by its two nibbles through a
71
+ shuffle table as simdjson does. The shuffle is `pshufb`, which is SSSE3 and
72
+ not in the x86-64 baseline, and a two-table classify is only exact for a
73
+ set whose bytes fit eight nibble classes; an arbitrary set of sixteen bytes
74
+ does not. A compare per set byte is exact for every set, and the sets this
75
+ was built for -- a quote and a backslash, JSON's six structural bytes -- are
76
+ small enough that the compares cost less than the shuffles would. */
77
+ #if defined(NISH_SIMD_SSE2)
78
+ static int64_t nish_any_base(const nish_str *s, const nish_str *set, int64_t from) {
79
+ uint64_t k = set->len, n = s->len, i = (uint64_t)from;
80
+ if (k - 1 >= 16) return nish_any_scalar(s, set, from);
81
+ __m128i want[16];
82
+ for (uint64_t j = 0; j < k; j++) want[j] = _mm_set1_epi8(set->data[j]);
83
+ for (; i + 16 <= n; i += 16) {
84
+ __m128i v = _mm_loadu_si128((const __m128i *)(s->data + i));
85
+ __m128i hit = _mm_cmpeq_epi8(v, want[0]);
86
+ for (uint64_t j = 1; j < k; j++) hit = _mm_or_si128(hit, _mm_cmpeq_epi8(v, want[j]));
87
+ unsigned m = (unsigned)_mm_movemask_epi8(hit);
88
+ if (m) return (int64_t)(i + (uint64_t)__builtin_ctz(m));
89
+ }
90
+ return nish_any_scalar(s, set, (int64_t)i);
91
+ }
92
+
93
+ /* Thirty-two bytes a block, then the base path for what is left: a tail of
94
+ up to thirty-one bytes is at most one sixteen-byte block and a scalar run. */
95
+ __attribute__((target("avx2"))) static int64_t nish_any_avx2(const nish_str *s, const nish_str *set,
96
+ int64_t from) {
97
+ uint64_t k = set->len, n = s->len, i = (uint64_t)from;
98
+ if (k - 1 >= 16) return nish_any_scalar(s, set, from);
99
+ __m256i want[16];
100
+ for (uint64_t j = 0; j < k; j++) want[j] = _mm256_set1_epi8(set->data[j]);
101
+ for (; i + 32 <= n; i += 32) {
102
+ __m256i v = _mm256_loadu_si256((const __m256i *)(s->data + i));
103
+ __m256i hit = _mm256_cmpeq_epi8(v, want[0]);
104
+ for (uint64_t j = 1; j < k; j++) hit = _mm256_or_si256(hit, _mm256_cmpeq_epi8(v, want[j]));
105
+ unsigned m = (unsigned)_mm256_movemask_epi8(hit);
106
+ if (m) return (int64_t)(i + (uint64_t)__builtin_ctz(m));
107
+ }
108
+ return nish_any_base(s, set, (int64_t)i);
109
+ }
110
+ #elif defined(NISH_SIMD_NEON)
111
+ static int64_t nish_any_base(const nish_str *s, const nish_str *set, int64_t from) {
112
+ uint64_t k = set->len, n = s->len, i = (uint64_t)from;
113
+ if (k - 1 >= 16) return nish_any_scalar(s, set, from);
114
+ uint8x16_t want[16];
115
+ for (uint64_t j = 0; j < k; j++) want[j] = vdupq_n_u8((uint8_t)set->data[j]);
116
+ for (; i + 16 <= n; i += 16) {
117
+ uint8x16_t v = vld1q_u8((const uint8_t *)s->data + i);
118
+ uint8x16_t hit = vceqq_u8(v, want[0]);
119
+ for (uint64_t j = 1; j < k; j++) hit = vorrq_u8(hit, vceqq_u8(v, want[j]));
120
+ /* NEON has no movemask. Shifting each 16-bit pair of answers right by four
121
+ and narrowing keeps four bits of every byte's answer, in order, in one
122
+ 64-bit lane, so the first hit is the lowest set bit over four. The
123
+ idea is Danila Kutenin's ("Porting x86 vector bitmask optimizations to
124
+ Arm NEON", Arm Community blog, 2022). */
125
+ uint64_t m = vget_lane_u64(vreinterpret_u64_u8(vshrn_n_u16(vreinterpretq_u16_u8(hit), 4)), 0);
126
+ if (m) return (int64_t)(i + ((uint64_t)__builtin_ctzll(m) >> 2));
127
+ }
128
+ return nish_any_scalar(s, set, (int64_t)i);
129
+ }
130
+ #elif defined(NISH_SIMD_WASM)
131
+ static int64_t nish_any_base(const nish_str *s, const nish_str *set, int64_t from) {
132
+ uint64_t k = set->len, n = s->len, i = (uint64_t)from;
133
+ if (k - 1 >= 16) return nish_any_scalar(s, set, from);
134
+ v128_t want[16];
135
+ for (uint64_t j = 0; j < k; j++) want[j] = wasm_i8x16_splat(set->data[j]);
136
+ for (; i + 16 <= n; i += 16) {
137
+ v128_t v = wasm_v128_load(s->data + i);
138
+ v128_t hit = wasm_i8x16_eq(v, want[0]);
139
+ for (uint64_t j = 1; j < k; j++) hit = wasm_v128_or(hit, wasm_i8x16_eq(v, want[j]));
140
+ uint32_t m = wasm_i8x16_bitmask(hit);
141
+ if (m) return (int64_t)(i + (uint64_t)__builtin_ctz(m));
142
+ }
143
+ return nish_any_scalar(s, set, (int64_t)i);
144
+ }
145
+ #else
146
+ /* A target with no vectors in its baseline: wasm without `-msimd128`, the
147
+ default there, among them. Its base path is the scalar one. */
148
+ #define nish_any_base nish_any_scalar
149
+ #endif
150
+
151
+ #if defined(NISH_SIMD_SSE2)
152
+ /* Whether this machine runs AVX2 code: the CPU has the extension (leaf 7,
153
+ EBX bit 5) and the operating system saves the 256-bit registers across a
154
+ context switch (OSXSAVE, then XCR0's SSE and AVX bits): the procedure the
155
+ Intel SDM gives for detecting AVX2, and the three facts
156
+ `__builtin_cpu_supports("avx2")` reads. That builtin is not used,
157
+ because its answer lives in libgcc's `__cpu_model`, and the object that
158
+ defines it carries a constructor that fills it in at start-up: once a link
159
+ names it, every program pays, a kernel called or not. Measured on
160
+ `examples/hello.ts` at `--profile speed`, that object took the binary from
161
+ 4,712 bytes to 10,000 and the run 374 instructions longer. `cpuid.h` is inline
162
+ assembly and nothing else, so here the question is asked on the first call
163
+ and never before it. */
164
+ static int nish_cpu_has_avx2(void) {
165
+ unsigned a, b, c, d;
166
+ if (!__get_cpuid(1, &a, &b, &c, &d) || !(c & bit_OSXSAVE) || !(c & bit_AVX)) return 0;
167
+ unsigned lo, hi;
168
+ __asm__("xgetbv" : "=a"(lo), "=d"(hi) : "c"(0));
169
+ if ((lo & 6) != 6) return 0;
170
+ return __get_cpuid_count(7, 0, &a, &b, &c, &d) && (b & bit_AVX2);
171
+ }
172
+ #endif
173
+
174
+ /* Which path `NISH_SIMD` and this machine pick. AVX2 is asked of the CPU only
175
+ when the answer could matter, so a pinned `scalar` or `base` never reads
176
+ the CPU's feature bits at all. */
177
+ static nish_any_path nish_any_choose(void) {
178
+ const char *pin = getenv("NISH_SIMD");
179
+ if (pin != NULL && strcmp(pin, "scalar") == 0) return nish_any_scalar;
180
+ if (pin != NULL && strcmp(pin, "base") == 0) return nish_any_base;
181
+ #if defined(NISH_SIMD_SSE2)
182
+ if (nish_cpu_has_avx2()) return nish_any_avx2;
183
+ #endif
184
+ return nish_any_base;
185
+ }
186
+
187
+ static int64_t nish_any_resolve(const nish_str *s, const nish_str *set, int64_t from);
188
+
189
+ /* The path every call goes through, the resolver until the first call has
190
+ run. The loads and the store are relaxed atomics, which are plain moves on
191
+ every target, because under `--threads` two workers may make the first call
192
+ at once; both pick the same path, so whichever store lands is the answer. */
193
+ static nish_any_path nish_any = nish_any_resolve;
194
+
195
+ static int64_t nish_any_resolve(const nish_str *s, const nish_str *set, int64_t from) {
196
+ nish_any_path path = nish_any_choose();
197
+ __atomic_store_n(&nish_any, path, __ATOMIC_RELAXED);
198
+ return path(s, set, from);
199
+ }
200
+
201
+ int64_t nish_str_index_of_any(const nish_str *s, const nish_str *set, int64_t from) {
202
+ return __atomic_load_n(&nish_any, __ATOMIC_RELAXED)(s, set, from);
203
+ }
@@ -115,6 +115,17 @@ void nish_array_grow(nish_array *a, uint64_t elem_size) {
115
115
  a->cap = cap;
116
116
  }
117
117
 
118
+ /* ---- Math.min and Math.max of a float: llvm.minnum / llvm.maxnum, which wasm32
119
+ * has no instruction for. Its `f64.min` answers NaN when either operand is NaN,
120
+ * and minnum answers the other operand, so the backend lowers minnum to a libm
121
+ * call, and these are that libm. They answer what minnum answers natively: a
122
+ * NaN operand is dropped, and two NaNs give NaN (tests/cases/math_minmax_float,
123
+ * held to these by tests/run.js). Between `0` and `-0` minnum may answer either. */
124
+ double fmin(double a, double b) { return a != a ? b : b != b ? a : a < b ? a : b; }
125
+ double fmax(double a, double b) { return a != a ? b : b != b ? a : a > b ? a : b; }
126
+ float fminf(float a, float b) { return a != a ? b : b != b ? a : a < b ? a : b; }
127
+ float fmaxf(float a, float b) { return a != a ? b : b != b ? a : a > b ? a : b; }
128
+
118
129
  /* ---- Panics: `unreachable`, which the host sees as a RuntimeError */
119
130
  void nish_panic_index(uint64_t idx, uint64_t len) { (void)idx; (void)len; __builtin_trap(); }
120
131
  void nish_panic_slice(int64_t s, int64_t e, int64_t len) { (void)s; (void)e; (void)len; __builtin_trap(); }
@@ -122,7 +133,9 @@ void nish_panic_div(_Bool by_zero) { (void)by_zero; __builtin_trap(); }
122
133
  void nish_panic_overflow(uint32_t op) { (void)op; __builtin_trap(); }
123
134
  void nish_exit(int32_t code) { (void)code; __builtin_trap(); }
124
135
  /* A panic's message (`panic`, `expect`, a failed range entry) has no stderr to
125
- * go to here; the `nish_exit` that follows it is the trap. An exported function
126
- * with a ranged parameter checks it on entry (WP31 §9), so this is what lets
127
- * such a module link; the loader throws a RangeError before the call reaches it. */
136
+ * go to here, so `nish_panic` is the trap. An exported function with a ranged
137
+ * parameter checks it on entry (WP31 §9), so this is what lets such a module
138
+ * link; the loader throws a RangeError before the call reaches it. */
139
+ void nish_panic(const void *s) { (void)s; __builtin_trap(); }
140
+ /* Output has nowhere to go either. */
128
141
  void nish_write(const void *s, int32_t fd, _Bool newline) { (void)s; (void)fd; (void)newline; }
package/runtime/runtime.c CHANGED
@@ -18,6 +18,7 @@
18
18
  * both use `-flto`, and the cold paths that lost the inlining are cold.
19
19
  */
20
20
  #define _POSIX_C_SOURCE 200809L
21
+ #include <errno.h>
21
22
  #include <inttypes.h>
22
23
  #include <math.h>
23
24
  #include <stddef.h>
@@ -69,6 +70,11 @@ int __main_argc_argv(int argc, char **argv) { return nish_c_main(argc, argv); }
69
70
  allocates from compiled code. The asserts below are the contract, checked
70
71
  wherever this file is compiled. */
71
72
  typedef struct nish_chunk { struct nish_chunk *next; uint64_t cap; } nish_chunk;
73
+ /* runtime-cross.c walks the chunks through a copy of this definition, which
74
+ asserts the same three facts. */
75
+ _Static_assert(sizeof(nish_chunk) == 16, "runtime-cross.c's nish_xchunk: { next, cap }, the bytes after it");
76
+ _Static_assert(offsetof(nish_chunk, next) == 0, "runtime-cross.c's nish_xchunk: next first");
77
+ _Static_assert(offsetof(nish_chunk, cap) == 8, "runtime-cross.c's nish_xchunk: cap second");
72
78
  struct nish_arena { char *buf; uint64_t off; uint64_t cap; nish_chunk *chunks; };
73
79
  /* One per thread under -DNISH_THREADS, one per process otherwise; the layout is
74
80
  the same either way, which is why nothing else in this file has to know. */
@@ -79,12 +85,45 @@ _Static_assert(offsetof(struct nish_arena, off) == 8, "the inlined allocator bum
79
85
  _Static_assert(offsetof(struct nish_arena, cap) == 16, "the inlined allocator reads field 2");
80
86
  _Static_assert(offsetof(struct nish_arena, chunks) == 24, "arena layout is ABI");
81
87
 
82
- /* `fputs` rather than one `write`: libc's own loop finishes a short write,
83
- which a bare `write` would cut off (docs/security/runtime.md, RT-11), and
84
- stderr is unbuffered, so the message leaves before `_exit`. A C host that
85
- gives stderr a buffer of its own would lose it here. */
88
+ /* ---- Panics: the first one wins.
89
+
90
+ Every way a program fails -- a failed check, `panic`, out of memory -- calls
91
+ `nish_panic_claim` before it prints. Two tasks of a scope, or two workers of
92
+ a parallel map, can fail at once, and without the claim both messages would
93
+ reach stderr interleaved and both threads would race to the exit. The first
94
+ to claim prints its whole message and exits; any later one parks until
95
+ that exit ends the process, so exactly one message is printed and the exit
96
+ code is the first panic's. How each path exits is unchanged by the claim:
97
+ `panic`, `expect` and a failed range entry (`nish_panic`) call `exit(1)`, as
98
+ they did through `nish_exit`, so atexit handlers run and stdio is flushed;
99
+ a failed check and out of memory call `_exit(1)`, as they always have. Only
100
+ one thread can reach either exit, because the claim comes first. Only a
101
+ threads build (`-DNISH_THREADS`) can have two threads panic, so only it
102
+ pays for the claim; everywhere else it is an empty inline function. Under wasi with threads, the trap is the
103
+ defensive answer: no host there gives a parked thread a way to wait. */
104
+ #ifdef NISH_THREADS
105
+ static int nish_panicked;
106
+ void nish_panic_claim(void) {
107
+ if (__atomic_exchange_n(&nish_panicked, 1, __ATOMIC_ACQ_REL)) {
108
+ #if defined(__wasi__)
109
+ __builtin_trap();
110
+ #else
111
+ for (;;) pause();
112
+ #endif
113
+ }
114
+ }
115
+ #else
116
+ static inline void nish_panic_claim(void) {}
117
+ #endif
118
+
119
+ /* The message goes out through `nish_write_all`, below, which finishes a
120
+ short write and retries one a signal interrupts (docs/security/runtime.md,
121
+ RT-11); it is unbuffered, so the message leaves before `_exit`. It used
122
+ `fputs`, whose libc loop finishes a short write but gives up on `EINTR`. */
123
+ static void nish_write_all(int fd, const char *p, uint64_t n);
86
124
  static NISH_COLD void nish_die(const char *msg) {
87
- fputs(msg, stderr);
125
+ nish_panic_claim();
126
+ nish_write_all(2, msg, strlen(msg));
88
127
  _exit(1);
89
128
  }
90
129
  /* A function rather than a macro around `nish_die`: seven call sites each
@@ -251,19 +290,29 @@ _Bool nish_str_at(const nish_str *s, int64_t at, const nish_str *sub) {
251
290
  exactly such a header from `examples/strings.ts`, and the interop tests
252
291
  caught runtime.c picking it up and inheriting `nish.h` through it. A C host
253
292
  would hit the same. A fifth of the time is not worth making the runtime
254
- sensitive to its includer's `-I` paths. */
255
- int64_t nish_str_index_of(const nish_str *s, const nish_str *sub) {
256
- if (sub->len == 0) return 0;
257
- if (sub->len > s->len) return -1;
258
- const char *p = s->data, *end = s->data + s->len - sub->len + 1;
259
- while (p < end) {
260
- const char *c = memchr(p, sub->data[0], (size_t)(end - p));
261
- if (!c) return -1;
262
- if (memcmp(c + 1, sub->data + 1, sub->len - 1) == 0) return (int64_t)(c - s->data);
263
- p = c + 1;
293
+ sensitive to its includer's `-I` paths.
294
+
295
+ `s.indexOf(sub, from)` is the same search from byte `from`, and
296
+ `s.indexOf(sub)` is it from 0: one loop, not two. The compiler clamps `from`
297
+ into [0, s.length] first, as JavaScript does, with `llvm.smin`/`smax` where
298
+ LLVM can fold them, so the clamp costs this file nothing; an empty needle
299
+ answers that clamped `from`. The clamp is a precondition: a caller that
300
+ skips it never reads outside the string, but gets -1 for a non-empty needle
301
+ (a negative `from` reads as one past the end) and `from` itself, unchanged,
302
+ for an empty one. */
303
+ int64_t nish_str_index_of_from(const nish_str *s, const nish_str *sub, int64_t from) {
304
+ uint64_t at = (uint64_t)from;
305
+ if (sub->len == 0) return from;
306
+ while (at <= s->len && sub->len <= s->len - at) {
307
+ const char *c = memchr(s->data + at, sub->data[0], s->len - at - sub->len + 1);
308
+ if (!c) break;
309
+ at = (uint64_t)(c - s->data);
310
+ if (memcmp(c + 1, sub->data + 1, sub->len - 1) == 0) return (int64_t)at;
311
+ at++;
264
312
  }
265
313
  return -1;
266
314
  }
315
+ int64_t nish_str_index_of(const nish_str *s, const nish_str *sub) { return nish_str_index_of_from(s, sub, 0); }
267
316
 
268
317
  /* `console.log` / `console.error` / `write` / `writeError` and the message of
269
318
  `panic`: fd 1 or 2, with or without the trailing newline.
@@ -273,19 +322,34 @@ int64_t nish_str_index_of(const nish_str *s, const nish_str *sub) {
273
322
  does the same. So the bytes are written until all of them are out, or until
274
323
  a write fails (docs/security/runtime.md, RT-11). A failure still ends the
275
324
  output, `EAGAIN` on a non-blocking stdout included, since spinning on it
276
- would burn a core. `EINTR` is not retried here: it needs a handler
277
- installed without `SA_RESTART`, and the only one this runtime installs
278
- (runtime-host.c) has it, so an interrupted write either restarts in the
279
- kernel or comes back short. The newline needs no loop: one byte is never
280
- written in part. */
281
- void nish_write(const nish_str *s, int32_t fd, _Bool newline) {
325
+ would burn a core. `EINTR` is retried, the newline's included: the only
326
+ handler this runtime installs (runtime-host.c) has `SA_RESTART`, but a C
327
+ host may install one without it. `panic`'s message, `expect`'s, and the
328
+ arithmetic and out-of-memory panics (`nish_die`) are written here. The
329
+ index and slice panics and the file and entropy failures use `dprintf`, and
330
+ runtime-parallel.c's out of memory one `write`: neither retries `EINTR`. */
331
+ static void nish_write_all(int fd, const char *p, uint64_t n) {
282
332
  uint64_t done = 0;
283
333
  ssize_t k;
284
- while (done < s->len && (k = write(fd, s->data + done, s->len - done)) > 0) done += k;
285
- if (newline) (void)!write(fd, "\n", 1);
334
+ while (done < n) {
335
+ k = write(fd, p + done, n - done);
336
+ if (k > 0) done += k;
337
+ else if (k == 0 || errno != EINTR) break;
338
+ }
339
+ }
340
+ void nish_write(const nish_str *s, int32_t fd, _Bool newline) {
341
+ nish_write_all(fd, s->data, s->len);
342
+ if (newline) nish_write_all(fd, "\n", 1);
286
343
  }
287
344
  /* console.log(s) */
288
345
  void nish_print(const nish_str *s) { nish_write(s, 1, 1); }
346
+ /* `panic(message)`, `r.expect(message)` and a failed range entry: the message
347
+ and a newline on stderr, then `exit(1)`, the first panic only. */
348
+ __attribute__((noreturn)) void nish_panic(const nish_str *s) {
349
+ nish_panic_claim();
350
+ nish_write(s, 2, 1);
351
+ exit(1);
352
+ }
289
353
 
290
354
  /* Decimal digits of `u`, with a '-' in front when `neg`. 20 digits is the
291
355
  widest a uint64_t can be, plus the sign. */
@@ -1235,6 +1299,7 @@ __attribute__((noinline)) void nish_wipe(nish_array *bytes) {
1235
1299
  }
1236
1300
 
1237
1301
  void nish_panic_index(uint64_t idx, uint64_t len) {
1302
+ nish_panic_claim();
1238
1303
  dprintf(2, "index out of range: %" PRIu64 " >= %" PRIu64 "\n", idx, len);
1239
1304
  _exit(1);
1240
1305
  }
@@ -1248,6 +1313,7 @@ void nish_panic_index(uint64_t idx, uint64_t len) {
1248
1313
  a reader who wrote `s.slice(i - 1)` wants to be told `-1` rather than
1249
1314
  18446744073709551615. The length cannot be negative either way. */
1250
1315
  void nish_panic_slice(int64_t start, int64_t end, int64_t len) {
1316
+ nish_panic_claim();
1251
1317
  dprintf(2, "slice out of range: [%" PRId64 ", %" PRId64 ") of length %" PRId64 "\n", start, end, len);
1252
1318
  _exit(1);
1253
1319
  }
package/runtime/shim.mjs CHANGED
@@ -22,7 +22,8 @@
22
22
  * `indexOf` answers with one. `slice` cuts on byte offsets too and panics
23
23
  * on a range the string does not contain, where JavaScript would clamp and
24
24
  * read a negative offset from the end. `String.fromCharCode` builds a
25
- * one-byte string from the low 8 bits.
25
+ * one-byte string from the low 8 bits, and `String.fromBytes` one such
26
+ * string per byte of its range.
26
27
  * - `a[i]` is bounds-checked: out of range prints
27
28
  * `index out of range: <i> >= <len>` to stderr and exits 1, and so is
28
29
  * `a.pop()` on an empty array, which has no `undefined` to return.
@@ -347,6 +348,20 @@ export function fromCharCode(code) {
347
348
  return Buffer.from([toIndex(code) & 0xff]).toString("latin1");
348
349
  }
349
350
 
351
+ /**
352
+ * `String.fromBytes(bytes, start, end)`: the bytes of `[start, end)`, one
353
+ * code unit each as `fromCharCode` builds them, checked as `s.slice`'s range
354
+ * is and failing with its panic.
355
+ */
356
+ export function fromBytes(bytes, start, end) {
357
+ const from = toIndex(start);
358
+ const to = toIndex(end);
359
+ if (!(from >= 0 && from <= to && to <= bytes.length)) panicSlice(from, to, bytes.length);
360
+ const out = Buffer.allocUnsafe(to - from);
361
+ for (let i = from; i < to; i++) out[i - from] = bytes[i];
362
+ return out.toString("latin1");
363
+ }
364
+
350
365
  /** `console.log(x)`: `String(x)` (no `n` suffix for i64) plus a newline, written synchronously. */
351
366
  export function log(x) {
352
367
  fs.writeSync(1, `${String(x)}\n`);
@@ -726,6 +741,61 @@ export function spawnSyncTo(argv, out, err) {
726
741
  return spawnImpl(argv, out, err);
727
742
  }
728
743
 
744
+ /**
745
+ * `spawn(argv)` and `waitPid(pid, flags)` (WP34 N12) have no faithful reading
746
+ * under Node, for `signalFd`'s reason: Node reaps its children itself, on the
747
+ * event loop, and hands their status to an `exit` listener, so no synchronous
748
+ * function here can wait for one, and a child Node started is never there for
749
+ * a wait-less poll to find. Each throws, naming itself.
750
+ */
751
+ function noProcessReading(name) {
752
+ throw new Error(
753
+ `\`${name}\` has no synchronous reading under Node: a child is reaped by the event loop, which a synchronous wait never returns to (docs/wp33-round-trip.md)`
754
+ );
755
+ }
756
+
757
+ export function spawn() {
758
+ return noProcessReading("spawn");
759
+ }
760
+
761
+ export function waitPid() {
762
+ return noProcessReading("waitPid");
763
+ }
764
+
765
+ /**
766
+ * `kill(pid, sig)`: 0, or a negative errno, as `runtime-os.c`'s `nish_kill`
767
+ * answers. Only 0, 2, 9 and 15 are sent and a pid of 0 or below is refused,
768
+ * both -22, before Node is asked; Node's error then names its errno, which
769
+ * `os.constants.errno` turns back into the number (-3 for no such process).
770
+ */
771
+ export function kill(pid, sig) {
772
+ if (pid <= 0 || !(sig === 0 || sig === 2 || sig === 9 || sig === 15)) return -22;
773
+ try {
774
+ process.kill(pid, sig);
775
+ return 0;
776
+ } catch (e) {
777
+ return -(os.constants.errno[e.code] ?? 22);
778
+ }
779
+ }
780
+
781
+ /**
782
+ * `availableParallelism()`: Node's own count, which reads the affinity mask as
783
+ * the native one does, lowered by a positive `NISH_NUM_THREADS` the way the
784
+ * native count is. The native count also reads a cgroup v2 CPU quota, which
785
+ * Node does not, so inside a container with a quota the two may differ; both
786
+ * are at least 1.
787
+ */
788
+ export function availableParallelism() {
789
+ const n = os.availableParallelism();
790
+ const want = /^[0-9]+$/.test(process.env.NISH_NUM_THREADS ?? "") ? Number(process.env.NISH_NUM_THREADS) : 0;
791
+ return want > 0 && want < n ? want : n;
792
+ }
793
+
794
+ /** `totalMemory()`: `os.totalmem()`, as a BigInt because an `i64` is held as one here. */
795
+ export function totalMemory() {
796
+ return BigInt(os.totalmem());
797
+ }
798
+
729
799
  /**
730
800
  * `monotonicNanos()`: `process.hrtime.bigint()`, a monotonic clock in
731
801
  * nanoseconds (`CLOCK_MONOTONIC` on every platform that has it, which is the