@amritk/nish-x86_64-linux 0.16.0 → 0.18.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 (58) hide show
  1. package/INSTALL.md +43 -34
  2. package/bin/nish +0 -0
  3. package/package.json +1 -1
  4. package/runtime/nish.d.ts +117 -8
  5. package/runtime/nish.h +50 -13
  6. package/runtime/nish.mjs +71 -7
  7. package/runtime/runtime-host.c +43 -24
  8. package/runtime/runtime-net.c +57 -10
  9. package/runtime/runtime-os.c +62 -20
  10. package/runtime/runtime-simd.c +203 -0
  11. package/runtime/runtime-wasm.c +12 -0
  12. package/runtime/runtime.c +66 -18
  13. package/runtime/shim.mjs +114 -5
  14. package/scripts/build.sh +14 -12
  15. package/std/README.md +168 -36
  16. package/std/crypto/aes.ts +51 -8
  17. package/std/crypto/base64url.ts +25 -12
  18. package/std/crypto/chacha20poly1305.ts +43 -29
  19. package/std/crypto/ct.ts +6 -1
  20. package/std/crypto/hkdf.ts +364 -3
  21. package/std/crypto/hmac.ts +307 -7
  22. package/std/crypto/p256.ts +618 -34
  23. package/std/crypto/sha1.ts +147 -0
  24. package/std/crypto/sha256.ts +3 -2
  25. package/std/crypto/x25519.ts +88 -36
  26. package/std/crypto/x509.ts +130 -47
  27. package/std/json.ts +297 -41
  28. package/std/net/hpack.ts +1094 -0
  29. package/std/net/http-fields.ts +604 -0
  30. package/std/net/http1-server.ts +1389 -0
  31. package/std/net/http1.ts +1492 -0
  32. package/std/net/http2-frame.ts +745 -0
  33. package/std/net/http2-tls.ts +216 -0
  34. package/std/net/http2.ts +1433 -0
  35. package/std/net/http3-frame.ts +457 -0
  36. package/std/net/http3-server.ts +911 -0
  37. package/std/net/http3.ts +2145 -0
  38. package/std/net/qpack.ts +1163 -0
  39. package/std/net/quic-conn-ack.ts +206 -0
  40. package/std/net/quic-conn-cid.ts +425 -0
  41. package/std/net/quic-conn-params.ts +462 -0
  42. package/std/net/quic-datagram.ts +130 -0
  43. package/std/net/quic-frame.ts +1206 -0
  44. package/std/net/quic-listener.ts +929 -0
  45. package/std/net/quic-packet.ts +1698 -0
  46. package/std/net/quic-recovery.ts +849 -0
  47. package/std/net/quic-stream.ts +1609 -0
  48. package/std/net/quic.ts +3578 -0
  49. package/std/net/tls/codec.ts +1089 -0
  50. package/std/net/tls/record-server.ts +1173 -0
  51. package/std/net/tls/record.ts +621 -0
  52. package/std/net/tls/schedule.ts +429 -0
  53. package/std/net/tls-tcp.ts +414 -0
  54. package/std/net/tls.ts +1437 -0
  55. package/std/net/websocket.ts +823 -0
  56. package/std/net/webtransport.ts +1334 -0
  57. package/std/secret.ts +62 -0
  58. package/std/text.ts +77 -0
package/INSTALL.md CHANGED
@@ -61,8 +61,9 @@ The `wasi` profile links the runtime against wasi-libc so that whole programs
61
61
  needs a WASI sysroot and compiler-rt's wasm32 builtins next to your clang:
62
62
 
63
63
  ```bash
64
- # Ubuntu / Debian: the packaged sysroot lands in /usr/lib/wasi-sysroot
64
+ # Ubuntu / Debian: the packaged sysroot lands in /usr (/usr/include/wasm32-wasi, /usr/lib/wasm32-wasi)
65
65
  sudo apt-get install -y wasi-libc libclang-rt-18-dev-wasm32
66
+ export WASI_SYSROOT=/usr # add to your shell profile
66
67
 
67
68
  # Any platform: wasi-sdk's sysroot and builtins tarballs (versions that match your clang)
68
69
  curl -L -o - https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sysroot-24.0.tar.gz | tar -xz -C /opt
@@ -74,7 +75,11 @@ export WASI_SYSROOT=/opt/wasi-sysroot-24.0 # add to your shell profile
74
75
  `/usr/lib/wasi-sysroot`, `/opt/wasi-sdk/share/wasi-sysroot` and
75
76
  `/usr/share/wasi-sysroot`, and for `libclang_rt.builtins-wasm32.a` in clang's
76
77
  resource directory, next to the sysroot (as the tarball above unpacks it),
77
- in `<sysroot>/lib/wasm32-wasi/`, or at `WASI_BUILTINS=<file>`. Then:
78
+ in `<sysroot>/lib/wasm32-wasi/`, or at `WASI_BUILTINS=<file>`.
79
+
80
+ The packaged wasi-libc predates wasi-libc's `realpath`, so a module linked
81
+ against it answers `realpathSync` with `null` for every path. Use wasi-sdk's
82
+ sysroot (22 or later) when a program needs `realpathSync` to resolve. Then:
78
83
 
79
84
  ```bash
80
85
  nish examples/argv.ts --link build/argv.wasm --profile wasi
@@ -218,7 +223,8 @@ routes are
218
223
  # in the glibc container, where build/nish runs
219
224
  build/nish app.ts -o app.ll
220
225
  # on the musl host, with its own clang and the runtime from this repository
