@amritk/nish-x86_64-linux 0.29.0 → 0.31.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
@@ -2222,6 +2222,13 @@ system call either way. `setIdlePriority()` moves the program to Linux's `SCHED_
2222
2222
  `main`, never in a task. An unprivileged process can never leave `SCHED_IDLE`
2223
2223
  and an idle one can starve on a saturated machine, so keep a normal-priority
2224
2224
  fallback. Neither exists on wasm; both throw under Node.
2225
+ `setCpuAffinity(cpu)` pins the process to one CPU, narrow-only: a CPU outside
2226
+ the mask it already has is `-22`; `-95` on Darwin and under Node, refused on
2227
+ wasm. `nish/net/prefork`'s `PreforkConfig` takes `pin = true` to pin worker
2228
+ `k` to CPU `k` (more workers than CPUs panics), and `listenPort` (with
2229
+ `listenHost`, `listenBacklog`, `listenFlags` 0 or 2) to have the supervisor
2230
+ bind one `SO_REUSEPORT` listener per worker, steered on Linux by the receiving
2231
+ CPU when pinned; a worker takes its own with `preforkListener(process.argv)`.
2225
2232
 
2226
2233
  **Sockets** (`nish:net`, or the same names as globals). `tcpListen(host, port,
2227
2234
  backlog)`, `tcpAccept(fd, peer)`, `netRead(fd, buf, off, len)`,
@@ -2236,10 +2243,10 @@ wait for it to be writable, then `connectResult(fd)` is 0 or the failure. Buffer
2236
2243
  are `u8[]` only; an address is 18 bytes of one (IPv4 as `::ffff:a.b.c.d`, then
2237
2244
  the port big-endian); hosts are numeric literals; to reach a name, resolve it first with
2238
2245
  `nish/net/dns` (`DnsClient` on your loop, or the blocking `resolveHost`).
2239
- To fetch a URL from a CLI, skip all of this: `nish/net/http-client`'s
2240
- `httpFetch(new HttpRequest("GET", url), new HttpOptions())` blocks until the
2246
+ To fetch a URL from a CLI, skip all of this: `nish/net/fetch`'s
2247
+ `fetchSync(new HttpRequest("GET", url), new HttpOptions())` blocks until the
2241
2248
  response's head is in and answers it with `body.readAll(limit)`, or null on a
2242
- failure (an `HttpClient` keeps the connection and says why in `error`).
2249
+ failure (a `FetchSyncClient` keeps the connection and says why in `error`).
2243
2250
  `netRead` / `netWrite` panic on a range outside the buffer.
2244
2251
  `tcpListenFlags(host, port, backlog, flags)` is `tcpListen` with flag 1
