@amritk/nish-x86_64-linux 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-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
File without changes