221
- clang app.ll runtime/runtime.c runtime/runtime-os.c -lm -o app
226
+ clang app.ll runtime/runtime.c runtime/runtime-os.c runtime/runtime-parallel.c \
227
+ runtime/runtime-host.c runtime/runtime-net.c -lm -o app
222
228
  ```
223
229
 
224
230
  `--link`ing inside the container is the mistake to avoid: it shells out to the
@@ -285,21 +291,20 @@ to the machine that built it.
285
291
 
286
292
  The last column is there because a release is a past event and a workflow is
287
293
  not. `release.yml` builds all four, but a release already published cannot grow
288
- an asset: **v0.2.0, the current release, attaches `x86_64-linux` only**. Those
294
+ an asset: **the releases before v0.4.0 attach `x86_64-linux` only**. Those
289
295
  versions are not prose — they are `attachedSince` in
290
296
  [`.github/seed-targets.json`](https://github.com/amritk/nish/blob/main/.github/seed-targets.json),
291
297
  the same file the release workflow builds its matrix from, so this table and
292
298
  the assets cannot drift apart without a test failing.
293
299
 
294
- **The three new rows say v0.4.0 rather than v0.3.0 on purpose.** The workflow
295
- builds all four now, but nothing in this repository has ever run on macOS or on
296
- ARM Linux, and a row that has never run is not something to put in front of a
297
- release: `release` needs the whole matrix, so one red row blocks the publish.
298
- They wait for a release after the run that exercises them. The two macOS rows
299
- wait on a second thing as well — the bootstrap's `stage3 == stage2` check does
300
- not hold as a raw byte comparison under `ld64`, and which bytes actually differ
301
- has not been measured on real hardware, so a darwin row may turn up work rather
302
- than a green tick. `scripts/verify-binaries.sh`'s header is the long version.
300
+ **The three newer rows say v0.4.0 rather than v0.3.0 on purpose.** Nothing in
301
+ this repository had run on macOS or on ARM Linux before, and a row that has
302
+ never run is not something to put in front of a release: `release` needs the
303
+ whole matrix, so one red row blocks the publish. So they waited until a run on
304
+ their own hardware had exercised them. The two macOS rows waited on a second
305
+ thing as well: the bootstrap's `stage3 == stage2` check did not hold as a raw
306
+ byte comparison under `ld64` until both stages were linked at one output path.
307
+ `scripts/verify-binaries.sh`'s header is the long version.
303
308
 
304
309
  Check [the releases page](https://github.com/amritk/nish/releases/latest) for
305
310
  what a given version actually carries; on a platform whose row has not shipped
@@ -307,15 +312,15 @@ yet, take the npm package above, or build from source below.
307
312
 
308
313
  ```bash
309
314
  # pick the row above that matches `uname -s` and `uname -m`
310
- curl -LO https://github.com/amritk/nish/releases/download/v0.2.0/nish-0.2.0-x86_64-linux.tar.gz
311
- tar -xzf nish-0.2.0-x86_64-linux.tar.gz
312
- nish-0.2.0-x86_64-linux/bin/nish --version
315
+ curl -LO https://github.com/amritk/nish/releases/download/v0.16.0/nish-0.16.0-x86_64-linux.tar.gz
316
+ tar -xzf nish-0.16.0-x86_64-linux.tar.gz
317
+ nish-0.16.0-x86_64-linux/bin/nish --version
313
318
  ```
314
319
 
315
320
  Both URLs name the version rather than using GitHub's version-neutral
316
321
  `/releases/latest/download/` form, because that form needs the asset's exact
317
- file name and every asset name carries the version in it. **v0.2.0 is the
318
- current release**; v0.1.1 before it was the first one with a binary attached.
322
+ file name and every asset name carries the version in it. The example names
323
+ v0.16.0; v0.1.1 was the first release with a binary attached.
319
324
  The `v0.1.0` tag exists but has no release behind it and no assets, so every
320
325
  `v0.1.0` download URL is a 404 — the tag was pushed by a workflow, and GitHub
321
326
  raises no event for that, so nothing ever built it
@@ -333,8 +338,7 @@ find `scripts/build.sh`.
333
338
 
334
339
  It still needs `clang` and `lld` on `PATH` for `--link` (§1), because linking
335
340
  is the C toolchain's job, not the compiler's; what it does not need is Node.
336
- x86_64 Linux is the only platform built today — on anything else, take the
337
- `.tgz` above or build from a checkout.
341
+ On a platform outside the table, build from a checkout.
338
342
 
339
343
  From a checkout:
340
344
 
@@ -354,7 +358,7 @@ it into `build/seed/`; `NISH_BOOTSTRAP=<path>` names one you already have and
354
358
  skips the download. §2a has what the build does with it.
355
359
 
356
360
  The published package ships `bin/` (the `nish` command, which is a launcher),
357
- `runtime/` (the C runtime — two translation units and their header),
361
+ `runtime/` (the C runtime — five translation units and their header),
358
362
  `scripts/build.sh` (the link pipeline) and `std/` (the standard library). The
359
363
  compiler itself is not in it: it arrives as one prebuilt
360
364
  `@amritk/nish-<asset>` package per platform, and `bin/nish` hands over to
@@ -364,22 +368,27 @@ working directory — and the binary runs from inside its own platform package
364
368
  for exactly that reason.
365
369
 
366
370
  Building the IR yourself rather than through `--link` means naming the runtime
367
- on the `clang` line, and it is two files:
371
+ on the `clang` line, and it is five files:
368
372
 
369
373
  ```bash
