@amritk/nish-x86_64-linux 0.16.0 → 0.18.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 (58) hide show
  1. package/INSTALL.md +43 -34
  2. package/bin/nish +0 -0
  3. package/package.json +1 -1
  4. package/runtime/nish.d.ts +117 -8
  5. package/runtime/nish.h +50 -13
  6. package/runtime/nish.mjs +71 -7
  7. package/runtime/runtime-host.c +43 -24
  8. package/runtime/runtime-net.c +57 -10
  9. package/runtime/runtime-os.c +62 -20
  10. package/runtime/runtime-simd.c +203 -0
  11. package/runtime/runtime-wasm.c +12 -0
  12. package/runtime/runtime.c +66 -18
  13. package/runtime/shim.mjs +114 -5
  14. package/scripts/build.sh +14 -12
  15. package/std/README.md +168 -36
  16. package/std/crypto/aes.ts +51 -8
  17. package/std/crypto/base64url.ts +25 -12
  18. package/std/crypto/chacha20poly1305.ts +43 -29
  19. package/std/crypto/ct.ts +6 -1
  20. package/std/crypto/hkdf.ts +364 -3
  21. package/std/crypto/hmac.ts +307 -7
  22. package/std/crypto/p256.ts +618 -34
  23. package/std/crypto/sha1.ts +147 -0
  24. package/std/crypto/sha256.ts +3 -2
  25. package/std/crypto/x25519.ts +88 -36
  26. package/std/crypto/x509.ts +130 -47
  27. package/std/json.ts +297 -41
  28. package/std/net/hpack.ts +1094 -0
  29. package/std/net/http-fields.ts +604 -0
  30. package/std/net/http1-server.ts +1389 -0
  31. package/std/net/http1.ts +1492 -0
  32. package/std/net/http2-frame.ts +745 -0
  33. package/std/net/http2-tls.ts +216 -0
  34. package/std/net/http2.ts +1433 -0
  35. package/std/net/http3-frame.ts +457 -0
  36. package/std/net/http3-server.ts +911 -0
  37. package/std/net/http3.ts +2145 -0
  38. package/std/net/qpack.ts +1163 -0
  39. package/std/net/quic-conn-ack.ts +206 -0
  40. package/std/net/quic-conn-cid.ts +425 -0
  41. package/std/net/quic-conn-params.ts +462 -0
  42. package/std/net/quic-datagram.ts +130 -0
  43. package/std/net/quic-frame.ts +1206 -0
  44. package/std/net/quic-listener.ts +929 -0
  45. package/std/net/quic-packet.ts +1698 -0
  46. package/std/net/quic-recovery.ts +849 -0
  47. package/std/net/quic-stream.ts +1609 -0
  48. package/std/net/quic.ts +3578 -0
  49. package/std/net/tls/codec.ts +1089 -0
  50. package/std/net/tls/record-server.ts +1173 -0
  51. package/std/net/tls/record.ts +621 -0
  52. package/std/net/tls/schedule.ts +429 -0
  53. package/std/net/tls-tcp.ts +414 -0
  54. package/std/net/tls.ts +1437 -0
  55. package/std/net/websocket.ts +823 -0
  56. package/std/net/webtransport.ts +1334 -0
  57. package/std/secret.ts +62 -0
  58. package/std/text.ts +77 -0
