@amritk/nish-x86_64-linux 0.29.0 → 0.30.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 CHANGED
@@ -256,7 +256,7 @@ routes are
256
256
  runtime/runtime-cross.c runtime/runtime-host.c runtime/runtime-net.c \
257
257
  runtime/runtime-simd.c runtime/runtime-aes.c runtime/runtime-sha256.c runtime/runtime-term.c \
258
258
  runtime/runtime-bytes.c runtime/runtime-checksum.c runtime/runtime-sandbox.c \
259
- runtime/runtime-shm.c -lm -o app
259
+ runtime/runtime-shm.c runtime/runtime-region.c -lm -o app
260
260
  ```
261
261
 
262
262
  `--link`ing inside the container is the mistake to avoid: it shells out to the
@@ -392,7 +392,7 @@ it into `build/seed/`; `NISH_BOOTSTRAP=<path>` names one you already have and
392
392
  skips the download. §2a has what the build does with it.
393
393
 
394
394
  The published package ships `bin/` (the `nish` command, which is a launcher),
395
- `runtime/` (the C runtime — fourteen translation units, the wasm subset and their
395
+ `runtime/` (the C runtime — fifteen translation units, the wasm subset and their
396
396
  header — and `runtime/sqlite/`, SQLite's vendored source),
397
397
  `scripts/build.sh` (the link pipeline), `std/` (the standard library) and
398
398
  `docs/AI.md` (the rules card). The
@@ -405,7 +405,7 @@ working directory — and the binary runs from inside its own platform package
405
405
  for exactly that reason.
406
406
 
407
407
  Building the IR yourself rather than through `--link` means naming the runtime
408
- on the `clang` line, and it is fourteen files:
408
+ on the `clang` line, and it is fifteen files:
409
409
 
410
410
  ```bash
411
411
  clang app.ll runtime/runtime.c runtime/runtime-os.c runtime/runtime-parallel.c \
package/bin/nish CHANGED
Binary file
package/docs/AI.md CHANGED
@@ -2743,6 +2743,21 @@ request. It reads no clock: when to take is the loop's choice, or a `PgBatchCloc
2743
2743
  guide's [PostgreSQL page](guide/using-nish/postgres.md#batching-keyed-reads)
2744
2744
  its walk-through.)
2745
2745
 
