@amritk/nish-x86_64-linux 0.30.0 → 0.32.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/bin/nish CHANGED
Binary file
package/docs/AI.md CHANGED
@@ -49,7 +49,7 @@ stdout, nothing on stderr, every field 1-based with `endLine`/`endColumn`
49
49
  exclusive:
50
50
 
51
51
  ```json
52
- {"file":"p.ts","line":3,"column":17,"endLine":3,"endColumn":20,"severity":"error","code":"NL2249","message":"Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill)"}
52
+ {"file":"p.ts","line":3,"column":17,"endLine":3,"endColumn":20,"severity":"error","code":"NL2249","message":"Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill, sort)"}
53
53
  ```
54
54
 
55
55
  - **Key on `code`, never on `message`.** A code is a promise: `NL2249` means
@@ -159,7 +159,7 @@ rejects. This table is the highest-value part of the page.
159
159
  | `x == y` | `Loose equality is forbidden` | `x === y` |
160
160
  | `throw new Error(m)` | `` `throw` is forbidden `` | `return Err(m)`, or `panic(m)` to end the process |
161
161
  | `try { … } catch { … }` | `` `try`/`catch`/`finally` is forbidden `` | `Result<T, E>` and `isErr()` |
162
- | `xs.map(f)`, `.filter`, `.reduce`, `.forEach`, `.slice`, `.sort`, `.shift` | `` Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill) `` | a `for` loop, or a top-level `map(xs, f)` with a [function parameter](#function-parameters) |
162
+ | `xs.map(f)`, `.filter`, `.reduce`, `.forEach`, `.slice`, `.shift` | `` Unknown method `map` on i32[] (supported: push, pop, indexOf, join, set, fill, sort) `` | a `for` loop, or a top-level `map(xs, f)` with a [function parameter](#function-parameters) |
163
163
  | `s.toUpperCase()`, `s.split()`, `s.trim()`, `s.replace()` | `` Unknown method … on string `` | index bytes with `charCodeAt` / `substring` |
164
164
  | `a?.b`, `a ?? b` | `?.` only in a chain that ends in `??`; `??` only on a `T \| null`, a chain or `m.get(k)` | `a?.b ?? d`, `a ?? d`, or `if (a !== null)` first |
165
165
  | `x as T`, `<T>x`, `x satisfies T` | `Unsupported expression in Phase 1: AsExpression` (to `any` or `unknown`: `` Type assertion to `any` is forbidden ``); `x!` is a syntax error | there are no casts; `implements` is the only widening |
@@ -852,7 +852,8 @@ export const main = (): i32 => {
852
852
  (`` The arrow reads `k`, a `let` of the function it is written in: ... bind the value to a `const` before the call ``),
853
853
  read `this` (bind `const self = this`), or read a function parameter of its
854
854
  function. An arrow inside another arrow, or given to a method, a
855
- constructor, `expose` or `exposeWith`, captures nothing.
855
+ constructor, `expose` or `exposeWith`, captures nothing; `xs.sort(cmp)`'s
856
+ comparator is the exception, and captures as an arrow argument does.
856
857
  - **The argument's type must match exactly**: `(x: i32) => i64` for a
857
858
  `(x: i32) => i32` parameter is refused. A generic function cannot be passed
858
859
  by name — write `(x) => identity(x)`.
@@ -2138,7 +2139,12 @@ there is no `undefined` to return), `a.indexOf(v)`, `a.join(sep)` — **`join` i
2138
2139
  `string[]` only** — and, on an array of numbers only, `dst.set(src[, offset])`
2139
2140
  (one `memmove`; a range past the end panics) and `a.fill(v[, start[, end]])`
2140
2141
  (ends clamped and counted back from the end when negative, as in JavaScript;
2141
- never panics). Both are statements. That is every array method there is. A
2142
+ never panics). Both are statements. `a.sort(cmp)` sorts in place, stably, and
2143
+ answers `a` — the same order Node gives; the comparator is required (`a.sort()`
2144
+ is refused, since JavaScript would compare strings), a readonly array and
2145
+ `process.argv` are refused, `a - b` panics on `i32` values a full range apart
2146
+ (compare with `a > b ? 1 : a < b ? -1 : 0` there), and the comparator may read the caller's `const` locals and parameters but not
2147
+ a `let`, as any arrow argument may. That is every array method there is. A
2142
2148
  window into a buffer is `(buf, offset, length)`; there is no view type.
2143
2149
 
2144
2150
  **Strings.** `s.length` (**bytes**), `s.charCodeAt(i)` (the byte, bounds-checked),
@@ -2222,6 +2228,13 @@ system call either way. `setIdlePriority()` moves the program to Linux's `SCHED_
2222
2228
  `main`, never in a task. An unprivileged process can never leave `SCHED_IDLE`
2223
2229
  and an idle one can starve on a saturated machine, so keep a normal-priority
2224
2230
  fallback. Neither exists on wasm; both throw under Node.
2231
+ `setCpuAffinity(cpu)` pins the process to one CPU, narrow-only: a CPU outside
2232
+ the mask it already has is `-22`; `-95` on Darwin and under Node, refused on
2233
+ wasm. `nish/net/prefork`'s `PreforkConfig` takes `pin = true` to pin worker
2234
+ `k` to CPU `k` (more workers than CPUs panics), and `listenPort` (with
2235
+ `listenHost`, `listenBacklog`, `listenFlags` 0 or 2) to have the supervisor
2236
+ bind one `SO_REUSEPORT` listener per worker, steered on Linux by the receiving
2237
+ CPU when pinned; a worker takes its own with `preforkListener(process.argv)`.
2225
2238
 
2226
2239
  **Sockets** (`nish:net`, or the same names as globals). `tcpListen(host, port,
2227
2240
  backlog)`, `tcpAccept(fd, peer)`, `netRead(fd, buf, off, len)`,
@@ -2236,10 +2249,10 @@ wait for it to be writable, then `connectResult(fd)` is 0 or the failure. Buffer
2236
2249
  are `u8[]` only; an address is 18 bytes of one (IPv4 as `::ffff:a.b.c.d`, then
2237
2250
  the port big-endian); hosts are numeric literals; to reach a name, resolve it first with
2238
2251
  `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
2252
+ To fetch a URL from a CLI, skip all of this: `nish/net/fetch`'s
2253
+ `fetchSync(new HttpRequest("GET", url), new HttpOptions())` blocks until the
2241
2254
  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`).