@@ -156,46 +156,65 @@ _Bool nish_is_executable(const nish_str *path) {
156
156
  * async-signal-safe. The write end is non-blocking, so a thousand unread
157
157
  * signals lose the newest rather than wedge the handler, and `errno` is put
158
158
  * back for the code the signal interrupted. The descriptor is made once and
159
- * every later `signalFd()` answers the same one. */
160
- static int nish_signal_read_end = -1;
161
- static int nish_signal_write_end = -1;
159
+ * every later `signalFd()` answers the same one.
160
+ *
161
+ * Both ends live in one word, the read end in the high half and the write end
162
+ * in the low, -1 until the pipe exists, so that publishing them is a single
163
+ * compare-and-swap (docs/security/runtime.md, RT-10). Two threads whose first
164
+ * calls overlap can each make a pipe; one swap wins, the other caller closes
165
+ * its own pipe and answers the winner's read end. With two separate words the
166
+ * call that finished last answered its own pipe while the handler wrote to
167
+ * the other, so one caller never heard a signal and a pipe leaked. Each first
168
+ * caller installs the handler itself before it returns, so neither can hand
169
+ * back a descriptor before a signal reaches it. */
170
+ static int64_t nish_signal_pipe = -1;
162
171
 
163
172
  static void nish_on_signal(int sig) {
164
173
  int saved = errno;
165
174
  unsigned char b = (unsigned char)sig;
166
- (void)!write(nish_signal_write_end, &b, 1);
175
+ /* The low half: the write end. */
176
+ (void)!write((int)__atomic_load_n(&nish_signal_pipe, __ATOMIC_RELAXED), &b, 1);
167
177
  errno = saved;
168
178
  }
169
179
 
170
180
  int32_t nish_signal_fd(void) {
171
- if (nish_signal_read_end >= 0) return nish_signal_read_end;
172
- int p[2];
181
+ int64_t cur = __atomic_load_n(&nish_signal_pipe, __ATOMIC_ACQUIRE);
182
+ if (cur < 0) {
183
+ int p[2];
173
184
  #if defined(__linux__)
174
- /* Both ends close-on-exec from the moment they exist, so a child a sibling
175
- thread spawns meanwhile cannot inherit either. */
176
- if (pipe2(p, O_CLOEXEC) != 0) return -1;
185
+ /* Both ends close-on-exec from the moment they exist, so a child a sibling
186
+ thread spawns meanwhile cannot inherit either. */
187
+ if (pipe2(p, O_CLOEXEC) != 0) return -1;
177
188
  #else
178
- /* Darwin has no `pipe2`, so there is a window between `pipe` and the two
179
- `fcntl`s in which a child spawned by another thread inherits the ends. */
180
- if (pipe(p) != 0) return -1;
181
- fcntl(p[0], F_SETFD, FD_CLOEXEC);
182
- fcntl(p[1], F_SETFD, FD_CLOEXEC);
189
+ /* Darwin has no `pipe2`, so there is a window between `pipe` and the two
190
+ `fcntl`s in which a child spawned by another thread inherits the ends. */
191
+ if (pipe(p) != 0) return -1;
192
+ fcntl(p[0], F_SETFD, FD_CLOEXEC);
193
+ fcntl(p[1], F_SETFD, FD_CLOEXEC);
183
194
  #endif
184
- fcntl(p[1], F_SETFL, O_NONBLOCK);
185
- nish_signal_write_end = p[1];
186
- struct sigaction sa;
187
- sa.sa_handler = nish_on_signal;
188
- sa.sa_flags = SA_RESTART;
189
- sigemptyset(&sa.sa_mask);
190
- sigaction(SIGINT, &sa, 0);
191
- sigaction(SIGTERM, &sa, 0);
192
- return nish_signal_read_end = p[0];
195
+ fcntl(p[1], F_SETFL, O_NONBLOCK);
196
+ int64_t mine = (int64_t)p[0] << 32 | (uint32_t)p[1];
197
+ /* A failed swap leaves the winner's pair in `cur`. */
198
+ if (__atomic_compare_exchange_n(&nish_signal_pipe, &cur, mine, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) {
199
+ cur = mine;
200
+ } else {
201
+ close(p[0]);
202
+ close(p[1]);
203
+ }
204
+ struct sigaction sa;
205
+ sa.sa_handler = nish_on_signal;
206
+ sa.sa_flags = SA_RESTART;
207
+ sigemptyset(&sa.sa_mask);
208
+ sigaction(SIGINT, &sa, 0);
209
+ sigaction(SIGTERM, &sa, 0);
210
+ }
211
+ return (int32_t)(cur >> 32);
193
212
  }
194
213
 
195
214
  /* Block until a signal's byte arrives and answer it: 15 or 2, or -1 for any
196
215
  `fd` that is not the signal descriptor and for a read that fails. */
197
216
  int32_t nish_read_signal(int32_t fd) {
198
- if (fd != nish_signal_read_end) return -1;
217
+ if (fd != (int32_t)(__atomic_load_n(&nish_signal_pipe, __ATOMIC_ACQUIRE) >> 32)) return -1;
199
218
  unsigned char b;
200
219
  ssize_t n;
201
220
  do n = read(fd, &b, 1);
@@ -12,13 +12,15 @@
12
12
  * scripts/build.sh pairs this file with runtime.c like the other halves, so a
13
13
  * link line still names one runtime.
14
14
  *
15
- * The error convention is the one `signalFd` set: an `i32`, `>= 0` for
16
- * success (a descriptor, a byte count, 0), and a negative errno otherwise.
17
- * The codes a loop branches on are Linux's numbers on every platform (-11
15
+ * Errors take the shape `signalFd` set, an `i32` that is `>= 0` for success
16
+ * (a descriptor, a byte count, 0) and negative for a failure, and add the
17
+ * errno to it: where `signalFd` and `readSignal` answer a plain -1, a call
18
+ * here answers the failure's errno, negated. The codes a loop branches on are Linux's numbers on every platform (-11
18
19
  * would block, -95 unsupported, -32 the peer is gone, -104 reset, -98 the
19
- * address is in use, -22 a bad argument), so `nish_net_err` translates
20
- * Darwin's; any other failure is the host's own `-errno`. Nothing here
21
- * allocates: the addresses a call reads or writes are the caller's `u8[]`.
20
+ * address is in use, -111 refused, -110 timed out, -22 a bad argument), so
21
+ * `nish_net_err` translates Darwin's; any other failure is the host's own
22
+ * `-errno`. Nothing here allocates: the addresses a call reads or writes are
23
+ * the caller's `u8[]`.
22
24
  *
23
25
  * An address is 18 bytes of that array: the 16 bytes of an IPv6 address, an
24
26
  * IPv4 one as `::ffff:a.b.c.d`, then the port, big-endian. An IPv4 address is
@@ -33,7 +35,10 @@
33
35
  * both, never SIGPIPE), the errno numbers, UDP's segmentation offload
34
36
  * (`UDP_SEGMENT`, `UDP_GRO`) and ECN marks, which Linux has and for which
35
37
  * Darwin answers -95, and the readiness loop, which is epoll on Linux and
36
- * kqueue on Darwin. The Darwin branch is compiled by CI's Darwin bootstrap
38
+ * kqueue on Darwin. Only the sending side and the GRO bind say -95 there: a
39
+ * receive leaves `meta` at 0, 0, and a `meta[1]` of 0 is also what a real
40
+ * Not-ECT datagram reads as, so a program learns that Darwin has no ECN from
41
+ * the send, never from the receive. The Darwin branch is compiled by CI's Darwin bootstrap
37
42
  * rows and run by nothing. A WASI build has none of this (the checker refuses
38
43
  * every `nish:net` call under a wasm target), so there the file is empty.
39
44
  */
@@ -91,6 +96,8 @@ static int32_t nish_net_err(int e) {
91
96
  if (e == EOPNOTSUPP || e == ENOTSUP) return -95;
92
97
  if (e == ECONNRESET) return -104;
93
98
  if (e == EADDRINUSE) return -98;
99
+ if (e == ECONNREFUSED) return -111;
100
+ if (e == ETIMEDOUT) return -110;
94
101
  #endif
95
102
  return -e;
96
103
  }
@@ -111,8 +118,11 @@ static int nish_net_parse(const nish_str *s, unsigned char a[16]) {
111
118
  return inet_pton(AF_INET6, host, a) == 1;
112
119
  }
113
120
 
114
- /* `a` and `port` as a socket address of the family `v4` names. */
115
- static socklen_t nish_net_sockaddr(nish_sockaddr *s, const unsigned char a[16], int32_t port, int v4) {
121
+ /* `a` and `port` as a socket address of the family `v4` names. `noinline`:
122
+ three calls share one body, which inlined into each was 42 bytes more of
123
+ `.text` than the calls cost. */
124
+ __attribute__((noinline)) static socklen_t nish_net_sockaddr(nish_sockaddr *s, const unsigned char a[16],
125
+ int32_t port, int v4) {
116
126
  memset(s, 0, sizeof *s);
117
127
  if (v4) {
118
128
  s->v4.sin_family = AF_INET;
@@ -269,6 +279,41 @@ int32_t nish_tcp_accept(int32_t fd, nish_array *peer) {
269
279
  return c;
270
280
  }
271
281
 
282
+ /* `tcpConnect(addr)`: a new socket, non-blocking and close-on-exec, connecting
283
+ to the address in `addr`; an IPv4 one is an `AF_INET` socket, any other
284
+ `AF_INET6`. A non-blocking `connect` seldom finishes at once (on Linux not
285
+ even over loopback), and its `EINPROGRESS` (-115) is not a failure: the
286
+ descriptor is answered all the same, and the connection is made once the
287
+ loop reports it writable, when `connectResult` says how it went. Any other
288
+ failure closes the socket and is answered instead of it. An `addr` shorter
289
+ than 18 bytes is -22 before any socket exists. */
290
+ int32_t nish_tcp_connect(const nish_array *addr) {
291
+ if (addr->len < NISH_ADDR_BYTES) return -22;
292
+ const unsigned char *p = (const unsigned char *)addr->data;
293
+ int v4 = memcmp(p, nish_mapped_prefix, 12) == 0;
294
+ int fd = nish_net_socket(v4 ? AF_INET : AF_INET6, SOCK_STREAM);
295
+ if (fd < 0) return nish_net_fail();
296
+ nish_sockaddr s;
297
+ socklen_t n = nish_net_sockaddr(&s, p, (p[16] << 8) | p[17], v4);
298
+ if (connect(fd, &s.sa, n) != 0 && errno != EINPROGRESS) {
299
+ int e = errno;
300
+ close(fd);
301
+ return nish_net_err(e);
302
+ }
303
+ return fd;
304
+ }
305
+
306
+ /* `connectResult(fd)`: how the connection `tcpConnect` started went, once the
307
+ descriptor is writable: 0 when it is made, or its failure (-111 refused,
308
+ -110 timed out) from `SO_ERROR`. The kernel clears `SO_ERROR` as it is
309
+ read, so a failure is answered once. */
310
+ int32_t nish_connect_result(int32_t fd) {
311
+ int e = 0;
312
+ socklen_t n = sizeof e;
313
+ if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &e, &n) != 0) return nish_net_fail();
314
+ return e != 0 ? nish_net_err(e) : 0;
315
+ }
316
+
272
317
  /* `netRead(fd, buf, off, len)`: at most `len` bytes into `buf` from `off`.
273
318
  The compiled call has already checked `0 <= off <= off + len <=
274
319
  buf.length`, unless the program was built with `--unchecked-indexing`. */
@@ -390,7 +435,9 @@ int32_t nish_udp_recv_from(int32_t fd, nish_array *buf, int64_t off, int64_t len
390
435
  .msg_name = &s, .msg_namelen = sizeof s, .msg_iov = &iov, .msg_iovlen = 1, .msg_control = c.b, .msg_controllen = sizeof c};
391
436
  ssize_t got = recvmsg(fd, &m, 0);
392
437
  if (got < 0) return nish_net_fail();
393
- /* Darwin sets neither option, so both stay 0 there. */
438
+ /* Darwin sets neither option, so both stay 0 there: a `meta[1]` that reads
439
+ as Not-ECT whatever arrived, which is why the -95 of a marked send, not
440
+ this, is how a program learns ECN is unsupported. */
394
441
  int32_t info[2] = {0, 0};
395
442
  #if defined(__linux__)
396
443
  for (struct cmsghdr *h = CMSG_FIRSTHDR(&m); h != NULL; h = CMSG_NXTHDR(&m, h)) {
@@ -34,6 +34,7 @@
34
34
  the LEGACY symbol, for which a NULL second argument is undefined. */
35
35
  #define _XOPEN_SOURCE 700
36
36
  #include <dirent.h>
37
+ #include <errno.h>
37
38
  #include <limits.h>
38
39
  #include <fcntl.h>
39
40
  #include <stdint.h>
@@ -59,6 +60,11 @@ extern char **environ;
59
60
  #if defined(__APPLE__) && !defined(O_NOFOLLOW)
60
61
  #define O_NOFOLLOW 0x00000100
61
62
  #endif
63
+ /* `O_CLOEXEC` is POSIX 2008 and declared at that level everywhere; the
64
+ fallback is for an older Darwin SDK, with its value from the same header. */
65
+ #if defined(__APPLE__) && !defined(O_CLOEXEC)
66
+ #define O_CLOEXEC 0x01000000
67
+ #endif
62
68
 
63
69
  /* The same spelling runtime.c uses for its own cold paths: a function that
64
70
  reports and exits is never on a path worth optimising for. */
@@ -96,9 +102,16 @@ static NISH_COLD void nish_io_fail(const char *what, const nish_str *path) {
96
102
  * narrower guard would have made it `null` here and split the two compilers
97
103
  * again for the entry a command line can name. `fstat` on the descriptor
98
104
  * rather than `stat` on the path, so the answer is about the file that was
99
- * opened and not about whatever the name means a moment later. */
105
+ * opened and not about whatever the name means a moment later.
106
+ *
107
+ * The descriptor is `O_CLOEXEC` (docs/security/runtime.md, RT-12): a child
108
+ * that a `scope()` task spawns while another task reads would otherwise
109
+ * inherit it. A `pread` that a signal interrupts before it moves a byte is
110
+ * retried rather than taken for the end of the file (RT-13). A local disk
111
+ * never interrupts one, but FUSE and NFS can, and stopping there would hand
112
+ * back the bytes so far as the whole file. */
100
113
  nish_str *nish_read_file_or_null(const nish_str *path) {
101
- int fd = open(nish_cpath(path), O_RDONLY);
114
+ int fd = open(nish_cpath(path), O_RDONLY | O_CLOEXEC);
102
115
  if (fd < 0) return 0;
103
116
  struct stat st;
104
117
  off_t len = fstat(fd, &st) == 0 && !S_ISDIR(st.st_mode) ? lseek(fd, 0, SEEK_END) : -1;
@@ -114,7 +127,13 @@ nish_str *nish_read_file_or_null(const nish_str *path) {
114
127
  nish_str *s = nish_alloc_struct(8 + len + 1);
115
128
  uint64_t got = 0;
116
129
  ssize_t n;
117
- while ((n = pread(fd, s->data + got, len - got, got)) > 0) got += n;
130
+ while ((n = pread(fd, s->data + got, len - got, got)) != 0) {
131
+ if (n > 0) {
132
+ got += n;
133
+ } else if (errno != EINTR) {
134
+ break;
135
+ }
136
+ }
118
137
  close(fd);
119
138
  s->len = got;
120
139
  s->data[got] = 0;
@@ -147,14 +166,21 @@ nish_str *nish_read_file(const nish_str *path) {
147
166
  followed to whatever file it names. `nish file.ts` writes `file.ll` beside
148
167
  its source, so a link planted at that name in a checkout would otherwise
149
168
  truncate any file the user can write. A link among the directories on the
150
- way is still followed. */
169
+ way is still followed.
170
+
171
+ `O_CLOEXEC` for the reason the read has it (RT-12), and `EINTR` is retried
172
+ as the read retries it (RT-13): a write cut short is finished, and one that
173
+ fails is `cannot write`, never a silently short file. */
151
174
  static void nish_put_file(const nish_str *path, const nish_str *data, int flags) {
152
- int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | flags, 0644);
175
+ int fd = open(nish_cpath(path), O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, 0644);
153
176
  if (fd < 0) nish_io_fail("write ", path);
154
177
  for (uint64_t done = 0; done < data->len;) {
155
178
  ssize_t n = write(fd, data->data + done, data->len - done);
156
- if (n <= 0) nish_io_fail("write ", path);
157
- done += n;
179
+ if (n > 0) {
180
+ done += n;
181
+ } else if (n == 0 || errno != EINTR) {
182
+ nish_io_fail("write ", path);
183
+ }
158
184
  }
159
185
  close(fd);
160
186
  }
@@ -214,24 +240,22 @@ static int32_t nish_spawn_impl(const nish_array *argv, const nish_str *out, cons
214
240
  if (v[i] != s[i]->data) return -1;
215
241
  }
216
242
  v[i] = 0;
243
+ /* One list of file actions whether or not a stream is redirected: an empty
244
+ list is the same spawn as none, and one path through is less code than
245
+ creating the list only when a stream needs it. */
217
246
  posix_spawn_file_actions_t fa;
218
- posix_spawn_file_actions_t *fap = 0;
247
+ if (posix_spawn_file_actions_init(&fa)) return -1;
219
248
  const nish_str *to[2] = { out, err };
220
249
  for (int k = 0; k < 2; k++) {
221
- if (!to[k]) continue;
222
- if (!fap) {
223
- if (posix_spawn_file_actions_init(&fa)) return -1;
224
- fap = &fa;
225
- }
226
250
  /* The child opens the file, not this process: a redirect that this process
227
251
  performed would have to be undone afterwards, and a failed open would
228
252
  leave its own stdout pointing at the file. */
229
- posix_spawn_file_actions_addopen(fap, k + 1, nish_cpath(to[k]), O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0644);
253
+ if (to[k]) posix_spawn_file_actions_addopen(&fa, k + 1, nish_cpath(to[k]), O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0644);
230
254
  }
231
255
  pid_t pid;
232
256
  int status;
233
- int failed = posix_spawnp(&pid, v[0], fap, 0, v, environ);
234
- if (fap) posix_spawn_file_actions_destroy(fap);
257
+ int failed = posix_spawnp(&pid, v[0], &fa, 0, v, environ);
258
+ posix_spawn_file_actions_destroy(&fa);
235
259
  if (failed) return -1;
236
260
  if (waitpid(pid, &status, 0) < 0) return -1;
237
261
  return WIFSIGNALED(status) ? 128 + WTERMSIG(status) : WEXITSTATUS(status);
@@ -329,6 +353,10 @@ int64_t nish_monotonic_nanos(void) {
329
353
  return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
330
354
  }
331
355
 
356
+ /* A C string copied into the arena, or NULL for NULL: the shape of every
357
+ answer libc hands back by pointer, `getenv`'s here and `realpath`'s below. */
358
+ static nish_str *nish_str_or_null(const char *v) { return v ? nish_str_new(v, strlen(v)) : 0; }
359
+
332
360
  /* ---- The environment (WP19 R1): `getenv(name)`, the one environment read the
333
361
  language has. A driver needs it to honour `CC` the way `scripts/build.sh`
334
362
  does before it spawns that script, which is the whole reason it exists.
@@ -340,8 +368,7 @@ int64_t nish_monotonic_nanos(void) {
340
368
  string with the lifetime every other one has. NULL for an unset variable,
341
369
  which is the language's `string | null`. Contract: nish.h. */
342
370
  nish_str *nish_getenv(const nish_str *name) {
343
- const char *v = getenv(nish_cpath(name));
344
- return v ? nish_str_new(v, strlen(v)) : 0;
371
+ return nish_str_or_null(getenv(nish_cpath(name)));
345
372
  }
346
373
 
347
374
  /* ---- Symlinks (WP19 §5a item 4): `realpathSync(path)`, the one path
@@ -363,10 +390,25 @@ nish_str *nish_getenv(const nish_str *name) {
363
390
  and the stack buffer is gone at the return. NULL when the path does not
364
391
  resolve -- a path that does not exist included -- which is the language's
365
392
  `string | null` rather than an error, because the caller is asking whether
366
- it resolves. Contract: nish.h. */
393
+ it resolves. Contract: nish.h.
394
+
395
+ A wasi-libc from before 2024, which is what Ubuntu and Debian package,
396
+ neither declares nor defines `realpath`, so there every path is one that
397
+ does not resolve and the answer is NULL. WebAssembly/wasi-libc#463 declared
398
+ it and #473 defined it a month later, with no wasi-sdk release between, and
399
+ #463 also gave `<dirent.h>` its `DT_SOCK`, which the old headers spell only
400
+ for upstream musl. So a missing `DT_SOCK` is the test: it names the libc
401
+ without the function, and wasi-sdk 22 and later keep the real `realpath`.
402
+ It is a preprocessor test rather than a declaration of our own because the
403
+ old libc.a has no definition to link. */
367
404
  nish_str *nish_realpath(const nish_str *path) {
405
+ #if defined(__wasi__) && !defined(DT_SOCK)
406
+ (void)path;
407
+ return 0;
408
+ #else
368
409
  char buf[PATH_MAX];
369
- return realpath(nish_cpath(path), buf) ? nish_str_new(buf, strlen(buf)) : 0;
410
+ return nish_str_or_null(realpath(nish_cpath(path), buf));
411
+ #endif
370
412
  }
371
413
 
372
414
  /* ---- What machine this is (WP14 §7a): `process.platform` and `process.arch`,
@@ -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/run.js, and runtime.c had
6
+ * no room left when this arrived. Section GC means a program that never calls a
7
+ * kernel links none of this file, and scripts/build.sh pairs it with runtime.c
8
+ * 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,10 +115,22 @@ 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(); }
121
132
  void nish_panic_div(_Bool by_zero) { (void)by_zero; __builtin_trap(); }
133
+ void nish_panic_overflow(uint32_t op) { (void)op; __builtin_trap(); }
122
134
  void nish_exit(int32_t code) { (void)code; __builtin_trap(); }
123
135
  /* A panic's message (`panic`, `expect`, a failed range entry) has no stderr to
124
136
  * go to here; the `nish_exit` that follows it is the trap. An exported function