@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.
- package/INSTALL.md +43 -25
- package/bin/nish +0 -0
- package/package.json +2 -2
- package/runtime/nish.d.ts +55 -5
- package/runtime/nish.h +231 -20
- package/runtime/nish.mjs +16 -0
- package/runtime/runtime-aes.c +490 -0
- package/runtime/runtime-cross.c +366 -0
- package/runtime/runtime-host.c +7 -6
- package/runtime/runtime-net.c +93 -34
- package/runtime/runtime-os.c +213 -15
- package/runtime/runtime-parallel.c +590 -107
- package/runtime/runtime-simd.c +203 -0
- package/runtime/runtime-wasm.c +16 -3
- package/runtime/runtime.c +89 -23
- package/runtime/shim.mjs +71 -1
- package/scripts/build.sh +225 -20
- package/std/README.md +86 -55
- package/std/bytes.ts +163 -0
- package/std/crypto/LICENSE-libsecp256k1 +19 -0
- package/std/crypto/aes.ts +238 -76
- package/std/crypto/chacha20poly1305.ts +43 -9
- package/std/crypto/ed25519.ts +840 -0
- package/std/crypto/hkdf.ts +304 -12
- package/std/crypto/hmac.ts +245 -53
- package/std/crypto/p256.ts +1376 -42
- package/std/crypto/x25519.ts +55 -37
- package/std/crypto/x509-random.ts +71 -0
- package/std/crypto/x509.ts +9 -9
- package/std/json-tokenizer.ts +1296 -0
- package/std/json-typed.ts +1014 -0
- package/std/json-value.ts +1163 -0
- package/std/json.ts +1412 -70
- package/std/net/buffer-pool.ts +151 -0
- package/std/net/hpack.ts +416 -112
- package/std/net/http-fields.ts +93 -0
- package/std/net/http1-server.ts +417 -55
- package/std/net/http1.ts +31 -6
- package/std/net/http2-tls.ts +112 -24
- package/std/net/http2.ts +333 -75
- package/std/net/http3-server.ts +231 -45
- package/std/net/http3.ts +187 -28
- package/std/net/prefork.ts +412 -0
- package/std/net/quic-conn-params.ts +86 -16
- package/std/net/quic-listener.ts +576 -219
- package/std/net/quic-packet.ts +443 -58
- package/std/net/quic-recovery.ts +361 -30
- package/std/net/quic-stream.ts +434 -28
- package/std/net/quic.ts +922 -206
- package/std/net/router.ts +676 -0
- package/std/net/tls/codec.ts +426 -27
- package/std/net/tls/record-server.ts +478 -132
- package/std/net/tls/record.ts +133 -42
- package/std/net/tls/schedule.ts +464 -55
- package/std/net/tls/ticket.ts +610 -0
- package/std/net/tls-tcp.ts +204 -57
- package/std/net/tls.ts +1132 -305
- package/std/net/url.ts +571 -0
- package/std/random.ts +94 -0
- package/std/text.ts +77 -0
- 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
|
+
}
|
package/runtime/runtime-wasm.c
CHANGED
|
@@ -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
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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
|
-
/*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 <
|
|
285
|
-
|
|
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
|