2255
+ failure (a `FetchSyncClient` keeps the connection and says why in `error`).
2243
2256
  `netRead` / `netWrite` panic on a range outside the buffer.
2244
2257
  `tcpListenFlags(host, port, backlog, flags)` is `tcpListen` with flag 1
2245
2258
  `SO_REUSEPORT`, so several listeners, in one process or many, share a port (Linux
@@ -2596,6 +2609,46 @@ returns. To give memory back sooner, scope it with `using a = arena()`;
2596
2609
  `Arena.release` and `Arena.reset` are gone, and a call to either is an unknown
2597
2610
  builtin (NL2244).
2598
2611
 
2612
+ ### Regions: `nish:region`
2613
+
2614
+ **For memory a long-running program frees itself — per connection, per cache
2615
+ entry — use a region.** `region(f)` / `regionWith(arg, f)` make one and run
2616
+ `f` in it, and what `f` answers is the root; `enter(r, f)` /
2617
+ `enterWith(r, arg, f)` run `f(root)` / `f(root, arg)` in it again; `free(r)`
2618
+ gives all of it back. `f` is a named function or an arrow that captures
2619
+ nothing. `Region<T>` is a number, not a pointer.
2620
+
2621
+ ```ts nish:ok
2622
+ import { Region, enter, free, region } from "nish:region";
2623
+
2624
+ class Session {
2625
+ hits: i32 = 0;
2626
+ }
2627
+
2628
+ const open = (): Session => new Session();
2629
+ const touch = (s: Session): i32 => {
2630
+ s.hits = s.hits + 1;
2631
+ return s.hits;
2632
+ };
2633
+
2634
+ export const main = (): i32 => {
2635
+ const r: Region<Session> = region(open);
2636
+ console.log(enter(r, touch));
2637
+ free(r);
2638
+ return 0;
2639
+ };
2640
+ ```
2641
+
2642
+ The checker refuses every way region memory could outlive `free`, and every way
2643
+ outer memory could be pointed at from inside: only numbers, booleans, enums and
2644
+ `Region` handles cross. `enter` answers a scalar or a handle (NL2611), `f` may
2645
+ not store region memory into `arg` or other outer memory (NL2609) nor outer
2646
+ memory into the region (NL2610), nor `push`/`set`/`add` to an outer collection
2647
+ (NL2615). A handle stays on its thread (NL2612, NL2613, NL2351), is never a
2648
+ `using` (NL2387) and is never `new Region` (NL2618). A bad handle, a double
2649
+ `free` or a nested `enter` of one region stops the program. `--emit-ts` refuses
2650
+ the module (NL4015).
2651
+
2599
2652
  ## Recipes for what is missing
2600
2653
 
2601
2654
  **Iterate and transform** — there is no `map`, and no function to pass it:
@@ -2705,7 +2758,12 @@ parameter. `paramIntArray(values, start, count)` binds an `i64[]` window;
2705
2758
  element, then `arrayEnd()`, builds a `text[]` straight into the Bind from
2706
2759
  windows of a buffer. An element refused for room answers `false` and leaves
2707
2760
  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
2761
+ `param...` call and `bindEnd()` panic until `arrayEnd()`. For a long
2762
+ `int4[]` or `int8[]`, `paramInt4Array` / `paramInt8Array` (same window) or
2763
+ `binaryArrayStart(23 or 20)`, `binaryArrayInt` / `binaryArrayNull`,
2764
+ `arrayEnd()` send it in binary, at a tenth of the client CPU; the element type
2765
+ must be the statement's (`int4[]` to an `int8[]` parameter is SQLSTATE
2766
+ `42804`), and an `int4` element out of range panics. (No block, as for
2709
2767
  `nish/text`; `tests/postgres/live` runs it against a server, and the user
2710
2768
  guide's [PostgreSQL page](guide/using-nish/postgres.md#array-parameters) its
2711
2769
  walk-through.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish-x86_64-linux",
3
- "version": "0.30.0",
3
+ "version": "0.32.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
@@ -577,6 +577,11 @@ int32_t nish_set_nice(int32_t n);
577
577
  * negative errno; -95 (Linux's EOPNOTSUPP) elsewhere, -ENOSYS under WASI. An
578
578
  * unprivileged process cannot leave the policy again. */
579
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);
580
585
 
581
586
  /* ---- The environment (WP19 R1) ------------------------------------------
582
587
  * `getenv(name)`: the value of environment variable `name`, copied into the
@@ -679,7 +684,10 @@ bool nish_is_executable(const nish_str *path);
679
684
  * `nish_tcp_listen(host, port, backlog)`: `SO_REUSEADDR`, and `::` hears
680
685
  * both families, or IPv4 alone on a kernel without IPv6.
681
686
  * `nish_tcp_listen_flags(host, port, backlog, flags)`: the same listener,
682
- * 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.
683
691
  * `nish_tcp_accept(fd, peer)`: a new descriptor and the peer's address.
684
692
  * `nish_net_read` / `nish_net_write(fd, buf, off, len)`: bytes `[off, off +
685
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
@@ -173,14 +173,26 @@ uint64_t nish_region_new(void) {
173
173
  s = &nish_region_pages[index / NISH_REGION_PAGE][index % NISH_REGION_PAGE];
174
174
  nish_region_free_list = s->outer;
175
175
  } else {
176
- if (nish_region_used == NISH_REGION_SLOTS) nish_region_die(NISH_REGION_FULL);
177
- index = nish_region_used++;
176
+ /* Each panic lets go of the lock first: an atexit handler, or another
177
+ thread while the exit is on its way, may still make or free a region
178
+ (docs/security/runtime.md, RT-46). */
179
+ if (nish_region_used == NISH_REGION_SLOTS) {
180
+ nish_region_unlock();
181
+ nish_region_die(NISH_REGION_FULL);
182
+ }
183
+ /* The slot is counted once its page is there: a failed page leaves the
184
+ count as it was (RT-46). */
185
+ index = nish_region_used;
178
186
  nish_region_slot **page = &nish_region_pages[index / NISH_REGION_PAGE];
179
187
  if (!*page) {
180
188
  nish_region_slot *p = calloc(NISH_REGION_PAGE, sizeof *p);
181
- if (!p) nish_region_die(NISH_REGION_OOM);
189
+ if (!p) {
190
+ nish_region_unlock();
191
+ nish_region_die(NISH_REGION_OOM);
192
+ }
182
193
  __atomic_store_n(page, p, __ATOMIC_RELEASE);
183
194
  }
195
+ nish_region_used = index + 1;
184
196
  s = &(*page)[index % NISH_REGION_PAGE];
185
197
  __atomic_store_n(&s->gen, 1, __ATOMIC_RELAXED);
186
198
  }