2245
2252
  `SO_REUSEPORT`, so several listeners, in one process or many, share a port (Linux
@@ -2596,6 +2603,46 @@ returns. To give memory back sooner, scope it with `using a = arena()`;
2596
2603
  `Arena.release` and `Arena.reset` are gone, and a call to either is an unknown
2597
2604
  builtin (NL2244).
2598
2605
 
2606
+ ### Regions: `nish:region`
2607
+
2608
+ **For memory a long-running program frees itself — per connection, per cache
2609
+ entry — use a region.** `region(f)` / `regionWith(arg, f)` make one and run
2610
+ `f` in it, and what `f` answers is the root; `enter(r, f)` /
2611
+ `enterWith(r, arg, f)` run `f(root)` / `f(root, arg)` in it again; `free(r)`
2612
+ gives all of it back. `f` is a named function or an arrow that captures
2613
+ nothing. `Region<T>` is a number, not a pointer.
2614
+
2615
+ ```ts nish:ok
2616
+ import { Region, enter, free, region } from "nish:region";
2617
+
2618
+ class Session {
2619
+ hits: i32 = 0;
2620
+ }
2621
+
2622
+ const open = (): Session => new Session();
2623
+ const touch = (s: Session): i32 => {
2624
+ s.hits = s.hits + 1;
2625
+ return s.hits;
2626
+ };
2627
+
2628
+ export const main = (): i32 => {
2629
+ const r: Region<Session> = region(open);
2630
+ console.log(enter(r, touch));
2631
+ free(r);
2632
+ return 0;
2633
+ };
2634
+ ```
2635
+
2636
+ The checker refuses every way region memory could outlive `free`, and every way
2637
+ outer memory could be pointed at from inside: only numbers, booleans, enums and
2638
+ `Region` handles cross. `enter` answers a scalar or a handle (NL2611), `f` may
2639
+ not store region memory into `arg` or other outer memory (NL2609) nor outer
2640
+ memory into the region (NL2610), nor `push`/`set`/`add` to an outer collection
2641
+ (NL2615). A handle stays on its thread (NL2612, NL2613, NL2351), is never a
2642
+ `using` (NL2387) and is never `new Region` (NL2618). A bad handle, a double
2643
+ `free` or a nested `enter` of one region stops the program. `--emit-ts` refuses
2644
+ the module (NL4015).
2645
+
2599
2646
  ## Recipes for what is missing
2600
2647
 
2601
2648
  **Iterate and transform** — there is no `map`, and no function to pass it:
@@ -2705,7 +2752,12 @@ parameter. `paramIntArray(values, start, count)` binds an `i64[]` window;
2705
2752
  element, then `arrayEnd()`, builds a `text[]` straight into the Bind from
2706
2753
  windows of a buffer. An element refused for room answers `false` and leaves
2707
2754
  the array open: end it and send the rest in the next statement. Every other
2708
- `param...` call and `bindEnd()` panic until `arrayEnd()`. (No block, as for
2755
+ `param...` call and `bindEnd()` panic until `arrayEnd()`. For a long
2756
+ `int4[]` or `int8[]`, `paramInt4Array` / `paramInt8Array` (same window) or
2757
+ `binaryArrayStart(23 or 20)`, `binaryArrayInt` / `binaryArrayNull`,
2758
+ `arrayEnd()` send it in binary, at a tenth of the client CPU; the element type
2759
+ must be the statement's (`int4[]` to an `int8[]` parameter is SQLSTATE
2760
+ `42804`), and an `int4` element out of range panics. (No block, as for
2709
2761
  `nish/text`; `tests/postgres/live` runs it against a server, and the user
2710
2762
  guide's [PostgreSQL page](guide/using-nish/postgres.md#array-parameters) its
2711
2763
  walk-through.)
@@ -2743,6 +2795,21 @@ request. It reads no clock: when to take is the loop's choice, or a `PgBatchCloc
2743
2795
  guide's [PostgreSQL page](guide/using-nish/postgres.md#batching-keyed-reads)
2744
2796
  its walk-through.)
2745
2797
 
