@amritk/nish-aarch64-darwin 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.
@@ -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(); }