@amritk/nish-aarch64-linux 0.10.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 +468 -0
- package/LICENSE +21 -0
- package/bin/nish +0 -0
- package/package.json +31 -0
- package/runtime/nish.d.ts +290 -0
- package/runtime/nish.h +340 -0
- package/runtime/nish.mjs +143 -0
- package/runtime/runtime.c +1184 -0
- package/runtime/runtime_os.c +351 -0
- package/runtime/runtime_parallel.c +156 -0
- package/runtime/runtime_wasm.c +99 -0
- package/runtime/shim.mjs +672 -0
- package/scripts/build.sh +279 -0
- package/std/README.md +185 -0
- package/std/json.ts +402 -0
- package/std/pair.ts +28 -0
- package/std/testing.ts +347 -0
- package/std/text.ts +193 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/* Nish runtime, the operating system half: files, directories, subprocesses,
|
|
2
|
+
* the environment, the clock, and which machine this is.
|
|
3
|
+
*
|
|
4
|
+
* Split out of runtime.c so that the two halves can be measured apart. Every
|
|
5
|
+
* function here wraps a system call, which is the reason it has to be C at all:
|
|
6
|
+
* the language cannot express `openat` or `posix_spawnp`, so the surface a
|
|
7
|
+
* program uses to reach the operating system grows in this file and nowhere
|
|
8
|
+
* else. runtime.c holds what every program touches whatever it does — the
|
|
9
|
+
* arena, strings, arrays, number formatting, the panics — and that is a closed
|
|
10
|
+
* set. Before the split the two shared one `.text*` budget, so a new syscall
|
|
11
|
+
* wrapper raised the ceiling over the arena and the string functions as well,
|
|
12
|
+
* and the number a reader saw for "the runtime" moved for a reason that had
|
|
13
|
+
* nothing to do with it. Each file now carries its own measured ceiling in
|
|
14
|
+
* tests/run.js; docs/wp7-runtime.md §"Runtime additions and budget" has both
|
|
15
|
+
* rows and the reasoning.
|
|
16
|
+
*
|
|
17
|
+
* What a linked binary pays is unchanged: `-ffunction-sections
|
|
18
|
+
* -Wl,--gc-sections` drops every function no program calls, whichever
|
|
19
|
+
* translation unit defined it, and the symbol names are the same frozen ABI
|
|
20
|
+
* they were.
|
|
21
|
+
*
|
|
22
|
+
* The declarations come from nish.h, the public ABI header, rather than from a
|
|
23
|
+
* private copy: this file calls three of the core's symbols (`nish_alloc_struct`,
|
|
24
|
+
* `nish_str_new`, `nish_array_grow`) and shares two of its layouts, and
|
|
25
|
+
* compiling against the header a C host sees is what keeps a definition here
|
|
26
|
+
* from drifting from the contract it is published under.
|
|
27
|
+
*/
|
|
28
|
+
#define _POSIX_C_SOURCE 200809L
|
|
29
|
+
/* `realpath` is XSI rather than POSIX base, so _POSIX_C_SOURCE alone does not
|
|
30
|
+
declare it (measured on glibc 2.39: undeclared, and clang then infers
|
|
31
|
+
`int`, which is a pointer-truncating bug rather than a compile error before
|
|
32
|
+
C99). Declaring it here by hand would be worse than this line on Darwin,
|
|
33
|
+
where <stdlib.h> gives `realpath` an asm label -- a hand declaration binds
|
|
34
|
+
the LEGACY symbol, for which a NULL second argument is undefined. */
|
|
35
|
+
#define _XOPEN_SOURCE 700
|
|
36
|
+
#include <dirent.h>
|
|
37
|
+
#include <limits.h>
|
|
38
|
+
#include <fcntl.h>
|
|
39
|
+
#include <stdint.h>
|
|
40
|
+
#include <stdio.h>
|
|
41
|
+
#include <stdlib.h>
|
|
42
|
+
#include <string.h>
|
|
43
|
+
#include <sys/stat.h>
|
|
44
|
+
#include <time.h>
|
|
45
|
+
#include <unistd.h>
|
|
46
|
+
#ifndef __wasi__
|
|
47
|
+
/* WASI has no processes, so `spawn.h` and `wait.h` do not exist there. */
|
|
48
|
+
#include <spawn.h>
|
|
49
|
+
#include <sys/wait.h>
|
|
50
|
+
extern char **environ;
|
|
51
|
+
#endif
|
|
52
|
+
|
|
53
|
+
#include "nish.h"
|
|
54
|
+
|
|
55
|
+
/* The same spelling runtime.c uses for its own cold paths: a function that
|
|
56
|
+
reports and exits is never on a path worth optimising for. */
|
|
57
|
+
#define NISH_COLD __attribute__((noreturn, cold, noinline))
|
|
58
|
+
|
|
59
|
+
/* ---- Process and files */
|
|
60
|
+
void nish_exit(int32_t code) { exit(code); }
|
|
61
|
+
static NISH_COLD void nish_io_fail(const char *what, const nish_str *path) {
|
|
62
|
+
dprintf(2, "nish: cannot %s%.*s\n", what, (int)path->len, path->data);
|
|
63
|
+
_exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* `readFileSyncOrNull(path)`: null rather than a message, so a program can
|
|
67
|
+
turn a missing file into its own diagnostic and carry on.
|
|
68
|
+
*
|
|
69
|
+
* The `S_ISDIR` guard is what makes "or null" true of a **directory**, which
|
|
70
|
+
* `open(O_RDONLY)` accepts: `lseek` then answers `LONG_MAX` on Linux and the
|
|
71
|
+
* arena is asked for that many bytes, so the program dies with `out of memory`
|
|
72
|
+
* instead of answering null. Node's `readFileSync` raises `EISDIR` and the
|
|
73
|
+
* stage0 twin (the TypeScript compiler, deleted in R6) turned that into null,
|
|
74
|
+
* so without this the two compilers answered differently for one tree — a
|
|
75
|
+
* package whose `exports` names a directory, which WP21 S2 made reachable
|
|
76
|
+
* (`tests/link/package_dir_target`).
|
|
77
|
+
*
|
|
78
|
+
* The test is `!S_ISDIR` rather than `S_ISREG` on purpose: a directory is the
|
|
79
|
+
* only thing whose `lseek(SEEK_END)` answers `LONG_MAX`, and refusing anything
|
|
80
|
+
* else would answer null where Node answers bytes. `/dev/null` is the case to
|
|
81
|
+
* keep in mind — `readFileSync` there is `""` in Node and in `shim.mjs`, and a
|
|
82
|
+
* narrower guard would have made it `null` here and split the two compilers
|
|
83
|
+
* again for the entry a command line can name. `fstat` on the descriptor
|
|
84
|
+
* rather than `stat` on the path, so the answer is about the file that was
|
|
85
|
+
* opened and not about whatever the name means a moment later. */
|
|
86
|
+
nish_str *nish_read_file_or_null(const nish_str *path) {
|
|
87
|
+
int fd = open(path->data, O_RDONLY);
|
|
88
|
+
if (fd < 0) return 0;
|
|
89
|
+
struct stat st;
|
|
90
|
+
off_t len = fstat(fd, &st) == 0 && !S_ISDIR(st.st_mode) ? lseek(fd, 0, SEEK_END) : -1;
|
|
91
|
+
if (len < 0) {
|
|
92
|
+
close(fd);
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
nish_str *s = nish_alloc_struct(8 + len + 1);
|
|
96
|
+
uint64_t got = 0;
|
|
97
|
+
ssize_t n;
|
|
98
|
+
while ((n = pread(fd, s->data + got, len - got, got)) > 0) got += n;
|
|
99
|
+
close(fd);
|
|
100
|
+
s->len = got;
|
|
101
|
+
s->data[got] = 0;
|
|
102
|
+
return s;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
nish_str *nish_read_file(const nish_str *path) {
|
|
106
|
+
nish_str *s = nish_read_file_or_null(path);
|
|
107
|
+
if (!s) nish_io_fail("read ", path);
|
|
108
|
+
return s;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
static void nish_put_file(const nish_str *path, const nish_str *data, int flags) {
|
|
112
|
+
int fd = open(path->data, O_WRONLY | O_CREAT | flags, 0644);
|
|
113
|
+
if (fd < 0) nish_io_fail("write ", path);
|
|
114
|
+
for (uint64_t done = 0; done < data->len;) {
|
|
115
|
+
ssize_t n = write(fd, data->data + done, data->len - done);
|
|
116
|
+
if (n <= 0) nish_io_fail("write ", path);
|
|
117
|
+
done += n;
|
|
118
|
+
}
|
|
119
|
+
close(fd);
|
|
120
|
+
}
|
|
121
|
+
void nish_write_file(const nish_str *path, const nish_str *data) { nish_put_file(path, data, O_TRUNC); }
|
|
122
|
+
void nish_append_file(const nish_str *path, const nish_str *data) { nish_put_file(path, data, O_APPEND); }
|
|
123
|
+
|
|
124
|
+
/* ---- Directories and subprocesses (WP14 D4): what a driver needs to create
|
|
125
|
+
its own output directory and shell out for `--link`. Both answer a value
|
|
126
|
+
instead of exiting, as `nish_read_file_or_null` does — there are no
|
|
127
|
+
exceptions, so the caller owns the diagnostic — and so does the `stat`
|
|
128
|
+
beside them (WP14 §7a), which is what tells `-o <dir>` from `-o <file>`.
|
|
129
|
+
Contracts: nish.h. */
|
|
130
|
+
|
|
131
|
+
/* `isDirectorySync(path)` (WP14 §7a): the one question a driver asks of a path
|
|
132
|
+
it was handed — is there a directory here? A missing path, a plain file and
|
|
133
|
+
a parent it cannot search are the same false, because they are the same
|
|
134
|
+
answer to that question. */
|
|
135
|
+
_Bool nish_is_dir(const nish_str *path) {
|
|
136
|
+
struct stat st;
|
|
137
|
+
return stat(path->data, &st) == 0 && S_ISDIR(st.st_mode);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* One directory, not recursive. The retry is the `stat` above rather than
|
|
141
|
+
`errno == EEXIST` so that a plain file at the path answers false, which is
|
|
142
|
+
what the promise "a directory is there afterwards" means. */
|
|
143
|
+
_Bool nish_mkdir(const nish_str *path) {
|
|
144
|
+
return mkdir(path->data, 0777) == 0 || nish_is_dir(path);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* `posix_spawnp` is one libc call where fork/execvp/waitpid would be three,
|
|
148
|
+
it searches PATH, and glibc reports a failed exec through its return
|
|
149
|
+
value rather than through a child that has already run.
|
|
150
|
+
|
|
151
|
+
`out` and `err` are paths for the child's stdout and stderr, or NULL to let
|
|
152
|
+
it inherit this process's. Both spawn builtins are this function with those
|
|
153
|
+
two arguments answered differently, so the argv vector, the wait and the
|
|
154
|
+
signal convention are written once; it is `static`, so a program that spawns
|
|
155
|
+
nothing still loses all three to `--gc-sections`. */
|
|
156
|
+
static int32_t nish_spawn_impl(const nish_array *argv, const char *out, const char *err) {
|
|
157
|
+
#ifdef __wasi__
|
|
158
|
+
(void)argv;
|
|
159
|
+
(void)out;
|
|
160
|
+
(void)err;
|
|
161
|
+
return -1;
|
|
162
|
+
#else
|
|
163
|
+
if (!argv->len) return -1; /* argv[0] would read past an empty array */
|
|
164
|
+
nish_str **s = (nish_str **)argv->data;
|
|
165
|
+
/* NULL-terminated, borrowing each string's bytes; the arena owns it, so no
|
|
166
|
+
path out of here has anything to free. */
|
|
167
|
+
char **v = nish_alloc_struct((argv->len + 1) * sizeof *v);
|
|
168
|
+
uint64_t i = 0;
|
|
169
|
+
for (; i < argv->len; i++) v[i] = s[i]->data;
|
|
170
|
+
v[i] = 0;
|
|
171
|
+
posix_spawn_file_actions_t fa;
|
|
172
|
+
posix_spawn_file_actions_t *fap = 0;
|
|
173
|
+
if (out || err) {
|
|
174
|
+
if (posix_spawn_file_actions_init(&fa)) return -1;
|
|
175
|
+
fap = &fa;
|
|
176
|
+
/* The child opens the file, not this process: a redirect that this process
|
|
177
|
+
performed would have to be undone afterwards, and a failed open would
|
|
178
|
+
leave its own stdout pointing at the file. */
|
|
179
|
+
if (out) posix_spawn_file_actions_addopen(fap, 1, out, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
180
|
+
if (err) posix_spawn_file_actions_addopen(fap, 2, err, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
|
181
|
+
}
|
|
182
|
+
pid_t pid;
|
|
183
|
+
int status;
|
|
184
|
+
int failed = posix_spawnp(&pid, v[0], fap, 0, v, environ);
|
|
185
|
+
if (fap) posix_spawn_file_actions_destroy(fap);
|
|
186
|
+
if (failed) return -1;
|
|
187
|
+
if (waitpid(pid, &status, 0) < 0) return -1;
|
|
188
|
+
return WIFSIGNALED(status) ? 128 + WTERMSIG(status) : WEXITSTATUS(status);
|
|
189
|
+
#endif
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
int32_t nish_spawn(const nish_array *argv) { return nish_spawn_impl(argv, 0, 0); }
|
|
193
|
+
|
|
194
|
+
/* `spawnSyncTo(argv, stdoutPath, stderrPath)`: the same run with a stream sent
|
|
195
|
+
to a file, which is what comparing a program's output against a golden needs
|
|
196
|
+
— `nish_spawn` answers a status and the output is gone. An **empty** path
|
|
197
|
+
leaves that stream inherited, so one call can capture stdout and let stderr
|
|
198
|
+
through to the terminal. Each file is created or truncated at 0644, as
|
|
199
|
+
`writeFileSync` does.
|
|
200
|
+
|
|
201
|
+
Two streams must not name one path: each would be opened separately, with
|
|
202
|
+
its own offset, and the two would overwrite each other rather than
|
|
203
|
+
interleave. Capturing them apart and concatenating is the way to merge. */
|
|
204
|
+
int32_t nish_spawn_to(const nish_array *argv, const nish_str *out, const nish_str *err) {
|
|
205
|
+
return nish_spawn_impl(argv, out->len ? out->data : 0, err->len ? err->data : 0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* `readdirSync(path)`: the listing a driver needs to discover its own inputs.
|
|
209
|
+
|
|
210
|
+
**Sorted by bytes**, which `readdir(3)` and Node are not, for two reasons
|
|
211
|
+
that both belong to this language rather than to taste: there is no `sort`,
|
|
212
|
+
so every caller of an unsorted listing would have to write one, and an order
|
|
213
|
+
that is the file system's is an order that differs between two machines
|
|
214
|
+
running the same suite. `.` and `..` are dropped, as Node drops them; every
|
|
215
|
+
other dotfile is kept.
|
|
216
|
+
|
|
217
|
+
NULL when the directory cannot be read — the language's `string[] | null` —
|
|
218
|
+
so a missing directory is a diagnostic the caller writes and not an exit. An
|
|
219
|
+
empty directory is an empty array, which is a different answer. */
|
|
220
|
+
nish_array *nish_readdir(const nish_str *path) {
|
|
221
|
+
#ifdef __wasi__
|
|
222
|
+
/* The WASI form of this is a different contract, not a port of this one:
|
|
223
|
+
`fd_readdir` lists a preopened directory rather than a path, and the wasi
|
|
224
|
+
profile has no check in the default run. NULL until it has both. */
|
|
225
|
+
(void)path;
|
|
226
|
+
return 0;
|
|
227
|
+
#else
|
|
228
|
+
DIR *d = opendir(path->data);
|
|
229
|
+
if (!d) return 0;
|
|
230
|
+
nish_array *a = nish_alloc_struct(sizeof *a);
|
|
231
|
+
*a = (nish_array){ 0, 0, 0 };
|
|
232
|
+
const struct dirent *e;
|
|
233
|
+
while ((e = readdir(d))) {
|
|
234
|
+
const char *n = e->d_name;
|
|
235
|
+
if (n[0] == '.' && (!n[1] || (n[1] == '.' && !n[2]))) continue;
|
|
236
|
+
if (a->len == a->cap) nish_array_grow(a, sizeof(nish_str *));
|
|
237
|
+
((nish_str **)a->data)[a->len++] = nish_str_new(n, strlen(n));
|
|
238
|
+
}
|
|
239
|
+
closedir(d);
|
|
240
|
+
/* Insertion sort over pointers: a directory is short, and `qsort` would cost
|
|
241
|
+
a comparator symbol and its unwind entry for a call that is never hot. */
|
|
242
|
+
nish_str **v = (nish_str **)a->data;
|
|
243
|
+
for (uint64_t i = 1; i < a->len; i++) {
|
|
244
|
+
nish_str *s = v[i];
|
|
245
|
+
uint64_t j = i;
|
|
246
|
+
while (j && strcmp(v[j - 1]->data, s->data) > 0) {
|
|
247
|
+
v[j] = v[j - 1];
|
|
248
|
+
j--;
|
|
249
|
+
}
|
|
250
|
+
v[j] = s;
|
|
251
|
+
}
|
|
252
|
+
return a;
|
|
253
|
+
#endif
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* `monotonicNanos()`: `CLOCK_MONOTONIC` in nanoseconds, which is what timing a
|
|
257
|
+
run needs. Not the wall clock: a wall clock corrected mid-run can go
|
|
258
|
+
backwards and make an elapsed time negative. Only the difference between two
|
|
259
|
+
reads means anything — the origin is arbitrary and is not comparable across
|
|
260
|
+
processes or machines — and an i64 of nanoseconds is 292 years of it. */
|
|
261
|
+
int64_t nish_monotonic_nanos(void) {
|
|
262
|
+
struct timespec ts;
|
|
263
|
+
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
264
|
+
return (int64_t)ts.tv_sec * 1000000000 + ts.tv_nsec;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* ---- The environment (WP19 R1): `getenv(name)`, the one environment read the
|
|
268
|
+
language has. A driver needs it to honour `CC` the way `scripts/build.sh`
|
|
269
|
+
does before it spawns that script, which is the whole reason it exists.
|
|
270
|
+
|
|
271
|
+
The bytes are copied into the arena rather than handed back where libc put
|
|
272
|
+
them: `getenv` answers a pointer into `environ`, and a later `setenv` in the
|
|
273
|
+
same process may move or overwrite that block, so a string the program is
|
|
274
|
+
still holding would change under it. Copying makes it an ordinary arena
|
|
275
|
+
string with the lifetime every other one has. NULL for an unset variable,
|
|
276
|
+
which is the language's `string | null`. Contract: nish.h. */
|
|
277
|
+
nish_str *nish_getenv(const nish_str *name) {
|
|
278
|
+
const char *v = getenv(name->data);
|
|
279
|
+
if (!v) return 0;
|
|
280
|
+
uint64_t len = strlen(v);
|
|
281
|
+
nish_str *s = nish_alloc_struct(8 + len + 1);
|
|
282
|
+
s->len = len;
|
|
283
|
+
memcpy(s->data, v, len + 1);
|
|
284
|
+
return s;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* ---- Symlinks (WP19 §5a item 4): `realpathSync(path)`, the one path
|
|
288
|
+
resolution the language has. A compiler that finds `scripts/build.sh` and
|
|
289
|
+
`std/` relative to its own `argv[0]` cannot do it through a symlink without
|
|
290
|
+
this: `ln -s /opt/nish/bin/nish /usr/local/bin/nish` is the ordinary way a
|
|
291
|
+
binary reaches `$PATH`, and npm links every command the same way, so the
|
|
292
|
+
directory `argv[0]` names is the link's rather than the package's.
|
|
293
|
+
|
|
294
|
+
A PATH_MAX buffer rather than `realpath(p, NULL)`, which is the shorter
|
|
295
|
+
spelling and the wrong one here. Allocating the result is POSIX 2008, and
|
|
296
|
+
on Darwin it is the `__DARWIN_EXTSN` variant that implements it -- the
|
|
297
|
+
legacy symbol of the same name leaves a NULL second argument undefined. A
|
|
298
|
+
caller-supplied buffer is defined for both, so it is the spelling that
|
|
299
|
+
cannot depend on which variant a given feature-macro level binds.
|
|
300
|
+
|
|
301
|
+
The result is copied into the arena, for the reason `nish_getenv` copies:
|
|
302
|
+
what the arena owns has the lifetime every other string in the program has,
|
|
303
|
+
and the stack buffer is gone at the return. NULL when the path does not
|
|
304
|
+
resolve -- a path that does not exist included -- which is the language's
|
|
305
|
+
`string | null` rather than an error, because the caller is asking whether
|
|
306
|
+
it resolves. Contract: nish.h. */
|
|
307
|
+
nish_str *nish_realpath(const nish_str *path) {
|
|
308
|
+
char buf[PATH_MAX];
|
|
309
|
+
if (!realpath(path->data, buf)) return 0;
|
|
310
|
+
uint64_t len = strlen(buf);
|
|
311
|
+
nish_str *s = nish_alloc_struct(8 + len + 1);
|
|
312
|
+
s->len = len;
|
|
313
|
+
memcpy(s->data, buf, len + 1);
|
|
314
|
+
return s;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* ---- What machine this is (WP14 §7a): `process.platform` and `process.arch`,
|
|
318
|
+
the two halves `--target host` composes a triple from. Both are settled when
|
|
319
|
+
this file is compiled — a cross build compiles the runtime for the target,
|
|
320
|
+
so the answer is the target's — which is why each is a string in constant
|
|
321
|
+
data handed back by address: no allocation and no load, and `readnone` on
|
|
322
|
+
the declaration (self/runtime.ts) is a fact rather than a hope. The
|
|
323
|
+
spellings are Node's, so a program reads the same answer from this runtime
|
|
324
|
+
and from `runtime/shim.mjs`; anything neither branch names is "unknown",
|
|
325
|
+
which is what `--target host` then refuses. Contracts: nish.h.
|
|
326
|
+
|
|
327
|
+
The static object is spelled with a sized array because a flexible array
|
|
328
|
+
member cannot be initialised, and handed out as an `nish_str *`: the two
|
|
329
|
+
layouts are the same bytes, and nothing anywhere writes through either
|
|
330
|
+
pointer, so there is no aliasing question to answer. */
|
|
331
|
+
#define NISH_TEXT(name, s) \
|
|
332
|
+
static const struct { uint64_t len; char data[sizeof s]; } name = { sizeof s - 1, s }
|
|
333
|
+
|
|
334
|
+
#if defined(__APPLE__)
|
|
335
|
+
NISH_TEXT(nish_platform_text, "darwin");
|
|
336
|
+
#elif defined(__linux__)
|
|
337
|
+
NISH_TEXT(nish_platform_text, "linux");
|
|
338
|
+
#else
|
|
339
|
+
NISH_TEXT(nish_platform_text, "unknown");
|
|
340
|
+
#endif
|
|
341
|
+
|
|
342
|
+
#if defined(__x86_64__)
|
|
343
|
+
NISH_TEXT(nish_arch_text, "x64");
|
|
344
|
+
#elif defined(__aarch64__)
|
|
345
|
+
NISH_TEXT(nish_arch_text, "arm64");
|
|
346
|
+
#else
|
|
347
|
+
NISH_TEXT(nish_arch_text, "unknown");
|
|
348
|
+
#endif
|
|
349
|
+
|
|
350
|
+
const nish_str *nish_platform(void) { return (const nish_str *)&nish_platform_text; }
|
|
351
|
+
const nish_str *nish_arch(void) { return (const nish_str *)&nish_arch_text; }
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/* Nish runtime, the parallel half: how one range of work becomes several
|
|
2
|
+
* threads, and nothing else.
|
|
3
|
+
*
|
|
4
|
+
* A third translation unit rather than a third of `runtime_os.c`, for the
|
|
5
|
+
* reason that file's header gives for being a second one: a budget should mean
|
|
6
|
+
* one thing. `runtime_os.c` is the syscall wrappers and it had 29 bytes of its
|
|
7
|
+
* ceiling left; `pthread_create` plus a partitioner does not fit in 29 bytes,
|
|
8
|
+
* and raising the syscall half's ceiling to make room would move the number a
|
|
9
|
+
* reader sees for "the operating-system surface" for a reason that has nothing
|
|
10
|
+
* to do with the operating-system surface. So this file carries its own
|
|
11
|
+
* measured ceiling in tests/run.js beside the other two, and a program that
|
|
12
|
+
* runs nothing in parallel pays nothing for it: `-ffunction-sections
|
|
13
|
+
* -Wl,--gc-sections` drops every function no program calls, whichever
|
|
14
|
+
* translation unit defined it.
|
|
15
|
+
*
|
|
16
|
+
* It compiles in both configurations on purpose. With `-DNISH_THREADS` it
|
|
17
|
+
* spawns; without it, and on WASI where there are no threads to spawn, the same
|
|
18
|
+
* entry point runs the whole range on the calling thread. That is what keeps it
|
|
19
|
+
* linkable beside a default build and keeps the caller from needing two
|
|
20
|
+
* spellings -- the flag decides whether the work is divided, never whether the
|
|
21
|
+
* function exists. The thread-local arena the divided case needs is WP20 T0 and
|
|
22
|
+
* arrives with the same macro.
|
|
23
|
+
*
|
|
24
|
+
* There is no language surface here and nothing in the language calls this yet:
|
|
25
|
+
* docs/wp29-thread-surface.md is the surface, and this is the stage under it.
|
|
26
|
+
*/
|
|
27
|
+
#include "nish.h"
|
|
28
|
+
|
|
29
|
+
#if defined(NISH_THREADS) && !defined(__wasi__) && !defined(__wasm__)
|
|
30
|
+
#define NISH_PAR_REAL 1
|
|
31
|
+
#include <pthread.h>
|
|
32
|
+
#include <unistd.h>
|
|
33
|
+
#endif
|
|
34
|
+
|
|
35
|
+
/* The ceiling on how many threads one region may use. A fixed bound keeps the
|
|
36
|
+
* job and handle arrays on the stack -- 64 jobs is 2 KB and 64 handles 512
|
|
37
|
+
* bytes -- so a region never allocates to divide itself, which matters because
|
|
38
|
+
* the arena it would allocate from is the very thing being made per-thread. */
|
|
39
|
+
#define NISH_PAR_MAX 64
|
|
40
|
+
|
|
41
|
+
/* Cached, because `sysconf(_SC_NPROCESSORS_ONLN)` is not a cheap read: glibc
|
|
42
|
+
* answers it from `sched_getaffinity` or by parsing
|
|
43
|
+
* /sys/devices/system/cpu/online, and measured here it is 3.7 us -- which
|
|
44
|
+
* `nish_parallel_range` would otherwise pay on every region, including the ones
|
|
45
|
+
* too small to divide, where it was the entire cost. The race between two
|
|
46
|
+
* threads filling it is benign (both compute the same number) but it is still a
|
|
47
|
+
* race, so the word is read and written with relaxed atomic builtins (a plain
|
|
48
|
+
* `int64_t`, since `__atomic_*` does not take an `_Atomic`-qualified pointer):
|
|
49
|
+
* on x86-64 that is
|
|
50
|
+
* a plain load and store and costs nothing, and it is defined behaviour.
|
|
51
|
+
*
|
|
52
|
+
* A machine whose online CPU count changes under a running process therefore
|
|
53
|
+
* keeps the count it started with, which is the right trade for a number that
|
|
54
|
+
* decides how many threads a loop is divided into. */
|
|
55
|
+
static int64_t nish_cpu_cached = 0;
|
|
56
|
+
|
|
57
|
+
int64_t nish_cpu_count(void) {
|
|
58
|
+
int64_t n = __atomic_load_n(&nish_cpu_cached, __ATOMIC_RELAXED);
|
|
59
|
+
if (n != 0) return n;
|
|
60
|
+
#ifdef NISH_PAR_REAL
|
|
61
|
+
long got = sysconf(_SC_NPROCESSORS_ONLN);
|
|
62
|
+
n = got > 0 ? (int64_t)got : 1;
|
|
63
|
+
#else
|
|
64
|
+
n = 1;
|
|
65
|
+
#endif
|
|
66
|
+
__atomic_store_n(&nish_cpu_cached, n, __ATOMIC_RELAXED);
|
|
67
|
+
return n;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#ifdef NISH_PAR_REAL
|
|
71
|
+
/* One chunk of the range, and where to run it. Passed by pointer to the
|
|
72
|
+
* worker, so it lives in the caller's frame until the join. */
|
|
73
|
+
typedef struct {
|
|
74
|
+
nish_par_body body;
|
|
75
|
+
void *ctx;
|
|
76
|
+
int64_t lo;
|
|
77
|
+
int64_t hi;
|
|
78
|
+
} nish_par_job;
|
|
79
|
+
|
|
80
|
+
/* Non-zero inside a region, so a body that itself calls `nish_parallel_range`
|
|
81
|
+
* runs its range on one thread instead of multiplying the thread count by the
|
|
82
|
+
* nesting depth. Thread-local, so the guard is per worker rather than a lock.
|
|
83
|
+
* Sequential nesting is the conservative answer and the only one that needs no
|
|
84
|
+
* scheduler: work stealing is what a library would do here and it is not what
|
|
85
|
+
* this file is for. */
|
|
86
|
+
static NISH_TLS int nish_par_depth = 0;
|
|
87
|
+
|
|
88
|
+
static void *nish_par_worker(void *p) {
|
|
89
|
+
nish_par_job *j = (nish_par_job *)p;
|
|
90
|
+
nish_par_depth = 1;
|
|
91
|
+
j->body(j->lo, j->hi, j->ctx);
|
|
92
|
+
/* This thread's arena, not the parent's: the storage class is what makes the
|
|
93
|
+
* two disjoint (WP20 T0), so a worker can release every byte it bumped
|
|
94
|
+
* without the parent losing one. A worker that did not do this would leak its
|
|
95
|
+
* whole arena at thread exit, since nothing else has a pointer to it. */
|
|
96
|
+
nish_free_arena();
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
#endif
|
|
100
|
+
|
|
101
|
+
void nish_parallel_range(nish_par_body body, void *ctx, int64_t len, int64_t grain) {
|
|
102
|
+
if (len <= 0) return;
|
|
103
|
+
#ifdef NISH_PAR_REAL
|
|
104
|
+
if (grain < 1) grain = 1;
|
|
105
|
+
/* Never more threads than there is work for at the requested granularity,
|
|
106
|
+
* and never more than the machine has. */
|
|
107
|
+
int64_t chunks = (len + grain - 1) / grain;
|
|
108
|
+
int64_t n = nish_cpu_count();
|
|
109
|
+
if (n > NISH_PAR_MAX) n = NISH_PAR_MAX;
|
|
110
|
+
if (chunks < n) n = chunks;
|
|
111
|
+
if (n <= 1 || nish_par_depth != 0) {
|
|
112
|
+
body(0, len, ctx);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
nish_par_job jobs[NISH_PAR_MAX];
|
|
117
|
+
pthread_t th[NISH_PAR_MAX];
|
|
118
|
+
/* Contiguous chunks, the remainder spread one element at a time over the
|
|
119
|
+
* first `len % n` of them, so the longest chunk is never more than one
|
|
120
|
+
* element longer than the shortest. Contiguous rather than strided because
|
|
121
|
+
* two workers then never write the same cache line except at one boundary. */
|
|
122
|
+
int64_t base = len / n;
|
|
123
|
+
int64_t rem = len % n;
|
|
124
|
+
int64_t at = 0;
|
|
125
|
+
for (int64_t i = 0; i < n; i++) {
|
|
126
|
+
int64_t take = base + (i < rem ? 1 : 0);
|
|
127
|
+
jobs[i].body = body;
|
|
128
|
+
jobs[i].ctx = ctx;
|
|
129
|
+
jobs[i].lo = at;
|
|
130
|
+
jobs[i].hi = at + take;
|
|
131
|
+
at += take;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* The caller runs chunk 0 itself, so N-way parallelism costs N-1 spawns and
|
|
135
|
+
* the calling thread is not idle while it waits. Its chunk also runs in the
|
|
136
|
+
* caller's arena, which means chunk 0 behaves exactly as a sequential call
|
|
137
|
+
* would -- the asymmetry is deliberate and is why a body whose result is
|
|
138
|
+
* allocated is the surface's error rather than this file's. */
|
|
139
|
+
int started = 0;
|
|
140
|
+
for (int64_t i = 1; i < n; i++) {
|
|
141
|
+
if (pthread_create(&th[started], 0, nish_par_worker, &jobs[i]) != 0) break;
|
|
142
|
+
started++;
|
|
143
|
+
}
|
|
144
|
+
nish_par_depth++;
|
|
145
|
+
jobs[0].body(jobs[0].lo, jobs[0].hi, jobs[0].ctx);
|
|
146
|
+
/* A chunk whose thread could not be created runs here. Running short of
|
|
147
|
+
* threads must cost speed and never correctness, so there is no path on which
|
|
148
|
+
* a range is silently skipped. */
|
|
149
|
+
for (int64_t i = (int64_t)started + 1; i < n; i++) jobs[i].body(jobs[i].lo, jobs[i].hi, jobs[i].ctx);
|
|
150
|
+
nish_par_depth--;
|
|
151
|
+
for (int i = 0; i < started; i++) pthread_join(th[i], 0);
|
|
152
|
+
#else
|
|
153
|
+
(void)grain;
|
|
154
|
+
body(0, len, ctx);
|
|
155
|
+
#endif
|
|
156
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/* Nish runtime for the freestanding wasm profile (WP8): the arena and the
|
|
2
|
+
* array cold paths, without libc. Link it next to the module when a function
|
|
3
|
+
* takes or returns an array:
|
|
4
|
+
* scripts/build.sh x.ll runtime/runtime_wasm.c -o x.wasm --profile wasm
|
|
5
|
+
*
|
|
6
|
+
* Linear memory past `__heap_base` (the linker's end-of-data symbol) is one
|
|
7
|
+
* arena chunk that grows with `memory.grow`; there is no chunk list because
|
|
8
|
+
* wasm memory never shrinks. Field widths follow the IR contract
|
|
9
|
+
* (`%struct.nish_arena = type { i8*, i64, i64, i8* }`), not size_t: wasm32
|
|
10
|
+
* pointers are 4 bytes, and the inlined fast path in every compiled function
|
|
11
|
+
* bumps the 64-bit `off` field directly. Panics are `unreachable` (a JS
|
|
12
|
+
* RuntimeError); there is no stderr to write a message to. Strings and I/O
|
|
13
|
+
* need a WASI runtime and are not provided here, so `--emit-dts` lists
|
|
14
|
+
* string functions as not exported. */
|
|
15
|
+
#include <stddef.h>
|
|
16
|
+
#include <stdint.h>
|
|
17
|
+
|
|
18
|
+
/* Thread-local storage for the arena (WP20 T0), mirroring runtime.c and
|
|
19
|
+
* nish.h so the three definitions of this macro cannot drift. A wasm module
|
|
20
|
+
* built this way still has exactly one thread: wasm threads need shared memory
|
|
21
|
+
* plus the atomics and bulk-memory features and a worker on the host side, and
|
|
22
|
+
* WP20 §6 defers all of that. The macro is here so that `nish --threads`
|
|
23
|
+
* targeting wasm still links — `_Thread_local` in a non-shared memory is one
|
|
24
|
+
* ordinary block of linear memory — rather than failing over a storage class
|
|
25
|
+
* that means nothing here. */
|
|
26
|
+
#ifdef NISH_THREADS
|
|
27
|
+
#define NISH_TLS _Thread_local
|
|
28
|
+
#else
|
|
29
|
+
#define NISH_TLS
|
|
30
|
+
#endif
|
|
31
|
+
|
|
32
|
+
struct nish_arena { char *buf; uint64_t off; uint64_t cap; void *chunks; };
|
|
33
|
+
NISH_TLS struct nish_arena nish_arena;
|
|
34
|
+
|
|
35
|
+
_Static_assert(sizeof(struct nish_arena) == 32, "arena layout is ABI: runtime.ts, nish.h");
|
|
36
|
+
_Static_assert(offsetof(struct nish_arena, off) == 8, "the inlined allocator bumps field 1");
|
|
37
|
+
_Static_assert(offsetof(struct nish_arena, cap) == 16, "the inlined allocator reads field 2");
|
|
38
|
+
_Static_assert(offsetof(struct nish_arena, chunks) == 24, "arena layout is ABI");
|
|
39
|
+
|
|
40
|
+
extern unsigned char __heap_base;
|
|
41
|
+
#define NISH_PAGE ((uint64_t)65536)
|
|
42
|
+
|
|
43
|
+
/* Slow path of the inlined allocator (`size` is already 8-byte rounded). */
|
|
44
|
+
void *nish_arena_grow(uint64_t size) {
|
|
45
|
+
if (!nish_arena.buf) {
|
|
46
|
+
uintptr_t base = ((uintptr_t)&__heap_base + 7) & ~(uintptr_t)7;
|
|
47
|
+
nish_arena.buf = (char *)base;
|
|
48
|
+
nish_arena.cap = (uint64_t)__builtin_wasm_memory_size(0) * NISH_PAGE - base;
|
|
49
|
+
}
|
|
50
|
+
uint64_t need = nish_arena.off + size;
|
|
51
|
+
if (need > nish_arena.cap) {
|
|
52
|
+
uint64_t pages = (need - nish_arena.cap + NISH_PAGE - 1) / NISH_PAGE;
|
|
53
|
+
if (__builtin_wasm_memory_grow(0, (uintptr_t)pages) == (uintptr_t)-1) __builtin_trap();
|
|
54
|
+
nish_arena.cap += pages * NISH_PAGE;
|
|
55
|
+
}
|
|
56
|
+
void *p = nish_arena.buf + nish_arena.off;
|
|
57
|
+
nish_arena.off = need;
|
|
58
|
+
return p;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
void *nish_alloc_struct(uint64_t size) {
|
|
62
|
+
size = (size + 7) & ~(uint64_t)7;
|
|
63
|
+
if (nish_arena.buf && nish_arena.off + size <= nish_arena.cap) {
|
|
64
|
+
void *p = nish_arena.buf + nish_arena.off;
|
|
65
|
+
nish_arena.off += size;
|
|
66
|
+
return p;
|
|
67
|
+
}
|
|
68
|
+
return nish_arena_grow(size);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
void nish_reset_arena(void) { nish_arena.off = 0; }
|
|
72
|
+
void nish_free_arena(void) { nish_arena.off = 0; } /* memory cannot be returned */
|
|
73
|
+
uint64_t nish_arena_used(void) { return nish_arena.off; }
|
|
74
|
+
uint64_t nish_arena_mark(void) { return nish_arena.buf ? (uint64_t)(uintptr_t)(nish_arena.buf + nish_arena.off) : 0; }
|
|
75
|
+
void nish_arena_release(uint64_t mark) { nish_arena.off = mark ? mark - (uintptr_t)nish_arena.buf : 0; }
|
|
76
|
+
|
|
77
|
+
/* ---- Arrays: %struct.nish_array = type { i64, i64, i8* } */
|
|
78
|
+
typedef struct nish_array { uint64_t len; uint64_t cap; char *data; } nish_array;
|
|
79
|
+
|
|
80
|
+
nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len) {
|
|
81
|
+
nish_array *a = (nish_array *)nish_alloc_struct(sizeof *a);
|
|
82
|
+
a->len = a->cap = len;
|
|
83
|
+
a->data = (char *)nish_alloc_struct(len * elem_size);
|
|
84
|
+
return a;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
void nish_array_grow(nish_array *a, uint64_t elem_size) {
|
|
88
|
+
uint64_t cap = a->cap ? a->cap * 2 : 4;
|
|
89
|
+
char *data = (char *)nish_alloc_struct(cap * elem_size);
|
|
90
|
+
if (a->len) __builtin_memcpy(data, a->data, (uintptr_t)(a->len * elem_size)); /* memory.copy (-mbulk-memory) */
|
|
91
|
+
a->data = data;
|
|
92
|
+
a->cap = cap;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ---- Panics: `unreachable`, which the host sees as a RuntimeError */
|
|
96
|
+
void nish_panic_index(uint64_t idx, uint64_t len) { (void)idx; (void)len; __builtin_trap(); }
|
|
97
|
+
void nish_panic_slice(int64_t s, int64_t e, int64_t len) { (void)s; (void)e; (void)len; __builtin_trap(); }
|
|
98
|
+
void nish_panic_div(_Bool by_zero) { (void)by_zero; __builtin_trap(); }
|
|
99
|
+
void nish_exit(int32_t code) { (void)code; __builtin_trap(); }
|