2798
+ **Call another service from a route handler** — there is no `await fetch()`
2799
+ in a handler and no event-loop callback: `nish/net/upstream`'s `Upstream(h,
2800
+ origin, limits)` is a pool of keep-alive connections to one `http:` origin on
2801
+ the `HttpHandler`'s own loop. `users.get(req, path)` returns at once — test it
2802
+ for `UPSTREAM_E_FULL`, which holds nothing — and keeps `req` held; after each
2803
+ `h.wait(ms)`, `while (users.ready())` `take()`s every finished call, answers
2804
+ `r.req` (`r.ok`, `r.status` and `r.body[0 .. r.length)`, or `r.error`: the
2805
+ codec's `HTTP1_E_` refusal or an `UPSTREAM_E_` code such as `TIMEOUT`) and
2806
+ `release`s it once. The origin is looked up once at start-up; set
2807
+ `UpstreamLimits.outbound` when it comes from input. Each connection takes a
2808
+ handler watch id from `firstId` (8) up, so keep your own `h.watch` ids below
2809
+ it. (No block, as for `nish/text`; `tests/link/net_upstream` is its program,
2810
+ and the user guide's [calling-services page](guide/using-nish/calling-services.md)
2811
+ its walk-through.)
2812
+
2746
2813
  **Draw to the whole terminal** (a game, a dashboard) — there is no curses and
2747
2814
  no `process.stdout.on("resize")`: `runTerminal(options, state, update, render)`
2748
2815
  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.31.0",
4
4
  "description": "The nish native compiler for Linux on x86_64",
5
5
  "license": "MIT",
6
6
  "os": [
@@ -82,6 +82,8 @@ export declare function readSignal(fd: i32): i32;
82
82
  /** These two have no reading under Node: each throws, naming itself. */
83
83
  export declare function setNice(n: i32): i32;
84
84
  export declare function setIdlePriority(): i32;
85
+ /** -95: there is no affinity call under Node. */
86
+ export declare function setCpuAffinity(cpu: i32): i32;
85
87
  /** What each `nish:net` function is under the core: a throw naming `name`. */
86
88
  export declare function noNetReading(name: string): never;
87
89
  /** What `readStdin`, `waitStdin`, `setRawMode` and `setTerminalRestore` are under the core: a throw naming `name`. */
@@ -335,6 +335,15 @@ export function setIdlePriority() {
335
335
  return noSchedulingReading("setIdlePriority");
336
336
  }
337
337
 
338
+ /**
339
+ * `setCpuAffinity(cpu)` answers -95, the native build's answer on a host with
340
+ * no affinity call: unlike the two above it has a defined "not supported"
341
+ * value, so a program that pins where it can runs unpinned here.
342
+ */
343
+ export function setCpuAffinity() {
344
+ return -95;
345
+ }
346
+
338
347
  /**
339
348
  * `signalFd()` and `readSignal(fd)` have no faithful reading under Node, and
340
349
  * these say so rather than answer something else. Node delivers a signal to
package/runtime/nish.d.ts CHANGED
@@ -378,6 +378,14 @@ declare function setNice(n: i32): i32;
378
378
  * normal-priority fallback for work that must finish. Throws under Node.
379
379
  */
380
380
  declare function setIdlePriority(): i32;
381
+ /**
382
+ * Pin this process to CPU `cpu` alone (`sched_setaffinity`): 0, or a negative
383
+ * errno. Narrow-only: a CPU outside the mask the process already has (an
384
+ * operator's `taskset` or cpuset), a negative one, or one past the kernel's
385
+ * set size is -22 with no system call. -95 where the host has no affinity
386
+ * call (Darwin, and under Node).
387
+ */
388
+ declare function setCpuAffinity(cpu: i32): i32;
381
389
  /** One environment variable, or `null` when it is unset (an empty value is a set variable). */
382
390
  declare function getenv(name: string): string | null;
383
391
  /**
@@ -422,7 +430,13 @@ declare function netAddress(out: u8[], host: string, port: i32): i32;
422
430
  declare function netLocalPort(fd: i32): i32;
423
431
  /** A listening TCP socket with `SO_REUSEADDR`; `"::"` hears IPv4 and IPv6. */
424
432
  declare function tcpListen(host: string, port: i32, backlog: i32): i32;
425
- /** `tcpListen`'s socket, with `SO_REUSEPORT` for flag 1 and `TCP_NODELAY` on each accepted socket for flag 2; any other bit is -22. */
433
+ /**
434
+ * `tcpListen`'s socket, with `SO_REUSEPORT` for flag 1 and `TCP_NODELAY` on
435
+ * each accepted socket for flag 2. Flag 4, with flag 1, steers by CPU: a new
436
+ * connection goes to the group's listener whose bind order is the index of the
437
+ * CPU that received it (Linux; -95 elsewhere). Any other bit, or 4 without 1,
438
+ * is -22.
439
+ */
426
440
  declare function tcpListenFlags(host: string, port: i32, backlog: i32, flags: i32): i32;
427
441
  /** The next connection's descriptor, its address in `peer`; -11 when none waits. */
428
442
  declare function tcpAccept(fd: i32, peer: u8[]): i32;
@@ -667,6 +681,8 @@ declare module "nish:process" {
667
681
  * on a saturated machine, so a caller keeps a normal-priority fallback.
668
682
  */
669
683
  export function setIdlePriority(): i32;
684
+ /** Pin this process to CPU `cpu` alone: 0, -22 for a CPU outside the current mask (narrow-only), or -95 off Linux. */
685
+ export function setCpuAffinity(cpu: i32): i32;
670
686
  /**
671
687
  * A monotonic clock in nanoseconds. The origin is arbitrary, so only the
672
688
  * difference between two reads is meaningful.
@@ -693,7 +709,13 @@ declare module "nish:net" {
693
709
  export function netLocalPort(fd: i32): i32;
694
710
  /** A listening TCP socket with `SO_REUSEADDR`; `"::"` hears IPv4 and IPv6. */
695
711
  export function tcpListen(host: string, port: i32, backlog: i32): i32;
696
- /** `tcpListen`'s socket, with `SO_REUSEPORT` for flag 1 and `TCP_NODELAY` on each accepted socket for flag 2; any other bit is -22. */
712
+ /**
713
+ * `tcpListen`'s socket, with `SO_REUSEPORT` for flag 1 and `TCP_NODELAY` on
714
+ * each accepted socket for flag 2. Flag 4, with flag 1, steers by CPU: a new
715
+ * connection goes to the group's listener whose bind order is the index of the
716
+ * CPU that received it (Linux; -95 elsewhere). Any other bit, or 4 without 1,
717
+ * is -22.
718
+ */
697
719
  export function tcpListenFlags(host: string, port: i32, backlog: i32, flags: i32): i32;
698
720
  /** The next connection's descriptor, its address in `peer`; -11 when none waits. */
699
721
  export function tcpAccept(fd: i32, peer: u8[]): i32;
@@ -936,6 +958,30 @@ declare module "nish:secret" {
936
958
  export function wipe(target: object): void;
937
959
  }
938
960
 
961
+ // `nish:region` has source behind it, `std/region.ts`, but a program imports it
962
+ // by this name. The class has a private constructor, so `tsc` refuses
963
+ // `new Region()` as `nish` does (NL2618). What `nish` refuses about the
964
+ // functions a region runs — keeping its memory outside it, growing outer
965
+ // storage, reaching threads — is flow, which `tsc` cannot see
966
+ // (docs/LANGUAGE.md -> Regions).
967
+ declare module "nish:region" {
968
+ /** A handle to a region whose root is a `T`: 64 bits, not a pointer. */
969
+ export class Region<T> {
970
+ private constructor();
971
+ private readonly root: T;
972
+ }
973
+ /** A new region whose root is what `f` answers; everything `f` allocates is in it. */
974
+ export function region<T>(f: () => T): Region<T>;
975
+ /** `region` with one outside argument for `f`, which `f` may read but not keep. */
976
+ export function regionWith<A, T>(arg: A, f: (arg: A) => T): Region<T>;
977
+ /** `f(root)`, allocating in the region. What `f` answers is a scalar or a handle. */
978
+ export function enter<T, R>(r: Region<T>, f: (root: T) => R): R;
979
+ /** `f(root, arg)`, under `enter`'s rules; `f` may not keep or grow `arg`. */
980
+ export function enterWith<T, A, R>(r: Region<T>, arg: A, f: (root: T, arg: A) => R): R;
981
+ /** Give the region's memory back. A freed handle used again stops the program. */
982
+ export function free<T>(r: Region<T>): void;
983
+ }
984
+
939
985
  // ---- Arena (docs/LANGUAGE.md -> Arena) ---------------------------------------
940
986
 
941
987
  declare const Arena: {
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);
@@ -557,6 +577,11 @@ int32_t nish_set_nice(int32_t n);
557
577
  * negative errno; -95 (Linux's EOPNOTSUPP) elsewhere, -ENOSYS under WASI. An
558
578
  * unprivileged process cannot leave the policy again. */
559
579
  int32_t nish_set_idle_priority(void);
580
+ /* `setCpuAffinity(cpu)`: this process pinned to CPU `cpu` alone
581
+ * (`sched_setaffinity`, Linux), 0 or a negative errno. Narrow-only: a CPU not
582
+ * in the current mask, negative, or past `CPU_SETSIZE` is -22 (-EINVAL) with
583
+ * no system call. -95 (Linux's EOPNOTSUPP) elsewhere, -ENOSYS under WASI. */
584
+ int32_t nish_set_cpu_affinity(int32_t cpu);
560
585
 
561
586
  /* ---- The environment (WP19 R1) ------------------------------------------
562
587
  * `getenv(name)`: the value of environment variable `name`, copied into the
@@ -659,7 +684,10 @@ bool nish_is_executable(const nish_str *path);
659
684
  * `nish_tcp_listen(host, port, backlog)`: `SO_REUSEADDR`, and `::` hears
660
685
  * both families, or IPv4 alone on a kernel without IPv6.
661
686
  * `nish_tcp_listen_flags(host, port, backlog, flags)`: the same listener,
662
- * with `SO_REUSEPORT` for flag 1 (any other bit is -22).
687
+ * with `SO_REUSEPORT` for flag 1, `TCP_NODELAY` on each accepted socket for
688
+ * flag 2, and, for flag 4 with flag 1, a reuseport program steering each new
689
+ * connection to the listener bound at the index of the CPU that received it
690
+ * (Linux; -95 elsewhere). Any other bit, or 4 without 1, is -22.
663
691
  * `nish_tcp_accept(fd, peer)`: a new descriptor and the peer's address.
664
692
  * `nish_net_read` / `nish_net_write(fd, buf, off, len)`: bytes `[off, off +
665
693
  * len)` of `buf`, which the compiled call has range-checked; a write to a
package/runtime/nish.mjs CHANGED
@@ -121,6 +121,9 @@ for (const name of ["setNice", "setIdlePriority"]) {
121
121
  throw new Error(`\`${name}\` has no reading under Node: it changes how the kernel schedules the native program`);
