@amritk/nish-x86_64-linux 0.27.2 → 0.29.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.
Files changed (54) hide show
  1. package/INSTALL.md +16 -8
  2. package/bin/nish +0 -0
  3. package/docs/AI.md +118 -26
  4. package/package.json +1 -1
  5. package/runtime/core/host.d.ts +2 -1
  6. package/runtime/core/host.mjs +4 -0
  7. package/runtime/core.d.ts +47 -0
  8. package/runtime/core.mjs +67 -0
  9. package/runtime/nish.d.ts +176 -0
  10. package/runtime/nish.h +120 -6
  11. package/runtime/nish.mjs +24 -4
  12. package/runtime/runtime-checksum.c +270 -0
  13. package/runtime/runtime-net.c +386 -5
  14. package/runtime/runtime-os.c +59 -8
  15. package/runtime/runtime-parallel.c +81 -5
  16. package/runtime/runtime-sandbox.c +479 -0
  17. package/runtime/runtime-sha256.c +239 -0
  18. package/runtime/runtime-shm.c +541 -0
  19. package/runtime/runtime.c +827 -791
  20. package/scripts/build.sh +79 -28
  21. package/std/README.md +25 -13
  22. package/std/collections.ts +5 -5
  23. package/std/compress/checksum.ts +88 -75
  24. package/std/compress/deflate.ts +224 -0
  25. package/std/compress/gzip.ts +52 -0
  26. package/std/crypto/bcrypt.ts +816 -280
  27. package/std/crypto/hmac.ts +305 -2
  28. package/std/crypto/jwt.ts +1661 -0
  29. package/std/json-equal.ts +335 -0
  30. package/std/json-format.ts +1070 -0
  31. package/std/json-number.ts +428 -0
  32. package/std/json-pointer.ts +245 -0
  33. package/std/json-typed.ts +34 -0
  34. package/std/json-value.ts +510 -17
  35. package/std/json.ts +35 -6
  36. package/std/net/handler.ts +1348 -0
  37. package/std/net/http-client.ts +97 -16
  38. package/std/net/http-compress.ts +301 -16
  39. package/std/net/http1-client.ts +104 -19
  40. package/std/net/http1-server.ts +127 -24
  41. package/std/net/http1.ts +24 -4
  42. package/std/net/outbound.ts +409 -0
  43. package/std/net/postgres.ts +405 -0
  44. package/std/net/prefork.ts +344 -20
  45. package/std/net/websocket.ts +295 -14
  46. package/std/regex.ts +613 -52
  47. package/std/secret.ts +1 -1
  48. package/std/sync/ring.ts +587 -0
  49. package/std/sync/shm.ts +284 -0
  50. package/std/term-io.ts +35 -8
  51. package/std/term-rate.ts +13 -1
  52. package/std/threads.ts +35 -3
  53. package/std/yaml-json.ts +841 -0
  54. /package/runtime/{LICENSE-ryu → LICENSE-zmij} +0 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * `nish/sync/shm` — memory that several processes map at once, and the