370
- clang app.ll runtime/runtime.c runtime/runtime-os.c -lm -o app
374
+ clang app.ll runtime/runtime.c runtime/runtime-os.c runtime/runtime-parallel.c \
375
+ runtime/runtime-host.c runtime/runtime-net.c -lm -o app
371
376
  ```
372
377
 
373
- `runtime.c` is the half every program touches — the arena, strings, arrays,
374
- number formatting, the panics — and `runtime-os.c` is the half that wraps the
375
- system calls: files, directories, subprocesses, `getenv`, the monotonic clock.
376
- They are separate so that each carries its own measured size ceiling
377
- ([docs/wp7-runtime.md](wp7-runtime.md)); nothing in the core calls into the
378
- system-call half, so an older line that names `runtime.c` alone still links a
379
- program that reads no files and spawns nothing. `scripts/build.sh` compiles
380
- `runtime-os.c` beside any `runtime.c` it is handed, so a build that goes
381
- through it — every `--link`, and every `--profile` recipe in these documents —
382
- needs to name only the one.
378
+ `runtime.c` is the part every program touches — the arena, strings, arrays,
379
+ number formatting, the panics — and `runtime-os.c` wraps the system calls:
380
+ files, directories, subprocesses, `getenv`, the monotonic clock.
381
+ `runtime-parallel.c` divides work across threads for `nish/threads` (a
382
+ program built with `--threads`, as every importer of `nish/threads` is, also
383
+ needs `-DNISH_THREADS=1 -pthread` on the line), `runtime-host.c` holds the
384
+ wall clock, entropy, file times and signals, and `runtime-net.c` the sockets
385
+ of `nish:net`. They are separate so that each
386
+ carries its own measured size ceiling ([docs/wp7-runtime.md](wp7-runtime.md));
387
+ nothing in the core calls into the others, so a line that names only
388
+ `runtime.c` and `runtime-os.c` still links a program that uses none of the
389
+ other three. `scripts/build.sh` compiles the other four beside any
390
+ `runtime.c` it is handed, so a build that goes through it — every `--link`,
391
+ and every `--profile` recipe in these documents — needs to name only the one.
383
392
 
384
393
  ## 2a. What `npm run build` does
385
394
 
@@ -402,7 +411,7 @@ release is expected to move it. `--stages 1` stops one link sooner.
402
411
  `build/nish` is then the compiler you run: it takes the same `-o`, `--link`
403
412
  and `--profile` spellings as an installed `nish`, because it is the same
404
413
  program, and makes every directory in the way of the IR, a sidecar or the
405
- binary itself. It looks for `scripts/build.sh` and the two `runtime/*.c` files
414
+ binary itself. It looks for `scripts/build.sh` and the `runtime/*.c` files
406
415
  one level up from wherever it was invoked (and one level up from the file a
407
416
  symbolic link to it names), so it wants a checkout or an installed package
408
417
  around it the way `nish` does. It falls back to the working directory only
package/bin/nish CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/nish-x86_64-linux",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "The @amritk/nish native compiler for Linux on x86_64",
5
5
  "license": "MIT",
6
6
  "os": [
package/runtime/nish.d.ts CHANGED
@@ -112,9 +112,10 @@ declare function Err<T, E>(error: E): Result<T, E>;
112
112
 
113
113
  /**
114
114
  * The disposable protocol `using` reads (WP29 P2, docs/wp29-thread-surface.md
115
- * §5): declared here so that a program using `nish/threads`'s scope needs no
116
- * `"ESNext.Disposable"` in its `lib`. `nish` itself takes `using` only for a
117
- * `scope()`, and `[Symbol.dispose]` only in `nish/threads`.
115
+ * §5): declared here so that a program using `nish/threads`'s scope or
116
+ * `arena()` needs no `"ESNext.Disposable"` in its `lib`. `nish` itself takes
117
+ * `using` only for a `scope()` or an `arena()`, and `[Symbol.dispose]` only in
118
+ * `nish/threads`.
118
119
  */