122
122
  });
123
123
  }
124
+ // `setCpuAffinity` answers -95, as on Darwin: Node has no affinity call, and
125
+ // "not supported" is a value a caller already branches on.
126
+ provide("setCpuAffinity", shim.setCpuAffinity);
124
127
  provide("getenv", shim.getenv);
125
128
  provide("statMtimeSync", shim.statMtimeSync);
126
129
  provide("signalFd", shim.signalFd);
@@ -328,11 +331,41 @@ provide("arena", shim.arena);
328
331
  // zero-fills it. `nish:unsafe` and `nish:term` are each answered by a module
329
332
  // that hands back the globals installed above: its functions are globals of this module either
330
333
  // way, and the import is what the compiler requires a program to write.
334
+ // `nish:region` is answered by a module of its own, below.
331
335
  // `nish:simd` is `runtime/core.mjs`, whose vectors are typed arrays made once
332
336
  // and never written after, so `--emit-ts` output and a program run as it was
333
337
  // written share one implementation.
334
338
  const UNSAFE_EXPORTS = ["uncheckedGet", "uncheckedSet", "wrappingAdd", "wrappingSub", "wrappingMul"];
335
339
  const unsafeSource = UNSAFE_EXPORTS.map((name) => `export const ${name} = globalThis.${name};`).join("\n");