3
+ * atomics that order what they write to it.
4
+ *
5
+ * import { createRegion, openRegion, unlinkRegion, ACQUIRE, RELEASE } from "nish/sync/shm"
6
+ *
7
+ * // The process that owns it: a fresh, zeroed region of 4096 bytes.
8
+ * const stale = unlinkRegion("jobs") // a region a crashed run left, if any
9
+ * const region = createRegion("jobs", 4096).expect("create")
10
+ * region.storeI64(0, toI64(42), RELEASE)
11
+ *
12
+ * // Any other process of the same user: the same bytes.
13
+ * const view = openRegion("jobs").expect("open")
14
+ * const n: i64 = view.loadI64(0, ACQUIRE) // 42 once the store is seen
15
+ *
16
+ * **A region is a named POSIX shared-memory object** (`/dev/shm/<name>` on
17
+ * Linux, `shm_open("/<name>")` on Darwin) mapped shared, so every process that
18
+ * opens the name sees the same bytes. A name is 1 to 30 of `A-Z a-z 0-9 . _ -`,
19
+ * not starting with `.`. `createRegion` makes it with mode 0600 and refuses a
20
+ * name that exists (`SHM_EXISTS`), so a name another user planted is never
21
+ * adopted; `openRegion` refuses a region this user does not own, that anyone
22
+ * else may read or write, or (on Linux) that is not a regular file or has a
23
+ * second hard link (`SHM_ACCESS`). The name stays until `unlinkRegion`,
24
+ * even after every process has exited, which is how a process that starts later
25
+ * finds it; the mapping goes away with `close` or with the process.
26
+ *
27
+ * **Why named, and no anonymous region made before `fork`.** A Nish program
28
+ * has no `fork`: `spawn` and `nish/net/prefork` start a program with `exec`,
29
+ * and every descriptor is close-on-exec, so neither an anonymous mapping nor a
30
+ * `memfd` reaches the child. A name does. The supervisor creates the region
31
+ * before `preforkWith`, every worker opens it by name, and the supervisor
32
+ * unlinks it after (`examples/prefork-jobs.ts`, docs/wp47-shared-memory.md).
33
+ *
34
+ * **Every access is checked.** An offset is an `i32` byte offset into the
35
+ * region. A 32-bit slot sits at a multiple of 4 and a 64-bit one at a multiple
36
+ * of 8, wholly inside the region; a byte copy fits the region and the buffer.
37
+ * Anything else panics, naming the offset and the region's size, exactly as an
38
+ * array index out of range does. An ordering an access cannot take (a load
39
+ * with `RELEASE`) panics too. A region is never read or written past its end.
40
+ *
41
+ * **Orderings are C11's.** `RELAXED`, `ACQUIRE`, `RELEASE`, `ACQ_REL` and
42
+ * `SEQ_CST`; a load takes the first, second and last, a store the first, third
43
+ * and last, and a read-modify-write any of them. Each access is one LLVM
44
+ * atomic instruction on the speed and size profiles, where the runtime is
45
+ * inlined into the program; a compare-exchange that fails orders as C++
46
+ * derives it (`ACQ_REL` fails `ACQUIRE`, `RELEASE` fails `RELAXED`). Bytes
47
+ * copied by `read` and `write` are plain copies, ordered by the atomics around
48
+ * them, and a copy that races a writer in another process can be torn but is
49
+ * never read past its window.
50
+ *
51
+ * **Waiting.** `waitI32` sleeps while a 32-bit slot holds a value and `wake`
52
+ * wakes the processes sleeping on it: Linux's futex, shared between processes.
53
+ * Darwin and wasi answer `SHM_UNSUPPORTED` (-95) from both, as
54
+ * `setIdlePriority` does on Darwin, and a program there polls instead.
55
+ *
56
+ * **A region belongs to the thread that mapped it.** Under `--threads` the
57
+ * runtime's table of regions is thread-local, so a `SharedRegion` handed to
58
+ * another thread names nothing there and panics; that thread opens the name
59
+ * again. Native only: `--emit-ts` refuses a module that declares C functions,
60
+ * and creating a region on wasi answers `SHM_UNSUPPORTED`. Calling C is the
61
+ * `ffi` capability, so a program that imports this module is reported with
62
+ * `ffi` and `--deny ffi` refuses it.
63
+ *
64
+ * Private helpers share the importing program's flat symbol namespace
65
+ * (`docs/wp26-stdlib.md` §3e), which is why each one carries the module's name.
66
+ */
67
+
68
+ // biome-ignore-all lint/style/useNamingConvention: the declarations name C symbols
69
+ // runtime/runtime-shm.c. The names are `nishshm_`, not `nish_shm_`: `nish_`
70
+ // names are the runtime's own (NL2202).
71
+ declare function nishshm_create(name: string, bytes: i32): i32
72
+ declare function nishshm_open(name: string): i32
73
+ declare function nishshm_unlink(name: string): i32
74
+ declare function nishshm_close(h: i32): i32
75
+ declare function nishshm_size(h: i32): i32
76
+ declare function nishshm_load_i32(h: i32, at: i32, order: i32): i32
77
+ declare function nishshm_store_i32(h: i32, at: i32, v: i32, order: i32): void
78
+ declare function nishshm_add_i32(h: i32, at: i32, delta: i32, order: i32): i32
79
+ declare function nishshm_exchange_i32(h: i32, at: i32, v: i32, order: i32): i32
80
+ declare function nishshm_cas_i32(h: i32, at: i32, expected: i32, desired: i32, order: i32): i32
81
+ declare function nishshm_load_i64(h: i32, at: i32, order: i32): i64
82
+ declare function nishshm_store_i64(h: i32, at: i32, v: i64, order: i32): void
83
+ declare function nishshm_add_i64(h: i32, at: i32, delta: i64, order: i32): i64
84
+ declare function nishshm_exchange_i64(h: i32, at: i32, v: i64, order: i32): i64
85
+ declare function nishshm_cas_i64(h: i32, at: i32, expected: i64, desired: i64, order: i32): i64
86
+ declare function nishshm_fence(order: i32): void
87
+ declare function nishshm_read(h: i32, at: i32, dst: u8[], n: u64, off: u64, len: u64): void
88
+ declare function nishshm_write(h: i32, at: i32, src: readonly u8[], n: u64, off: u64, len: u64): void
89
+ declare function nishshm_wait_i32(h: i32, at: i32, expected: i32, timeoutMs: i32): i32
90
+ declare function nishshm_wake(h: i32, at: i32, count: i32): i32
91
+ declare function strerror(errnum: i32): string
92
+
93
+ /** No ordering beyond the access itself being whole. */
94
+ export const RELAXED: i32 = 0
95
+ /** A load that sees everything written before the `RELEASE` store it reads. */
96
+ export const ACQUIRE: i32 = 2
97
+ /** A store after which everything written before it is seen by an `ACQUIRE` load of it. */
98
+ export const RELEASE: i32 = 3
99
+ /** A read-modify-write that is both. */
100
+ export const ACQ_REL: i32 = 4
101
+ /** One order every `SEQ_CST` access in every process agrees on. */
102
+ export const SEQ_CST: i32 = 5
103
+
104
+ /** `waitI32`: woken, or for no reason the caller can see; load the slot again. */
105
+ export const SHM_WOKEN: i32 = 0
106
+ /** `waitI32`: the slot did not hold the value, so it did not sleep. */
107
+ export const SHM_NOT_EQUAL: i32 = -11
108
+ /** `waitI32`: the time ran out. */
109
+ export const SHM_TIMED_OUT: i32 = -110
110
+ /** `waitI32` and `wake` on Darwin and wasi, and every region on wasi: not here. */
111
+ export const SHM_UNSUPPORTED: i32 = -95
112
+
113
+ /** `ShmError.code`: no region of that name. */
114
+ export const SHM_NOT_FOUND: i32 = 2
115
+ /** `ShmError.code`: another user's region, one others may use, or not one region under one name. */
116
+ export const SHM_ACCESS: i32 = 13
117
+ /** `ShmError.code`: a region of that name exists already. */
118
+ export const SHM_EXISTS: i32 = 17
119
+ /** `ShmError.code`: a name that is not 1 to 30 of `A-Za-z0-9._-`, or a size outside 1 byte to 1 GiB. */
120
+ export const SHM_INVALID: i32 = 22
121
+ /** `ShmError.code`: this thread has 63 regions open. */
122
+ export const SHM_TOO_MANY: i32 = 24
123
+
124
+ /** Why a region could not be created, opened, unlinked or closed. */
125
+ export class ShmError {
126
+ /** The errno, positive: `SHM_NOT_FOUND`, `SHM_EXISTS` and the rest. */
127
+ code: i32
128
+ /** What it means, such as `File exists`. */
129
+ message: string
130
+
131
+ constructor(code: i32) {
132
+ this.code = code
133
+ this.message = code === 95 ? "not supported on this platform" : strerror(code)
134
+ }
135
+ }
136
+
137
+ /** `Ok()` for 0, the error for a negated errno. */
138
+ const shmDone = (rc: i32): Result<void, ShmError> => {
139
+ if (rc < 0) {
140
+ return Err(new ShmError(-rc))
141
+ }
142
+ return Ok()
143
+ }
144
+
145
+ /**
146
+ * Creates the region `name`, `bytes` long and zeroed, mode 0600, and maps it.
147
+ * `SHM_EXISTS` when the name is taken: unlink a stale one first.
148
+ */
149
+ export const createRegion = (name: string, bytes: i32): Result<SharedRegion, ShmError> => {
150
+ const h: i32 = nishshm_create(name, bytes)
151
+ if (h < 0) {
152
+ return Err(new ShmError(-h))
153
+ }
154
+ return Ok(new SharedRegion(h))
155
+ }
156
+
157
+ /**
158
+ * Maps the region `name` that some process of this user created. Its size is
159
+ * the region's (on Darwin, rounded up to a page).
160
+ */
161
+ export const openRegion = (name: string): Result<SharedRegion, ShmError> => {
162
+ const h: i32 = nishshm_open(name)
163
+ if (h < 0) {
164
+ return Err(new ShmError(-h))
165
+ }
166
+ return Ok(new SharedRegion(h))
167
+ }
168
+
169
+ /**
170
+ * Removes the name, so no process can open it again; every mapping of it
171
+ * stays valid until it is closed. `SHM_NOT_FOUND` when there is none.
172
+ */
173
+ export const unlinkRegion = (name: string): Result<void, ShmError> => shmDone(nishshm_unlink(name))
174
+
175
+ /** A fence of `order`, for the accesses around it. */
176
+ export const fence = (order: i32): void => {
177
+ nishshm_fence(order)
178
+ }
179
+
180
+ /**
181
+ * A mapped region. Every slot offset is in bytes from its start; an offset
182
+ * outside it, a misaligned slot or an ordering the access cannot take panics.
183
+ */
184
+ export class SharedRegion {
185
+ /** The runtime's handle; it names nothing after `close`. */
186
+ handle: i32
187
+
188
+ constructor(handle: i32) {
189
+ this.handle = handle
190
+ }
191
+
192
+ /** The region's length in bytes. */
193
+ size(): i32 {
194
+ return nishshm_size(this.handle)
195
+ }
196
+
197
+ /** The 32-bit slot at `at`. */
198
+ loadI32(at: i32, order: i32): i32 {
199
+ return nishshm_load_i32(this.handle, at, order)
200
+ }
201
+
202
+ /** Sets the 32-bit slot at `at` to `value`. */
203
+ storeI32(at: i32, value: i32, order: i32): void {
204
+ nishshm_store_i32(this.handle, at, value, order)
205
+ }
206
+
207
+ /** Adds `delta` to the 32-bit slot at `at`, wrapping, and answers the value before. */
208
+ addI32(at: i32, delta: i32, order: i32): i32 {
209
+ return nishshm_add_i32(this.handle, at, delta, order)
210
+ }
211
+
212
+ /** Sets the 32-bit slot at `at` to `value` and answers the value before. */
213
+ exchangeI32(at: i32, value: i32, order: i32): i32 {
214
+ return nishshm_exchange_i32(this.handle, at, value, order)
215
+ }
216
+
217
+ /**
218
+ * Sets the 32-bit slot at `at` to `desired` if it holds `expected`, and
219
+ * answers what it held: the exchange happened when that is `expected`.
220
+ */
221
+ compareExchangeI32(at: i32, expected: i32, desired: i32, order: i32): i32 {
222
+ return nishshm_cas_i32(this.handle, at, expected, desired, order)
223
+ }
224
+
225
+ /** The 64-bit slot at `at`. */
226
+ loadI64(at: i32, order: i32): i64 {
227
+ return nishshm_load_i64(this.handle, at, order)
228
+ }
229
+
230
+ /** Sets the 64-bit slot at `at` to `value`. */
231
+ storeI64(at: i32, value: i64, order: i32): void {
232
+ nishshm_store_i64(this.handle, at, value, order)
233
+ }
234
+
235
+ /** Adds `delta` to the 64-bit slot at `at`, wrapping, and answers the value before. */
236
+ addI64(at: i32, delta: i64, order: i32): i64 {
237
+ return nishshm_add_i64(this.handle, at, delta, order)
238
+ }
239
+
240
+ /** Sets the 64-bit slot at `at` to `value` and answers the value before. */
241
+ exchangeI64(at: i32, value: i64, order: i32): i64 {
242
+ return nishshm_exchange_i64(this.handle, at, value, order)
243
+ }
244
+
245
+ /**
246
+ * Sets the 64-bit slot at `at` to `desired` if it holds `expected`, and
247
+ * answers what it held: the exchange happened when that is `expected`.
248
+ */
249
+ compareExchangeI64(at: i32, expected: i64, desired: i64, order: i32): i64 {
250
+ return nishshm_cas_i64(this.handle, at, expected, desired, order)
251
+ }
252
+
253
+ /** Copies `len` bytes of the region at `at` into `dst[off, off + len)`. */
254
+ read(at: i32, dst: u8[], off: i32, len: i32): void {
255
+ nishshm_read(this.handle, at, dst, toU64(dst.length), toU64(off), toU64(len))
256
+ }
257
+
258
+ /** Copies `src[off, off + len)` into the region at `at`. */
259
+ write(at: i32, src: readonly u8[], off: i32, len: i32): void {
260
+ nishshm_write(this.handle, at, src, toU64(src.length), toU64(off), toU64(len))
261
+ }
262
+
263
+ /**
264
+ * Sleeps while the 32-bit slot at `at` holds `expected`, at most
265
+ * `timeoutMs` (negative: no limit): `SHM_WOKEN`, `SHM_NOT_EQUAL`,
266
+ * `SHM_TIMED_OUT`, or `SHM_UNSUPPORTED` off Linux.
267
+ */
268
+ waitI32(at: i32, expected: i32, timeoutMs: i32): i32 {
269
+ return nishshm_wait_i32(this.handle, at, expected, timeoutMs)
270
+ }
271
+
272
+ /**
273
+ * Wakes at most `count` of the processes sleeping on the 32-bit slot at
274
+ * `at`: how many it woke, or `SHM_UNSUPPORTED` off Linux.
275
+ */
276
+ wake(at: i32, count: i32): i32 {
277
+ return nishshm_wake(this.handle, at, count)
278
+ }
279
+
280
+ /** Unmaps the region; the handle names nothing after. The name stays until `unlinkRegion`. */
281
+ close(): Result<void, ShmError> {
282
+ return shmDone(nishshm_close(this.handle))
283
+ }
284
+ }
package/std/term-io.ts CHANGED
@@ -148,6 +148,13 @@ export class TerminalOptions {
148
148
  framebufferHeight: i32
149
149
  /** The corner (`CORNER_*` from `nish/term-rate`) to draw the stats overlay in, or -1 for none (-1). */
150
150
  overlay: i32
151
+ /**
152
+ * The backend to use, as `NISH_TERM_GRAPHICS` names one ("kitty", "sixel",
153
+ * "halfblock" or "ascii"), or "" to take the best the terminal has (""). The
154
+ * variable, when it is set, wins; the probe refuses a backend the terminal
155
+ * did not answer for, as it does the variable's.
156
+ */
157
+ graphics: string
151
158
 
152
159
  constructor() {
153
160
  this.stepsPerSecond = 60
@@ -165,6 +172,7 @@ export class TerminalOptions {
165
172
  this.framebufferWidth = 0
166
173
  this.framebufferHeight = 0
167
174
  this.overlay = -1
175
+ this.graphics = ""
168
176
  }
169
177
  }
170
178
 
@@ -180,8 +188,8 @@ const termIoGetenv = (name: string): string => {
180
188
  return v === null ? "" : v
181
189
  }
182
190
 
183
- /** The variables `nish/term-probe` reads. */
184
- const termIoEnv = (): TermEnv => {
191
+ /** The variables `nish/term-probe` reads, with `graphics` standing for `NISH_TERM_GRAPHICS` when it is unset. */
192
+ const termIoEnv = (graphics: string): TermEnv => {
185
193
  const env: TermEnv = new TermEnv()
186
194
  env.term = termIoGetenv("TERM")
187
195
  env.colorterm = termIoGetenv("COLORTERM")
@@ -192,7 +200,8 @@ const termIoEnv = (): TermEnv => {
192
200
  env.sshClient = termIoGetenv("SSH_CLIENT")
193
201
  env.sshTty = termIoGetenv("SSH_TTY")
194
202
  env.noColor = termIoGetenv("NO_COLOR")
195
- env.nishTermGraphics = termIoGetenv("NISH_TERM_GRAPHICS")
203
+ const forced: string = termIoGetenv("NISH_TERM_GRAPHICS")
204
+ env.nishTermGraphics = forced === "" ? graphics : forced
196
205
  env.nishTermFps = termIoGetenv("NISH_TERM_FPS")
197
206
  return env
198
207
  }
@@ -279,10 +288,12 @@ export class Terminal {
279
288
  workNs: f64
280
289
  /** When the last frame's write began, on the monotonic clock. */
281
290
  sendStartNs: i64
282
- /** The cells `rect` was fitted to. */
291
+ /** The cells, backend and framebuffer size `rect` was fitted to. */
283
292
  rectCols: i32
284
293
  rectRows: i32
285
294
  rectBackend: i32
295
+ rectWidth: i32
296
+ rectHeight: i32
286
297
  /** The best backend the terminal has, a `GRAPHICS_*`. */
287
298
  graphics: i32
288
299
  /** The backend the last frame used. */
@@ -304,6 +315,8 @@ export class Terminal {
304
315
  focusOn: boolean
305
316
  /** Whether a kitty image may be on the screen, so closing deletes it. */
306
317
  kittyImage: boolean
318
+ /** Whether the last frame sent pixels, so a framebuffer emptied since takes them off the screen. */
319
+ pixelsShown: boolean
307
320
  /** Whether `screen` was drawn in 256 colours last frame. */
308
321
  palette256: boolean
309
322
 
@@ -350,6 +363,9 @@ export class Terminal {
350
363
  this.rectCols = -1
351
364
  this.rectRows = -1
352
365
  this.rectBackend = -1
366
+ this.rectWidth = -1
367
+ this.rectHeight = -1
368
+ this.pixelsShown = false
353
369
  this.graphics = termIoGraphics(caps.graphics)
354
370
  this.backend = this.graphics
355
371
  this.cellWidthPx = termIoCellPx(caps.cellWidthPx)
@@ -676,19 +692,23 @@ export class Terminal {
676
692
  return !ended
677
693
  }
678
694
 
679
- /** Fit the framebuffer to the screen for `backend`, unless the size and backend are those of the last fit. */
695
+ /** Fit the framebuffer to the screen for `backend`, unless the sizes and backend are those of the last fit. */
680
696
  fit(backend: i32): void {
697
+ const fb: Framebuffer = this.framebuffer
681
698
  if (
682
699
  this.rectCols === this.screen.width &&
683
700
  this.rectRows === this.screen.height &&
684
- this.rectBackend === backend
701
+ this.rectBackend === backend &&
702
+ this.rectWidth === fb.width &&
703
+ this.rectHeight === fb.height
685
704
  ) {
686
705
  return
687
706
  }
688
707
  this.rectCols = this.screen.width
689
708
  this.rectRows = this.screen.height
690
709
  this.rectBackend = backend
691
- const fb: Framebuffer = this.framebuffer
710
+ this.rectWidth = fb.width
711
+ this.rectHeight = fb.height
692
712
  this.rect = fitFramebuffer(
693
713
  fb.width,
694
714
  fb.height,
@@ -768,7 +788,14 @@ export class Terminal {
768
788
  } else {
769
789
  drawAscii(src, this.screen, this.rect, this.caps.colors)
770
790
  }
791
+ } else if (this.pixelsShown) {
792
+ // The framebuffer was emptied: take the last image off, which the screen's cells do not cover.
793
+ if (backend === GRAPHICS_KITTY && this.kittyImage) {
794
+ write(kittyDelete(TERM_IO_IMAGE_ID))
795
+ }
796
+ this.screen.invalidate()
771
797
  }
798
+ this.pixelsShown = withPixels
772
799
  if (this.options.overlay >= 0) {
773
800
  drawStatsOverlay(this.screen, this.stats(), this.options.overlay)
774
801
  }
@@ -803,7 +830,7 @@ export const openTerminal = (options: TerminalOptions): Result<Terminal, string>
803
830
  if (setRawMode(true) !== 0) {
804
831
  return Err("the terminal refused raw mode")
805
832
  }
806
- const env: TermEnv = termIoEnv()
833
+ const env: TermEnv = termIoEnv(options.graphics)
807
834
  const size: i32[] = terminalSize()
808
835
  const sized: boolean = toI32(size.length) >= 2 && size[0] > 0 && size[1] > 0
809
836
  const cols: i32 = sized ? size[0] : TERM_IO_COLS
package/std/term-rate.ts CHANGED
@@ -96,6 +96,13 @@
96
96
  * next decision. Falling fast and rising slowly keeps a flickering link from
97
97
  * flipping the rate.
98
98
  *
99
+ * It starts at the best tier on a local terminal: the probe answered under
100
+ * 5 ms, or no `SSH_*` hint and no slow answer. Falling costs half a second and
101
+ * rising one tier costs three, so a terminal that keeps up is never made to
102
+ * climb. A probe that took 5 ms or more starts at 30, and a remote session
103
+ * (the hint, and no fast answer) at 15, as a backlog on a slow link takes far
104
+ * longer to drain than to avoid.
105
+ *
99
106
  * Below the floor tier (the least tier at or above `minFps`, under
100
107
  * `RATE_FIRST`) the controller cuts bytes instead of frames, one step at a
101
108
  * time: half the resolution, a quarter, the backend capped at half blocks, 256
@@ -431,7 +438,12 @@ export class RateController {
431
438
  this.floorFps = this.prefer === DETAIL_FIRST ? 5 : minTier
432
439
  const remote: boolean =
433
440
  options.remoteHint && !(options.probeRttMs > 0.0 && options.probeRttMs < rateLocalRttMs)
434
- const want: i32 = remote ? 15 : 30
441
+ let want: i32 = this.tiers[0]
442
+ if (remote) {
443
+ want = 15
444
+ } else if (options.probeRttMs >= rateLocalRttMs) {
445
+ want = 30
446
+ }
435
447
  let start: i32 = this.tiers[this.tiers.length - 1]
436
448
  for (const t of this.tiers) {
437
449
  if (t <= want && t > start) {
package/std/threads.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * `std/threads` — data parallelism, a scope for heterogeneous tasks, and a
3
3
  * lock that owns its data (docs/wp29-thread-surface.md §4.1, §4.2 and §4.3).
4
4
  *
5
- * import { AtomicI32, Mutex, parallelFrom, parallelMap, parallelMapInto, parallelReduce, scope } from "nish/threads";
5
+ * import { AtomicI32, Mutex, parallelFrom, parallelMap, parallelMapInto, parallelReduce, scope, scopeWithStack } from "nish/threads";
6
6
  *
7
7
  * parallelMapInto(src, dst, (x) => x * 3);
8
8
  * const ys = parallelMap(src, (x) => x * k + b); // k, b: `const` locals, read by the body
@@ -13,6 +13,10 @@
13
13
  * s.spawn(sumOf, xs, out, 0);
14
14
  * s.spawn(maxOf, ys, out, 1);
15
15
  * } // both tasks have run, and `out` holds both answers
16
+ * {
17
+ * using s = scopeWithStack(64 * 1024 * 1024); // each task's thread has a 64 MiB stack
18
+ * s.spawn(checkModule, ast, out, 0);
19
+ * }
16
20
  *
17
21
  * **What is written here is the meaning, not the implementation.** Each body
18
22
  * below is the sequential program, and it is what runs under Node, what
@@ -280,8 +284,11 @@ const slotWritten = (at: i32): boolean => at < 0 || at >= 0
280
284
  */
281
285
  export class ThreadScope {
282
286
  /**
283
- * Nothing reads or writes it. It is here so that each scope is an object of
284
- * its own, whose address is what the runtime files the scope's tasks under.
287
+ * The stack, in bytes, of each thread the scope's tasks run on, or 0 for the
288
+ * platform's default. Only `scopeWithStack` writes it, and only
289
+ * `nish_scope_join` (runtime/runtime-parallel.c) reads it, as the scope's
290
+ * first word. It also makes each scope an object of its own, whose address is
291
+ * what the runtime files the scope's tasks under.
285
292
  */
286
293
  tag: i32 = 0
287
294
 
@@ -338,6 +345,31 @@ export class ThreadScope {
338
345
  /** A new scope, for a `using` declaration: `using s = scope()`. */
339
346
  export const scope = (): ThreadScope => new ThreadScope()
340
347
 
348
+ /**
349
+ * The smallest stack `scopeWithStack` takes: 128 KiB, the largest
350
+ * `PTHREAD_STACK_MIN` among the targets (aarch64 Linux). A constant below it
351
+ * is refused at compile time (NL2584); a computed one panics here, under Node
352
+ * as natively.
353
+ */
354
+ const MIN_STACK: i32 = 131072
355
+
356
+ /**
357
+ * A new scope whose tasks each run on a thread with a stack of at least
358
+ * `bytes` bytes, for a `using` declaration: `using s = scopeWithStack(n)`.
359
+ * The tasks run on new threads, never on the one that joins. The runtime
360
+ * rounds `bytes` up to the page size and adds a guard page below it, so a task
361
+ * that overflows it faults instead of writing past it. Under Node, and on a
362
+ * target without threads, the size is checked and has no other effect.
363
+ */
364
+ export const scopeWithStack = (bytes: i32): ThreadScope => {
365
+ if (bytes < MIN_STACK) {
366
+ panic(`scopeWithStack: a stack of ${bytes} bytes is below the minimum of ${MIN_STACK}`)
367
+ }
368
+ const s = scope()
369
+ s.tag = bytes
370
+ return s
371
+ }
372
+
341
373
  /**
342
374
  * What a `Mutex` hands out: its data, as `value`, for as long as the block of
343
375
  * the `using` declaration that took the lock (docs/LANGUAGE.md, "A lock that