package/runtime/runtime.c CHANGED
@@ -180,8 +180,15 @@ static NISH_COLD void nish_oom(void) { nish_die("nish: out of memory\n"); }
180
180
  this file still links alone. */
181
181
  NISH_TLS uint64_t nish_region_chunk;
182
182
 
183
+ /* The most a region's next chunk grows to: past it the chunks stop doubling,
184
+ so a region that keeps allocating never asks malloc for one block of
185
+ gigabytes when smaller ones would do (docs/security/runtime.md, RT-47). A
186
+ single allocation larger than this still gets a chunk of its own size. */
187
+ #define NISH_REGION_CHUNK_MAX ((uint64_t)1 << 20)
188
+
183
189
  /* 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. */
190
+ next size), bump from it. A region's chunks double from its first, up to
191
+ NISH_REGION_CHUNK_MAX. */
185
192
  void *nish_arena_grow(uint64_t size) {
186
193
  #ifdef NISH_ASAN
187
194
  /* An exact chunk leaves off == cap, so the inline bump never fits again and
@@ -191,7 +198,7 @@ void *nish_arena_grow(uint64_t size) {
191
198
  uint64_t next = nish_region_chunk;
192
199
  uint64_t floor = next ? next : 65536;
193
200
  uint64_t cap = size > floor ? size : floor;
194
- if (next) nish_region_chunk = cap * 2;
201
+ if (next) nish_region_chunk = cap < NISH_REGION_CHUNK_MAX / 2 ? cap * 2 : NISH_REGION_CHUNK_MAX;
195
202
  #endif
196
203
  /* A 64-bit request cannot always be asked for: wasm32's `size_t` is 32 bits,
197
204
  and malloc would silently take the low half of a chunk nobody can address. */