2746
+ **Call another service from a route handler** — there is no `await fetch()`
2747
+ in a handler and no event-loop callback: `nish/net/upstream`'s `Upstream(h,
2748
+ origin, limits)` is a pool of keep-alive connections to one `http:` origin on
2749
+ the `HttpHandler`'s own loop. `users.get(req, path)` returns at once — test it
2750
+ for `UPSTREAM_E_FULL`, which holds nothing — and keeps `req` held; after each
2751
+ `h.wait(ms)`, `while (users.ready())` `take()`s every finished call, answers
2752
+ `r.req` (`r.ok`, `r.status` and `r.body[0 .. r.length)`, or `r.error`: the
2753
+ codec's `HTTP1_E_` refusal or an `UPSTREAM_E_` code such as `TIMEOUT`) and
2754
+ `release`s it once. The origin is looked up once at start-up; set
2755
+ `UpstreamLimits.outbound` when it comes from input. Each connection takes a
2756
+ handler watch id from `firstId` (8) up, so keep your own `h.watch` ids below
2757
+ it. (No block, as for `nish/text`; `tests/link/net_upstream` is its program,
2758
+ and the user guide's [calling-services page](guide/using-nish/calling-services.md)
2759
+ its walk-through.)
2760
+
2746
2761
  **Draw to the whole terminal** (a game, a dashboard) — there is no curses and
2747
2762
  no `process.stdout.on("resize")`: `runTerminal(options, state, update, render)`
2748
2763
  from `nish/term-io` opens the terminal, probes what it can draw, steps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish-x86_64-linux",
3
- "version": "0.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "The nish native compiler for Linux on x86_64",
5
5
  "license": "MIT",
6
6
  "os": [
package/runtime/nish.h CHANGED
@@ -104,8 +104,9 @@ extern NISH_TLS struct nish_arena nish_arena;
104
104
 
105
105
  /* Bump allocation: 8-byte rounded and aligned, uninitialised. */
106
106
  void *nish_alloc_struct(uint64_t size);
107
- /* Slow path: push a new chunk (at least 64 KB) and bump from it. Called by
108
- * the inlined fast path when the current chunk is full; rarely by hosts. */
107
+ /* Slow path: push a new chunk (at least 64 KB, or a region's next size) and
108
+ * bump from it. Called by the inlined fast path when the current chunk is
109
+ * full; rarely by hosts. */
109
110
  void *nish_arena_grow(uint64_t size);
110
111
  /* Recycle everything in O(1): keeps the newest chunk, frees the rest. Every
111
112
  * arena string and object becomes invalid. Call it between batches. */
@@ -132,6 +133,25 @@ void *nish_arena_keep(uint64_t mark, void *p);
132
133
  /* Bytes bumped in the current chunk (`Arena.used()`); a steady-state loop keeps it flat. */
133
134
  uint64_t nish_arena_used(void);
134
135
 
136
+ /* ---- Regions (runtime-region.c) -----------------------------------------
137
+ * Arenas made, entered, left and freed one at a time
138
+ * (docs/wp46-long-running-memory.md). A handle is a nonzero `uint64_t`: a
139
+ * 24-bit slot and its 40-bit generation. Every call below panics on a zero
140
+ * handle, a freed (stale) one and one made on another thread.
141
+ * `nish_region_new` makes an empty region, owned by the calling thread.
142
+ * `nish_region_enter` makes the region the thread's arena until the matching
143
+ * `nish_region_leave`, so every allocation in between lands in it, and
144
+ * answers the root `leave` last recorded (null at first); entering a region
145
+ * already entered panics. `nish_region_leave` must name the region entered
146
+ * last on this thread, puts back the arena its enter displaced and records
147
+ * `root`. `nish_region_free` frees every chunk of a region that is not
148
+ * entered and makes its handle stale. The wasm profile has none of these
149
+ * (scripts/build.sh refuses IR that names them); wasi has. */
150
+ uint64_t nish_region_new(void);
151
+ void *nish_region_enter(uint64_t r);
152
+ void nish_region_leave(uint64_t r, void *root);
153
+ void nish_region_free(uint64_t r);
154
+
135
155
  /* ---- String operations ------------------------------------------------- */
136
156
  /* Copy `len` bytes into a new arena string (`bytes` need not be terminated). */
137
157
  nish_str *nish_str_new(const char *bytes, uint64_t len);
@@ -0,0 +1,231 @@
1
+ /* Nish runtime, the region half: arenas a program makes, enters, leaves and
2
+ * frees one at a time, so a long-running program can give back the memory of
3
+ * one session, one cache entry or one connection without a garbage collector
4
+ * (docs/wp46-long-running-memory.md, section 2.1, "Runtime and ABI").
5
+ *
6
+ * A translation unit of its own, paired with runtime.c in scripts/build.sh's
7
+ * `halves` and src/run-cache.ts's `runtimeUnits()` like the others, so it has
8
+ * its own measured `.text*` ceiling in tests/runtime-budgets.js and a program
9
+ * that never makes a region links none of it. The wasm profile has no malloc
10
+ * to give a region its chunks, so `scripts/build.sh --profile wasm` refuses IR
11
+ * that names `@nish_region_`; the wasi profile links this file.
12
+ *
13
+ * **A handle is a number.** `nish_region_new` answers an `i64`: the slot's
14
+ * index in the low 24 bits and the slot's generation in the 40 above them.
15
+ * Generations start at 1, so 0 -- what a fresh `Region<T>[]` holds -- is never
16
+ * a handle. `nish_region_free` moves the slot's generation on, so a copy of a
17
+ * freed handle is stale; a slot whose generation would pass 40 bits is
18
+ * retired rather than reused, so no handle ever names a slot twice. Every
19
+ * call that takes a handle checks it first and panics on a zero handle, a
20
+ * stale one, and one made on another thread. The 24-bit index caps a process
21
+ * at 16,777,216 live regions; `nish_region_new` past that panics.
22
+ *
23
+ * **The table** is one per process: a directory of pages, each page 1,024
24
+ * slots allocated (zeroed) the first time a slot in it is handed out, so a
25
+ * program that makes ten regions holds one page. Taking a slot and giving one
26
+ * back take the table's lock, once per `new` and once per `free`; `enter` and
27
+ * `leave` take none, because a slot is only ever entered by the thread that
28
+ * made it and the check reads the two fields another thread's `free` could
29
+ * write (generation and thread) with atomic loads. Only a threads build
30
+ * (`-DNISH_THREADS`) has a second thread to lock against, so only it pays for
31
+ * the lock.
32
+ *
33
+ * **Entering swaps arenas.** A slot holds a whole `struct nish_arena`.
34
+ * `nish_region_enter` swaps it with the thread's `nish_arena`, so every
35
+ * allocation the program makes until `nish_region_leave` -- the inlined bump
36
+ * as well as the runtime's own -- lands in the region; `leave` swaps them
37
+ * back. While a region is entered its slot holds the arena it displaced, so
38
+ * entering a second region inside the first nests: each leave restores what
39
+ * its enter took. A slot that is entered cannot be entered again (it would
40
+ * swap its own arena with itself) or freed, and `leave` must name the region
41
+ * entered last on this thread.
42
+ *
43
+ * **Chunk size.** A thread's own arena grows by 64 KiB chunks, which is right
44
+ * for one arena a thread and wrong for a hundred thousand of them. A region's
45
+ * first chunk is the size of its first allocation, and each later chunk
46
+ * doubles, or takes the allocation's own size when that is more. The size of
47
+ * the next chunk is runtime.c's thread-local `nish_region_chunk`, which
48
+ * `nish_arena_grow` reads on its slow path and which `enter` and `leave` swap
49
+ * with the slot's as they swap the arena; 0 there means the thread's own
50
+ * arena and the 64 KiB floor. A region starts it at 8, the smallest block.
51
+ *
52
+ * **The root.** `Region<T>` is only the number, so the slot keeps the root
53
+ * object `region`'s function answered: `nish_region_leave` records it and
54
+ * `nish_region_enter` answers it.
55
+ *
56
+ * Freeing a region frees its chunks through runtime.c's `nish_free_arena`,
57
+ * with the region's arena swapped in for the call, so an `-DNISH_ASAN` build
58
+ * forgets the freed blocks as it does for the thread's arena. */
59
+
60
+ #include <stdint.h>
61
+ #include <stdlib.h>
62
+
63
+ #include "nish.h"
64
+
65
+ #define NISH_COLD __attribute__((noreturn, cold, noinline))
66
+
67
+ /* runtime.c's: the size of the next chunk `nish_arena_grow` makes for the
68
+ arena in `nish_arena`, or 0 for a thread's own, which takes 64 KiB. */
69
+ extern NISH_TLS uint64_t nish_region_chunk;
70
+
71
+ #define NISH_REGION_SLOT_BITS 24
72
+ #define NISH_REGION_SLOTS ((uint64_t)1 << NISH_REGION_SLOT_BITS)
73
+ #define NISH_REGION_GEN_END ((uint64_t)1 << 40)
74
+ #define NISH_REGION_PAGE 1024
75
+
76
+ typedef struct nish_region_slot {
77
+ struct nish_arena arena; /* the region's arena, or, while entered, the one it displaced */
78
+ uint64_t gen; /* the live handle's generation; NISH_REGION_GEN_END once retired */
79
+ uint64_t thread; /* the thread that made it, 0 while the slot is free */
80
+ uint64_t chunk; /* the region's next chunk size, or, while entered, the displaced one */
81
+ uint64_t outer; /* while entered: the handle entered before it; while free: the next free slot + 1 */
82
+ void *root; /* what `leave` recorded, answered by `enter` */
83
+ uint64_t entered;
84
+ } nish_region_slot;
85
+
86
+ static nish_region_slot *nish_region_pages[NISH_REGION_SLOTS / NISH_REGION_PAGE];
87
+ static uint64_t nish_region_used; /* slots ever handed out: the next never-used index */
88
+ static uint64_t nish_region_free_list; /* a free slot's index + 1, or 0 */
89
+ /* The handle entered last on this thread, or 0. */
90
+ static NISH_TLS uint64_t nish_region_current;
91
+
92
+ #ifdef NISH_THREADS
93
+ static int nish_region_lock_word;
94
+ static uint64_t nish_region_threads;
95
+ static NISH_TLS uint64_t nish_region_thread_id;
96
+ static void nish_region_lock(void) {
97
+ while (__atomic_exchange_n(&nish_region_lock_word, 1, __ATOMIC_ACQUIRE))
98
+ while (__atomic_load_n(&nish_region_lock_word, __ATOMIC_RELAXED)) {}
99
+ }
100
+ static void nish_region_unlock(void) { __atomic_store_n(&nish_region_lock_word, 0, __ATOMIC_RELEASE); }
101
+ /* A number no other thread of the process has: 64 bits, so it never wraps
102
+ back onto a live thread's. */
103
+ static uint64_t nish_region_me(void) {
104
+ if (!nish_region_thread_id) nish_region_thread_id = __atomic_add_fetch(&nish_region_threads, 1, __ATOMIC_RELAXED);
105
+ return nish_region_thread_id;
106
+ }
107
+ #else
108
+ static void nish_region_lock(void) {}
109
+ static void nish_region_unlock(void) {}
110
+ static uint64_t nish_region_me(void) { return 1; }
111
+ #endif
112
+
113
+ /* The panics, by number: one cold function and one table, rather than a
114
+ message's address loaded at each call site. Each ends in `nish_panic`, so
115
+ only the first panic of a process prints and atexit handlers run. */
116
+ enum {
117
+ NISH_REGION_ZERO,
118
+ NISH_REGION_STALE,
119
+ NISH_REGION_THREAD,
120
+ NISH_REGION_ENTERED,
121
+ NISH_REGION_FREE_ENTERED,
122
+ NISH_REGION_LEAVE,
123
+ NISH_REGION_FULL,
124
+ NISH_REGION_OOM,
125
+ };
126
+ #define NISH_REGION_MSG(text) { sizeof text - 1, text }
127
+ static const struct { uint64_t len; char data[72]; } nish_region_messages[] = {
128
+ NISH_REGION_MSG("nish: a zero region handle: no region was made for it"),
129
+ NISH_REGION_MSG("nish: a stale region handle: the region was freed"),
130
+ NISH_REGION_MSG("nish: a region handle made on another thread"),
131
+ NISH_REGION_MSG("nish: enter of a region that is already entered"),
132
+ NISH_REGION_MSG("nish: free of a region that is entered"),
133
+ NISH_REGION_MSG("nish: leave of a region that is not the one entered last"),
134
+ NISH_REGION_MSG("nish: more than 16777216 live regions"),
135
+ NISH_REGION_MSG("nish: out of memory"),
136
+ };
137
+ static NISH_COLD void nish_region_die(int why) {
138
+ nish_panic((const nish_str *)&nish_region_messages[why]);
139
+ }
140
+
141
+ /* The slot `r` names, or a panic: zero, stale, another thread's. */
142
+ static nish_region_slot *nish_region_slot_of(uint64_t r) {
143
+ if (!r) nish_region_die(NISH_REGION_ZERO);
144
+ uint64_t index = r & (NISH_REGION_SLOTS - 1);
145
+ nish_region_slot *page = __atomic_load_n(&nish_region_pages[index / NISH_REGION_PAGE], __ATOMIC_ACQUIRE);
146
+ if (!page) nish_region_die(NISH_REGION_STALE);
147
+ nish_region_slot *s = &page[index % NISH_REGION_PAGE];
148
+ uint64_t thread = __atomic_load_n(&s->thread, __ATOMIC_RELAXED);
149
+ if (!thread || __atomic_load_n(&s->gen, __ATOMIC_RELAXED) != r >> NISH_REGION_SLOT_BITS)
150
+ nish_region_die(NISH_REGION_STALE);
151
+ if (thread != nish_region_me()) nish_region_die(NISH_REGION_THREAD);
152
+ return s;
153
+ }
154
+
155
+ /* Swap the slot's arena and chunk size with the thread's: `enter` and `leave`
156
+ both, and `free` around its `nish_free_arena`. */
157
+ static void nish_region_swap(nish_region_slot *s) {
158
+ struct nish_arena a = nish_arena;
159
+ nish_arena = s->arena;
160
+ s->arena = a;
161
+ uint64_t c = nish_region_chunk;
162
+ nish_region_chunk = s->chunk;
163
+ s->chunk = c;
164
+ }
165
+
166
+ uint64_t nish_region_new(void) {
167
+ uint64_t me = nish_region_me();
168
+ nish_region_lock();
169
+ uint64_t index;
170
+ nish_region_slot *s;
171
+ if (nish_region_free_list) {
172
+ index = nish_region_free_list - 1;
173
+ s = &nish_region_pages[index / NISH_REGION_PAGE][index % NISH_REGION_PAGE];
174
+ nish_region_free_list = s->outer;
175
+ } else {
176
+ if (nish_region_used == NISH_REGION_SLOTS) nish_region_die(NISH_REGION_FULL);
177
+ index = nish_region_used++;
178
+ nish_region_slot **page = &nish_region_pages[index / NISH_REGION_PAGE];
179
+ if (!*page) {
180
+ nish_region_slot *p = calloc(NISH_REGION_PAGE, sizeof *p);
181
+ if (!p) nish_region_die(NISH_REGION_OOM);
182
+ __atomic_store_n(page, p, __ATOMIC_RELEASE);
183
+ }
184
+ s = &(*page)[index % NISH_REGION_PAGE];
185
+ __atomic_store_n(&s->gen, 1, __ATOMIC_RELAXED);
186
+ }
187
+ s->chunk = 8;
188
+ s->outer = 0;
189
+ __atomic_store_n(&s->thread, me, __ATOMIC_RELAXED);
190
+ uint64_t r = s->gen << NISH_REGION_SLOT_BITS | index;
191
+ nish_region_unlock();
192
+ return r;
193
+ }
194
+
195
+ void *nish_region_enter(uint64_t r) {
196
+ nish_region_slot *s = nish_region_slot_of(r);
197
+ if (s->entered) nish_region_die(NISH_REGION_ENTERED);
198
+ s->entered = 1;
199
+ s->outer = nish_region_current;
200
+ nish_region_current = r;
201
+ nish_region_swap(s);
202
+ return s->root;
203
+ }
204
+
205
+ void nish_region_leave(uint64_t r, void *root) {
206
+ nish_region_slot *s = nish_region_slot_of(r);
207
+ if (nish_region_current != r) nish_region_die(NISH_REGION_LEAVE);
208
+ nish_region_swap(s);
209
+ nish_region_current = s->outer;
210
+ s->entered = 0;
211
+ s->root = root;
212
+ }
213
+
214
+ void nish_region_free(uint64_t r) {
215
+ nish_region_slot *s = nish_region_slot_of(r);
216
+ if (s->entered) nish_region_die(NISH_REGION_FREE_ENTERED);
217
+ nish_region_swap(s);
218
+ nish_free_arena();
219
+ nish_region_swap(s);
220
+ s->root = 0;
221
+ uint64_t index = r & (NISH_REGION_SLOTS - 1);
222
+ nish_region_lock();
223
+ __atomic_store_n(&s->thread, 0, __ATOMIC_RELAXED);
224
+ uint64_t gen = s->gen + 1;
225
+ __atomic_store_n(&s->gen, gen, __ATOMIC_RELAXED);
226
+ if (gen < NISH_REGION_GEN_END) {
227
+ s->outer = nish_region_free_list;
228
+ nish_region_free_list = index + 1;
229
+ }
230
+ nish_region_unlock();
231
+ }
package/runtime/runtime.c CHANGED
@@ -174,14 +174,24 @@ static NISH_COLD void nish_oom(void) { nish_die("nish: out of memory\n"); }
174
174
  `nish_array_grow` and `nish_str_concat`. */
175
175
  #define NISH_ARRAY_MAX 2147483647u
176
176
 
177
- /* Slow path (size 8-byte rounded): push a chunk (>= 64 KB), bump from it. */
177
+ /* The size of the next chunk while a region is entered (runtime-region.c
178
+ swaps it in and out with the region's arena), or 0 for the thread's own
179
+ arena, whose chunks are at least 64 KB. Defined here, not there, so that
180
+ this file still links alone. */
181
+ NISH_TLS uint64_t nish_region_chunk;
182
+
183
+ /* Slow path (size 8-byte rounded): push a chunk (>= 64 KB, or a region's
184
+ next size), bump from it. A region's chunks double from its first. */
178
185
  void *nish_arena_grow(uint64_t size) {
179
186
  #ifdef NISH_ASAN
180
187
  /* An exact chunk leaves off == cap, so the inline bump never fits again and
181
188
  every block comes through here: one malloc, one redzone, per block. */
182
189
  uint64_t cap = size;
183
190
  #else
184
- uint64_t cap = size > 65536 ? size : 65536;
191
+ uint64_t next = nish_region_chunk;
192
+ uint64_t floor = next ? next : 65536;
193
+ uint64_t cap = size > floor ? size : floor;
194
+ if (next) nish_region_chunk = cap * 2;
185
195
  #endif
186
196
  /* A 64-bit request cannot always be asked for: wasm32's `size_t` is 32 bits,
187
197
  and malloc would silently take the low half of a chunk nobody can address. */
package/scripts/build.sh CHANGED
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # scripts/build.sh <module.ll> [more .ll/.c files...] -o <out> [--profile debug|speed|size|wasm|wasi|napi]
5
5
  #
6
- # The C runtime is fourteen translation units and is named as one: an input
6
+ # The C runtime is fifteen translation units and is named as one: an input
7
7
  # <dir>/runtime.c also compiles <dir>/runtime-os.c, the half that wraps the
8
8
  # system calls (files, directories, subprocesses, the environment, the clock),
9
9
  # <dir>/runtime-parallel.c, the half that divides a range of work across
@@ -17,8 +17,9 @@
17
17
  # <dir>/runtime-checksum.c, CRC-32 on its carry-less multiply or CRC instructions,
18
18
  # <dir>/runtime-term.c, stdin and the terminal of `nish:term`,
19
19
  # <dir>/runtime-bytes.c, the byte-class runs of `nish/bytes`,
20
- # <dir>/runtime-sandbox.c, the confinement `--sandbox` applies, and
21
- # <dir>/runtime-shm.c, the shared-memory regions of `nish/sync/shm`. Each of those
20
+ # <dir>/runtime-sandbox.c, the confinement `--sandbox` applies,
21
+ # <dir>/runtime-shm.c, the shared-memory regions of `nish/sync/shm`, and
22
+ # <dir>/runtime-region.c, the arenas a program makes and frees one at a time. Each of those
22
23
  # files says why they are compiled and measured apart.
23
24
  #
24
25
  # Profiles:
@@ -222,11 +223,11 @@ if [ -n "$sqlite_to" ] || [ -n "$sqlite_from" ]; then
222
223
  fi
223
224
  [ -n "$out" ] || [ -n "$objects_to" ] || [ -n "$sqlite_to" ] || { echo "error: -o <out> is required" >&2; exit 2; }
224
225
 
225
- # The runtime is fourteen translation units, and a caller names one: whoever passes
226
+ # The runtime is fifteen translation units, and a caller names one: whoever passes
226
227
  # <dir>/runtime.c gets <dir>/runtime-os.c, <dir>/runtime-parallel.c,
227
228
  # <dir>/runtime-cross.c, <dir>/runtime-host.c, <dir>/runtime-net.c, <dir>/runtime-simd.c,
228
229
  # <dir>/runtime-aes.c, <dir>/runtime-sha256.c, <dir>/runtime-checksum.c, <dir>/runtime-term.c,
229
- # <dir>/runtime-bytes.c, <dir>/runtime-sandbox.c and <dir>/runtime-shm.c compiled beside it, on every profile,
230
+ # <dir>/runtime-bytes.c, <dir>/runtime-sandbox.c, <dir>/runtime-shm.c and <dir>/runtime-region.c compiled beside it, on every profile,
230
231
  # wasi included: runtime-simd.c and runtime-bytes.c take their scalar paths there
231
232
  # unless the build adds -msimd128,
232
233
  # runtime-aes.c, runtime-sha256.c and runtime-checksum.c compile to an answer of 0 and
@@ -243,7 +244,7 @@ fi
243
244
  # here keeps every one of those correct, and keeps "the runtime" one thing to
244
245
  # name from the outside. A caller that names one itself is left alone, because
245
246
  # naming one object twice is a duplicate-symbol error.
246
- halves="runtime-os.c runtime-parallel.c runtime-cross.c runtime-host.c runtime-net.c runtime-simd.c runtime-aes.c runtime-sha256.c runtime-checksum.c runtime-term.c runtime-bytes.c runtime-sandbox.c runtime-shm.c"
247
+ halves="runtime-os.c runtime-parallel.c runtime-cross.c runtime-host.c runtime-net.c runtime-simd.c runtime-aes.c runtime-sha256.c runtime-checksum.c runtime-term.c runtime-bytes.c runtime-sandbox.c runtime-shm.c runtime-region.c"
247
248
  for i in ${inputs[@]+"${inputs[@]}"}; do
248
249
  case "$i" in
249
250
  */runtime.c|runtime.c)
@@ -671,6 +672,18 @@ case "$profile" in
671
672
  "$CC" "${common[@]}" "${optimise[@]}" ${lto_parts[@]+"${lto_parts[@]}"} \
672
673
  "${gc[@]}" ${strip_flag[@]+"${strip_flag[@]}"} "${inputs[@]}" -lm -o "$out" ;;
673
674
  wasm)
675
+ # A region's chunks come from malloc, and the freestanding wasm runtime
676
+ # (runtime-wasm.c) has none, so IR that makes or enters one is refused here
677
+ # rather than failing at the link on an undefined `nish_region_*`. The wasi
678
+ # profile links runtime-region.c with the rest of the runtime.
679
+ for i in ${inputs[@]+"${inputs[@]}"}; do
680
+ case "$i" in
681
+ *.ll) if grep -q "@nish_region_" "$i"; then
682
+ echo "error: $i uses regions, which the wasm profile cannot link (it has no malloc); build it with --profile wasi" >&2
683
+ exit 2
684
+ fi ;;
685
+ esac
686
+ done
674
687
  # clang resolves wasm-ld next to its own binary first, then on PATH; ask it
675
688
  # rather than probing PATH so a Homebrew llvm without a PATH entry still works.
676
689
  wasm_ld=$("$CC" -print-prog-name=wasm-ld)
package/std/README.md CHANGED
@@ -250,7 +250,9 @@ reads one, and it still takes its randomness as an argument.
250
250
  | [`net/http-compress.ts`](./net/http-compress.ts) | Content coding for HTTP responses: `httpAcceptEncoding(buf, start, end, offered)` reads an `Accept-Encoding` value (q-values, `*`, `identity`, `x-gzip`) into `HTTP_CODING_GZIP`, `HTTP_CODING_DEFLATE`, `HTTP_CODING_IDENTITY` or `HTTP_CODING_NONE`, allocating nothing; `HttpCompressor(level, minLength, maxLength)` gzips a body into its own buffer when it is worth it (-1 to send it as it is) and builds the response fields (`addFields`: `Content-Encoding: gzip` and, while `vary`, `Vary: Accept-Encoding`). It sends gzip only when it pays: `minLength`, `minSaving` bytes or `minSavingPercent`, and `segmentMode` (fewer TCP segments of `mss`, head included, than as it is); `decision` says why (`HTTP_COMPRESS_SENT`, `_NOT_ACCEPTED`, `_TOO_SMALL`, `_TOO_LARGE`, `_TOO_LITTLE_SAVED`, `_NO_FEWER_SEGMENTS`). Bodies up to `fixedLimit` use the fixed-Huffman encoder (`encoder`: `HTTP_ENCODER_AUTO`, `_FIXED`, `_LEVEL`). On HTTP/1.1, `Http1Connection.acceptEncoding()` and `respondEncoded(status, names, values, body, off, len, enc, coding)` answer a request in one call with the compressed `Content-Length`, or nothing at all (`H1_AGAIN`, `H1_INVALID`); `identity;q=0` is honoured. Opt-in; BREACH is the program's to avoid (`docs/security/compress.md`) | RFC 9110 §8.4 and §12.5.3: the negotiation table, and sans-IO responses gunzipped back (`tests/link/std_http_compress`) |
251
251
  | [`net/router.ts`](./net/router.ts) | `Router`, a method and path router that knows no carrier and allocates nothing per match: `add(method, pattern)` answers the route's index (0, 1, 2 in the order added) or a refusal — `ROUTER_BAD_PATTERN`, `ROUTER_BAD_METHOD`, `ROUTER_TOO_LONG`, `ROUTER_AMBIGUOUS` (`*` not last), `ROUTER_DUPLICATE` (the same shape and method, whatever the parameter names) or `ROUTER_FULL`, against `RouterLimits`; `match(method, mStart, mEnd, target, tStart, tEnd, params)` answers the index, `ROUTER_NOT_FOUND` or `ROUTER_METHOD_NOT_ALLOWED`, writing each `:name` and a final `*` as a `[start, end)` pair into the caller's `params` (`ROUTER_TOO_MANY_PARAMS` when it is short). Literal before parameter before `*`, segment by segment, backtracking; a lookup enters each node at most once. The query is left unparsed and nothing is decoded: `routerDecode` decodes one parameter (`ROUTER_BAD_ESCAPE`, `ROUTER_NO_ROOM`) (`docs/security/router.md`) | Every refusal code, precedence and backtracking, 405 against 404, `%2F` kept in one segment, the query, `*` and authority-form targets, a target deeper than any route, the lookup bound, params past storage, and the arena flat over matches (`tests/link/net_router`, `net_router_f64`, five `net_router_negative_*`) |
252
252
  | [`net/handler.ts`](./net/handler.ts) | `HttpHandler`, complete requests and their answers over the HTTP/1.1 carrier: it owns the readiness loop, routes each request through a `Router` at its head, gathers its body up to `HandlerLimits.http.maxBody` (1 MiB; past it the carrier answers 413), and hands it over whole. The program pulls — `wait(ms)`, `ready()`, `take()` — and switches on `req.route`; `HttpRequest` reads parameters, form-decoded query values and fields as ranges into its own buffers. Each request is answered once, now or in a later turn, by `send`, `sendFields`, `empty`, `json()` + `sendJson`, `problem` (RFC 9457), `refuse` (404, or 405 with `Allow`) or `start`/`write`/`end`; an answer whose connection closed first is `HANDLER_DROPPED` and reaches no later peer. `HEAD` takes a `GET` route. `watch`/`fired` add the program's own descriptors (`signalFd()`), and `drain` stops gracefully. Nothing is allocated per request once warm (`docs/security/handler.md`) | Routing, parameters and form-decoded queries, `HEAD`, 404 and 405 problems, chunked bodies in and out, the body cap at and one past it, pipelining order, late answers dropped across a reused slot, the arena and pool flat over two thousand requests, and the drain (`tests/link/net_handler`, and `net_http_app` over `examples/http-app`) |
253
+ | [`net/upstream.ts`](./net/upstream.ts) | `Upstream`, outbound HTTP/1.1 calls from a route handler that never block it: a pool of keep-alive connections to one `http:` origin on the `HttpHandler`'s own loop (watch ids `UpstreamLimits.firstId` and up). `get(req, path)` returns at once, or `UPSTREAM_E_FULL` with nothing held; each call comes back once through `ready()`/`take()` tagged with `r.req`, `ok` with its status and `body[0 .. length)`, or failed with the codec's `HTTP1_E_` refusal or an `UPSTREAM_E_` code (timeout, closed, connect, origin, lookup, blocked, request), and goes back with `release`. The origin is looked up once at start-up and pinned; `UpstreamLimits.outbound` checks it against an `OutboundPolicy`. A failed call's connection is closed. Each connection keeps one codec in spans mode, so a warm pool allocates nothing per call | A slow upstream while other clients and a second upstream are answered, the timeout, keep-alive and tagging, five codec refusals as failures, the policy refusing loopback, origin and request-target refusals, three thousand warm calls on one connection with `Arena.used()` flat (`tests/link/net_upstream`), and a second `release` panicking (`net_upstream_release`) |
253
254
  | [`net/postgres.ts`](./net/postgres.ts) | `PgConnection`, a PostgreSQL client that reads no socket: the program fills `inputBuffer()` and sends `outputBuffer()`, and `next()` answers `PG_NEED_MORE`, `PG_READY`, `PG_DESCRIBED`, `PG_ROW`, `PG_COMPLETE`, `PG_ERROR`, `PG_NOTICE`, `PG_GROUP_DONE` or the final `PG_FAILED` (`failure`, one of the `PG_ERR_*` codes). Protocol 3.0: startup, `trust` and SCRAM-SHA-256 with the server's signature checked (cleartext and MD5 refused), the extended query protocol — `prepare`, `bindStart`, `paramInt` / `paramText` / `paramBytes` / `paramTextArray` / `paramIntArray` / `paramNull`, an array built element by element into the Bind (`arrayStart`, `arrayElement`, `arrayInt`, `arrayNull`, `arrayEnd`), `bindEnd`, `bindStartBinary` for a Bind whose result columns come in binary (read in place by `binaryInt`, `binaryBool`, `binaryFloat` and `PgBinary`'s `timestamp` and `iso`, and a one-dimensional `int2[]`, `int4[]` or `int8[]` by `PgBinaryArray`), `bindCancel` to drop a Bind not yet ended, `closeStatement`, `sync` — with up to `maxInFlight` groups pipelined. `PgBatcher` queues keyed reads of one statement shape (`addInt`, `addText`) and `take(conn)` binds up to `maxRequests` of them, each key once, as one execution over `unnest($1) with ordinality`; `row`, `end` and `fail` route its rows by their server-supplied ordinal, checked first (read with `binaryInt` when `PgBatchConfig.binary` is set, so the batch's rows come in binary too), to `next(run)`'s per-request `PG_BATCH_ROW` and `PG_BATCH_DONE` / `PG_BATCH_MISSING` / `PG_BATCH_FAILED`; `take` answers a fresh handle each time, so a server's late or repeated event on a spent one is counted in `stray`, never routed or panicked on. It reads no clock and allocates nothing per request or row. `PgBatchClock` says when to take, from times the program passes in: once max(`minCount`, the moving average of batch sizes × `factor`) wait, the oldest has waited `maxWaitUs`, or, with the idle bypass, nothing is in flight and the load is light (its defaults are the fixed window); `PgBatcher.takeDue` takes only then, and `PgBatchTurns` lets one free connection collect while the others execute. A row's columns are windows onto the receive buffer (`colStart`, `colLength`), read in place by `int`, `bool`, `isNull` and `PgText`'s decoders (`timestamptz`, `PgArrayWalker` for `text[]`); no per-row or per-column allocation. The password is a `Secret<u8[]>`, wiped once the proof is made. Caps on message size, columns, groups in flight and SCRAM iterations (`docs/security/postgres.md`) | PostgreSQL 18's frontend/backend protocol chapter, RFC 5802 and RFC 7677: every builder, event and refusal (`tests/link/std_postgres`), and exchanges recorded from PostgreSQL 18 by `tests/postgres/record.mjs` replayed byte for byte (`tests/link/std_postgres_replay`); the batcher's queueing, caps and ordinal refusals (`tests/link/std_postgres_batch`); the clock's reasons, backlog and turns (`tests/link/std_postgres_batch_clock`) |
255
+ | [`net/postgres-carrier.ts`](./net/postgres-carrier.ts) | `PgCarrier`, a `PgPool`'s queries on an `HttpHandler`'s loop: its sockets sit in the handler's epoll set, a route handler hands a request to `run` (or `runInt` / `runText` for one of up to 8 attached `PgBatcher`s) and gets its number at once, and `next()` answers `PG_CARRIER_BUILD` to bind a plain query on `connection()`, `PG_CARRIER_ROW`, `PG_CARRIER_COMPLETE`, `PG_CARRIER_ERROR` and one final `PG_CARRIER_DONE`, `PG_CARRIER_MISSING` or `PG_CARRIER_FAILED` (`failure`, `socketError`) with `request()` the request to answer and `tag` the caller's. It connects the pool's slots itself (TCP or a Unix socket), times out a connect after `connectTimeoutMs`, sheds batched keys when no slot can connect, and sizes every table from `PgCarrierLimits` at construction: `run` answers `PG_CARRIER_FULL` rather than grow one, and once connections are up it allocates nothing per query. Kept apart from `net/postgres.ts` so that module reaches no capability (`docs/security/postgres.md`) | A loopback backend in the test itself: routing, a slow query not stalling another client, lost and refused connections, a connect timeout, and `Arena.used()` flat over thousands of queries (`tests/link/std_postgres_carrier`); a live server by `tests/postgres/live/run.mjs` |
254
256
  | [`net/prefork.ts`](./net/prefork.ts) | A server on every core: `preforkWorkers(process.argv)` or `preforkWith(process.argv, config)` answers a worker's index in each worker it starts again from the same program, and in the supervisor runs them until SIGTERM or SIGINT, restarting one that exits after a backoff. `PreforkConfig.supervisorChannels` gives each worker a `unixSocketPair` to the supervisor, which `preforkSupervise(args, config, state, onChannel)` hands to a callback; `serviceWorker` gives every other worker a pair to that one, and makes the workers one group that restarts together on fresh pairs. The ends arrive from descriptor 3 through `spawnWithFds`, found with `preforkSupervisorChannel`, `preforkServiceChannel` and `preforkServiceChannelOf`. Native only | `tests/link/net_prefork` (the spread, a dead worker, SIGTERM, the backoff); `tests/link/net_prefork_channels` (each worker holds exactly its own ends, a restart, the supervisor's descriptors flat) |
255
257
 
256
258
  **How a carrier drives `TlsServer`.** The server speaks in handshake