340
+ // `nish:region` has no allocator to swap under Node: a region is an entry in a
341
+ // table, keyed by its handle (a BigInt, as every `i64` is here), `enter` runs
342
+ // its function on the root, and `free` drops the entry. The handle checks the
343
+ // native runtime makes are made here too, so a freed handle or a second
344
+ // `enter` throws where the native program stops.
345
+ const regionSource = `
346
+ const roots = new Map();
347
+ const entered = new Set();
348
+ let next = 0n;
349
+ const rootOf = (r) => {
350
+ if (!roots.has(r)) throw new Error("nish:region: the region is freed or was never made");
351
+ if (entered.has(r)) throw new Error("nish:region: the region is entered already");
352
+ return roots.get(r);
353
+ };
354
+ const run = (r, f, arg, withArg) => {
355
+ const root = rootOf(r);
356
+ entered.add(r);
357
+ try { return withArg ? f(root, arg) : f(root); } finally { entered.delete(r); }
358
+ };
359
+ export class Region {}
360
+ export const region = (f) => { next += 1n; roots.set(next, f()); return next; };
361
+ export const regionWith = (arg, f) => { next += 1n; roots.set(next, f(arg)); return next; };
362
+ export const enter = (r, f) => run(r, f, undefined, false);
363
+ export const enterWith = (r, arg, f) => run(r, f, arg, true);
364
+ export const free = (r) => {
365
+ if (entered.has(r)) throw new Error("nish:region: the region is freed while it is entered");
366
+ if (!roots.delete(r)) throw new Error("nish:region: the region is freed or was never made");
367
+ };
368
+ `;
336
369
  const TERM_EXPORTS = ["readStdin", "waitStdin", "isTerminal", "terminalSize", "setRawMode", "setTerminalRestore"];
