@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.
@@ -51,6 +51,26 @@
51
51
  * gone longer than that. With `restart` off nothing is signalled; a worker sees
52
52
  * its peer's exit as a hang-up.
53
53
  *
54
+ * **Listeners and pinning.** With `listenPort` set (0 or above), the
55
+ * supervisor binds one `SO_REUSEPORT` listener per worker itself, in index
56
+ * order, keeps them for its whole run and hands listener `k` to every start of
57
+ * worker `k` after the channel ends; `preforkListener(args)` answers it in the
58
+ * worker (`--nish-prefork-listener=`, before the channels flag), so a worker
59
+ * serves that descriptor instead of binding its own. Port 0 binds the first
60
+ * listener anywhere and the rest to its port. With `pin` on as well, each
61
+ * listener steers by CPU (`tcpListenFlags`' flag 4): the kernel hands a
62
+ * connection that arrived on CPU `k` to listener `k`, and a worker that is
63
+ * restarted takes the same listener, so the order holds across restarts,
64
+ * which it would not if each worker bound its own. `pin` alone makes each
65
+ * worker call `setCpuAffinity(k)` before `preforkWith` answers it, so worker
66
+ * `k` runs on CPU `k` only; a worker the kernel refuses (CPU `k` outside the
67
+ * process's mask) says so on stderr and runs unpinned. More workers than
68
+ * `availableParallelism()` is refused with a panic, since a worker past the
69
+ * last CPU would be pinned nowhere and, steered, handed nothing. Pinning and
70
+ * steering are Linux's: elsewhere the supervisor says once that the workers
71
+ * run unpinned, and the listeners are plain `SO_REUSEPORT` ones. Both are off
72
+ * by default.
73
+ *
54
74
  * **How many.** `NISH_WORKERS` when it is a positive whole number, else
55
75
  * `PreforkConfig.workers` when that is positive, else
56
76
  * `availableParallelism()` on Linux and 1 elsewhere: Darwin shares a
@@ -83,10 +103,12 @@ import {
83
103
  NET_UNIX_SEQPACKET,
84
104
  NET_UNIX_STREAM,
85
105
  netClose,
106
+ netLocalPort,
86
107
  netRead,
87
108
  pollAdd,
88
109
  pollCreate,
89
110
  pollWait,
111
+ tcpListenFlags,
90
112
  unixSocketPair,
91
113
  } from "nish:net"
92
114
  import {
@@ -96,6 +118,7 @@ import {
96
118
  monotonicNanos,
97
119
  platform,
98
120
  readSignal,
121
+ setCpuAffinity,
99
122
  signalFd,
100
123
  spawn,
101
124
  spawnWithFds,
@@ -111,6 +134,12 @@ export const PREFORK_WORKER_FLAG: string = "--nish-prefork-worker="
111
134
  * for none.
112
135
  */
113
136
  export const PREFORK_CHANNELS_FLAG: string = "--nish-prefork-channels="
137
+ /**
138
+ * The argument before the channels flag, or before the worker flag when there
139
+ * is none, when the supervisor holds the listeners: the worker's listener's
140
+ * descriptor.
141
+ */
142
+ export const PREFORK_LISTENER_FLAG: string = "--nish-prefork-listener="
114
143
 
115
144
  /** What `preforkWith` answers in the supervisor once its workers are gone. */
116
145
  export const PREFORK_STOPPED: i32 = -1
@@ -127,6 +156,8 @@ export const PREFORK_WORKERS_EXITED: i32 = -3
127
156
 
128
157
  /** The supervisor's numbers; every one is checked by `preforkWith`. */
129
158
  export class PreforkConfig {
159
+ /** The address the listeners of `listenPort` bind. */
160
+ listenHost: string = "0.0.0.0"
130
161
  /** Workers to start; 0 takes `NISH_WORKERS` or the platform's default. */
131
162
  workers: i32 = 0
132
163
  /** How often, in milliseconds, the supervisor reaps when nothing wakes it. */
@@ -139,6 +170,23 @@ export class PreforkConfig {
139
170
  quickExitMs: i32 = 1000
140
171
  /** How long the workers get to exit after SIGTERM before SIGKILL. */
141
172
  graceMs: i32 = 5000
173
+ /** The worker every other worker gets a socketpair to; -1 for none. */
174
+ serviceWorker: i32 = -1
175
+ /**
176
+ * What every channel is: `NET_UNIX_STREAM` (1), or `NET_UNIX_SEQPACKET` (2),
177
+ * which keeps each write one message but only Linux has. Stream is the
178
+ * default because it works on Darwin too.
179
+ */
180
+ channelKind: i32 = 1
181
+ /**
182
+ * The port the supervisor binds one listener per worker on, 0 for any; -1,
183
+ * the default, leaves each worker to bind its own.
184
+ */
185
+ listenPort: i32 = -1
186
+ /** Their backlog. */
187
+ listenBacklog: i32 = 1024
188
+ /** Their `tcpListenFlags` beyond `SO_REUSEPORT`, which is always set: 0, or 2 for `TCP_NODELAY`. */
189
+ listenFlags: i32 = 0
142
190
  /**
143
191
  * Start a worker that exited again. Off, a worker that exits stays gone,
144
192
  * and the supervisor returns once none is left.
@@ -146,14 +194,14 @@ export class PreforkConfig {
146
194
  restart: boolean = true
147
195
  /** Give each worker a socketpair to the supervisor (`preforkSupervise`). */
148
196
  supervisorChannels: boolean = false
149
- /** The worker every other worker gets a socketpair to; -1 for none. */
150
- serviceWorker: i32 = -1
151
197
  /**
152
- * What every channel is: `NET_UNIX_STREAM` (1), or `NET_UNIX_SEQPACKET` (2),
153
- * which keeps each write one message but only Linux has. Stream is the
154
- * default because it works on Darwin too.
198
+ * Pin worker `k` to CPU `k` (`setCpuAffinity`), and with `listenPort`, steer
199
+ * each connection to the listener of the CPU that received it. Linux only;
200
+ * elsewhere the workers run unpinned. `k` is the CPU number, not the `k`-th
201
+ * allowed CPU: under a mask that skips CPU `k`, worker `k` runs unpinned and
202
+ * a connection received past the listeners falls back to the kernel's hash.
155
203
  */
156
- channelKind: i32 = 1
204
+ pin: boolean = false
157
205
  }
158
206
 
159
207
  /** True when `s` is one or more ASCII digits and nothing else. */
@@ -206,8 +254,8 @@ export const preforkIndex = (args: string[]): i32 => {
206
254
  }
207
255
 
208
256
  /**
209
- * `args` (`process.argv`) without the worker flag and the channels flag before
210
- * it, the same in every process.
257
+ * `args` (`process.argv`) without the worker flag and the channels and
258
+ * listener flags before it, the same in every process.
211
259
  */
212
260
  export const preforkArgs = (args: string[]): string[] => {
213
261
  let keep: i32 = toI32(args.length)
@@ -216,6 +264,9 @@ export const preforkArgs = (args: string[]): string[] => {
216
264
  if (keep > 1 && args[keep - 1].startsWith(PREFORK_CHANNELS_FLAG)) {
217
265
  keep = keep - 1
218
266
  }
267
+ if (keep > 1 && args[keep - 1].startsWith(PREFORK_LISTENER_FLAG)) {
268
+ keep = keep - 1
269
+ }
219
270
  }
220
271
  const out: string[] = []
221
272
  for (let i: i32 = 0; i < toI32(args.length); i++) {
@@ -258,6 +309,21 @@ export const preforkServiceChannel = (args: string[]): i32 => preforkChannel(arg
258
309
  export const preforkServiceChannelOf = (args: string[], index: i32): i32 =>
259
310
  index < 0 || index > 2147483645 ? -1 : preforkChannel(args, 2 + index)
260
311
 
312
+ /** In a worker, the listener the supervisor bound for it (`listenPort`); -1 when none. */
313
+ export const preforkListener = (args: string[]): i32 => {
314
+ if (preforkIndex(args) < 0) {
315
+ return -1
316
+ }
317
+ for (let back: i32 = 2; back <= 3 && back < toI32(args.length); back++) {
318
+ const flag: string = args[toI32(args.length) - back]
319
+ if (flag.startsWith(PREFORK_LISTENER_FLAG)) {
320
+ const digits: string = flag.substring(PREFORK_LISTENER_FLAG.length)
321
+ return preforkDigits(digits) ? parseInt(digits) : -1
322
+ }
323
+ }
324
+ return -1
325
+ }
326
+
261
327
  /** Milliseconds on the monotonic clock. */
262
328
  const preforkNowMs = (): i64 => monotonicNanos() / 1000000
263
329
 
@@ -298,6 +364,8 @@ class PreforkSupervisor {
298
364
  service: i32
299
365
  /** The group is being stopped, to be started again once none of it is left. */
300
366
  cascading: boolean = false
367
+ /** Worker i's listener at i, held for the whole run; empty without `listenPort`. */
368
+ listeners: i32[]
301
369
 
302
370
  constructor(config: PreforkConfig, args: string[], count: i32, loop: i32) {
303
371
  this.config = config
@@ -305,6 +373,7 @@ class PreforkSupervisor {
305
373
  this.service = config.serviceWorker
306
374
  this.toService = []
307
375
  this.fromWorkers = []
376
+ this.listeners = []
308
377
  this.args = preforkArgs(args)
309
378
  const self: string | null = platform === "linux" ? realpathSync("/proc/self/exe") : null
310
379
  this.program = self !== null ? self : this.args[0]
@@ -359,7 +428,12 @@ class PreforkSupervisor {
359
428
  for (let i: i32 = 1; i < toI32(this.args.length); i++) {
360
429
  argv.push(this.args[i])
361
430
  }
362
- if (fds.length > 0) {
431
+ const channels: boolean = fds.length > 0
432
+ if (this.listeners.length > 0) {
433
+ fds.push(this.listeners[index])
434
+ argv.push(`${PREFORK_LISTENER_FLAG}${2 + fds.length}`)
435
+ }
436
+ if (channels) {
363
437
  argv.push(`${PREFORK_CHANNELS_FLAG}${fields.join(",")}`)
364
438
  }
365
439
  argv.push(`${PREFORK_WORKER_FLAG}${index}`)
@@ -576,6 +650,37 @@ class PreforkSupervisor {
576
650
  return true
577
651
  }
578
652
 
653
+ /**
654
+ * One listener per worker, in index order, which is the order steering
655
+ * counts CPUs in; false, with the ones made closed, when one cannot bind.
656
+ */
657
+ listen(steer: boolean): boolean {
658
+ const config: PreforkConfig = this.config
659
+ const flags: i32 = config.listenFlags | 1 | (steer ? 4 : 0)
660
+ let port: i32 = config.listenPort
661
+ for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
662
+ const fd: i32 = tcpListenFlags(config.listenHost, port, config.listenBacklog, flags)
663
+ if (fd < 0) {
664
+ console.error(`nish/net/prefork: cannot listen on ${config.listenHost}:${port} (${fd})`)
665
+ this.closeListeners()
666
+ return false
667
+ }
668
+ this.listeners.push(fd)
669
+ if (port === 0) {
670
+ port = netLocalPort(fd)
671
+ }
672
+ }
673
+ return true
674
+ }
675
+
676
+ /** Close the listeners `listen` made. */
677
+ closeListeners(): void {
678
+ for (let i: i32 = 0; i < toI32(this.listeners.length); i++) {
679
+ netClose(this.listeners[i])
680
+ }
681
+ this.listeners = []
682
+ }
683
+
579
684
  /** Send `sig` to every running worker. */
580
685
  signal(sig: i32): void {
581
686
  for (let i: i32 = 0; i < toI32(this.slots.length); i++) {
@@ -644,6 +749,15 @@ const preforkInvalid = (config: PreforkConfig): string => {
644
749
  if (config.serviceWorker < -1) {
645
750
  return "serviceWorker below -1"
646
751
  }
752
+ if (config.listenPort < -1 || config.listenPort > 65535) {
753
+ return "listenPort not -1 or 0..65535"
754
+ }
755
+ if (config.listenBacklog <= 0) {
756
+ return "listenBacklog not above 0"
757
+ }
758
+ if (config.listenFlags !== 0 && config.listenFlags !== 2) {
759
+ return "listenFlags not 0 or 2"
760
+ }
647
761
  return ""
648
762
  }
649
763
 
@@ -679,8 +793,11 @@ export const preforkWith = (args: string[], config: PreforkConfig): i32 => {
679
793
  * supervisor closes it after a hang-up, when the worker is reaped and when it
680
794
  * stops, and a restarted worker comes with a new one. Panics, besides, when
681
795
  * `serviceWorker` is not below the worker count or the count is above 64 with
682
- * a service worker, the most `spawnWithFds` hands one child; and, through
683
- * `preforkInvalid`, on seqpacket channels off Linux.
796
+ * a service worker (63 with `listenPort`), the most `spawnWithFds` hands one
797
+ * child; when `pin`, on Linux, asks for more workers than
798
+ * `availableParallelism()`; and, through `preforkInvalid`, on seqpacket
799
+ * channels off Linux. A listener that cannot bind answers
800
+ * `PREFORK_SPAWN_FAILED` before any worker starts.
684
801
  */
685
802
  export const preforkSupervise = <S>(
686
803
  args: string[],
@@ -692,13 +809,27 @@ export const preforkSupervise = <S>(
692
809
  if (invalid !== "") {
693
810
  panic(`nish/net/prefork: ${invalid}`)
694
811
  }
812
+ const pin: boolean = config.pin && platform === "linux"
695
813
  const index: i32 = preforkIndex(args)
696
814
  if (index >= 0) {
815
+ const pinned: i32 = pin ? setCpuAffinity(index) : 0
816
+ if (pinned < 0) {
817
+ console.error(`nish/net/prefork: worker ${index} runs unpinned (${pinned})`)
818
+ }
697
819
  return index
698
820
  }
699
821
  const count: i32 = preforkWorkerCount(config.workers, getenv("NISH_WORKERS"))
700
- if (config.serviceWorker >= count || (config.serviceWorker >= 0 && count > 64)) {
701
- panic(`nish/net/prefork: serviceWorker not below ${count} workers, or above 64 workers`)
822
+ // A held listener is one more descriptor for every worker, the service one too.
823
+ const fdMax: i32 = config.listenPort >= 0 ? 63 : 64
824
+ if (config.serviceWorker >= count || (config.serviceWorker >= 0 && count > fdMax)) {
825
+ panic(`nish/net/prefork: serviceWorker not below ${count} workers, or above ${fdMax} workers`)
826
+ }
827
+ const cpus: i32 = pin ? availableParallelism() : 0
828
+ if (pin && count > cpus) {
829
+ panic(`nish/net/prefork: pin with ${count} workers on ${cpus} CPUs`)
830
+ }
831
+ if (config.pin && !pin) {
832
+ console.error("nish/net/prefork: pinning needs Linux; the workers run unpinned")
702
833
  }
703
834
  const signals: i32 = signalFd()
704
835
  const loop: i32 = pollCreate()
@@ -712,6 +843,10 @@ export const preforkSupervise = <S>(
712
843
  return PREFORK_SPAWN_FAILED
713
844
  }
714
845
  const supervisor = new PreforkSupervisor(config, args, count, loop)
846
+ if (config.listenPort >= 0 && !supervisor.listen(pin)) {
847
+ netClose(loop)
848
+ return PREFORK_SPAWN_FAILED
849
+ }
715
850
  // Token 0 is the signal pipe, token i + 1 worker i's channel.
716
851
  const ready: i32[] = new Array<i32>(2 * count + 2)
717
852
  let result: i32 = PREFORK_STOPPED
@@ -748,6 +883,7 @@ export const preforkSupervise = <S>(
748
883
  }
749
884
  }
750
885
  supervisor.stop(loop, signals, ready)
886
+ supervisor.closeListeners()
751
887
  netClose(loop)
752
888
  return result
753
889
  }
package/std/net/router.ts CHANGED
@@ -69,7 +69,7 @@ export const ROUTER_BAD_ESCAPE: i32 = -10
69
69
  /** `routerDecode`: the decoded bytes do not fit `out`; nothing was written. */
70
70
  export const ROUTER_NO_ROOM: i32 = -11
71
71
 
72
- /** The longest method `add` takes, as `nish/net/http1`'s `HTTP1_MAX_METHOD`. */
72
+ /** The longest method `add` takes, as `nish/net/http1-codec`'s `HTTP1_MAX_METHOD`. */
73
73
  const ROUTER_MAX_METHOD: i32 = 32
74
74
  /** No node or route: an `i32` spelled once, so that `--number-mode f64` does not read a bare `-1` as a double. */
75
75
  const ROUTER_NONE: i32 = -1
@@ -2,7 +2,7 @@
2
2
  * `nish/net/upstream` — outbound HTTP/1.1 calls from a route handler that
3
3
  * never block its worker: a pool of keep-alive connections to one origin,
4
4
  * registered on the `HttpHandler`'s own poll loop and read through the
5
- * `nish/net/http1-client` codec.
5
+ * `nish/net/http1-client-codec` codec.
6
6
  *
7
7
  * const h = new HttpHandler(new HandlerLimits(), listener, router)
8
8
  * const users = new Upstream(h, "http://127.0.0.1:9000", new UpstreamLimits())
@@ -87,8 +87,8 @@ import {
87
87
  HTTP1_HEAD,
88
88
  HTTP1_NEED_MORE,
89
89
  HTTP1_NO_BODY,
90
- } from "nish/net/http1"
91
- import { HTTP1_INFORMATIONAL, Http1ResponseParser, http1WriteRequestHead } from "nish/net/http1-client"
90
+ } from "nish/net/http1-codec"
91
+ import { HTTP1_INFORMATIONAL, Http1ResponseParser, http1WriteRequestHead } from "nish/net/http1-client-codec"
92
92
  import { OutboundPolicy, outboundAddressText, outboundFilter, outboundHostAllowed } from "nish/net/outbound"
93
93
  import { Url, urlParse } from "nish/net/url"
94
94
  import {
@@ -2,7 +2,7 @@
2
2
  * `nish/net/websocket` — RFC 6455 WebSocket framing and both sides of the
3
3
  * opening handshake, sans-IO: bytes in, events and bytes out.
4
4
  *
5
- * **The server's handshake.** `nish/net/http1` reads the client's request;
5
+ * **The server's handshake.** `nish/net/http1-codec` reads the client's request;
6
6
  * `websocketRequestKey(parser)` answers its `Sec-WebSocket-Key` when the head
7
7
  * is a valid opening handshake (§4.2.1), `websocketOfferedProtocols(parser)`
8
8
  * the subprotocols it offers, and `websocketUpgradeResponse(key, protocol)`
@@ -15,7 +15,7 @@
15
15
  * random bytes as the key, `websocketUpgradeRequest(host, target, key,
16
16
  * protocols)` writes the GET that asks to upgrade, and
17
17
  * `websocketCheckResponse(parser, key, offered)` reads the server's answer,
18
- * parsed by `nish/net/http1-client` after `expectUpgrade()`: it answers the
18
+ * parsed by `nish/net/http1-client-codec` after `expectUpgrade()`: it answers the
19
19
  * negotiated subprotocol, or which rule of §4.1 the response broke. What
20
20
  * `HTTP1_UPGRADE`'s `tunnelBytes()` answers is the first input for
21
21
  * `WsDecoder(false, …)`. This module draws no randomness: the nonce and every
@@ -77,8 +77,8 @@ import {
77
77
  http1ResponseHead,
78
78
  http1SameWord,
79
79
  http1Text,
80
- } from "nish/net/http1"
81
- import { Http1ResponseParser, http1UpgradeRequestHead } from "nish/net/http1-client"
80
+ } from "nish/net/http1-codec"
81
+ import { Http1ResponseParser, http1UpgradeRequestHead } from "nish/net/http1-client-codec"
82
82
 
83
83
  /** The GUID RFC 6455 §1.3 appends to the client's key before hashing it. */
84
84
  export const WS_GUID: string = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
@@ -954,7 +954,7 @@ export const websocketClientKey = (nonce: u8[]): string | null => {
954
954
  * with `Host`, `Sec-WebSocket-Key`, `Sec-WebSocket-Version: 13`, the offered
955
955
  * `protocols` in one `Sec-WebSocket-Protocol` field when there are any, and
956
956
  * `Upgrade: websocket` and `Connection: Upgrade`, written by
957
- * `nish/net/http1-client`'s `http1UpgradeRequestHead`. Answers `null` for a
957
+ * `nish/net/http1-client-codec`'s `http1UpgradeRequestHead`. Answers `null` for a
958
958
  * key `websocketKeyIsValid` refuses, a protocol that is not a token (an empty
959
959
  * one, or one holding a comma, a space, CR or LF), a protocol offered twice, an
960
960
  * empty host, and everything the request writer refuses (CR, LF or another
package/std/region.ts ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `nish:region` — memory a program frees when it says so (docs/LANGUAGE.md,
3
+ * "Regions").
4
+ *
5
+ * import { Region, enterWith, free, regionWith } from "nish:region"
6
+ *
7
+ * const r: Region<Session> = regionWith(id, open)
8
+ * const n: i32 = enterWith(r, out, serve)
9
+ * free(r)
10
+ *
11
+ * This file is the source behind the builtin module, and it is imported only
12
+ * by that name: `nish/region` is refused, so there is one spelling for the
13
+ * checker's rules to be read under (`src/region-check.ts`). What is written
14
+ * here is the meaning, over five intrinsics the compiler gives bodies of their
15
+ * own (`src/emit-region.ts`), which swap the thread's arena for the region's
16
+ * around the function they run:
17
+ *
18
+ * - `region(f)` makes a region, runs `f` with every allocation landing in
19
+ * it, and keeps what `f` answers as the region's root;
20
+ * - `enter(r, f)` runs `f(root)` the same way, and answers `f`'s scalar;
21
+ * - `free(r)` gives the region's memory back. A handle used after it is
22
+ * freed, or entered twice, or freed while entered, stops the program.
23
+ *
24
+ * A `Region<T>` is a handle, a 64-bit integer, and not a pointer: the root
25
+ * never leaves the region except through `enter`'s function, and that
26
+ * function answers a scalar, so nothing outside a region can point into it.
27
+ *
28
+ * Under Node the module is answered by `runtime/nish.mjs`, where a region is
29
+ * an entry in a table and `free` drops it.
30
+ */
31
+
32
+ /**
33
+ * A region whose root is a `T`. The compiler resolves the name to a type of
34
+ * its own, a 64-bit handle; this declaration is what `tsc` and the editor
35
+ * read through `runtime/nish.d.ts`.
36
+ */
37
+ export class Region<T> {
38
+ /** Never read: the compiler's type has no fields. */
39
+ root: T | null = null
40
+ }
41
+
42
+ // The five intrinsics below are the runtime's region calls. Each is written
43
+ // here as a call of itself, so it has a type and nothing more; the compiler
44
+ // gives every instance a body of its own (`src/emit-region.ts`), one call into
45
+ // `runtime/runtime-region.c`.
46
+
47
+ /** `nish_region_new`: a fresh handle, never 0. */
48
+ const handleNew = (): i64 => handleNew()
49
+
50
+ /** `nish_region_enter` on a new region, whose root is still null. */
51
+ const open = (h: i64): void => open(h)
52
+
53
+ /** `nish_region_leave` keeping `root`: the handle, typed by its root. */
54
+ const close = <T>(h: i64, root: T): Region<T> => close(h, root)
55
+
56
+ /** `nish_region_enter` on a made region: its root. */
57
+ const reopen = <T>(r: Region<T>): T => reopen(r)
58
+
59
+ /** `nish_region_leave` with the root `reopen` gave. */
60
+ const leave = <T>(r: Region<T>, root: T): void => leave(r, root)
61
+
62
+ /** A new region, whose root is what `f` answers; everything `f` allocates is in it. */
63
+ export const region = <T>(f: () => T): Region<T> => {
64
+ const h: i64 = handleNew()
65
+ open(h)
66
+ return close(h, f())
67
+ }
68
+
69
+ /** `region` with one argument for `f`, because the arrow `region` runs captures nothing. */
70
+ export const regionWith = <A, T>(arg: A, f: (arg: A) => T): Region<T> => {
71
+ const h: i64 = handleNew()
72
+ open(h)
73
+ return close(h, f(arg))
74
+ }
75
+
76
+ /** `f(root)`, allocating in the region: the one way to reach what a region holds. */
77
+ export const enter = <T, R>(r: Region<T>, f: (root: T) => R): R => {
78
+ const root: T = reopen(r)
79
+ const result: R = f(root)
80
+ leave(r, root)
81
+ return result
82
+ }
83
+
84
+ /** `enter` with one argument for `f`. */
85
+ export const enterWith = <T, A, R>(r: Region<T>, arg: A, f: (root: T, arg: A) => R): R => {
86
+ const root: T = reopen(r)
87
+ const result: R = f(root, arg)
88
+ leave(r, root)
89
+ return result
90
+ }
91
+
92
+ /** Give the region's memory back. The handle is dead after it. The compiler writes the body. */
93
+ export const free = <T>(_r: Region<T>): void => {
94
+ // `nish_region_free`: see above.
95
+ }
package/std/sort.ts ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * `std/sort` — `xs.sort(cmp)` on an array, in Nish: a stable merge sort,
3
+ * compiled once per comparator.
4
+ *
5
+ * A program does not import this module. A `.sort(cmp)` call is what loads it
6
+ * (the checker's `importSort`), and the checker routes the call to `sortWith`
7
+ * here. `cmp` is a compile-time function parameter, so each comparator is an
8
+ * instantiation of its own whose body calls the comparator directly — there is
9
+ * no function pointer. Under Node and `tsc` the call is the platform's
10
+ * `Array.prototype.sort`, so nothing here runs there.
11
+ *
12
+ * **Same order as Node.** `Array.prototype.sort` is stable (ES2019), so for a
13
+ * comparator that is a consistent total preorder there is exactly one answer,
14
+ * and any stable sort gives it. A comparator's result is read only as "greater
15
+ * than zero or not": a later element moves before an earlier one only when
16
+ * the comparator answers a positive number for (earlier, later), so a negative
17
+ * zero and NaN are zero, as Node treats them.
18
+ *
19
+ * **Shape.** The array is copied once into a scratch array of the same
20
+ * length, the only allocation, which the function's arena scope gives back
21
+ * before it returns, so a sort leaves `Arena.used()` where it was. Runs of
22
+ * `RUN` are sorted by insertion from the scratch into the array (a strictly
23
+ * descending run is copied reversed instead), then merged
24
+ * bottom-up, alternating between the two, and copied back once if the last
25
+ * pass ended in the scratch. A pair of runs already in order is copied without
26
+ * a merge. Every merge takes from the left run on a tie, which is what makes
27
+ * it stable.
28
+ *
29
+ * **No element is held across a write.** An `interface` element is stored
30
+ * inline, and `const v = xs[i]` is a pointer into the array, so an insertion
31
+ * sort in place would overwrite the element it is inserting. Every step here
32
+ * reads from one array and writes the other, and moves a whole slot, which for
33
+ * an inline record is one copy of its bytes.
34
+ *
35
+ * **Proofs.** Every index is proven in range by the guard written beside it,
36
+ * so no access keeps its bounds check. The run arithmetic wraps only where
37
+ * the operands are proven to lie in `0..n`, so it cannot wrap: a difference
38
+ * of two of them, or a sum taken only when it stays below `n`.
39
+ */
40
+
41
+ import { wrappingAdd, wrappingSub } from "nish:unsafe"
42
+
43
+ /** The length of the runs insertion sort makes before the first merge. */
44
+ const RUN: i32 = 32
45
+
46
+ /**
47
+ * `lo + step`, or `n` when that is not below `n`. Every caller has
48
+ * `0 <= lo <= n`, so `n - lo` cannot wrap, and the sum is taken only below `n`.
49
+ */
50
+ const stepTo = (lo: i32, step: i32, n: i32): i32 => (wrappingSub(n, lo) > step ? wrappingAdd(lo, step) : n)
51
+
52
+ /**
53
+ * `src[lo..hi)` sorted by insertion into `dst[lo..hi)`. The comparator call
54
+ * forgets every array length, so each access after it repeats its guard.
55
+ */
56
+ const insertionRun = <T>(src: T[], dst: T[], cmp: (a: T, b: T) => number, lo: i32, hi: i32): void => {
57
+ // A strictly descending run is copied reversed: no two of its elements are
58
+ // equal, so reversing it keeps the sort stable, and costs one comparison each.
59
+ // `lo < hi`, and `d` steps only while below `hi`, so neither `+ 1` wraps.
60
+ let d = wrappingAdd(lo, 1)
61
+ while (d > 0 && d < hi && d < toI32(src.length)) {
62
+ const p = d - 1
63
+ if (!(cmp(src[p], src[d]) > 0)) {
64
+ break
65
+ }
66
+ d = wrappingAdd(d, 1)
67
+ }
68
+ if (d >= hi) {
69
+ // `hi > lo >= 0`, so `hi - 1` does not wrap, and `r` steps down only from `r >= 0`.
70
+ let r = wrappingSub(hi, 1)
71
+ for (let k: i32 = lo; k >= 0 && k < hi && k < toI32(dst.length) && r >= 0 && r < toI32(src.length); k++) {
72
+ dst[k] = src[r]
73
+ r = wrappingSub(r, 1)
74
+ }
75
+ return
76
+ }
77
+ for (let i: i32 = lo; i >= 0 && i < hi; i++) {
78
+ let j = i
79
+ while (j > lo && j > 0 && j < toI32(dst.length) && i < toI32(src.length)) {
80
+ const k = j - 1
81
+ if (!(cmp(dst[k], src[i]) > 0) || k < 0 || k >= toI32(dst.length) || j >= toI32(dst.length)) {
82
+ break
83
+ }
84
+ dst[j] = dst[k]
85
+ j = k
86
+ }
87
+ if (i < toI32(src.length) && j >= 0 && j < toI32(dst.length)) {
88
+ dst[j] = src[i]
89
+ }
90
+ }
91
+ }
92
+
93
+ /** `src[lo..hi)` copied into `dst[lo..hi)`. */
94
+ const copyRun = <T>(src: T[], dst: T[], lo: i32, hi: i32): void => {
95
+ for (let k: i32 = lo; k >= 0 && k < hi && k < toI32(src.length) && k < toI32(dst.length); k++) {
96
+ dst[k] = src[k]
97
+ }
98
+ }
99
+
100
+ /**
101
+ * The sorted runs `src[lo..mid)` and `src[mid..hi)` merged into `dst[lo..hi)`,
102
+ * or copied when the last of the left run is not after the first of the right.
103
+ */
104
+ const mergeRuns = <T>(src: T[], dst: T[], cmp: (a: T, b: T) => number, lo: i32, mid: i32, hi: i32): void => {
105
+ if (mid > 0 && mid < hi && mid < toI32(src.length)) {
106
+ const last = mid - 1
107
+ if (last < toI32(src.length) && !(cmp(src[last], src[mid]) > 0)) {
108
+ copyRun(src, dst, lo, hi)
109
+ return
110
+ }
111
+ }
112
+ let i = lo
113
+ let j = mid
114
+ for (let k: i32 = lo; k >= 0 && k < hi; k++) {
115
+ let right = i >= mid
116
+ if (!right && j < hi && i >= 0 && i < toI32(src.length) && j >= 0 && j < toI32(src.length)) {
117
+ right = cmp(src[i], src[j]) > 0
118
+ }
119
+ if (right) {
120
+ if (j >= 0 && j < toI32(src.length) && k < toI32(dst.length)) {
121
+ dst[k] = src[j]
122
+ }
123
+ // `j` steps at most `hi - mid` times from `mid`, so it stays at most `hi`.
124
+ j = wrappingAdd(j, 1)
125
+ } else {
126
+ if (i >= 0 && i < toI32(src.length) && k < toI32(dst.length)) {
127
+ dst[k] = src[i]
128
+ }
129
+ // `i` steps at most `mid - lo` times from `lo`, so it stays at most `mid`.
130
+ i = wrappingAdd(i, 1)
131
+ }
132
+ }
133
+ }
134
+
135
+ /** One bottom-up pass: every pair of adjacent `width`-runs of `src` merged into `dst`. */
136
+ const mergePass = <T>(src: T[], dst: T[], cmp: (a: T, b: T) => number, n: i32, width: i32): void => {
137
+ let lo: i32 = 0
138
+ while (lo >= 0 && lo < n) {
139
+ const mid = stepTo(lo, width, n)
140
+ const hi = stepTo(mid, width, n)
141
+ mergeRuns(src, dst, cmp, lo, mid, hi)
142
+ lo = hi
143
+ }
144
+ }
145
+
146
+ /** `xs` sorted in place by `cmp`, stably, and answered: what `xs.sort(cmp)` compiles to. */
147
+ export const sortWith = <T>(xs: T[], cmp: (a: T, b: T) => number): T[] => {
148
+ const n: i32 = toI32(xs.length)
149
+ if (n < 2) {
150
+ return xs
151
+ }
152
+ // One allocation at full size, every slot written before any is read: the
153
+ // checker lets this `new Array<T>(n)` hold any `T`, and neither emitter
154
+ // zero-fills it (`isSortInstance`).
155
+ const tmp = new Array<T>(n)
156
+ copyRun(xs, tmp, 0, n)
157
+ let lo: i32 = 0
158
+ while (lo >= 0 && lo < n) {
159
+ const hi = stepTo(lo, RUN, n)
160
+ insertionRun(tmp, xs, cmp, lo, hi)
161
+ lo = hi
162
+ }
163
+ let inTmp = false
164
+ let width: i32 = RUN
165
+ while (width > 0 && width < n) {
166
+ if (inTmp) {
167
+ mergePass(tmp, xs, cmp, n, width)
168
+ } else {
169
+ mergePass(xs, tmp, cmp, n, width)
170
+ }
171
+ inTmp = !inTmp
172
+ width = stepTo(width, width, n)
173
+ }
174
+ if (inTmp) {
175
+ copyRun(tmp, xs, 0, n)
176
+ }
177
+ return xs
178
+ }