@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.
- package/INSTALL.md +43 -34
- package/bin/nish +0 -0
- package/package.json +1 -1
- package/runtime/nish.d.ts +117 -8
- package/runtime/nish.h +50 -13
- package/runtime/nish.mjs +71 -7
- package/runtime/runtime-host.c +43 -24
- package/runtime/runtime-net.c +57 -10
- package/runtime/runtime-os.c +62 -20
- package/runtime/runtime-simd.c +203 -0
- package/runtime/runtime-wasm.c +12 -0
- package/runtime/runtime.c +66 -18
- package/runtime/shim.mjs +114 -5
- package/scripts/build.sh +14 -12
- package/std/README.md +168 -36
- package/std/crypto/aes.ts +51 -8
- package/std/crypto/base64url.ts +25 -12
- package/std/crypto/chacha20poly1305.ts +43 -29
- package/std/crypto/ct.ts +6 -1
- package/std/crypto/hkdf.ts +364 -3
- package/std/crypto/hmac.ts +307 -7
- package/std/crypto/p256.ts +618 -34
- package/std/crypto/sha1.ts +147 -0
- package/std/crypto/sha256.ts +3 -2
- package/std/crypto/x25519.ts +88 -36
- package/std/crypto/x509.ts +130 -47
- package/std/json.ts +297 -41
- package/std/net/hpack.ts +1094 -0
- package/std/net/http-fields.ts +604 -0
- package/std/net/http1-server.ts +1389 -0
- package/std/net/http1.ts +1492 -0
- package/std/net/http2-frame.ts +745 -0
- package/std/net/http2-tls.ts +216 -0
- package/std/net/http2.ts +1433 -0
- package/std/net/http3-frame.ts +457 -0
- package/std/net/http3-server.ts +911 -0
- package/std/net/http3.ts +2145 -0
- package/std/net/qpack.ts +1163 -0
- package/std/net/quic-conn-ack.ts +206 -0
- package/std/net/quic-conn-cid.ts +425 -0
- package/std/net/quic-conn-params.ts +462 -0
- package/std/net/quic-datagram.ts +130 -0
- package/std/net/quic-frame.ts +1206 -0
- package/std/net/quic-listener.ts +929 -0
- package/std/net/quic-packet.ts +1698 -0
- package/std/net/quic-recovery.ts +849 -0
- package/std/net/quic-stream.ts +1609 -0
- package/std/net/quic.ts +3578 -0
- package/std/net/tls/codec.ts +1089 -0
- package/std/net/tls/record-server.ts +1173 -0
- package/std/net/tls/record.ts +621 -0
- package/std/net/tls/schedule.ts +429 -0
- package/std/net/tls-tcp.ts +414 -0
- package/std/net/tls.ts +1437 -0
- package/std/net/websocket.ts +823 -0
- package/std/net/webtransport.ts +1334 -0
- package/std/secret.ts +62 -0
- package/std/text.ts +77 -0
package/runtime/runtime-host.c
CHANGED
|
@@ -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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 !=
|
|
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);
|
package/runtime/runtime-net.c
CHANGED
|
@@ -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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
20
|
-
* Darwin's; any other failure is the host's own
|
|
21
|
-
* allocates: the addresses a call reads or writes are
|
|
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.
|
|
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
|
-
|
|
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)) {
|
package/runtime/runtime-os.c
CHANGED
|
@@ -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))
|
|
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
|
|
157
|
-
|
|
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
|
-
|
|
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(
|
|
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],
|
|
234
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
+
}
|
package/runtime/runtime-wasm.c
CHANGED
|
@@ -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
|