337
370
  const termSource = TERM_EXPORTS.map((name) => `export const ${name} = globalThis.${name};`).join("\n");
338
371
  registerHooks({
@@ -346,6 +379,9 @@ registerHooks({
346
379
  if (specifier === "nish:simd") {
347
380
  return next(new URL("./core.mjs", import.meta.url).href, context);
348
381
  }
382
+ if (specifier === "nish:region") {
383
+ return { url: `data:text/javascript,${encodeURIComponent(regionSource)}`, shortCircuit: true };
384
+ }
349
385
  if (specifier === "nish:term") {
350
386
  return { url: `data:text/javascript,${encodeURIComponent(termSource)}`, shortCircuit: true };
351
387
  }
@@ -73,6 +73,7 @@ typedef int nish_net_unused;
73
73
  #include <sys/un.h>
74
74
  #include <unistd.h>
75
75
  #if defined(__linux__)
76
+ #include <linux/filter.h>
76
77
  #include <netinet/udp.h>
77
78
  #include <sys/epoll.h>
78
79
  /* glibc before 2.29 names neither; the numbers are the kernel's ABI. */
@@ -82,6 +83,10 @@ typedef int nish_net_unused;
82
83
  #ifndef UDP_GRO
83
84
  #define UDP_GRO 104
84
85
  #endif
86
+ /* Linux 4.5's; a header older than the kernel may not name it. */
87
+ #ifndef SO_ATTACH_REUSEPORT_CBPF
88
+ #define SO_ATTACH_REUSEPORT_CBPF 51
89
+ #endif
85
90
  #else
86
91
  #include <sys/event.h>
87
92
  #include <sys/time.h>
@@ -243,7 +248,23 @@ static int32_t nish_net_bound(const nish_str *host, int32_t port, int type, int3
243
248
  }
244
249
  nish_sockaddr s;
245
250
  socklen_t n = nish_net_sockaddr(&s, a, port, v4);
246
- if (bad != 0 || bind(fd, &s.sa, n) != 0 || (type == SOCK_STREAM && listen(fd, arg) != 0)) {
251
+ #if defined(__linux__)
252
+ /* `tcpListenFlags` flag 4's program, two instructions, the kernel
253
+ selftest's: load the receiving CPU, return it. It is the reuseport
254
+ group's, so attaching it from every listener installs the same program
255
+ each time, and it goes on after `listen`, once the socket is in its
256
+ group. Only a stream listener's `opts` can hold 4. */
257
+ static struct sock_filter cpu[] = {
258
+ {BPF_LD | BPF_W | BPF_ABS, 0, 0, (uint32_t)(SKF_AD_OFF + SKF_AD_CPU)},
259
+ {BPF_RET | BPF_A, 0, 0, 0},
260
+ };
261
+ static const struct sock_fprog steer = {2, cpu};
262
+ #endif
263
+ if (bad != 0 || bind(fd, &s.sa, n) != 0 || (type == SOCK_STREAM && listen(fd, arg) != 0)
264
+ #if defined(__linux__)
265
+ || ((opts & 4) && setsockopt(fd, SOL_SOCKET, SO_ATTACH_REUSEPORT_CBPF, &steer, sizeof steer) != 0)
266
+ #endif
267
+ ) {
247
268
  int e = errno;
248
269
  close(fd);
249
270
  return nish_net_err(e);
@@ -262,9 +283,17 @@ int32_t nish_tcp_listen(const nish_str *host, int32_t port, int32_t backlog) {
262
283
  `SO_REUSEPORT` for flag 1, so several listeners of one uid share the port
263
284
  (Linux spreads new connections across them; Darwin hands them all to one),
264
285
  and `TCP_NODELAY` for flag 2, which the listener carries so that
265
- `tcpAccept` sets it on each connection it takes. Any other bit is -22. */
286
+ `tcpAccept` sets it on each connection it takes. Flag 4, which needs flag
287
+ 1, steers by CPU: a reuseport program (`SO_ATTACH_REUSEPORT_CBPF`) hands a
288
+ new connection to the group's listener whose index is the CPU that received
289
+ it, the index being the order the group's sockets were bound in, and the
290
+ kernel falls back to its hash when there is no such listener. Linux alone
291
+ has it; elsewhere flag 4 is -95. Any other bit, or 4 without 1, is -22. */
266
292
  int32_t nish_tcp_listen_flags(const nish_str *host, int32_t port, int32_t backlog, int32_t flags) {
267
- if ((uint32_t)flags > 3) return -22;
293
+ if ((uint32_t)flags > 7 || (flags & 5) == 4) return -22;
294
+ #if !defined(__linux__)
295
+ if (flags & 4) return -95;
296
+ #endif
268
297
  return nish_net_bound(host, port, SOCK_STREAM, backlog, flags);
269
298
  }
270
299
 
@@ -26,7 +26,8 @@
26
26
  * from drifting from the contract it is published under.
27
27
  */
28
28
  #if defined(__linux__) && !defined(_GNU_SOURCE)
29
- #define _GNU_SOURCE /* `sched_getaffinity` and `CPU_COUNT`, for `nish_cpu_online` */
29
+ #define _GNU_SOURCE /* `sched_getaffinity`, `sched_setaffinity` and `CPU_COUNT`, for
30
+ `nish_cpu_online` and `nish_set_cpu_affinity` */
30
31
  #endif
31
32
  #define _POSIX_C_SOURCE 200809L
32
33
  /* `realpath` is XSI rather than POSIX base, so _POSIX_C_SOURCE alone does not
@@ -860,6 +861,37 @@ int32_t nish_set_idle_priority(void) {
860
861
  #endif
861
862
  }
862
863
 
864
+ /* `setCpuAffinity(cpu)`: this process pinned to CPU `cpu` alone, 0 or a
865
+ negative errno. Narrow-only: `cpu` must be in the mask the process already
866
+ has (`sched_getaffinity`), so a pin never escapes an operator's `taskset` or
867
+ cpuset; a CPU outside it, a negative one, or one past `CPU_SETSIZE` is
868
+ -EINVAL with no `sched_setaffinity` call. Linux alone has the call; Darwin
869
+ answers -95 as `setIdlePriority` does, WASI -ENOSYS. Like the nice value,
870
+ the mask is the calling thread's and every thread started after it. */
871
+ int32_t nish_set_cpu_affinity(int32_t cpu) {
872
+ #if defined(__linux__)
873
+ /* The mask as words: the set to pin is the word holding `cpu` with only its
874
+ bit, the words below it zero, and the kernel zero-fills a shorter mask.
875
+ `sched_getaffinity` fails only on a bad pid or size, neither of which
876
+ this call can pass, so its failure shares the range check's answer. */
877
+ unsigned long m[CPU_SETSIZE / (8 * sizeof(unsigned long))];
878
+ uint32_t w = (uint32_t)cpu / (8 * sizeof m[0]);
879
+ unsigned long bit = 1UL << ((uint32_t)cpu % (8 * sizeof m[0]));
880
+ if ((uint32_t)cpu >= CPU_SETSIZE || sched_getaffinity(0, sizeof m, (cpu_set_t *)m) != 0 ||
881
+ !(m[w] & bit))
882
+ return -EINVAL;
883
+ for (uint32_t i = 0; i < w; i++) m[i] = 0;
884
+ m[w] = bit;
885
+ return sched_setaffinity(0, (w + 1) * sizeof m[0], (cpu_set_t *)m) == 0 ? 0 : -errno;
886
+ #elif defined(__wasi__)
887
+ (void)cpu;
888
+ return -ENOSYS;
889
+ #else
890
+ (void)cpu;
891
+ return -95;
892
+ #endif
893
+ }
894
+
863
895
  /* `readdirSync(path)`: the listing a driver needs to discover its own inputs.
864
896
 
865
897
  **Sorted by bytes**, which `readdir(3)` and Node are not, for two reasons