119
120
  interface SymbolConstructor {
120
121
  readonly dispose: unique symbol;
@@ -172,6 +173,39 @@ declare function bitsToF64(bits: i64): f64;
172
173
  declare function ctSelect<T extends u32 | u64>(mask: T, a: T, b: T): T;
173
174
  /** All-ones of the operands' type when `a === b`, zero otherwise, without a branch. */
174
175
  declare function ctEq<T extends u32 | u64>(a: T, b: T): T;
176
+ /**
177
+ * Every byte of `bytes` set to zero, in stores neither `-O2` nor `-flto` may
178
+ * drop as dead (#385): how a key, a secret scalar or another secret is cleared
179
+ * once it is used. A statement.
180
+ */
181
+ declare function secureZero(bytes: u8[]): void;
182
+ /**
183
+ * One `lstat` of `path`, so a symbolic link answers for itself: the owner's uid
184
+ * in the high 32 bits and `st_mode` in the low 32, or -1 when the path does not
185
+ * resolve (#386).
186
+ */
187
+ declare function lstatOwnerModeSync(path: string): i64;
188
+ /** The effective user id, the one to compare an owner from `lstatOwnerModeSync` with. */
189
+ declare function geteuid(): i64;
190
+ /** Whether the real user may run `path`: `access(path, X_OK)`. */
191
+ declare function isExecutableSync(path: string): boolean;
192
+
193
+ // ---- Unsafe (`nish:unsafe`) --------------------------------------------------
194
+ //
195
+ // Declared as globals so `tsc` reads a program that imports them, as every
196
+ // `nish:` export is, but `nish` refuses a call to any of the five that is not
197
+ // imported from `nish:unsafe`: the import is the module's visible opt-in.
198
+
199
+ /** `xs[i]` with no bounds check: undefined behaviour when `i` is out of range. */
200
+ declare function uncheckedGet<T extends number>(xs: readonly T[], i: i32): T;
201
+ /** `xs[i] = v` with no bounds check: undefined behaviour when `i` is out of range. */
202
+ declare function uncheckedSet<T extends number>(xs: T[], i: i32, v: T): void;
203
+ /** `a + b` wrapped to the operands' width, `i32` or `i64`: defined for every pair. */
204
+ declare function wrappingAdd<T extends i32 | i64>(a: T, b: T): T;
205
+ /** `a - b` wrapped to the operands' width. */
206
+ declare function wrappingSub<T extends i32 | i64>(a: T, b: T): T;
207
+ /** `a * b` wrapped to the operands' width. */
208
+ declare function wrappingMul<T extends i32 | i64>(a: T, b: T): T;
175
209
 
176
210
  // ---- Streams and files (globals: Nish has no package resolution) ---------
177
211
 
@@ -200,7 +234,14 @@ declare function readFileSyncOrNull(path: string): string | null;
200
234
  * `readFileSyncOrNull` answers `null` for.
201
235
  */
202
236
  declare function readFileBytesSync(path: string): u8[] | null;
237
+ /**
238
+ * The file's contents replaced by `data`, created at 0644 when missing. A
239
+ * symbolic link as the last component of the path is refused rather than
240
+ * followed, natively and under Node alike, and a path that cannot be written
241
+ * prints `nish: cannot write <path>` and exits 1.
242
+ */
203
243
  declare function writeFileSync(path: string, data: string): void;
244
+ /** `data` added at the end of the file, with the same refusal of a symbolic link. */
204
245
  declare function appendFileSync(path: string, data: string): void;
205
246
  /** One directory, not recursive; whether a directory is there afterwards. */
206
247
  declare function mkdirSync(path: string): boolean;
@@ -221,7 +262,8 @@ declare function realpathSync(path: string): string | null;
221
262
  declare function spawnSync(argv: string[]): number;
222
263
  /**
223
264
  * The same run with each non-empty path receiving that stream, created or
224
- * truncated; an empty string leaves that stream inherited.
265
+ * truncated; an empty string leaves that stream inherited. A symbolic link at
266
+ * either path is refused, as `writeFileSync` refuses one, and the answer is -1.
225
267
  */
226
268
  declare function spawnSyncTo(argv: string[], stdoutPath: string, stderrPath: string): number;
227
269
  /** One environment variable, or `null` when it is unset (an empty value is a set variable). */
@@ -252,9 +294,10 @@ declare function readSignal(fd: i32): i32;
252
294
  //
253
295
  // Every call answers an `i32`: `>= 0` on success, a negative errno otherwise, in
254
296
  // Linux's numbering on every platform for -11 (would block), -95, -32, -104,
255
- // -98 and -22. An address is 18 bytes of a `u8[]`: 16 of IPv6 address (IPv4 as
256
- // `::ffff:a.b.c.d`), then the port, big-endian. Every socket is non-blocking and
257
- // close-on-exec. No reading under Node, where each throws.
297
+ // -98, -111 (refused), -110 (timed out) and -22. An address is 18 bytes of a
298
+ // `u8[]`: 16 of IPv6 address (IPv4 as `::ffff:a.b.c.d`), then the port,
299
+ // big-endian. Every socket is non-blocking and close-on-exec. No reading under
300
+ // Node, where each throws.
258
301
 
259
302
  /** Write the address form of a numeric `host` and `port` into `out`: 0, or -22. */
260
303
  declare function netAddress(out: u8[], host: string, port: i32): i32;
@@ -272,6 +315,10 @@ declare function netWrite(fd: i32, buf: readonly u8[], off: i32, len: i32): i32;
272
315
  declare function netShutdown(fd: i32, how: i32): i32;
273
316
  /** Close the descriptor. */
274
317
  declare function netClose(fd: i32): i32;
318
+ /** A TCP socket connecting to `addr`, answered while it connects: writable once it has, or has failed. */
319
+ declare function tcpConnect(addr: readonly u8[]): i32;
320
+ /** Once `fd` is writable: 0 when the connection is made, else its failure (-111 refused). */
321
+ declare function connectResult(fd: i32): i32;
275
322
  /** A UDP socket: flag 1 `SO_REUSEPORT`, flag 2 `UDP_GRO` (-95 on Darwin); ECN is always read. */
276
323
  declare function udpBind(host: string, port: i32, flags: i32): i32;
277
324
  /** `buf[off, off + len)` to `to`, cut into `segment`-byte datagrams when `segment > 0`, with ECN bits `ecn`. */
@@ -325,7 +372,9 @@ declare module "nish:fs" {
325
372
  export function readFileSync(path: string): string;
326
373
  export function readFileSyncOrNull(path: string): string | null;
327
374
  export function readFileBytesSync(path: string): u8[] | null;
375
+ /** Refuses a symbolic link as the last component of the path, as the global does. */
328
376
  export function writeFileSync(path: string, data: string): void;
377
+ /** Refuses a symbolic link as the last component of the path, as the global does. */
329
378
  export function appendFileSync(path: string, data: string): void;
330
379
  /** `true` when the directory was created, `false` when it already existed. */
331
380
  export function mkdirSync(path: string): boolean;
@@ -347,7 +396,8 @@ declare module "nish:process" {
347
396
  export function spawnSync(argv: string[]): number;
348
397
  /**
349
398
  * The same run with each non-empty path receiving that stream, created or
350
- * truncated; an empty string leaves that stream inherited.
399
+ * truncated; an empty string leaves that stream inherited. A symbolic link
400
+ * at either path is refused, and the answer is -1.
351
401
  */
352
402
  export function spawnSyncTo(argv: string[], stdoutPath: string, stderrPath: string): number;
353
403
  /**
@@ -383,6 +433,10 @@ declare module "nish:net" {
383
433
  /** Shut the read side (0), the write side (1) or both (2). */
384
434
  export function netShutdown(fd: i32, how: i32): i32;
385
435
  export function netClose(fd: i32): i32;
436
+ /** A TCP socket connecting to `addr`, answered while it connects: writable once it has, or has failed. */
437
+ export function tcpConnect(addr: readonly u8[]): i32;
438
+ /** Once `fd` is writable: 0 when the connection is made, else its failure (-111 refused). */
439
+ export function connectResult(fd: i32): i32;
386
440
  /** A UDP socket: flag 1 `SO_REUSEPORT`, flag 2 `UDP_GRO` (-95 on Darwin); ECN is always read. */
387
441
  export function udpBind(host: string, port: i32, flags: i32): i32;
388
442
  /** `buf[off, off + len)` to `to`, cut into `segment`-byte datagrams when `segment > 0`, with ECN bits `ecn`. */
@@ -409,6 +463,19 @@ declare module "nish:net" {
409
463
  export function pollWait(loop: i32, ready: i32[], timeoutMs: i32): i32;
410
464
  }
411
465
 
466
+ declare module "nish:unsafe" {
467
+ /** `xs[i]` with no bounds check: undefined behaviour when `i` is out of range. */
468
+ export function uncheckedGet<T extends number>(xs: readonly T[], i: i32): T;
469
+ /** `xs[i] = v` with no bounds check: undefined behaviour when `i` is out of range. */
470
+ export function uncheckedSet<T extends number>(xs: T[], i: i32, v: T): void;
471
+ /** `a + b` wrapped to the operands' width, `i32` or `i64`. */
472
+ export function wrappingAdd<T extends i32 | i64>(a: T, b: T): T;
473
+ /** `a - b` wrapped to the operands' width. */
474
+ export function wrappingSub<T extends i32 | i64>(a: T, b: T): T;
475
+ /** `a * b` wrapped to the operands' width. */
476
+ export function wrappingMul<T extends i32 | i64>(a: T, b: T): T;
477
+ }
478
+
412
479
  declare module "nish:io" {
413
480
  /** `s` to stdout with no trailing newline and no conversion. */
414
481
  export function write(s: string): void;
@@ -418,6 +485,39 @@ declare module "nish:io" {
418
485
  export function panic(message: string): never;
419
486
  }
420
487
 
488
+ // `nish:secret` has source behind it, `std/secret.ts`, but a program imports it
489
+ // by this name, so `tsc` reads these declarations rather than that file. The
490
+ // class is declared with a private constructor and a private field: `tsc` then
491
+ // refuses `new Secret(...)`, `s.value` and a structural look-alike, as `nish`
492
+ // does. Everything else `nish` refuses about a `Secret` — printing it,
493
+ // comparing it, branching on it, dropping it unwiped, an `expose` whose
494
+ // function does I/O — is flow, which `tsc` cannot see
495
+ // (docs/LANGUAGE.md -> Secrets).
496
+ declare module "nish:secret" {
497
+ /**
498
+ * Key material: an array of integers or a record of integer fields, held
499
+ * opaque. Made by `secret`, read only through `expose`, and wiped or
500
+ * returned by the function that made it.
501
+ */
502
+ export class Secret<T> {
503
+ private constructor();
504
+ private readonly value: T;
505
+ }
506
+ /** `value`, wrapped. A local handed in is moved: it may not be read again. */
507
+ export function secret<T>(value: T): Secret<T>;
508
+ /**
509
+ * `f(value)`. `f` reaches no I/O and no C, keeps nothing of `value`, and
510
+ * declares a return type that holds no `Secret`: that type is what leaves.
511
+ */
512
+ export function expose<T, R>(s: Secret<T>, f: (value: T) => R): R;
513
+ /** `f(value, arg)`, under `expose`'s rules; `f` may not write `arg` either. */
514
+ export function exposeWith<T, A, R>(s: Secret<T>, arg: A, f: (value: T, arg: A) => R): R;
515
+ /** Zero the value's whole storage, with a store the optimiser may not remove. */
516
+ export function wipe<T>(target: Secret<T>): void;
517
+ /** Zero an array or record of integers that `expose`'s function holds, likewise. */
518
+ export function wipe(target: object): void;
519
+ }
520
+
421
521
  // ---- Arena (docs/LANGUAGE.md -> Arena) ---------------------------------------
422
522
 
423
523
  declare const Arena: {
@@ -431,6 +531,15 @@ declare const Arena: {
431
531
  used(): i64;
432
532
  };
433
533
 
534
+ /**
535
+ * `using a = arena()` (docs/LANGUAGE.md -> Arena): everything the block
536
+ * allocates after this declaration is released when the block ends, on every
537
+ * exit, and the compiler refuses the block if anything allocated in it could
538
+ * outlive it. Only a `using` initialiser, and the binding is never read; under
539
+ * Node there is no arena and the disposal does nothing.
540
+ */
541
+ declare function arena(): Disposable;
542
+
434
543
  /**
435
544
  * `CPtr` (WP27 S2): the address a `declare function` hands back, opaque and
436
545
  * eight bytes wide. `docs/LANGUAGE.md` has the rules — it may be written in a
package/runtime/nish.h CHANGED
@@ -3,15 +3,17 @@
3
3
  * Include this from C drivers, N-API shims, or any other host that links the C
4
4
  * runtime next to compiled Nish modules. Everything here is a contract shared
5
5
  * with `src/runtime.ts` (the IR side) and the implementation, which is
6
- * two translation units: `runtime/runtime.c` holds the core every program
7
- * touches — the arena, strings, arrays, number formatting, the panics — and
6
+ * several translation units: `runtime/runtime.c` holds the core every program
7
+ * touches — the arena, strings, arrays, number formatting, the panics —
8
8
  * `runtime/runtime-os.c` holds everything that wraps a system call: the file
9
9
  * functions, the directory and subprocess calls, `nish_getenv`, the monotonic
10
- * clock, `nish_platform` / `nish_arch`. They are separate so that each carries
10
+ * clock, `nish_platform` / `nish_arch` — and runtime-parallel.c, runtime-host.c,
11
+ * runtime-net.c and runtime-simd.c (the byte searches that read a vector at a
12
+ * time) split off the same way since. They are separate so that each carries
11
13
  * its own measured code-size ceiling (docs/wp7-runtime.md, "Runtime additions
12
- * and budget"); a host links both, which `scripts/build.sh` does for you when
13
- * handed either. `tests/run.js` checks that every runtime function declared in
14
- * runtime.ts has a prototype in this file.
14
+ * and budget"); a host links all of them, which `scripts/build.sh` does for
15
+ * you when handed runtime.c. `tests/run.js` checks that every runtime function
16
+ * declared in runtime.ts has a prototype in this file.
15
17
  *
16
18
  * Headers generated by `nish --emit-header` include this one, so keep
17
19
  * it C11-clean under -Wall -Wextra -Werror and usable from C++.
@@ -137,6 +139,18 @@ bool nish_str_at(const nish_str *s, int64_t at, const nish_str *sub);
137
139
  /* `s.indexOf(sub)`: the first byte offset where `sub` occurs, or -1. An empty
138
140
  needle answers 0 and one longer than `s` answers -1, as in JavaScript. */
139
141
  int64_t nish_str_index_of(const nish_str *s, const nish_str *sub);
142
+ /* `s.indexOf(sub, from)`: the same search from byte `from`, which must already
143
+ be clamped into [0, s->len]; an empty needle answers `from`. Outside that
144
+ range nothing outside `s` is read: a non-empty needle answers -1 and an
145
+ empty one answers `from` unchanged. */
146
+ int64_t nish_str_index_of_from(const nish_str *s, const nish_str *sub, int64_t from);
147
+ /* First index i >= from with s[i] in set, or -1.
148
+ from is already clamped to [0, len] by the caller; set holds 1..16 bytes.
149
+ It lives in runtime/runtime-simd.c, which picks a scalar, a baseline vector
150
+ or an AVX2 path on the first call; `NISH_SIMD=scalar|base|avx2` pins it. A
151
+ set outside 1..16 bytes is answered by the scalar path rather than refused,
152
+ but only 1..16 is the contract. Variable-time: never call it on a secret. */
153
+ int64_t nish_str_index_of_any(const nish_str *s, const nish_str *set, int64_t from);
140
154
  /* `console.log(s)`: one write(2) of the bytes plus a newline to stdout. */
141
155
  void nish_print(const nish_str *s);
142
156
  /* `console.error` and the newline-free `write` / `writeError`: fd 1 or 2. */
@@ -225,6 +239,11 @@ nish_array *nish_alloc_array(uint64_t elem_size, uint64_t len);
225
239
  * elements themselves — which is why the checker refuses to let a program hold
226
240
  * a pointer to one across a `push` (WP15 section 2a). */
227
241
  void nish_array_grow(nish_array *a, uint64_t elem_size);
242
+ /* `secureZero(bytes)` (#385): sets every byte of `bytes` to zero through
243
+ * `volatile` stores, out of line, so that neither `-O2` nor `-flto` can drop
244
+ * them as dead when the buffer's life ends straight after the call. For
245
+ * secret material -- a private key, a secret scalar -- once it is used. */
246
+ void nish_wipe(nish_array *bytes);
228
247
  void nish_panic_index(uint64_t idx, uint64_t len);
229
248
  /* The failed range check of `s.slice(start, end)` (WP15 section 4): prints the
230
249
  half-open interval that was asked for and the byte length it left, then
@@ -313,7 +332,12 @@ nish_str *nish_getenv(const nish_str *name);
313
332
  * but the last must exist for POSIX `realpath` to answer at all.
314
333
  *
315
334
  * `path` is read and never retained. The answer is a fresh arena string, so
316
- * it outlives the call the way every other string here does. */
335
+ * it outlives the call the way every other string here does.
336
+ *
337
+ * Under the wasi profile the answer is the sysroot's `realpath`, which
338
+ * wasi-sdk 22 and later have. A wasi-libc without one (the one Ubuntu and
339
+ * Debian package) answers NULL for every path, which a caller already
340
+ * handles. */
317
341
  nish_str *nish_realpath(const nish_str *path);
318
342
 
319
343
  /* ---- What machine this is (WP14 §7a) ------------------------------------
@@ -361,8 +385,9 @@ double nish_stat_mtime(const nish_str *path);
361
385
  * or a read that fails. */
362
386
  int32_t nish_signal_fd(void);
363
387
  int32_t nish_read_signal(int32_t fd);
364
- /* Who owns a path, and whether it runs (docs/security/runtime.md, RT-9; no
365
- * builtin reaches these yet). `nish_lstat_owner_mode(path)`: `lstat`, so a
388
+ /* Who owns a path, and whether it runs (docs/security/runtime.md, RT-9; the
389
+ * builtins `lstatOwnerModeSync`, `geteuid` and `isExecutableSync`, #386).
390
+ * `nish_lstat_owner_mode(path)`: `lstat`, so a
366
391
  * symbolic link answers for itself, as the owner's uid in the high 32 bits
367
392
  * and `st_mode` in the low 32, or exactly -1 when the path does not resolve.
368
393
  * `nish_euid()`: the effective uid. `nish_is_executable(path)`:
@@ -375,10 +400,11 @@ bool nish_is_executable(const nish_str *path);
375
400
  * Every call answers an `int32_t`: `>= 0` on success (a descriptor, a byte
376
401
  * count, 0) and a negative errno otherwise, in Linux's numbering on every
377
402
  * platform for -11 (would block), -95 (unsupported), -32 (the peer is gone),
378
- * -104 (reset), -98 (address in use) and -22 (a bad argument). Every socket
379
- * is non-blocking and close-on-exec. An address is 18 bytes of a `u8[]`
380
- * (element type `uint8_t`): 16 of IPv6 address, IPv4 as `::ffff:a.b.c.d`,
381
- * then the port, big-endian; a shorter array is -22. Nothing allocates.
403
+ * -104 (reset), -98 (address in use), -111 (refused), -110 (timed out) and
404
+ * -22 (a bad argument). Every socket is non-blocking and close-on-exec. An
405
+ * address is 18 bytes of a `u8[]` (element type `uint8_t`): 16 of IPv6
406
+ * address, IPv4 as `::ffff:a.b.c.d`, then the port, big-endian; a shorter
407
+ * array is -22. Nothing allocates.
382
408
  *
383
409
  * `nish_net_address(out, host, port)`: the form of a numeric `host`.
384
410
  * `nish_net_local_port(fd)`: the port a socket is bound to.
@@ -389,6 +415,10 @@ bool nish_is_executable(const nish_str *path);
389
415
  * len)` of `buf`, which the compiled call has range-checked; a write to a
390
416
  * gone peer is -32, never SIGPIPE. `nish_net_shutdown(fd, how)`: 0 read, 1
391
417
  * write, 2 both. `nish_net_close(fd)`.
418
+ * `nish_tcp_connect(addr)`: a new socket connecting to `addr`, answered at
419
+ * once; its `EINPROGRESS` is not a failure, and any other closes it.
420
+ * `nish_connect_result(fd)`: `SO_ERROR` once the socket is writable, 0 when
421
+ * the connection is made.
392
422
  *
393
423
  * UDP: `nish_udp_bind(host, port, flags)` binds a datagram socket, flag 1
394
424
  * `SO_REUSEPORT` and flag 2 `UDP_GRO` (-95 on Darwin). `nish_udp_send_to(fd,
@@ -418,6 +448,8 @@ int32_t nish_net_read(int32_t fd, nish_array *buf, int64_t off, int64_t len);
418
448
  int32_t nish_net_write(int32_t fd, const nish_array *buf, int64_t off, int64_t len);
419
449
  int32_t nish_net_shutdown(int32_t fd, int32_t how);
420
450
  int32_t nish_net_close(int32_t fd);
451
+ int32_t nish_tcp_connect(const nish_array *addr);
452
+ int32_t nish_connect_result(int32_t fd);
421
453
  int32_t nish_udp_bind(const nish_str *host, int32_t port, int32_t flags);
422
454
  int32_t nish_udp_send_to(int32_t fd, const nish_array *buf, int64_t off, int64_t len, const nish_array *to,
423
455
  int32_t segment, int32_t ecn);
@@ -439,6 +471,11 @@ double nish_parse_number(const nish_str *s, int32_t mode);
439
471
  /* Checked integer division (Rust semantics): the failed-check path. */
440
472
  void nish_panic_div(bool by_zero);
441
473
 
474
+ /* Checked signed arithmetic: the failed-check path. `op` is 0 add, 1
475
+ * subtract, 2 multiply, 3 negate; prints "attempt to <op> with overflow" and
476
+ * exits 1. */
477
+ void nish_panic_overflow(uint32_t op);
478
+
442
479
  /* ---- Parallel work (WP20 T1 / wp29 stage P1), runtime/runtime-parallel.c ----
443
480
  *
444
481
  * One region of work, divided. `nish_parallel_range` calls `body(lo, hi, ctx)`
package/runtime/nish.mjs CHANGED
@@ -16,14 +16,14 @@
16
16
  * That is enough for a real and useful subset — **f64 mode**, where JavaScript's
17
17
  * `+ - * / %` on doubles *are* `fadd/fsub/fmul/fdiv/frem` and `Math.*` are the
18
18
  * same IEEE operations, so the arithmetic needs no help. It is not enough for
19
- * i32 mode, and it never will be: `number` is a wrapping 32-bit integer there
20
- * and every operator would have to change.
19
+ * i32 mode, and it never will be: `number` is a 32-bit integer there, whose
20
+ * signed overflow panics, and every operator would have to change.
21
21
  *
22
22
  * `docs/RUN_UNDER_NODE.md` states the overlap and the whole list of what stays
23
23
  * divergent. The short version, because it belongs next to the code too:
24
24
  *
25
- * - **i32 mode is out.** Wrapping, truncating `/`, and the divide-by-zero
26
- * panic are all operator-level and cannot be reached from here.
25
+ * - **i32 mode is out.** The overflow panic, truncating `/`, and the
26
+ * divide-by-zero panic are all operator-level and cannot be reached from here.
27
27
  * - **`s.length` is UTF-16 units under Node** and UTF-8 bytes natively, and
28
28
  * so is every string offset. ASCII agrees; nothing else does.
29
29
  * - **`a[i]` is unchecked**: out of range is `undefined` here and an exit-1
@@ -109,6 +109,8 @@ for (const name of [
109
109
  "netWrite",
110
110
  "netShutdown",
111
111
  "netClose",
112
+ "tcpConnect",
113
+ "connectResult",
112
114
  "udpBind",
113
115
  "udpSendTo",
114
116
  "udpRecvFrom",
@@ -174,6 +176,49 @@ provide("bitsToF64", shim.bitsToF64);
174
176
  // agree with a native run; the timing does not, and is not claimed here.
175
177
  provide("ctSelect", shim.ctSelect);
176
178
  provide("ctEq", shim.ctEq);
179
+ // `secureZero` (#385): the bytes are zero afterwards here too, though what a
180
+ // collector may have copied before is out of reach.
181
+ provide("secureZero", shim.secureZero);
182
+ // The RT-9 owner checks (#386): one `lstat`, the effective uid and
183
+ // `access(X_OK)`, answered as `runtime-host.c` answers them.
184
+ provide("lstatOwnerModeSync", shim.lstatOwnerModeSync);
185
+ provide("geteuid", shim.geteuid);
186
+ provide("isExecutableSync", shim.isExecutableSync);
187
+
188
+ // `nish:unsafe`. The element access is JavaScript's own, which never checks
189
+ // anything; the wraps are the two's-complement ones the native instructions
190
+ // perform: `| 0` and `Math.imul` at `i32`, and `BigInt.asIntN(64, ...)` on
191
+ // the BigInt an `i64` is here. Pure functions of their operands, so a program
192
+ // that stays in range agrees with a native run.
193
+ const wrapAt = (a, b, wide, narrow) => (typeof a === "bigint" ? BigInt.asIntN(64, wide(a, b)) : narrow(a, b));
194
+ provide("uncheckedGet", (xs, i) => xs[i]);
195
+ provide("uncheckedSet", (xs, i, v) => {
196
+ xs[i] = v;
197
+ });
198
+ provide("wrappingAdd", (a, b) =>
199
+ wrapAt(
200
+ a,
201
+ b,
202
+ (x, y) => x + y,
203
+ (x, y) => (x + y) | 0
204
+ )
205
+ );
206
+ provide("wrappingSub", (a, b) =>
207
+ wrapAt(
208
+ a,
209
+ b,
210
+ (x, y) => x - y,
211
+ (x, y) => (x - y) | 0
212
+ )
213
+ );
214
+ provide("wrappingMul", (a, b) =>
215
+ wrapAt(
216
+ a,
217
+ b,
218
+ (x, y) => x * y,
219
+ (x, y) => Math.imul(x, y)
220
+ )
221
+ );
177
222
 
178
223
  // `Result`. Everything works but `orReturn`, which needs the caller's control
179
224
  // flow and therefore the rewriter; see the header.
@@ -200,15 +245,34 @@ provide("Arena", {
200
245
  reset: shim.arenaReset,
201
246
  used: shim.arenaUsed,
202
247
  });
248
+ // `using a = arena()` releases nothing here either: what the block allocated
249
+ // is the garbage collector's, and the disposal is a no-op.
250
+ provide("arena", shim.arena);
203
251
 
204
252
  // The standard library. A program imports it as `nish/<module>`, which the
205
253
  // compiler resolves to `std/<module>.ts` beside itself; this does the same for
206
254
  // Node, relative to this file rather than to the program, so the same
207
255
  // specifier works from any directory. Only the `nish/` package is answered
208
256
  // here, and every other specifier goes to Node as it was written.
257
+ //
258
+ // `nish:secret` is answered here as well, because the standard library imports
259
+ // it (`std/crypto/p256.ts` and its neighbours): its names are
260
+ // `runtime/shim.mjs`'s, where a `Secret` is a plain wrapper and `wipe`
261
+ // zero-fills it. `nish:unsafe` is answered by a module that hands back the
262
+ // globals installed above: its functions are globals of this module either
263
+ // way, and the import is what the compiler requires a program to write.
264
+ const UNSAFE_EXPORTS = ["uncheckedGet", "uncheckedSet", "wrappingAdd", "wrappingSub", "wrappingMul"];
265
+ const unsafeSource = UNSAFE_EXPORTS.map((name) => `export const ${name} = globalThis.${name};`).join("\n");
209
266
  registerHooks({
210
- resolve: (specifier, context, next) =>
211
- specifier.startsWith("nish/")
267
+ resolve: (specifier, context, next) => {
268
+ if (specifier === "nish:secret") {
269
+ return next(new URL("./shim.mjs", import.meta.url).href, context);
270
+ }
271
+ if (specifier === "nish:unsafe") {
272
+ return { url: `data:text/javascript,${encodeURIComponent(unsafeSource)}`, shortCircuit: true };
273
+ }
274
+ return specifier.startsWith("nish/")
212
275
  ? next(new URL(`../std/${specifier.slice("nish/".length)}.ts`, import.meta.url).href, context)
213
- : next(specifier, context),
276
+ : next(specifier, context);
277
+ },
214
278
  });