diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 95919e69f0..ed9ee0532b 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -17,6 +17,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `numeric-kernels` | cpu | sieve, hashing, `number[][]` matmul, `Float64Array` | | `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage | | `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables | +| `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs | | `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates | | `region-builds` | cpu | a watch-mode build loop over `LifetimeRegion`: each build binds a program in a new region and releases the previous one, which a cache keeps referenced for two builds | | `optional-fields` | cpu | records and class instances with `number \| undefined` and other scalar-union fields | diff --git a/benchmarks/runtime/workloads.json b/benchmarks/runtime/workloads.json index c84001e08e..c0d643a583 100644 --- a/benchmarks/runtime/workloads.json +++ b/benchmarks/runtime/workloads.json @@ -127,6 +127,13 @@ "100000" ] }, + { + "name": "atomics-contended", + "entry": "workloads/atomics-contended.ts", + "args": [ + "2" + ] + }, { "name": "value-objects", "entry": "workloads/value-objects.ts", diff --git a/benchmarks/runtime/workloads/atomics-contended.ts b/benchmarks/runtime/workloads/atomics-contended.ts new file mode 100644 index 0000000000..7236f65b1c --- /dev/null +++ b/benchmarks/runtime/workloads/atomics-contended.ts @@ -0,0 +1,58 @@ +// Worker threads updating SharedArrayBuffer counters with Atomics: one +// counter for all workers, one counter each (on separate cache lines), a +// compareExchange loop, and a wait/notify handoff between pairs of workers. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +const WORKERS = 4; +const STRIDE = 16; // Int32 slots between counters: 64 bytes + +type Job = { buffer: SharedArrayBuffer; id: number; scale: number }; + +function counters(job: Job): void { + const cells = new Int32Array(job.buffer); + const own = STRIDE * (job.id + 4); + const ops = 400_000 * job.scale; + for (let i = 0; i < ops; i++) { + Atomics.add(cells, 0, 1); + Atomics.add(cells, own, 1); + } + for (let i = 0; i < ops / 4; i++) { + for (;;) { + const seen = Atomics.load(cells, STRIDE); + if (Atomics.compareExchange(cells, STRIDE, seen, seen + 1) === seen) break; + } + } + // Pairs (0, 1) and (2, 3) pass a token back and forth. + const turn = STRIDE * (2 + (job.id >> 1)); + const mine = job.id & 1; + const rounds = 2_000 * job.scale; + for (let i = 0; i < rounds; i++) { + while (Atomics.load(cells, turn) !== mine) Atomics.wait(cells, turn, 1 - mine); + Atomics.add(cells, turn + 1, 1); + Atomics.store(cells, turn, 1 - mine); + Atomics.notify(cells, turn); + } +} + +if (isMainThread) { + const scale = Number(process.argv[2] ?? "1"); + const buffer = new SharedArrayBuffer(4 * STRIDE * (WORKERS + 4)); + const cells = new Int32Array(buffer); + let exited = 0; + for (let id = 0; id < WORKERS; id++) { + const worker = new Worker(new URL(import.meta.url), { workerData: { buffer, id, scale } }); + worker.on("exit", (code: number) => { + if (code !== 0) console.log("worker exit", code); + exited++; + if (exited < WORKERS) return; + const own: number[] = []; + for (let t = 0; t < WORKERS; t++) own.push(Atomics.load(cells, STRIDE * (t + 4))); + console.log("shared", Atomics.load(cells, 0), "cas", Atomics.load(cells, STRIDE)); + console.log("own", own.join(",")); + console.log("handoffs", Atomics.load(cells, STRIDE * 2 + 1), Atomics.load(cells, STRIDE * 3 + 1)); + }); + } +} else { + counters(workerData as Job); + parentPort!.postMessage("done"); +} diff --git a/docs/content/limitations/builtin-apis/compression.mdx b/docs/content/limitations/builtin-apis/compression.mdx index 40fd958747..4d70588b18 100644 --- a/docs/content/limitations/builtin-apis/compression.mdx +++ b/docs/content/limitations/builtin-apis/compression.mdx @@ -1 +1 @@ -- **Compression:** `zlib.deflateSync`, `deflateRawSync`, and `gzipSync` accept default options or an inline `{ level: n }` with an integer literal from `-1` through `9`. Other options and runtime-valued levels are unsupported. +- **Compression:** `zlib.deflateSync`, `deflateRawSync`, and `gzipSync` accept default options or an inline `{ level: n }` with an integer literal from `-1` through `9`. Other options and runtime-valued levels are unsupported. The `Deflate`, `Inflate`, `Gzip`, `Gunzip`, `DeflateRaw`, `InflateRaw`, and `Unzip` streams, from `createGzip()` and the other factories or `new zlib.Gzip()`, accept Node's options and support writes, piping, `pipeline`, async iteration, `flush`, `params`, `close`, `reset`, and `bytesWritten`. Each output chunk is its own Buffer rather than a slice of a shared chunk-size buffer. `zlib.constants` members and their module-level copies read as Node's values. `util.inspect`, `instanceof zlib.Gzip`, and Brotli and Zstd compression are unsupported. diff --git a/docs/content/limitations/native-worker-threads.mdx b/docs/content/limitations/native-worker-threads.mdx index 7600d639bc..061d844f2e 100644 --- a/docs/content/limitations/native-worker-threads.mdx +++ b/docs/content/limitations/native-worker-threads.mdx @@ -2,6 +2,6 @@ Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. Functions, proxies, other native handles, and nonempty transfer lists are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads). -SharedArrayBuffer storage is fixed-length. Atomics supports integer `load`, `store`, `exchange`, `compareExchange`, `add`, `sub`, `and`, `or`, and `xor` over 8-, 16-, and 32-bit integer arrays. `wait` and `notify` use Int32Array views; BigInt atomics, `waitAsync`, and growable buffers are unsupported. `terminate` unwinds at runtime checkpoints, including loops and parked atomic waits, without running JavaScript catch or finally blocks. Blocking native calls finish before the next checkpoint. +SharedArrayBuffer storage is fixed-length. Atomics supports integer `load`, `store`, `exchange`, `compareExchange`, `add`, `sub`, `and`, `or`, and `xor` over 8-, 16-, and 32-bit integer arrays. `wait` and `notify` use Int32Array views; BigInt atomics, `waitAsync`, and growable buffers are unsupported. Atomics operations run as sequentially consistent atomic instructions and take no lock. Other reads and writes of shared storage access each element atomically; a copy, fill, or conversion that runs while another thread writes the same storage can observe some of those writes, as in Node. `terminate` unwinds at runtime checkpoints, including loops and parked atomic waits, without running JavaScript catch or finally blocks. Blocking native calls finish before the next checkpoint. Workers are unsupported in WASI, compiled libraries, and executables that use foreign-thread native callbacks. Ordinary callbacks that run on their owning script thread remain supported. Shared byte storage cannot expose unmanaged FFI pointers; copy it into an ordinary Buffer before passing it to native code. diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 73f55c4a3f..dee48bd323 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "39302473d10cd0c1b0bb", + "artifactVersion": "a6b4100283d614806a60", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index dfe7b0b72e..88c2ba3ac9 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -689,9 +689,9 @@ "apiEntries": 117, "static": { "supported": 0, - "partial": 20, + "partial": 35, "refused": 0, - "not-implemented": 97, + "not-implemented": 82, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -899,9 +899,9 @@ "apiEntries": 94, "static": { "supported": 0, - "partial": 30, - "refused": 4, - "not-implemented": 60, + "partial": 52, + "refused": 12, + "not-implemented": 30, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -66102,9 +66102,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66125,9 +66125,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66148,9 +66148,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66171,9 +66171,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66240,9 +66240,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66470,9 +66470,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66907,9 +66907,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66930,9 +66930,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -66953,9 +66953,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -67091,9 +67091,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -67114,9 +67114,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -67137,9 +67137,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -67183,9 +67183,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -67206,9 +67206,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -67229,9 +67229,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the documented scriptc module-loader subset.", + "verification": "test-backed" } }, { @@ -84806,9 +84806,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "refused", + "detail": "Explicitly rejected by a named compiler diagnostic.", + "verification": "explicit-refusal" } }, { @@ -84829,9 +84829,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "refused", + "detail": "Explicitly rejected by a named compiler diagnostic.", + "verification": "explicit-refusal" } }, { @@ -84852,9 +84852,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -84875,9 +84875,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -84898,9 +84898,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -84921,9 +84921,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -84944,9 +84944,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -84967,9 +84967,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -84990,9 +84990,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -85013,9 +85013,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -85036,9 +85036,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -85059,9 +85059,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -85082,9 +85082,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -85105,9 +85105,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -85128,9 +85128,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -85220,9 +85220,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "refused", + "detail": "Explicitly rejected by a named compiler diagnostic.", + "verification": "explicit-refusal" } }, { @@ -85243,9 +85243,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "refused", + "detail": "Explicitly rejected by a named compiler diagnostic.", + "verification": "explicit-refusal" } }, { @@ -85266,9 +85266,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85289,9 +85289,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85312,9 +85312,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85335,9 +85335,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85358,9 +85358,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85381,9 +85381,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85404,9 +85404,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Implemented for the call shapes accepted by the compiler lowering.", + "verification": "test-backed" } }, { @@ -85473,9 +85473,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "refused", + "detail": "Explicitly rejected by a named compiler diagnostic.", + "verification": "explicit-refusal" } }, { @@ -85519,9 +85519,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "refused", + "detail": "Explicitly rejected by a named compiler diagnostic.", + "verification": "explicit-refusal" } }, { @@ -85979,9 +85979,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Each zlib.constants member and its module-level copy reads as Node's value; the constants object itself is not a runtime value.", + "verification": "test-backed" } }, { @@ -86002,9 +86002,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "partial", + "detail": "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported.", + "verification": "test-backed" } }, { @@ -86071,9 +86071,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "refused", + "detail": "Explicitly rejected by a named compiler diagnostic.", + "verification": "explicit-refusal" } }, { @@ -86117,9 +86117,9 @@ "inherited": true }, "static": { - "status": "not-implemented", - "detail": "No static compiler lowering is registered for this API yet.", - "verification": "registry-gap" + "status": "refused", + "detail": "Explicitly rejected by a named compiler diagnostic.", + "verification": "explicit-refusal" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 20e788accf..d51a3e486b 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -7,9 +7,9 @@ "tierItemCount": 3472, "tiers": { "static": { - "audit-partial": 991, - "verify-gap": 2362, - "replace-refusal": 32, + "audit-partial": 1028, + "verify-gap": 2317, + "replace-refusal": 40, "classify": 2, "implement": 85 } @@ -67323,12 +67323,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.before", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -67348,12 +67352,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.beforeEach", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -67373,12 +67381,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.after", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -67398,12 +67410,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.afterEach", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -67475,12 +67491,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.plan", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -67733,12 +67753,16 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.context.fullName", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68212,12 +68236,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.suite.skip", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68237,12 +68266,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.suite.todo", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68262,12 +68296,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.suite.only", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68422,12 +68461,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.describe.skip", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68447,12 +68491,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.describe.todo", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68472,12 +68521,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.describe.only", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68524,12 +68578,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.it.skip", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68549,12 +68608,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.it.todo", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -68574,12 +68638,17 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:test", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:test.it.only", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] } } }, @@ -85708,11 +85777,11 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", + "status": "refused", + "action": "replace-refusal", + "verification": "explicit-refusal", + "confidence": "high", + "source": "surface-manifest:unsupported:node-builtin.zlib.BrotliCompress", "tests": [] } } @@ -85733,11 +85802,11 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", + "status": "refused", + "action": "replace-refusal", + "verification": "explicit-refusal", + "confidence": "high", + "source": "surface-manifest:unsupported:node-builtin.zlib.BrotliDecompress", "tests": [] } } @@ -85758,12 +85827,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.Deflate", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -85783,12 +85860,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.DeflateRaw", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -85808,12 +85893,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.Gunzip", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -85833,12 +85926,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.Gzip", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -85858,12 +85959,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.Inflate", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -85883,12 +85992,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.InflateRaw", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -85908,12 +86025,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.Unzip", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -85933,12 +86058,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.ZlibBase", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -85958,12 +86091,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.close", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -85983,12 +86124,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.flush", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -86008,12 +86157,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.params", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -86033,12 +86190,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.reset", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -86058,12 +86223,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.bytesWritten", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -86141,7 +86314,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86162,11 +86338,11 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", + "status": "refused", + "action": "replace-refusal", + "verification": "explicit-refusal", + "confidence": "high", + "source": "surface-manifest:unsupported:node-builtin.zlib.createBrotliCompress", "tests": [] } } @@ -86187,11 +86363,11 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", + "status": "refused", + "action": "replace-refusal", + "verification": "explicit-refusal", + "confidence": "high", + "source": "surface-manifest:unsupported:node-builtin.zlib.createBrotliDecompress", "tests": [] } } @@ -86212,12 +86388,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.zlib.createDeflate", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] } } }, @@ -86237,12 +86420,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.zlib.createDeflateRaw", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] } } }, @@ -86262,12 +86452,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.zlib.createGunzip", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] } } }, @@ -86287,12 +86484,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.zlib.createGzip", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] } } }, @@ -86312,12 +86516,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.zlib.createInflate", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] } } }, @@ -86337,12 +86548,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.zlib.createInflateRaw", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] } } }, @@ -86362,12 +86580,19 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "surface-manifest:node-builtin.zlib.createUnzip", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] } } }, @@ -86437,11 +86662,11 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", + "status": "refused", + "action": "replace-refusal", + "verification": "explicit-refusal", + "confidence": "high", + "source": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress", "tests": [] } } @@ -86487,11 +86712,11 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", + "status": "refused", + "action": "replace-refusal", + "verification": "explicit-refusal", + "confidence": "high", + "source": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress", "tests": [] } } @@ -86545,7 +86770,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86574,7 +86802,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86603,7 +86834,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86632,7 +86866,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86661,7 +86898,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86690,7 +86930,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86719,7 +86962,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86748,7 +86994,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86777,7 +87026,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86806,7 +87058,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86835,7 +87090,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86864,7 +87122,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86893,7 +87154,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -86922,7 +87186,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87043,12 +87310,15 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.zlib.constants", + "tests": [ + "tests/corpus/5376-zlib-constants.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts" + ] } } }, @@ -87068,12 +87338,20 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", - "tests": [] + "status": "partial", + "action": "audit-partial", + "verification": "test-backed", + "confidence": "high", + "source": "compiler-feature:zlib.Options", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] } } }, @@ -87143,11 +87421,11 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", + "status": "refused", + "action": "replace-refusal", + "verification": "explicit-refusal", + "confidence": "high", + "source": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress", "tests": [] } } @@ -87193,11 +87471,11 @@ "priority": "high", "tiers": { "static": { - "status": "not-implemented", - "action": "verify-gap", - "verification": "registry-gap", - "confidence": "low", - "source": "compiler-unmatched:zlib", + "status": "refused", + "action": "replace-refusal", + "verification": "explicit-refusal", + "confidence": "high", + "source": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress", "tests": [] } } @@ -87251,7 +87529,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87280,7 +87561,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87309,7 +87593,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87338,7 +87625,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87367,7 +87657,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87396,7 +87689,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87425,7 +87721,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87454,7 +87753,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87483,7 +87785,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87512,7 +87817,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87541,7 +87849,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87570,7 +87881,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87599,7 +87913,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } @@ -87628,7 +87945,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] } } diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index f45a631671..6265776a94 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -746,9 +746,9 @@ "apiEntries": 117, "static": { "supported": 0, - "partial": 20, + "partial": 35, "refused": 0, - "not-implemented": 97, + "not-implemented": 82, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -956,9 +956,9 @@ "apiEntries": 94, "static": { "supported": 0, - "partial": 30, - "refused": 4, - "not-implemented": 60, + "partial": 52, + "refused": 12, + "not-implemented": 30, "by-design": 0, "unreviewed": 0, "not-applicable": 0 @@ -90538,6 +90538,11 @@ "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts", + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts" ] }, @@ -92757,8 +92762,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.before", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -92780,8 +92790,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.beforeEach", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -92803,8 +92818,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.after", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -92826,8 +92846,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.afterEach", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -92898,8 +92923,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.plan", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93140,8 +93170,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.context.fullName", + "tests": [ + "tests/fixtures/node-test/cases/ctx-hooks.ts", + "tests/fixtures/node-test/cases/ctx-plan.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93583,8 +93618,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.suite.skip", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93606,8 +93647,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.suite.todo", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93629,8 +93676,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.suite.only", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93782,8 +93835,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.describe.skip", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93805,8 +93864,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.describe.todo", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93828,8 +93893,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.describe.only", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93877,8 +93948,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.it.skip", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93900,8 +93977,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.it.todo", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -93923,8 +94006,14 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:test" + "status": "partial", + "evidence": "compiler-feature:test.it.only", + "tests": [ + "tests/fixtures/node-test/cases/suite-variants.ts", + "tests/fixtures/node-test/cases/only-nested.ts", + "tests/fixtures/node-test/cases/todo-suites.ts", + "tests/harness/node-test.test.ts" + ] }, "anchorSource": "exact" }, @@ -114016,7 +114105,15 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5376-zlib-constants.cjs", + "tests/corpus/5377-zlib-stream-backpressure.cjs" ] }, "anchorSource": "chapter" @@ -114499,8 +114596,8 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "refused", + "evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliCompress" }, "anchorSource": "exact" }, @@ -114522,8 +114619,8 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "refused", + "evidence": "surface-manifest:unsupported:node-builtin.zlib.BrotliDecompress" }, "anchorSource": "exact" }, @@ -114545,8 +114642,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.Deflate", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114568,8 +114674,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.DeflateRaw", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114591,8 +114706,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.Gunzip", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114614,8 +114738,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.Gzip", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114637,8 +114770,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.Inflate", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114660,8 +114802,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.InflateRaw", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114683,8 +114834,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.Unzip", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114706,8 +114866,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.ZlibBase", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114729,8 +114898,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.close", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114752,8 +114930,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.flush", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114775,8 +114962,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.params", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114798,8 +114994,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.reset", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -114821,8 +115026,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.bytesWritten", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "ancestor" }, @@ -114895,7 +115109,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -114918,8 +115135,8 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "refused", + "evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliCompress" }, "anchorSource": "exact" }, @@ -114941,8 +115158,8 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "refused", + "evidence": "surface-manifest:unsupported:node-builtin.zlib.createBrotliDecompress" }, "anchorSource": "exact" }, @@ -114964,8 +115181,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.createDeflate", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] }, "anchorSource": "exact" }, @@ -114987,8 +115212,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.createDeflateRaw", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] }, "anchorSource": "exact" }, @@ -115010,8 +115243,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.createGunzip", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] }, "anchorSource": "exact" }, @@ -115033,8 +115274,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.createGzip", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] }, "anchorSource": "exact" }, @@ -115056,8 +115305,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.createInflate", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] }, "anchorSource": "exact" }, @@ -115079,8 +115336,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.createInflateRaw", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] }, "anchorSource": "exact" }, @@ -115102,8 +115367,16 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "surface-manifest:node-builtin.zlib.createUnzip", + "tests": [ + "tests/corpus/1404-zlib-crypto-bytes.ts", + "tests/corpus/2921-zlib-callbacks.ts", + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" + ] }, "anchorSource": "exact" }, @@ -115171,8 +115444,8 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "refused", + "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress" }, "anchorSource": "exact" }, @@ -115217,8 +115490,8 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "refused", + "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress" }, "anchorSource": "exact" }, @@ -115268,7 +115541,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115296,7 +115572,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115324,7 +115603,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115352,7 +115634,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115380,7 +115665,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115408,7 +115696,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115436,7 +115727,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115464,7 +115758,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115492,7 +115789,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115520,7 +115820,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115548,7 +115851,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115576,7 +115882,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115604,7 +115913,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115632,7 +115944,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "exact" @@ -115747,8 +116062,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.zlib.constants", + "tests": [ + "tests/corpus/5376-zlib-constants.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts" + ] }, "anchorSource": "exact" }, @@ -116138,8 +116457,17 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "partial", + "evidence": "compiler-feature:zlib.Options", + "tests": [ + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5372-zlib-stream-errors.cjs", + "tests/corpus/5373-zlib-stream-options.cjs", + "tests/corpus/5374-zlib-stream-flush-params.ts", + "tests/corpus/5375-zlib-stream-promises.ts", + "tests/corpus/5377-zlib-stream-backpressure.cjs" + ] }, "anchorSource": "exact" }, @@ -116230,8 +116558,8 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "refused", + "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliCompress" }, "anchorSource": "ancestor" }, @@ -116276,8 +116604,8 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:zlib" + "status": "refused", + "evidence": "surface-manifest:unsupported:node-builtin.zlib.brotliDecompress" }, "anchorSource": "ancestor" }, @@ -116327,7 +116655,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116355,7 +116686,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116383,7 +116717,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116411,7 +116748,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116439,7 +116779,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116467,7 +116810,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116495,7 +116841,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116523,7 +116872,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116551,7 +116903,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116579,7 +116934,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116607,7 +116965,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116635,7 +116996,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116663,7 +117027,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" @@ -116691,7 +117058,10 @@ "tests": [ "tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", - "tests/corpus/3115-zlib-static-compression-levels.ts" + "tests/corpus/3115-zlib-static-compression-levels.ts", + "tests/corpus/5370-zlib-stream-roundtrip.ts", + "tests/corpus/5371-zlib-stream-chunking.cjs", + "tests/corpus/5373-zlib-stream-options.cjs" ] }, "anchorSource": "ancestor" diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index 32fd1b5b64..dfefc6e486 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -626,6 +626,16 @@ function publicDetail(tier) { if (/^compiler-feature:process\.std(?:in|out|err)$/.test(source)) { return "Stored JavaScript process streams support native input listeners, pause/resume and buffered reads, synchronous output writes, and output write replacement; other stream operations remain unsupported."; } + if ( + /^compiler-feature:zlib\.(?:zlib\.(?:Deflate|Inflate|Gzip|Gunzip|DeflateRaw|InflateRaw|Unzip|ZlibBase|close|flush|params|reset)|bytesWritten|Options)$/.test( + source, + ) + ) { + return "Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw, and Unzip are Transform streams with Node's output chunking, flush, params, reset, error codes, and option validation; Brotli and Zstd streams remain unsupported."; + } + if (source === "compiler-feature:zlib.zlib.constants") { + return "Each zlib.constants member and its module-level copy reads as Node's value; the constants object itself is not a runtime value."; + } if (source === "compiler-feature:url.URL") { return "Supports absolute inputs and relative inputs with string or URL bases; setters and some WHATWG parsing behavior remain unsupported."; } diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 4a05d2195f..02d3135c74 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -167,7 +167,7 @@ "node-builtin.util.debug": ["tests/corpus/5192-util-debuglog.cjs", "tests/corpus/5193-util-debuglog.ts"], "node-builtin.util": ["tests/corpus/2440-console-inspect-args.ts", "tests/corpus/2912-promisify-fs-readfile.ts"], "node-builtin.worker_threads": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/native-worker-messages.ts", "tests/corpus/native-worker-references.ts"], - "node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts"], + "node-builtin.zlib": ["tests/corpus/1404-zlib-crypto-bytes.ts", "tests/corpus/2921-zlib-callbacks.ts", "tests/corpus/3115-zlib-static-compression-levels.ts", "tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5373-zlib-stream-options.cjs"], "stdlib.fetch": ["tests/harness/fetch-conformance.test.ts"], "stdlib.map": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/537-map-iter-drains.ts"], "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], @@ -330,8 +330,12 @@ { "chapter": "url", "signatures": ["`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/2946-url-port-hash.ts"] }, { "chapter": "url", "signatures": ["Legacy `urlObject`", "`urlObject.auth`", "`urlObject.hash`", "`urlObject.host`", "`urlObject.hostname`", "`urlObject.href`", "`urlObject.path`", "`urlObject.pathname`", "`urlObject.port`", "`urlObject.protocol`", "`urlObject.query`", "`urlObject.search`", "`urlObject.slashes`"], "status": "partial", "evidence": ["tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5224-url-format-objects.ts", "tests/fixtures/node-types/url-legacy.ts"] }, { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.diagnostic", "context.name"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] }, + { "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] }, + { "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] }, { "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts"] }, - { "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] } + { "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] }, + { "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] }, + { "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] } ], "dedicated": [ { "symbols": ["assert.doesNotThrow", "assert.doesNotReject"], "status": "partial", "evidence": ["tests/corpus/node24-no-error-assertions.ts", "tests/corpus/node24-no-error-assertions.cjs", "tests/corpus/node24-no-error-assertions-coercion.js", "tests/fixtures/node-types/no-error-assertions.ts", "tests/harness/project-config.test.ts"] }, diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index 94cc73248d..870dde633b 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -3116,6 +3116,7 @@ declare module "node:test" { } interface TestContext { readonly name: string; + readonly fullName: string; readonly assert: TestContextAssert; test(name: string, fn?: TestFn): Promise; test(name: string, options: TestOptions, fn?: TestFn): Promise; @@ -3515,9 +3516,12 @@ declare module "node:readline/promises" { /* node:zlib — the one-shot zlib/raw/gzip codecs lower for string/Buffer * inputs with Node's default options; libz links only into zlib-using * binaries. Callback forms use the executable worker pool. Options stay - * declared so reached uses receive a named SC2020 refusal; Brotli, - * streaming, and Zstd remain outside the lowered surface. */ + * declared so reached uses receive a named SC2020 refusal. The Deflate, + * Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw and Unzip streams are + * runtime Transform streams; Brotli and Zstd remain outside the lowered + * surface. */ declare module "zlib" { + import { Transform } from "stream"; export interface ZlibOptions { [option: string]: unknown; } @@ -3574,6 +3578,340 @@ declare module "zlib" { export function crc32(data: string | Uint8Array, value?: number): number; export function brotliCompressSync(data: string | Uint8Array): Buffer; export function brotliDecompressSync(data: Uint8Array): Buffer; + /* Brotli needs a codec the static runtime does not vendor: these are + * declared so their uses receive the named refusal. */ + export function brotliCompress(data: string | Uint8Array, callback: ZlibCallback): void; + export function brotliDecompress(data: Uint8Array, callback: ZlibCallback): void; + export function createBrotliCompress(options?: ZlibOptions): Transform; + export function createBrotliDecompress(options?: ZlibOptions): Transform; + export class BrotliCompress extends Transform { + constructor(options?: ZlibOptions); + } + export class BrotliDecompress extends Transform { + constructor(options?: ZlibOptions); + } + /* The stream classes: runtime Transform streams over the zlib engine + * (scr_zlib_stream.c). createGzip(options) and new Gzip(options) build + * the same stream; options are validated at runtime with Node's rules. */ + class ZlibBase extends Transform { + readonly bytesWritten: number; + flush(kind?: number, callback?: () => void): void; + flush(callback: () => void): void; + params(level: number, strategy: number, callback?: () => void): void; + close(callback?: () => void): void; + reset(): void; + } + export class Deflate extends ZlibBase { + constructor(options?: ZlibOptions); + } + export class Inflate extends ZlibBase { + constructor(options?: ZlibOptions); + } + export class Gzip extends ZlibBase { + constructor(options?: ZlibOptions); + } + export class Gunzip extends ZlibBase { + constructor(options?: ZlibOptions); + } + export class DeflateRaw extends ZlibBase { + constructor(options?: ZlibOptions); + } + export class InflateRaw extends ZlibBase { + constructor(options?: ZlibOptions); + } + export class Unzip extends ZlibBase { + constructor(options?: ZlibOptions); + } + export function createDeflate(options?: ZlibOptions): Deflate; + export function createInflate(options?: ZlibOptions): Inflate; + export function createGzip(options?: ZlibOptions): Gzip; + export function createGunzip(options?: ZlibOptions): Gunzip; + export function createDeflateRaw(options?: ZlibOptions): DeflateRaw; + export function createInflateRaw(options?: ZlibOptions): InflateRaw; + export function createUnzip(options?: ZlibOptions): Unzip; + /* Node v24 zlib.constants, literal-typed (the CryptoConstants stance): + * every access bakes as its literal; the object never materializes. The + * members outside BROTLI_* are also module properties (zlib.Z_SYNC_FLUSH). */ + export interface ZlibConstants { + readonly Z_NO_FLUSH: 0; + readonly Z_PARTIAL_FLUSH: 1; + readonly Z_SYNC_FLUSH: 2; + readonly Z_FULL_FLUSH: 3; + readonly Z_FINISH: 4; + readonly Z_BLOCK: 5; + readonly Z_OK: 0; + readonly Z_STREAM_END: 1; + readonly Z_NEED_DICT: 2; + readonly Z_ERRNO: -1; + readonly Z_STREAM_ERROR: -2; + readonly Z_DATA_ERROR: -3; + readonly Z_MEM_ERROR: -4; + readonly Z_BUF_ERROR: -5; + readonly Z_VERSION_ERROR: -6; + readonly Z_NO_COMPRESSION: 0; + readonly Z_BEST_SPEED: 1; + readonly Z_BEST_COMPRESSION: 9; + readonly Z_DEFAULT_COMPRESSION: -1; + readonly Z_FILTERED: 1; + readonly Z_HUFFMAN_ONLY: 2; + readonly Z_RLE: 3; + readonly Z_FIXED: 4; + readonly Z_DEFAULT_STRATEGY: 0; + readonly ZLIB_VERNUM: 4897; + readonly DEFLATE: 1; + readonly INFLATE: 2; + readonly GZIP: 3; + readonly GUNZIP: 4; + readonly DEFLATERAW: 5; + readonly INFLATERAW: 6; + readonly UNZIP: 7; + readonly BROTLI_DECODE: 8; + readonly BROTLI_ENCODE: 9; + readonly ZSTD_DECOMPRESS: 11; + readonly ZSTD_COMPRESS: 10; + readonly Z_MIN_WINDOWBITS: 8; + readonly Z_MAX_WINDOWBITS: 15; + readonly Z_DEFAULT_WINDOWBITS: 15; + readonly Z_MIN_CHUNK: 64; + readonly Z_MAX_CHUNK: number; + readonly Z_DEFAULT_CHUNK: 16384; + readonly Z_MIN_MEMLEVEL: 1; + readonly Z_MAX_MEMLEVEL: 9; + readonly Z_DEFAULT_MEMLEVEL: 8; + readonly Z_MIN_LEVEL: -1; + readonly Z_MAX_LEVEL: 9; + readonly Z_DEFAULT_LEVEL: -1; + readonly BROTLI_OPERATION_PROCESS: 0; + readonly BROTLI_OPERATION_FLUSH: 1; + readonly BROTLI_OPERATION_FINISH: 2; + readonly BROTLI_OPERATION_EMIT_METADATA: 3; + readonly BROTLI_PARAM_MODE: 0; + readonly BROTLI_MODE_GENERIC: 0; + readonly BROTLI_MODE_TEXT: 1; + readonly BROTLI_MODE_FONT: 2; + readonly BROTLI_DEFAULT_MODE: 0; + readonly BROTLI_PARAM_QUALITY: 1; + readonly BROTLI_MIN_QUALITY: 0; + readonly BROTLI_MAX_QUALITY: 11; + readonly BROTLI_DEFAULT_QUALITY: 11; + readonly BROTLI_PARAM_LGWIN: 2; + readonly BROTLI_MIN_WINDOW_BITS: 10; + readonly BROTLI_MAX_WINDOW_BITS: 24; + readonly BROTLI_LARGE_MAX_WINDOW_BITS: 30; + readonly BROTLI_DEFAULT_WINDOW: 22; + readonly BROTLI_PARAM_LGBLOCK: 3; + readonly BROTLI_MIN_INPUT_BLOCK_BITS: 16; + readonly BROTLI_MAX_INPUT_BLOCK_BITS: 24; + readonly BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING: 4; + readonly BROTLI_PARAM_SIZE_HINT: 5; + readonly BROTLI_PARAM_LARGE_WINDOW: 6; + readonly BROTLI_PARAM_NPOSTFIX: 7; + readonly BROTLI_PARAM_NDIRECT: 8; + readonly BROTLI_DECODER_RESULT_ERROR: 0; + readonly BROTLI_DECODER_RESULT_SUCCESS: 1; + readonly BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT: 2; + readonly BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT: 3; + readonly BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION: 0; + readonly BROTLI_DECODER_PARAM_LARGE_WINDOW: 1; + readonly BROTLI_DECODER_NO_ERROR: 0; + readonly BROTLI_DECODER_SUCCESS: 1; + readonly BROTLI_DECODER_NEEDS_MORE_INPUT: 2; + readonly BROTLI_DECODER_NEEDS_MORE_OUTPUT: 3; + readonly BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE: -1; + readonly BROTLI_DECODER_ERROR_FORMAT_RESERVED: -2; + readonly BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE: -3; + readonly BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET: -4; + readonly BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME: -5; + readonly BROTLI_DECODER_ERROR_FORMAT_CL_SPACE: -6; + readonly BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE: -7; + readonly BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT: -8; + readonly BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1: -9; + readonly BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2: -10; + readonly BROTLI_DECODER_ERROR_FORMAT_TRANSFORM: -11; + readonly BROTLI_DECODER_ERROR_FORMAT_DICTIONARY: -12; + readonly BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS: -13; + readonly BROTLI_DECODER_ERROR_FORMAT_PADDING_1: -14; + readonly BROTLI_DECODER_ERROR_FORMAT_PADDING_2: -15; + readonly BROTLI_DECODER_ERROR_FORMAT_DISTANCE: -16; + readonly BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET: -19; + readonly BROTLI_DECODER_ERROR_INVALID_ARGUMENTS: -20; + readonly BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES: -21; + readonly BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS: -22; + readonly BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP: -25; + readonly BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1: -26; + readonly BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2: -27; + readonly BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES: -30; + readonly BROTLI_DECODER_ERROR_UNREACHABLE: -31; + readonly ZSTD_e_continue: 0; + readonly ZSTD_e_flush: 1; + readonly ZSTD_e_end: 2; + readonly ZSTD_fast: 1; + readonly ZSTD_dfast: 2; + readonly ZSTD_greedy: 3; + readonly ZSTD_lazy: 4; + readonly ZSTD_lazy2: 5; + readonly ZSTD_btlazy2: 6; + readonly ZSTD_btopt: 7; + readonly ZSTD_btultra: 8; + readonly ZSTD_btultra2: 9; + readonly ZSTD_c_compressionLevel: 100; + readonly ZSTD_c_windowLog: 101; + readonly ZSTD_c_hashLog: 102; + readonly ZSTD_c_chainLog: 103; + readonly ZSTD_c_searchLog: 104; + readonly ZSTD_c_minMatch: 105; + readonly ZSTD_c_targetLength: 106; + readonly ZSTD_c_strategy: 107; + readonly ZSTD_c_enableLongDistanceMatching: 160; + readonly ZSTD_c_ldmHashLog: 161; + readonly ZSTD_c_ldmMinMatch: 162; + readonly ZSTD_c_ldmBucketSizeLog: 163; + readonly ZSTD_c_ldmHashRateLog: 164; + readonly ZSTD_c_contentSizeFlag: 200; + readonly ZSTD_c_checksumFlag: 201; + readonly ZSTD_c_dictIDFlag: 202; + readonly ZSTD_c_nbWorkers: 400; + readonly ZSTD_c_jobSize: 401; + readonly ZSTD_c_overlapLog: 402; + readonly ZSTD_d_windowLogMax: 100; + readonly ZSTD_CLEVEL_DEFAULT: 3; + readonly ZSTD_error_no_error: 0; + readonly ZSTD_error_GENERIC: 1; + readonly ZSTD_error_prefix_unknown: 10; + readonly ZSTD_error_version_unsupported: 12; + readonly ZSTD_error_frameParameter_unsupported: 14; + readonly ZSTD_error_frameParameter_windowTooLarge: 16; + readonly ZSTD_error_corruption_detected: 20; + readonly ZSTD_error_checksum_wrong: 22; + readonly ZSTD_error_literals_headerWrong: 24; + readonly ZSTD_error_dictionary_corrupted: 30; + readonly ZSTD_error_dictionary_wrong: 32; + readonly ZSTD_error_dictionaryCreation_failed: 34; + readonly ZSTD_error_parameter_unsupported: 40; + readonly ZSTD_error_parameter_combination_unsupported: 41; + readonly ZSTD_error_parameter_outOfBound: 42; + readonly ZSTD_error_tableLog_tooLarge: 44; + readonly ZSTD_error_maxSymbolValue_tooLarge: 46; + readonly ZSTD_error_maxSymbolValue_tooSmall: 48; + readonly ZSTD_error_stabilityCondition_notRespected: 50; + readonly ZSTD_error_stage_wrong: 60; + readonly ZSTD_error_init_missing: 62; + readonly ZSTD_error_memory_allocation: 64; + readonly ZSTD_error_workSpace_tooSmall: 66; + readonly ZSTD_error_dstSize_tooSmall: 70; + readonly ZSTD_error_srcSize_wrong: 72; + readonly ZSTD_error_dstBuffer_null: 74; + readonly ZSTD_error_noForwardProgress_destFull: 80; + readonly ZSTD_error_noForwardProgress_inputEmpty: 82; + } + export const constants: ZlibConstants; + export const Z_NO_FLUSH: 0; + export const Z_PARTIAL_FLUSH: 1; + export const Z_SYNC_FLUSH: 2; + export const Z_FULL_FLUSH: 3; + export const Z_FINISH: 4; + export const Z_BLOCK: 5; + export const Z_OK: 0; + export const Z_STREAM_END: 1; + export const Z_NEED_DICT: 2; + export const Z_ERRNO: -1; + export const Z_STREAM_ERROR: -2; + export const Z_DATA_ERROR: -3; + export const Z_MEM_ERROR: -4; + export const Z_BUF_ERROR: -5; + export const Z_VERSION_ERROR: -6; + export const Z_NO_COMPRESSION: 0; + export const Z_BEST_SPEED: 1; + export const Z_BEST_COMPRESSION: 9; + export const Z_DEFAULT_COMPRESSION: -1; + export const Z_FILTERED: 1; + export const Z_HUFFMAN_ONLY: 2; + export const Z_RLE: 3; + export const Z_FIXED: 4; + export const Z_DEFAULT_STRATEGY: 0; + export const ZLIB_VERNUM: 4897; + export const DEFLATE: 1; + export const INFLATE: 2; + export const GZIP: 3; + export const GUNZIP: 4; + export const DEFLATERAW: 5; + export const INFLATERAW: 6; + export const UNZIP: 7; + export const ZSTD_DECOMPRESS: 11; + export const ZSTD_COMPRESS: 10; + export const Z_MIN_WINDOWBITS: 8; + export const Z_MAX_WINDOWBITS: 15; + export const Z_DEFAULT_WINDOWBITS: 15; + export const Z_MIN_CHUNK: 64; + export const Z_MAX_CHUNK: number; + export const Z_DEFAULT_CHUNK: 16384; + export const Z_MIN_MEMLEVEL: 1; + export const Z_MAX_MEMLEVEL: 9; + export const Z_DEFAULT_MEMLEVEL: 8; + export const Z_MIN_LEVEL: -1; + export const Z_MAX_LEVEL: 9; + export const Z_DEFAULT_LEVEL: -1; + export const ZSTD_e_continue: 0; + export const ZSTD_e_flush: 1; + export const ZSTD_e_end: 2; + export const ZSTD_fast: 1; + export const ZSTD_dfast: 2; + export const ZSTD_greedy: 3; + export const ZSTD_lazy: 4; + export const ZSTD_lazy2: 5; + export const ZSTD_btlazy2: 6; + export const ZSTD_btopt: 7; + export const ZSTD_btultra: 8; + export const ZSTD_btultra2: 9; + export const ZSTD_c_compressionLevel: 100; + export const ZSTD_c_windowLog: 101; + export const ZSTD_c_hashLog: 102; + export const ZSTD_c_chainLog: 103; + export const ZSTD_c_searchLog: 104; + export const ZSTD_c_minMatch: 105; + export const ZSTD_c_targetLength: 106; + export const ZSTD_c_strategy: 107; + export const ZSTD_c_enableLongDistanceMatching: 160; + export const ZSTD_c_ldmHashLog: 161; + export const ZSTD_c_ldmMinMatch: 162; + export const ZSTD_c_ldmBucketSizeLog: 163; + export const ZSTD_c_ldmHashRateLog: 164; + export const ZSTD_c_contentSizeFlag: 200; + export const ZSTD_c_checksumFlag: 201; + export const ZSTD_c_dictIDFlag: 202; + export const ZSTD_c_nbWorkers: 400; + export const ZSTD_c_jobSize: 401; + export const ZSTD_c_overlapLog: 402; + export const ZSTD_d_windowLogMax: 100; + export const ZSTD_CLEVEL_DEFAULT: 3; + export const ZSTD_error_no_error: 0; + export const ZSTD_error_GENERIC: 1; + export const ZSTD_error_prefix_unknown: 10; + export const ZSTD_error_version_unsupported: 12; + export const ZSTD_error_frameParameter_unsupported: 14; + export const ZSTD_error_frameParameter_windowTooLarge: 16; + export const ZSTD_error_corruption_detected: 20; + export const ZSTD_error_checksum_wrong: 22; + export const ZSTD_error_literals_headerWrong: 24; + export const ZSTD_error_dictionary_corrupted: 30; + export const ZSTD_error_dictionary_wrong: 32; + export const ZSTD_error_dictionaryCreation_failed: 34; + export const ZSTD_error_parameter_unsupported: 40; + export const ZSTD_error_parameter_combination_unsupported: 41; + export const ZSTD_error_parameter_outOfBound: 42; + export const ZSTD_error_tableLog_tooLarge: 44; + export const ZSTD_error_maxSymbolValue_tooLarge: 46; + export const ZSTD_error_maxSymbolValue_tooSmall: 48; + export const ZSTD_error_stabilityCondition_notRespected: 50; + export const ZSTD_error_stage_wrong: 60; + export const ZSTD_error_init_missing: 62; + export const ZSTD_error_memory_allocation: 64; + export const ZSTD_error_workSpace_tooSmall: 66; + export const ZSTD_error_dstSize_tooSmall: 70; + export const ZSTD_error_srcSize_wrong: 72; + export const ZSTD_error_dstBuffer_null: 74; + export const ZSTD_error_noForwardProgress_destFull: 80; + export const ZSTD_error_noForwardProgress_inputEmpty: 82; } declare module "node:zlib" { export * from "zlib"; diff --git a/packages/compiler/src/backend/llvm/lib-filesystem.ts b/packages/compiler/src/backend/llvm/lib-filesystem.ts index e661fb7cda..3ecd4f5c90 100644 --- a/packages/compiler/src/backend/llvm/lib-filesystem.ts +++ b/packages/compiler/src/backend/llvm/lib-filesystem.ts @@ -211,6 +211,59 @@ export function emitFilesystemLibCall(host: LlvmEmitterContext, e: LibCallExpr): ); return { name: "", type: e.type }; } + if (e.fn === "zlib.streamNew") { + // The stream's engine steps run on the worker pool; the options value + // is borrowed and the +1 stream may be NULL with a pending throw. + host.usesTimers = true; + const args = e.args.map((a) => host.emitExpr(a)); + host.declare("declare ptr @scr_zlib_stream_new(double, ptr)"); + const t = B.tmp(); + B.line(`${t} = call ptr @scr_zlib_stream_new(double ${args[0]!.name}, ptr ${args[1]!.name})`); + const out = host.own({ name: t, type: e.type }); + host.emitPendingCheck(); + return out; + } + if (e.fn === "zlib.streamBytesWritten") { + const recv = host.emitExpr(e.args[0]!); + host.declare("declare double @scr_zlib_stream_bytes_written(ptr)"); + const t = B.tmp(); + B.line(`${t} = call double @scr_zlib_stream_bytes_written(ptr ${recv.name})`); + return { name: t, type: e.type }; + } + if ( + e.fn === "zlib.streamFlush" || + e.fn === "zlib.streamFlushCb" || + e.fn === "zlib.streamParams" || + e.fn === "zlib.streamParamsCb" || + e.fn === "zlib.streamClose" || + e.fn === "zlib.streamCloseCb" || + e.fn === "zlib.streamReset" + ) { + // Receiver and dyn operands borrowed; the trailing () => void callback + // of the Cb forms MOVES (NULL when absent). + const args = e.args.map((a) => host.emitExpr(a)); + const withCb = e.fn.endsWith("Cb"); + const base = withCb ? e.fn.slice(0, -2) : e.fn; + const operands = args.slice(0, withCb ? -1 : undefined).map((a) => `ptr ${a.name}`); + let cb = "null"; + if (withCb) { + host.moveTemp(args[args.length - 1]!); + cb = args[args.length - 1]!.name; + } + const sym = + base === "zlib.streamFlush" + ? "scr_zlib_stream_flush" + : base === "zlib.streamParams" + ? "scr_zlib_stream_params" + : base === "zlib.streamClose" + ? "scr_zlib_stream_close" + : "scr_zlib_stream_reset"; + const params = base === "zlib.streamReset" ? operands : [...operands, `ptr ${cb}`]; + host.declare(`declare void @${sym}(${params.map(() => "ptr").join(", ")})`); + B.line(`call void @${sym}(${params.join(", ")})`); + if (base !== "zlib.streamClose") host.emitPendingCheck(); + return { name: "", type: e.type }; + } if (e.fn === "zlib.deflateLevelSync") { const args = e.args.map((arg) => host.emitExpr(arg)); host.declare("declare ptr @scr_zlib_deflate_mode(ptr, double, double)"); diff --git a/packages/compiler/src/backend/llvm/lib-test.ts b/packages/compiler/src/backend/llvm/lib-test.ts index 971456a0cf..3e73606056 100644 --- a/packages/compiler/src/backend/llvm/lib-test.ts +++ b/packages/compiler/src/backend/llvm/lib-test.ts @@ -44,6 +44,15 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal return finishRuntimeCall(host, e, "scr_test_ctx_diagnostic", a); case "test.ctxName": return finishRuntimeCall(host, e, "scr_test_ctx_name", a); + case "test.ctxFullName": + return finishRuntimeCall(host, e, "scr_test_ctx_full_name", a); + case "test.ctxHook": + host.moveTemp(args[2]!); + return finishRuntimeCall(host, e, "scr_test_ctx_hook", a); + case "test.ctxPlan": + return finishRuntimeCall(host, e, "scr_test_ctx_plan", a); + case "test.ctxCount": + return finishRuntimeCall(host, e, "scr_test_ctx_count", a); default: throw new InternalCompilerError(`unhandled test call ${e.fn}`); } diff --git a/packages/compiler/src/backend/llvm/regions.ts b/packages/compiler/src/backend/llvm/regions.ts index eaacaa9523..9c272ac1f3 100644 --- a/packages/compiler/src/backend/llvm/regions.ts +++ b/packages/compiler/src/backend/llvm/regions.ts @@ -241,11 +241,15 @@ export class LifetimeRegions { * chunks of freeable regions have an owner: the default regions' objects * are never freed, so their references need no count. Worker executables * count only their own chunks; a published instance other threads read - * is never counted by them. */ + * is never counted by them. The counting path is marked unlikely: speed + * builds inline these bodies into every use of an instance, and in + * programs that never create a region the path is never taken, so it is + * laid out away from the callers' hot code. */ countBody(fnName: string, ret: "ptr" | "void", comment: string): string[] { const count = this.tag("count"); const workers = this.host.threadLocalAlloc === true; this.host.declare(`@scr_region_counting = external global i8`); + this.host.declare(`declare i1 @llvm.expect.i1(i1, i1)`); // Worker threads set and read the flag concurrently: an unordered // atomic load, which LLVM may still hoist like a plain one. const flag = workers @@ -257,7 +261,8 @@ export class LifetimeRegions { ` %on = ${flag}${this.tag("counting")}`, ` %idle = icmp eq i8 %on, 0`, ` %isnull = icmp eq ptr %o, null`, - ` %skip = or i1 %idle, %isnull`, + ` %skip0 = or i1 %idle, %isnull`, + ` %skip = call i1 @llvm.expect.i1(i1 %skip0, i1 true)`, ` br i1 %skip, label %done, label %chunk`, `chunk:`, ...this.chunkLines("c", "%o"), diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index 8f43c8ed36..bb68dced60 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -549,6 +549,9 @@ export async function compileCInternal( ? [rt(join(rtDir, "scr_util_callable.c"))] : []), ...(opts.stream ? [rt(join(rtDir, "scr_stream.c"))] : []), + // The zlib stream classes reference both the zlib engine and the + // stream unit. + ...(opts.zlib && opts.stream ? [rt(join(rtDir, "scr_zlib_stream.c"))] : []), ...(opts.stream && opts.dynAsync ? [rt(join(rtDir, "scr_stream_ops.c"))] : []), // The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD, // epoll on Linux, WSAPoll on Windows — each TU is empty off its diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts index 8aacf8dd87..ff304176f5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/compression.ts +++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts @@ -1,9 +1,22 @@ import { lowerBuiltinByteInput, errorFirstBytesCallback } from "./arguments.js"; -import { numLit } from "../../../ir/build.js"; +import { dynUndefinedExpr, numLit } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; -import { BYTES_U8, F64, type IrExpr, type IrLibFn, type SrcLoc, VOID } from "../../../ir/ir.js"; +import { locOf } from "../../program.js"; +import { isNodeTypesPath } from "../../dts-paths.js"; +import { + BYTES_U8, + DYN, + F64, + funcOf, + type IrExpr, + type IrLibFn, + type IrType, + type SrcLoc, + VOID, + ZLIB_STREAM_MODES, +} from "../../../ir/ir.js"; const ZLIB_SYNC_FNS: Readonly> = { deflateSync: "zlib.deflateSync", @@ -25,6 +38,204 @@ const ZLIB_CALLBACK_FNS: Readonly> = { unzip: "zlib.unzipCb", }; +const ZLIB_STREAM_T: IrType = { kind: "object", className: "%Transform" }; + +/** `zlib.createGzip(options?)` and `new zlib.Gzip(options?)`: the runtime + * Transform stream (scr_zlib_stream.c). The options value crosses as a dyn + * value and is validated at runtime with Node's ladder, so literal and + * computed options take the same path. */ +export function lowerZlibStreamNew( + lowerer: Lowerer, + args: readonly ts.Expression[], + mode: number, + what: string, + site: ts.Node, + loc: SrcLoc, +): IrExpr { + if (args.some(ts.isSpreadElement)) lowerer.noLowering(`${what} with spread arguments`, site); + if (args.length > 1) { + lowerer.noLowering( + `${what} with ${args.length} arguments`, + args[1]!, + "the supported form takes one options object", + ); + } + let options: IrExpr = dynUndefinedExpr(loc); + if (args[0] !== undefined) { + const raw = lowerer.lowerExpr(args[0]); + if (raw.type.kind === "dyn") options = raw; + else if (raw.kind === "unitLit" || lowerer.dynConvertible(raw.type)) { + options = { kind: "dynFrom", value: raw, type: DYN, loc }; + } else { + lowerer.noLowering( + `${what} with a '${lowerer.fmt(raw.type)}' options value`, + args[0], + "zlib stream options are a plain object of numbers, booleans and Buffers", + ); + } + } + return { + kind: "libCall", + fn: "zlib.streamNew", + args: [numLit(mode, loc), options], + type: ZLIB_STREAM_T, + loc, + }; +} + +/** The node_zlib_mode of a zlib stream CLASS symbol (`new zlib.Gzip()`, + * `new Gzip()` through any import spelling) declared by the shipped + * fallback, or null. */ +export function zlibStreamModeOf( + lowerer: Lowerer, + symbol: ts.Symbol | null | undefined, +): number | null { + if (!symbol) return null; + const target = + symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol; + const mode = ZLIB_STREAM_MODES.get(target.name); + if (mode === undefined) return null; + return lowerer.checker + .declarationsOf(target) + .some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d)) + ? mode + : null; +} + +function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean { + const file = d.getSourceFile(); + if (!lowerer.isStdlibFile(file) || isNodeTypesPath(file.fileName)) return false; + for (let node: ts.Node | undefined = d.parent; node; node = node.parent) { + if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) { + return node.name.text === "zlib" || node.name.text === "node:zlib"; + } + } + return false; +} + +/** True when `access` names a member the zlib stream base class declares + * (bytesWritten, flush, params, close, reset). */ +export function isZlibStreamMember(lowerer: Lowerer, access: ts.PropertyAccessExpression): boolean { + const symbol = lowerer.checker.getSymbolAtLocation(access.name); + if (!symbol) return false; + return lowerer.checker + .declarationsOf(symbol) + .some( + (d) => + d.parent !== undefined && + ts.isClassDeclaration(d.parent) && + d.parent.name?.text === "ZlibBase" && + isZlibFallbackDecl(lowerer, d.parent), + ); +} + +/** `stream.bytesWritten` on a zlib stream. */ +export function lowerZlibStreamBytesWritten( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrExpr { + const loc = locOf(access); + return { + kind: "libCall", + fn: "zlib.streamBytesWritten", + args: [lowerer.lowerExpr(access.expression)], + type: F64, + loc, + }; +} + +/** flush/params/close/reset on a zlib stream. Callbacks are () => void, + * the write-callback rule; numeric operands cross as dyn values for the + * runtime's range checks. */ +export function lowerZlibStreamMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr { + const loc = locOf(call); + const member = access.name.text; + const args = call.arguments; + if (args.some(ts.isSpreadElement)) lowerer.noLowering(`${member} with spread arguments`, call); + const isFunc = (node: ts.Expression): boolean => + lowerer.mapTypeOf(lowerer.typeOf(node))?.kind === "func"; + const callback = (node: ts.Expression): IrExpr => { + const cb = lowerer.lowerExprExpecting(node, funcOf([], VOID)); + if (cb.type.kind !== "func" || cb.type.params.length > 0) { + lowerer.noLowering( + `zlib ${member} callbacks taking arguments`, + node, + "() => void callbacks are supported (listen for 'error' to observe failures)", + ); + } + return cb; + }; + const value = (node: ts.Expression | undefined): IrExpr => { + if (node === undefined) return dynUndefinedExpr(loc); + const raw = lowerer.lowerExpr(node); + if (raw.type.kind === "dyn") return raw; + if (raw.kind === "unitLit" || lowerer.dynConvertible(raw.type)) { + return { kind: "dynFrom", value: raw, type: DYN, loc }; + } + lowerer.noLowering(`zlib ${member} with a '${lowerer.fmt(raw.type)}' argument`, node); + }; + const receiver = (): IrExpr => lowerer.lowerExpr(access.expression); + const call0 = (fn: IrLibFn, operands: IrExpr[]): IrExpr => ({ + kind: "libCall", + fn, + args: operands, + type: VOID, + loc, + }); + const tooMany = (max: number): void => { + if (args.length > max) lowerer.noLowering(`zlib ${member} with ${args.length} arguments`, call); + }; + switch (member) { + case "flush": { + tooMany(2); + // flush(cb) moves a function first argument into the callback slot. + const first = args[0]; + const cbNode = first !== undefined && isFunc(first) ? first : args[1]; + const kindNode = first !== undefined && first !== cbNode ? first : undefined; + const recv = receiver(); + const kind = value(kindNode); + return cbNode !== undefined + ? call0("zlib.streamFlushCb", [recv, kind, callback(cbNode)]) + : call0("zlib.streamFlush", [recv, kind]); + } + case "params": { + tooMany(3); + const recv = receiver(); + const level = value(args[0]); + const strategy = value(args[1]); + return args[2] !== undefined + ? call0("zlib.streamParamsCb", [recv, level, strategy, callback(args[2])]) + : call0("zlib.streamParams", [recv, level, strategy]); + } + case "close": { + tooMany(1); + const recv = receiver(); + return args[0] !== undefined + ? call0("zlib.streamCloseCb", [recv, callback(args[0])]) + : call0("zlib.streamClose", [recv]); + } + case "reset": + tooMany(0); + return call0("zlib.streamReset", [receiver()]); + default: + lowerer.noLowering(`zlib stream member '${member}'`, access); + } +} + +const ZLIB_STREAM_FACTORIES: Readonly> = { + createDeflate: 1, + createInflate: 2, + createGzip: 3, + createGunzip: 4, + createDeflateRaw: 5, + createInflateRaw: 6, + createUnzip: 7, +}; + export function lowerZlibModuleCall( lowerer: Lowerer, expr: ts.CallExpression, @@ -34,6 +245,10 @@ export function lowerZlibModuleCall( if (expr.arguments.some(ts.isSpreadElement)) { lowerer.noLowering(`zlib.${bi.member} with spread arguments`, expr); } + const streamMode = ZLIB_STREAM_FACTORIES[bi.member]; + if (streamMode !== undefined) { + return lowerZlibStreamNew(lowerer, expr.arguments, streamMode, `zlib.${bi.member}`, expr, loc); + } const syncFn = ZLIB_SYNC_FNS[bi.member]; if (syncFn !== undefined) { if ( diff --git a/packages/compiler/src/frontend/lowering/builtins/constants.ts b/packages/compiler/src/frontend/lowering/builtins/constants.ts index 100bc611e6..0325a0ef7c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/constants.ts +++ b/packages/compiler/src/frontend/lowering/builtins/constants.ts @@ -4,13 +4,14 @@ import { canonicalBuiltinModule, locOf, requireSpecOf } from "../../program.js"; import { builtinConstLit } from "../surfaces.js"; import { HTTP2_CONSTANTS } from "../http2-constants.js"; import { CRYPTO_CONSTANTS } from "../crypto-tables.js"; +import { ZLIB_CONSTANTS } from "../zlib-constants.js"; import { F64, type IrExpr } from "../../../ir/ir.js"; import { builtinImportOf } from "./module-bindings.js"; /** The builtin modules whose `constants` object bakes as literals at * every access site (the fs.constants precedent, scaled up): http2's - * full Node v24 table, and crypto's OpenSSL-constant table. The object - * itself never materializes at runtime. */ + * full Node v24 table, crypto's OpenSSL-constant table, and zlib's + * table. The object itself never materializes at runtime. */ const BUILTIN_CONSTANTS_TABLES: Record< string, { table: Record; hint: string } | undefined @@ -23,6 +24,10 @@ const BUILTIN_CONSTANTS_TABLES: Record< table: CRYPTO_CONSTANTS, hint: "the table bakes Node v24's crypto.constants members as literals — this name is not one of them", }, + zlib: { + table: ZLIB_CONSTANTS, + hint: "the table bakes Node v24's zlib.constants members as literals — this name is not one of them", + }, }; /** The module whose baked-constants OBJECT `node` denotes, or null — any diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 2eb1e27a60..07322cd069 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -14,6 +14,7 @@ import { import { objectEnumerationReceiver } from "./object-enumeration-receiver.js"; import { InternalCompilerError } from "../../errors.js"; import * as ts from "../ts7/adapter.js"; +import { isZlibStreamMember, lowerZlibStreamMethodCall } from "./builtins/compression.js"; import type { Lowerer } from "./lowerer.js"; import { lowerGenMethodCall } from "./lower-generators.js"; import { lowerClassSymbolMethodCall } from "./symbol-methods.js"; @@ -194,6 +195,8 @@ import { genericCallInstance, } from "./generic-functions.js"; +const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "close", "reset"]); + export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); @@ -8577,6 +8580,16 @@ export function lowerObjectMethodCall( const info = lowerer.classes.get(receiverIr.className); if (!info) lowerer.flushDeferredClass(receiverIr.className); const found = info ? lowerer.findMethodOn(info, access.name.text) : null; + // The zlib stream members (flush/params/close/reset) are declared on + // the zlib classes; their receivers are runtime Transform streams. + if ( + info && + !found && + ZLIB_STREAM_METHODS.has(access.name.text) && + isZlibStreamMember(lowerer, access) + ) { + return lowerZlibStreamMethodCall(lowerer, call, access); + } // The stream surface: API-named calls on stream-rooted receivers // lower through the stream spoke (checked before the emitter surface // — the two member sets are disjoint, but streams root at the emitter diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index da152b0a29..6a81f9a72e 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -8,6 +8,7 @@ import { InternalCompilerError } from "../../errors.js"; * accessor access, upcasts, `new` expressions, and the builtin Error * hierarchy registration. */ import * as ts from "../ts7/adapter.js"; +import { lowerZlibStreamNew, zlibStreamModeOf } from "./builtins/compression.js"; import type { FnCtx, Lowerer } from "./lowerer.js"; import { BOOL, @@ -8299,6 +8300,12 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // `require('stream').Readable`) construction form. const streamInfoNs = builtinStreamInfoOf(lowerer, resolved); if (streamInfoNs) return lowerStreamNew(lowerer, expr, streamInfoNs); + // `new zlib.Gzip(options?)` — the zlib stream classes. + const zlibModeNs = zlibStreamModeOf(lowerer, resolved); + if (zlibModeNs !== null) { + const what = `new zlib.${expr.expression.name.text}`; + return lowerZlibStreamNew(lowerer, expr.arguments ?? [], zlibModeNs, what, expr, loc); + } // `new N.C(...)` / `new a.Point(...)` — construction through a // USER namespace qualifier (import= alias chains included): the // member resolves to the registered program class, guarded by the @@ -8535,6 +8542,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { // object parses structurally in the stream spoke. const streamInfo = builtinStreamInfoOf(lowerer, symbol); if (streamInfo) return lowerStreamNew(lowerer, expr, streamInfo); + const zlibMode = zlibStreamModeOf(lowerer, symbol); + if (zlibMode !== null) { + const what = `new zlib.${symbol!.name}`; + return lowerZlibStreamNew(lowerer, expr.arguments ?? [], zlibMode, what, expr, loc); + } // `new URL(input)`: the WHATWG URL class (stdlib/@types provenance — // a user's own `class URL` resolves through classBySymbol below). // Invalid input or base throws a catchable TypeError ("Invalid URL"). diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index 4772dfeff5..384224ce90 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -24,6 +24,7 @@ import { literalUnionArm } from "../union-discriminants.js"; * ToBoolean/ToString coercion helpers, and field/element reads and writes * (FieldTarget). */ import * as ts from "../ts7/adapter.js"; +import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "./builtins/compression.js"; import { constituentTypes } from "../ts7/checker.js"; import { dirname } from "node:path"; import * as posix from "node:path/posix"; @@ -15164,6 +15165,9 @@ function lowerStreamObjectProperty( if (recvT?.kind !== "object") return null; const info = lowerer.classes.get(recvT.className); if (!info || streamSidesOf(lowerer, info) === null) return null; + if (expr.name.text === "bytesWritten" && isZlibStreamMember(lowerer, expr)) { + return lowerZlibStreamBytesWritten(lowerer, expr); + } return lowerStreamProperty(lowerer, expr, info); } diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index e14bb3f024..3f86fc419b 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -14,6 +14,7 @@ import { type IrType, RUNTIME_ERROR_CLASSES, STRING, + isUnitType, UNDEFINED_T, VOID, type SrcLoc, @@ -26,7 +27,7 @@ import { typeKey, } from "../../ir/ir.js"; import type { ClassInfo } from "./lower-classes.js"; -import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { symbolFieldDisplayName } from "./symbol-fields.js"; import { eventTargetRootOf } from "./lower-event-target.js"; import { lowerSystemErrorMap } from "./lower-system-errors.js"; @@ -1934,6 +1935,67 @@ export function lowerFormatCall( } return { kind: "jsonStringify", value, type: STRING, loc }; } + case 102: { + // %f — parseFloat(String(arg)) formatted: a number keeps its value + // except -0, which String() prints as "0"; strings parse with the + // global parseFloat; booleans and unit values (void calls and + // null | undefined unions included) parse to NaN. + const value = lowerer.lowerExpr(node); + const format = (number: IrExpr): IrExpr => ({ + kind: "libCall", + fn: "insp.f64", + args: [number], + type: STRING, + loc, + }); + const parse = (text: IrExpr): IrExpr => + format({ kind: "libCall", fn: "num.parseFloat", args: [text], type: F64, loc }); + if (value.type.kind === "f64") + return format({ + kind: "bin", + op: "+", + left: value, + right: numLit(0, loc), + type: F64, + loc, + }); + if (value.type.kind === "string") return parse(value); + if (value.type.kind === "dyn") { + return parse({ + kind: "libCall", + fn: "dyn.toStringCoerce", + args: [value], + type: STRING, + loc, + }); + } + const unitArms = (t: IrType): boolean => + t.kind === "union" && (lowerer.unions.get(t.unionId)?.arms.every(isUnitType) ?? false); + if ( + value.type.kind === "bool" || + value.type.kind === "void" || + isUnitType(value.type) || + unitArms(value.type) + ) { + // The argument still evaluates once, for its effects: the value + // already lowered, as a statement (never a second lowering of the + // node, whose lifted helpers and closures would be built twice). + if (isSafeToDiscard(value)) return strLit("NaN", loc); + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: value, loc }], + result: strLit("NaN", loc), + type: STRING, + loc, + }; + } + lowerer.noLowering( + `util.format %f of '${lowerer.fmt(value.type)}' values`, + node, + "pass a number, string or boolean, or convert the argument with String() first", + ); + return strLit("NaN", loc); + } case 79: // %O — inspect at the defaults return formatOArg(lowerer, node, 2, loc); case 111: // %o — showHidden semantics; depth 4 @@ -1952,6 +2014,7 @@ export function lowerFormatCall( case 100: // d case 79: // O case 111: // o + case 102: // f case 105: { // i const node = args[++a]!; @@ -1960,9 +2023,6 @@ export function lowerFormatCall( lastPos = i + 1; continue; } - case 102: // f — parseFloat's full grammar has no static lowering - lowerer.noLowering("util.format %f", expr, "parseFloat has no native lowering"); - break; case 99: // c — consumes its argument, contributes nothing a += 1; if (lastPos !== i - 1) parts.push(strLit(fmt.slice(lastPos, i - 1), loc)); diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts index 4ef9bd49f7..bf308abf8c 100644 --- a/packages/compiler/src/frontend/lowering/lower-test.ts +++ b/packages/compiler/src/frontend/lowering/lower-test.ts @@ -16,12 +16,20 @@ import * as ts from "../ts7/adapter.js"; import { resultIsDiscarded } from "./call-position.js"; import type { Lowerer } from "./lowerer.js"; import { locOf } from "../program.js"; -import { type IrExpr, type IrType, type SrcLoc, STRING, VOID, isUnitType } from "../../ir/ir.js"; +import { + type IrExpr, + type IrType, + type SrcLoc, + F64, + STRING, + VOID, + isUnitType, +} from "../../ir/ir.js"; import { numLit, strLit } from "../../ir/build.js"; const TEST_FN_HINT = "test bodies take () or (t: TestContext) and return void or Promise"; const TESTCTX_SURFACE_HINT = - "t.test, t.skip, t.todo, t.diagnostic, t.name, and t.assert.* are the supported TestContext members"; + "t.test, t.skip, t.todo, t.diagnostic, t.plan, t.before, t.after, t.beforeEach, t.afterEach, t.name, t.fullName, and t.assert.* are the supported TestContext members"; /** Registration calls return Node's Promise, but the promise only * resolves through the runner — consuming it outside `await t.test(...)` @@ -268,7 +276,15 @@ function lowerSuite( const at = strLit(atStringOf(expr), loc); const mode = numLit(opts.mode, loc); const msg = opts.msg ?? strLit("", loc); - return { kind: "libCall", fn: "test.suite", args: [name, mode, msg, cb, at], type: VOID, loc }; + // describe.only and { only: true } mark the suite (flag 4, as for tests). + const flags = numLit(methodMode === 3 || opts.only ? 4 : 0, loc); + return { + kind: "libCall", + fn: "test.suite", + args: [name, mode, msg, cb, at, flags], + type: VOID, + loc, + }; } /** before/after/beforeEach/afterEach — hooks on the enclosing suite @@ -429,7 +445,7 @@ export function lowerTestMethodCall( call, `${bi.member}.${member}(...)`, loc, - member === "skip" ? 1 : member === "todo" ? 2 : 0, + member === "skip" ? 1 : member === "todo" ? 2 : 3, ); } if (bi.member === "mock") { @@ -460,7 +476,27 @@ export function lowerTestMethodCall( lowerer.isStdlibMember(access.expression) ) { const served = lowerer.lowerAssertModuleCall(call, { module: "assert", member }, loc); - if (served) return served; + // Each t.assert call counts against the test's plan (t.plan). + if (served) + return { + kind: "seqExpr", + stmts: [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "test.ctxCount", + args: [lowerer.lowerExpr(access.expression.expression)], + type: VOID, + loc, + }, + loc, + }, + ], + result: served, + type: served.type, + loc, + }; lowerer.noLowering( `t.assert.${member}`, call, @@ -495,6 +531,40 @@ export function lowerTestMethodCall( loc, }; } + const hook = HOOK_WHICH[member]; + if (hook !== undefined) { + const what = `t.${member}(...)`; + requireStatementPosition(lowerer, call, what); + if (args.length !== 1) { + lowerer.noLowering( + `t.${member} with ${args.length} arguments`, + call, + "the supported form is one zero-parameter function (hook options have no lowering)", + ); + } + const receiver = lowerer.lowerExpr(access.expression); + const { cb, flags } = lowerBodyArg(lowerer, args[0]!, what, false); + return { + kind: "libCall", + fn: "test.ctxHook", + args: [receiver, numLit(hook, loc), cb, numLit(flags, loc)], + type: VOID, + loc, + }; + } + if (member === "plan") { + requireStatementPosition(lowerer, call, "t.plan(...)"); + if (args.length !== 1) { + lowerer.noLowering( + `t.plan with ${args.length} arguments`, + call, + "the supported form is t.plan(count) (plan options have no lowering)", + ); + } + const receiver = lowerer.lowerExpr(access.expression); + const count = lowerer.lowerExprExpecting(args[0]!, F64); + return { kind: "libCall", fn: "test.ctxPlan", args: [receiver, count], type: VOID, loc }; + } if (member === "diagnostic") { requireStatementPosition(lowerer, call, "t.diagnostic(...)"); if (args.length !== 1) { @@ -527,9 +597,15 @@ export function lowerTestCtxProperty( if (lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind !== "testCtx") return null; if (!lowerer.isStdlibMember(expr)) return null; const loc = locOf(expr); - if (expr.name.text === "name") { + if (expr.name.text === "name" || expr.name.text === "fullName") { const receiver = lowerer.lowerExpr(expr.expression); - return { kind: "libCall", fn: "test.ctxName", args: [receiver], type: STRING, loc }; + return { + kind: "libCall", + fn: expr.name.text === "name" ? "test.ctxName" : "test.ctxFullName", + args: [receiver], + type: STRING, + loc, + }; } if (expr.name.text === "assert") return null; // claimed by the call path lowerer.noLowering( diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index 9938701575..0c3fc9340d 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -23,6 +23,10 @@ import { arrayOf, } from "../../ir/ir.js"; import { isNodeTypesPath, requireSpecOf } from "../program.js"; +import { ZLIB_CONSTANTS } from "./zlib-constants.js"; + +/** zlib stream instances: runtime Transform streams (scr_zlib_stream.c). */ +const ZLIB_STREAM: IrType = { kind: "object", className: "%Transform" }; /** Statement-level constructs rejected wholesale, keyed by syntax kind. */ export const UNSUPPORTED_STMT: Partial< @@ -1526,6 +1530,15 @@ export const BUILTIN_MODULE_FNS: Record< unzip: { fn: "zlib.unzipCb", params: [], result: VOID }, unzipSync: { fn: "zlib.unzipSync", params: [BYTES_U8], result: BYTES_U8 }, crc32: { fn: "zlib.crc32", params: [], result: F64 }, + // The stream factories (special-cased in lowerZlibModuleCall): each + // answers a runtime Transform stream over the zlib engine. + createDeflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createInflate: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createGzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createGunzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createDeflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createInflateRaw: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, + createUnzip: { fn: "zlib.streamNew", params: [], result: ZLIB_STREAM }, }, url: { fileURLToPath: { @@ -1871,6 +1884,11 @@ export const BUILTIN_MODULE_CONSTS: Record< os: { EOL: "\n", devNull: "/dev/null" }, worker_threads: { isMainThread: true, threadId: 0 }, cluster: { isPrimary: true, isMaster: true, isWorker: false }, + // zlib's module-level copies of its constants (every member outside + // BROTLI_*, Node's own loop over zlib.constants). + zlib: Object.fromEntries( + Object.entries(ZLIB_CONSTANTS).filter(([name]) => !name.startsWith("BROTLI")), + ), }; /** ALTERNATE libCall spellings of tabled builtin members: lowerings the @@ -2724,7 +2742,12 @@ export function builtinModulesArrayLit(loc: { file: string; start: number; end: * no lowering. The default-options one-shot zlib/deflate-raw/gzip codecs * lower for strings and Buffers; the remaining surface points at them. */ const ZLIB_HINT = - "the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms plus crc32 are the lowered zlib surface"; + "the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, " + + "and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface"; +/** Brotli has no codec in the static runtime (zlib is the only vendored + * compression library). */ +const BROTLI_HINT = + "Brotli needs a Brotli codec, which the static runtime does not vendor — " + ZLIB_HINT; const ASSERT_MODULE_HINTS: Record = { ifError: @@ -2816,8 +2839,14 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< "randomBytes, and the createHash chain", }, zlib: { - brotliCompressSync: ZLIB_HINT, - brotliDecompressSync: ZLIB_HINT, + brotliCompressSync: BROTLI_HINT, + brotliDecompressSync: BROTLI_HINT, + brotliCompress: BROTLI_HINT, + brotliDecompress: BROTLI_HINT, + createBrotliCompress: BROTLI_HINT, + createBrotliDecompress: BROTLI_HINT, + BrotliCompress: BROTLI_HINT, + BrotliDecompress: BROTLI_HINT, }, http2: { connect: diff --git a/packages/compiler/src/frontend/lowering/zlib-constants.ts b/packages/compiler/src/frontend/lowering/zlib-constants.ts new file mode 100644 index 0000000000..f0b0f451f1 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/zlib-constants.ts @@ -0,0 +1,181 @@ +/* node:zlib constants — the table Node v24.22.0 exposes as zlib.constants + * (170 members: the Z_* flush, return-code and option values, the + * node_zlib_mode numbers, and the BROTLI_* and ZSTD_* parameters), generated + * with + * node -p "require('zlib').constants" + * and baked as literals at every access site (the http2.constants + * precedent). Members not starting with BROTLI are also the module's own + * read-only properties (zlib.Z_SYNC_FLUSH). */ + +export const ZLIB_CONSTANTS: Record = { + "Z_NO_FLUSH": 0, + "Z_PARTIAL_FLUSH": 1, + "Z_SYNC_FLUSH": 2, + "Z_FULL_FLUSH": 3, + "Z_FINISH": 4, + "Z_BLOCK": 5, + "Z_OK": 0, + "Z_STREAM_END": 1, + "Z_NEED_DICT": 2, + "Z_ERRNO": -1, + "Z_STREAM_ERROR": -2, + "Z_DATA_ERROR": -3, + "Z_MEM_ERROR": -4, + "Z_BUF_ERROR": -5, + "Z_VERSION_ERROR": -6, + "Z_NO_COMPRESSION": 0, + "Z_BEST_SPEED": 1, + "Z_BEST_COMPRESSION": 9, + "Z_DEFAULT_COMPRESSION": -1, + "Z_FILTERED": 1, + "Z_HUFFMAN_ONLY": 2, + "Z_RLE": 3, + "Z_FIXED": 4, + "Z_DEFAULT_STRATEGY": 0, + "ZLIB_VERNUM": 4897, + "DEFLATE": 1, + "INFLATE": 2, + "GZIP": 3, + "GUNZIP": 4, + "DEFLATERAW": 5, + "INFLATERAW": 6, + "UNZIP": 7, + "BROTLI_DECODE": 8, + "BROTLI_ENCODE": 9, + "ZSTD_DECOMPRESS": 11, + "ZSTD_COMPRESS": 10, + "Z_MIN_WINDOWBITS": 8, + "Z_MAX_WINDOWBITS": 15, + "Z_DEFAULT_WINDOWBITS": 15, + "Z_MIN_CHUNK": 64, + "Z_MAX_CHUNK": Infinity, + "Z_DEFAULT_CHUNK": 16384, + "Z_MIN_MEMLEVEL": 1, + "Z_MAX_MEMLEVEL": 9, + "Z_DEFAULT_MEMLEVEL": 8, + "Z_MIN_LEVEL": -1, + "Z_MAX_LEVEL": 9, + "Z_DEFAULT_LEVEL": -1, + "BROTLI_OPERATION_PROCESS": 0, + "BROTLI_OPERATION_FLUSH": 1, + "BROTLI_OPERATION_FINISH": 2, + "BROTLI_OPERATION_EMIT_METADATA": 3, + "BROTLI_PARAM_MODE": 0, + "BROTLI_MODE_GENERIC": 0, + "BROTLI_MODE_TEXT": 1, + "BROTLI_MODE_FONT": 2, + "BROTLI_DEFAULT_MODE": 0, + "BROTLI_PARAM_QUALITY": 1, + "BROTLI_MIN_QUALITY": 0, + "BROTLI_MAX_QUALITY": 11, + "BROTLI_DEFAULT_QUALITY": 11, + "BROTLI_PARAM_LGWIN": 2, + "BROTLI_MIN_WINDOW_BITS": 10, + "BROTLI_MAX_WINDOW_BITS": 24, + "BROTLI_LARGE_MAX_WINDOW_BITS": 30, + "BROTLI_DEFAULT_WINDOW": 22, + "BROTLI_PARAM_LGBLOCK": 3, + "BROTLI_MIN_INPUT_BLOCK_BITS": 16, + "BROTLI_MAX_INPUT_BLOCK_BITS": 24, + "BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING": 4, + "BROTLI_PARAM_SIZE_HINT": 5, + "BROTLI_PARAM_LARGE_WINDOW": 6, + "BROTLI_PARAM_NPOSTFIX": 7, + "BROTLI_PARAM_NDIRECT": 8, + "BROTLI_DECODER_RESULT_ERROR": 0, + "BROTLI_DECODER_RESULT_SUCCESS": 1, + "BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT": 2, + "BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT": 3, + "BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION": 0, + "BROTLI_DECODER_PARAM_LARGE_WINDOW": 1, + "BROTLI_DECODER_NO_ERROR": 0, + "BROTLI_DECODER_SUCCESS": 1, + "BROTLI_DECODER_NEEDS_MORE_INPUT": 2, + "BROTLI_DECODER_NEEDS_MORE_OUTPUT": 3, + "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE": -1, + "BROTLI_DECODER_ERROR_FORMAT_RESERVED": -2, + "BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE": -3, + "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET": -4, + "BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME": -5, + "BROTLI_DECODER_ERROR_FORMAT_CL_SPACE": -6, + "BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE": -7, + "BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT": -8, + "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1": -9, + "BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2": -10, + "BROTLI_DECODER_ERROR_FORMAT_TRANSFORM": -11, + "BROTLI_DECODER_ERROR_FORMAT_DICTIONARY": -12, + "BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS": -13, + "BROTLI_DECODER_ERROR_FORMAT_PADDING_1": -14, + "BROTLI_DECODER_ERROR_FORMAT_PADDING_2": -15, + "BROTLI_DECODER_ERROR_FORMAT_DISTANCE": -16, + "BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET": -19, + "BROTLI_DECODER_ERROR_INVALID_ARGUMENTS": -20, + "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES": -21, + "BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS": -22, + "BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP": -25, + "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1": -26, + "BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2": -27, + "BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES": -30, + "BROTLI_DECODER_ERROR_UNREACHABLE": -31, + "ZSTD_e_continue": 0, + "ZSTD_e_flush": 1, + "ZSTD_e_end": 2, + "ZSTD_fast": 1, + "ZSTD_dfast": 2, + "ZSTD_greedy": 3, + "ZSTD_lazy": 4, + "ZSTD_lazy2": 5, + "ZSTD_btlazy2": 6, + "ZSTD_btopt": 7, + "ZSTD_btultra": 8, + "ZSTD_btultra2": 9, + "ZSTD_c_compressionLevel": 100, + "ZSTD_c_windowLog": 101, + "ZSTD_c_hashLog": 102, + "ZSTD_c_chainLog": 103, + "ZSTD_c_searchLog": 104, + "ZSTD_c_minMatch": 105, + "ZSTD_c_targetLength": 106, + "ZSTD_c_strategy": 107, + "ZSTD_c_enableLongDistanceMatching": 160, + "ZSTD_c_ldmHashLog": 161, + "ZSTD_c_ldmMinMatch": 162, + "ZSTD_c_ldmBucketSizeLog": 163, + "ZSTD_c_ldmHashRateLog": 164, + "ZSTD_c_contentSizeFlag": 200, + "ZSTD_c_checksumFlag": 201, + "ZSTD_c_dictIDFlag": 202, + "ZSTD_c_nbWorkers": 400, + "ZSTD_c_jobSize": 401, + "ZSTD_c_overlapLog": 402, + "ZSTD_d_windowLogMax": 100, + "ZSTD_CLEVEL_DEFAULT": 3, + "ZSTD_error_no_error": 0, + "ZSTD_error_GENERIC": 1, + "ZSTD_error_prefix_unknown": 10, + "ZSTD_error_version_unsupported": 12, + "ZSTD_error_frameParameter_unsupported": 14, + "ZSTD_error_frameParameter_windowTooLarge": 16, + "ZSTD_error_corruption_detected": 20, + "ZSTD_error_checksum_wrong": 22, + "ZSTD_error_literals_headerWrong": 24, + "ZSTD_error_dictionary_corrupted": 30, + "ZSTD_error_dictionary_wrong": 32, + "ZSTD_error_dictionaryCreation_failed": 34, + "ZSTD_error_parameter_unsupported": 40, + "ZSTD_error_parameter_combination_unsupported": 41, + "ZSTD_error_parameter_outOfBound": 42, + "ZSTD_error_tableLog_tooLarge": 44, + "ZSTD_error_maxSymbolValue_tooLarge": 46, + "ZSTD_error_maxSymbolValue_tooSmall": 48, + "ZSTD_error_stabilityCondition_notRespected": 50, + "ZSTD_error_stage_wrong": 60, + "ZSTD_error_init_missing": 62, + "ZSTD_error_memory_allocation": 64, + "ZSTD_error_workSpace_tooSmall": 66, + "ZSTD_error_dstSize_tooSmall": 70, + "ZSTD_error_srcSize_wrong": 72, + "ZSTD_error_dstBuffer_null": 74, + "ZSTD_error_noForwardProgress_destFull": 80, + "ZSTD_error_noForwardProgress_inputEmpty": 82, +}; diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index 972ab0e83e..cf55da5853 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -27,6 +27,7 @@ import { RUNTIME_EMITTER_CLASS, RUNTIME_ERROR_CLASSES, RUNTIME_STREAM_CLASSES, + ZLIB_STREAM_MODES, setOf, STRING, SYMBOL_T, @@ -2722,6 +2723,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return { kind: "object", className: irName }; } } + // node:zlib's stream classes (the shipped fallback declarations): their + // instances are runtime Transform streams. + if ( + psym !== undefined && + ZLIB_STREAM_MODES.has(psym.name) && + checker + .declarationsOf(psym) + .some( + (d) => + ts.isClassDeclaration(d) && + ctx.isStdlibFile(d.getSourceFile()) && + !isNodeTypesPath(d.getSourceFile().fileName) && + isDeclaredInAmbientModule(d, "zlib"), + ) + ) { + return { kind: "object", className: "%Transform" }; + } if ( (psym?.name === "Readable" && checker diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index e13b241c3b..32be5625b3 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -665,6 +665,12 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "zlib.gunzipSync", "zlib.unzipSync", "zlib.crc32", + "zlib.streamNew", + "zlib.streamFlush", + "zlib.streamFlushCb", + "zlib.streamParams", + "zlib.streamParamsCb", + "zlib.streamReset", "date.toISOString", "date.toISOStringValue", "fs.mkdirRecursiveSync", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 7a943a1f19..1434ce7c28 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -1367,7 +1367,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "test.register": { argTypes: [STRING, F64, STRING, null, F64, STRING], result: VOID }, "test.registerEmpty": { argTypes: [STRING, F64, STRING, F64, STRING], result: VOID }, "test.suite": { - argTypes: [STRING, F64, STRING, { kind: "func", params: [], ret: VOID }, STRING], + argTypes: [STRING, F64, STRING, { kind: "func", params: [], ret: VOID }, STRING, F64], result: VOID, }, "test.hook": { argTypes: [F64, null, F64], result: VOID }, @@ -1380,6 +1380,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "test.ctxTodo": { argTypes: [TESTCTX_T, STRING], result: VOID }, "test.ctxDiagnostic": { argTypes: [TESTCTX_T, STRING], result: VOID }, "test.ctxName": { argTypes: [TESTCTX_T], result: STRING }, + "test.ctxFullName": { argTypes: [TESTCTX_T], result: STRING }, + // t.before/t.after/t.beforeEach/t.afterEach (which: 0 before, 1 after, + // 2 beforeEach, 3 afterEach); a throwing t.before is captured on the test. + "test.ctxHook": { argTypes: [TESTCTX_T, F64, null, F64], result: VOID }, + // t.plan(count), and the count of one t.assert call. + "test.ctxPlan": { argTypes: [TESTCTX_T, F64], result: VOID }, + "test.ctxCount": { argTypes: [TESTCTX_T], result: VOID }, // node:http (scr_http.c over scr_net.c). The handler and the data // listeners are program-dependent closures (null slots, checked in the // libCall case); reqHeader's result is the interned string|undefined @@ -2533,6 +2540,21 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "zlib.gunzipCb": { argTypes: [BYTES_U8, null], result: VOID }, "zlib.unzipCb": { argTypes: [BYTES_U8, null], result: VOID }, "zlib.crc32": { argTypes: [BYTES_U8, F64], result: F64 }, + /** The stream classes (scr_zlib_stream.c, linked with the stream unit): + * streamNew takes Node's node_zlib_mode number and the options value + * (undefined when absent), validates it with Node's ladder, and answers + * the +1 %Transform stream. flush takes the kind value, params the level + * and strategy values; the Cb forms move their () => void callback. The + * receivers are borrowed. */ + "zlib.streamNew": { argTypes: [F64, DYN], result: VOID }, + "zlib.streamBytesWritten": { argTypes: [null], result: F64 }, + "zlib.streamFlush": { argTypes: [null, DYN], result: VOID }, + "zlib.streamFlushCb": { argTypes: [null, DYN, null], result: VOID }, + "zlib.streamParams": { argTypes: [null, DYN, DYN], result: VOID }, + "zlib.streamParamsCb": { argTypes: [null, DYN, DYN, null], result: VOID }, + "zlib.streamClose": { argTypes: [null], result: VOID }, + "zlib.streamCloseCb": { argTypes: [null, null], result: VOID }, + "zlib.streamReset": { argTypes: [null], result: VOID }, /** The Buffer overloads of the raw stream writes — same promptly * submitted streams as process.stdoutWrite/stderrWrite, constantly true. * The encoding arg is evaluated but ignored for bytes, like Node. The Cb diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index a4dce311ed..b2615b6030 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -1440,6 +1440,20 @@ export const RUNTIME_STREAM_CLASSES: ReadonlyMap< export const RUNTIME_EVENT_TARGET_CLASS = "%EventTarget"; export const RUNTIME_EVENT_CLASS = "%Event"; +/** node:zlib's stream classes and their node_zlib_mode numbers. Instances + * are runtime Transform streams (`%Transform` values built by the + * zlib.streamNew libCall, scr_zlib_stream.c); the class name only sets + * the display name. */ +export const ZLIB_STREAM_MODES: ReadonlyMap = new Map([ + ["Deflate", 1], + ["Inflate", 2], + ["Gzip", 3], + ["Gunzip", 4], + ["DeflateRaw", 5], + ["InflateRaw", 6], + ["Unzip", 7], +]); + export interface IrRecordShape { /** Frontend-assigned shape id (`r0`, `r1`, ...). */ id: string; diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 69b8190861..f37334cac0 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -230,7 +230,8 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run fn === "stream.addAbortSignalFn" || fn.startsWith("sp.finishedOpts") || fn.startsWith("sp.pipelineOpts") || - fn.startsWith("stream.finishedOpts") + fn.startsWith("stream.finishedOpts") || + fn.startsWith("zlib.stream") ) { // the stream unit emits through the emitter unit, also when the // program constructs no stream (a pipeline of functions) @@ -318,7 +319,12 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run features.regex = true; } if (fn.startsWith("fs.watch") || fn.startsWith("watcher.")) features.fsWatch = true; - if (fn.startsWith("test.")) features.nodeTest = true; + // scr_test.c renders thrown strings in the failing-tests section with + // the inspect unit's string formatting. + if (fn.startsWith("test.")) { + features.nodeTest = true; + features.inspect = true; + } if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true; if (fn.startsWith("tlsca.")) features.tlsCa = true; // scr_cipher.c over the mbedTLS archive; DEP0182 reports through the diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 3fe0f6e074..ed280e6de8 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -5259,6 +5259,25 @@ function validateFunction( fixedArgs: number, result: IrType, ): boolean { + if (e.fn.startsWith("zlib.stream")) { + // The zlib stream classes: streamNew answers a runtime Transform; + // the members take one as their receiver, and the Cb forms end in + // the () => void callback. + const isTransform = (t: IrType | undefined): boolean => + t?.kind === "object" && t.className === "%Transform"; + if (e.fn === "zlib.streamNew") { + if (!isTransform(e.type)) err(`libCall ${e.fn} must return a %Transform`, e.loc); + return true; + } + if (!isTransform(e.args[0]?.type)) { + err(`libCall ${e.fn} receiver must be a %Transform`, e.loc); + } + if (e.fn.endsWith("Cb") && e.args[e.args.length - 1]?.type.kind !== "func") { + err(`libCall ${e.fn} callback must be a func`, e.loc); + } + if (!typeEquals(e.type, result)) err(`libCall ${e.fn} result type mismatch`, e.loc); + return true; + } if ( e.fn.startsWith("readable.") || e.fn.startsWith("writable.") || diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index b1df01fad1..d428c2cf53 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -2619,13 +2619,794 @@ "status": "static", "note": "recognized module (bare and node:-prefixed specifiers)" }, + { + "id": "node-builtin.zlib.BrotliCompress", + "kind": "node-builtin", + "name": "zlib.BrotliCompress", + "status": "unsupported", + "code": "SC2020", + "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + }, + { + "id": "node-builtin.zlib.BrotliDecompress", + "kind": "node-builtin", + "name": "zlib.BrotliDecompress", + "status": "unsupported", + "code": "SC2020", + "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + }, + { + "id": "node-builtin.zlib.DEFLATE", + "kind": "node-builtin", + "name": "zlib.DEFLATE", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.DEFLATERAW", + "kind": "node-builtin", + "name": "zlib.DEFLATERAW", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.GUNZIP", + "kind": "node-builtin", + "name": "zlib.GUNZIP", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.GZIP", + "kind": "node-builtin", + "name": "zlib.GZIP", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.INFLATE", + "kind": "node-builtin", + "name": "zlib.INFLATE", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.INFLATERAW", + "kind": "node-builtin", + "name": "zlib.INFLATERAW", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.UNZIP", + "kind": "node-builtin", + "name": "zlib.UNZIP", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZLIB_VERNUM", + "kind": "node-builtin", + "name": "zlib.ZLIB_VERNUM", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_CLEVEL_DEFAULT", + "kind": "node-builtin", + "name": "zlib.ZSTD_CLEVEL_DEFAULT", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_COMPRESS", + "kind": "node-builtin", + "name": "zlib.ZSTD_COMPRESS", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_DECOMPRESS", + "kind": "node-builtin", + "name": "zlib.ZSTD_DECOMPRESS", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_btlazy2", + "kind": "node-builtin", + "name": "zlib.ZSTD_btlazy2", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_btopt", + "kind": "node-builtin", + "name": "zlib.ZSTD_btopt", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_btultra", + "kind": "node-builtin", + "name": "zlib.ZSTD_btultra", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_btultra2", + "kind": "node-builtin", + "name": "zlib.ZSTD_btultra2", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_chainLog", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_chainLog", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_checksumFlag", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_checksumFlag", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_compressionLevel", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_compressionLevel", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_contentSizeFlag", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_contentSizeFlag", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_dictIDFlag", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_dictIDFlag", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_enableLongDistanceMatching", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_enableLongDistanceMatching", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_hashLog", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_hashLog", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_jobSize", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_jobSize", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_ldmBucketSizeLog", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_ldmBucketSizeLog", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_ldmHashLog", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_ldmHashLog", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_ldmHashRateLog", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_ldmHashRateLog", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_ldmMinMatch", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_ldmMinMatch", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_minMatch", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_minMatch", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_nbWorkers", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_nbWorkers", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_overlapLog", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_overlapLog", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_searchLog", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_searchLog", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_strategy", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_strategy", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_targetLength", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_targetLength", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_c_windowLog", + "kind": "node-builtin", + "name": "zlib.ZSTD_c_windowLog", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_d_windowLogMax", + "kind": "node-builtin", + "name": "zlib.ZSTD_d_windowLogMax", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_dfast", + "kind": "node-builtin", + "name": "zlib.ZSTD_dfast", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_e_continue", + "kind": "node-builtin", + "name": "zlib.ZSTD_e_continue", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_e_end", + "kind": "node-builtin", + "name": "zlib.ZSTD_e_end", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_e_flush", + "kind": "node-builtin", + "name": "zlib.ZSTD_e_flush", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_GENERIC", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_GENERIC", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_checksum_wrong", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_checksum_wrong", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_corruption_detected", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_corruption_detected", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_dictionaryCreation_failed", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_dictionaryCreation_failed", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_dictionary_corrupted", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_dictionary_corrupted", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_dictionary_wrong", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_dictionary_wrong", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_dstBuffer_null", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_dstBuffer_null", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_dstSize_tooSmall", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_dstSize_tooSmall", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_frameParameter_unsupported", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_frameParameter_unsupported", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_frameParameter_windowTooLarge", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_frameParameter_windowTooLarge", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_init_missing", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_init_missing", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_literals_headerWrong", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_literals_headerWrong", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_maxSymbolValue_tooLarge", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_maxSymbolValue_tooLarge", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_maxSymbolValue_tooSmall", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_maxSymbolValue_tooSmall", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_memory_allocation", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_memory_allocation", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_noForwardProgress_destFull", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_noForwardProgress_destFull", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_noForwardProgress_inputEmpty", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_noForwardProgress_inputEmpty", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_no_error", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_no_error", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_parameter_combination_unsupported", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_parameter_combination_unsupported", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_parameter_outOfBound", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_parameter_outOfBound", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_parameter_unsupported", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_parameter_unsupported", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_prefix_unknown", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_prefix_unknown", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_srcSize_wrong", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_srcSize_wrong", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_stabilityCondition_notRespected", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_stabilityCondition_notRespected", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_stage_wrong", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_stage_wrong", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_tableLog_tooLarge", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_tableLog_tooLarge", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_version_unsupported", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_version_unsupported", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_error_workSpace_tooSmall", + "kind": "node-builtin", + "name": "zlib.ZSTD_error_workSpace_tooSmall", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_fast", + "kind": "node-builtin", + "name": "zlib.ZSTD_fast", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_greedy", + "kind": "node-builtin", + "name": "zlib.ZSTD_greedy", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_lazy", + "kind": "node-builtin", + "name": "zlib.ZSTD_lazy", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.ZSTD_lazy2", + "kind": "node-builtin", + "name": "zlib.ZSTD_lazy2", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_BEST_COMPRESSION", + "kind": "node-builtin", + "name": "zlib.Z_BEST_COMPRESSION", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_BEST_SPEED", + "kind": "node-builtin", + "name": "zlib.Z_BEST_SPEED", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_BLOCK", + "kind": "node-builtin", + "name": "zlib.Z_BLOCK", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_BUF_ERROR", + "kind": "node-builtin", + "name": "zlib.Z_BUF_ERROR", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_DATA_ERROR", + "kind": "node-builtin", + "name": "zlib.Z_DATA_ERROR", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_DEFAULT_CHUNK", + "kind": "node-builtin", + "name": "zlib.Z_DEFAULT_CHUNK", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_DEFAULT_COMPRESSION", + "kind": "node-builtin", + "name": "zlib.Z_DEFAULT_COMPRESSION", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_DEFAULT_LEVEL", + "kind": "node-builtin", + "name": "zlib.Z_DEFAULT_LEVEL", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_DEFAULT_MEMLEVEL", + "kind": "node-builtin", + "name": "zlib.Z_DEFAULT_MEMLEVEL", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_DEFAULT_STRATEGY", + "kind": "node-builtin", + "name": "zlib.Z_DEFAULT_STRATEGY", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_DEFAULT_WINDOWBITS", + "kind": "node-builtin", + "name": "zlib.Z_DEFAULT_WINDOWBITS", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_ERRNO", + "kind": "node-builtin", + "name": "zlib.Z_ERRNO", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_FILTERED", + "kind": "node-builtin", + "name": "zlib.Z_FILTERED", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_FINISH", + "kind": "node-builtin", + "name": "zlib.Z_FINISH", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_FIXED", + "kind": "node-builtin", + "name": "zlib.Z_FIXED", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_FULL_FLUSH", + "kind": "node-builtin", + "name": "zlib.Z_FULL_FLUSH", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_HUFFMAN_ONLY", + "kind": "node-builtin", + "name": "zlib.Z_HUFFMAN_ONLY", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_MAX_CHUNK", + "kind": "node-builtin", + "name": "zlib.Z_MAX_CHUNK", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_MAX_LEVEL", + "kind": "node-builtin", + "name": "zlib.Z_MAX_LEVEL", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_MAX_MEMLEVEL", + "kind": "node-builtin", + "name": "zlib.Z_MAX_MEMLEVEL", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_MAX_WINDOWBITS", + "kind": "node-builtin", + "name": "zlib.Z_MAX_WINDOWBITS", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_MEM_ERROR", + "kind": "node-builtin", + "name": "zlib.Z_MEM_ERROR", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_MIN_CHUNK", + "kind": "node-builtin", + "name": "zlib.Z_MIN_CHUNK", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_MIN_LEVEL", + "kind": "node-builtin", + "name": "zlib.Z_MIN_LEVEL", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_MIN_MEMLEVEL", + "kind": "node-builtin", + "name": "zlib.Z_MIN_MEMLEVEL", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_MIN_WINDOWBITS", + "kind": "node-builtin", + "name": "zlib.Z_MIN_WINDOWBITS", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_NEED_DICT", + "kind": "node-builtin", + "name": "zlib.Z_NEED_DICT", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_NO_COMPRESSION", + "kind": "node-builtin", + "name": "zlib.Z_NO_COMPRESSION", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_NO_FLUSH", + "kind": "node-builtin", + "name": "zlib.Z_NO_FLUSH", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_OK", + "kind": "node-builtin", + "name": "zlib.Z_OK", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_PARTIAL_FLUSH", + "kind": "node-builtin", + "name": "zlib.Z_PARTIAL_FLUSH", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_RLE", + "kind": "node-builtin", + "name": "zlib.Z_RLE", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_STREAM_END", + "kind": "node-builtin", + "name": "zlib.Z_STREAM_END", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_STREAM_ERROR", + "kind": "node-builtin", + "name": "zlib.Z_STREAM_ERROR", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_SYNC_FLUSH", + "kind": "node-builtin", + "name": "zlib.Z_SYNC_FLUSH", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.Z_VERSION_ERROR", + "kind": "node-builtin", + "name": "zlib.Z_VERSION_ERROR", + "status": "static", + "note": "constant value read" + }, + { + "id": "node-builtin.zlib.brotliCompress", + "kind": "node-builtin", + "name": "zlib.brotliCompress", + "status": "unsupported", + "code": "SC2020", + "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + }, { "id": "node-builtin.zlib.brotliCompressSync", "kind": "node-builtin", "name": "zlib.brotliCompressSync", "status": "unsupported", "code": "SC2020", - "note": "the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms plus crc32 are the lowered zlib surface" + "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + }, + { + "id": "node-builtin.zlib.brotliDecompress", + "kind": "node-builtin", + "name": "zlib.brotliDecompress", + "status": "unsupported", + "code": "SC2020", + "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" }, { "id": "node-builtin.zlib.brotliDecompressSync", @@ -2633,7 +3414,7 @@ "name": "zlib.brotliDecompressSync", "status": "unsupported", "code": "SC2020", - "note": "the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms plus crc32 are the lowered zlib surface" + "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" }, { "id": "node-builtin.zlib.crc32", @@ -2641,6 +3422,64 @@ "name": "zlib.crc32", "status": "static" }, + { + "id": "node-builtin.zlib.createBrotliCompress", + "kind": "node-builtin", + "name": "zlib.createBrotliCompress", + "status": "unsupported", + "code": "SC2020", + "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + }, + { + "id": "node-builtin.zlib.createBrotliDecompress", + "kind": "node-builtin", + "name": "zlib.createBrotliDecompress", + "status": "unsupported", + "code": "SC2020", + "note": "Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface" + }, + { + "id": "node-builtin.zlib.createDeflate", + "kind": "node-builtin", + "name": "zlib.createDeflate", + "status": "static" + }, + { + "id": "node-builtin.zlib.createDeflateRaw", + "kind": "node-builtin", + "name": "zlib.createDeflateRaw", + "status": "static" + }, + { + "id": "node-builtin.zlib.createGunzip", + "kind": "node-builtin", + "name": "zlib.createGunzip", + "status": "static" + }, + { + "id": "node-builtin.zlib.createGzip", + "kind": "node-builtin", + "name": "zlib.createGzip", + "status": "static" + }, + { + "id": "node-builtin.zlib.createInflate", + "kind": "node-builtin", + "name": "zlib.createInflate", + "status": "static" + }, + { + "id": "node-builtin.zlib.createInflateRaw", + "kind": "node-builtin", + "name": "zlib.createInflateRaw", + "status": "static" + }, + { + "id": "node-builtin.zlib.createUnzip", + "kind": "node-builtin", + "name": "zlib.createUnzip", + "status": "static" + }, { "id": "node-builtin.zlib.deflate", "kind": "node-builtin", diff --git a/packages/compiler/test/format-float-arguments.test.ts b/packages/compiler/test/format-float-arguments.test.ts new file mode 100644 index 0000000000..8aa08469da --- /dev/null +++ b/packages/compiler/test/format-float-arguments.test.ts @@ -0,0 +1,35 @@ +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile, deserializeModule, validateModule } from "../src/index.js"; + +test("util.format %f lowers a boolean argument once", async () => { + const dir = await mkdtemp(join(tmpdir(), "scriptc-format-float-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + await writeFile( + entry, + [ + 'import { format } from "node:util";', + "const xs = [1, 2, 3];", + 'console.log(format("%f %f", xs.some((x) => x > 2), xs.every((x) => x > 0)));', + ].join("\n"), + ); + const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + const mod = deserializeModule(await readFile(outPath, "utf8")); + expect(validateModule(mod)).toEqual([]); + // One lifted helper per argument: the evaluated-for-effect statement + // reuses the lowered value instead of lowering the argument again. + const helpers = mod.functions + .map((f) => f.name) + .filter((name) => name.startsWith("%arr.")) + .map((name) => name.replace(/\.\d+$/, "")); + expect(helpers).toEqual(["%arr.some", "%arr.every"]); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/packages/compiler/test/node-pin.test.ts b/packages/compiler/test/node-pin.test.ts index 41861e8ea1..7f032c69eb 100644 --- a/packages/compiler/test/node-pin.test.ts +++ b/packages/compiler/test/node-pin.test.ts @@ -8,6 +8,7 @@ import { expect, test } from "vitest"; import { NODE24_FETCH_COMPAT_PROFILE } from "../src/compat/fetch-profile.js"; import { NODE_COMPAT_VERSION } from "../src/frontend/lowering/builtins/process.js"; import { CRYPTO_CONSTANTS } from "../src/frontend/lowering/crypto-tables.js"; +import { ZLIB_CONSTANTS } from "../src/frontend/lowering/zlib-constants.js"; const repoRoot = join(import.meta.dirname, "../../.."); const read = (path: string): string => readFileSync(join(repoRoot, path), "utf8"); @@ -43,3 +44,16 @@ test("the crypto.constants declaration matches the baked table", () => { ); expect(declared).toEqual(CRYPTO_CONSTANTS); }); + +test("the zlib.constants declaration matches the baked table", () => { + const dts = read("packages/compiler/ambient/scriptc-node-fallback.d.ts"); + const start = dts.indexOf("export interface ZlibConstants {"); + const body = dts.slice(start, dts.indexOf("\n }\n", start)); + const declared = Object.fromEntries( + [...body.matchAll(/readonly (\w+): (-?\d+|number);/g)].map(([, name, value]) => [ + name!, + value === "number" ? Infinity : Number(value), + ]), + ); + expect(declared).toEqual(ZLIB_CONSTANTS); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5370-zlib-stream-roundtrip.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5370-zlib-stream-roundtrip.ts.json new file mode 100644 index 0000000000..0edc2e5c25 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5370-zlib-stream-roundtrip.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5370-zlib-stream-roundtrip.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5371-zlib-stream-chunking.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5371-zlib-stream-chunking.cjs.json new file mode 100644 index 0000000000..5ee2502918 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5371-zlib-stream-chunking.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5371-zlib-stream-chunking.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5372-zlib-stream-errors.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5372-zlib-stream-errors.cjs.json new file mode 100644 index 0000000000..ebf00fbf26 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5372-zlib-stream-errors.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5372-zlib-stream-errors.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5373-zlib-stream-options.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5373-zlib-stream-options.cjs.json new file mode 100644 index 0000000000..e824a0684d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5373-zlib-stream-options.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5373-zlib-stream-options.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5374-zlib-stream-flush-params.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5374-zlib-stream-flush-params.ts.json new file mode 100644 index 0000000000..9f61aec6a6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5374-zlib-stream-flush-params.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5374-zlib-stream-flush-params.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5375-zlib-stream-promises.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5375-zlib-stream-promises.ts.json new file mode 100644 index 0000000000..c8ad6ea686 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5375-zlib-stream-promises.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5375-zlib-stream-promises.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5376-zlib-constants.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5376-zlib-constants.cjs.json new file mode 100644 index 0000000000..79c3be7026 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5376-zlib-constants.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5376-zlib-constants.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5377-zlib-stream-backpressure.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5377-zlib-stream-backpressure.cjs.json new file mode 100644 index 0000000000..10aa9acc60 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5377-zlib-stream-backpressure.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5377-zlib-stream-backpressure.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5621-util-format-float.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5621-util-format-float.ts.json new file mode 100644 index 0000000000..a2a81e219f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5621-util-format-float.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5621-util-format-float.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5740-atomics-operation-semantics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5740-atomics-operation-semantics.ts.json new file mode 100644 index 0000000000..518598cbdd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5740-atomics-operation-semantics.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5740-atomics-operation-semantics.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5741-atomics-contended-counters.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5741-atomics-contended-counters.ts.json new file mode 100644 index 0000000000..5431f03a9e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5741-atomics-contended-counters.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5741-atomics-contended-counters.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5742-atomics-lock-ordering.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5742-atomics-lock-ordering.ts.json new file mode 100644 index 0000000000..ae8b5490aa --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5742-atomics-lock-ordering.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5742-atomics-lock-ordering.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5743-atomics-wait-notify.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5743-atomics-wait-notify.ts.json new file mode 100644 index 0000000000..eadd7ca9eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5743-atomics-wait-notify.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5743-atomics-wait-notify.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5744-atomics-ring-buffer.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5744-atomics-ring-buffer.ts.json new file mode 100644 index 0000000000..77a245d480 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5744-atomics-ring-buffer.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5744-atomics-ring-buffer.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5745-atomics-semaphore-stress.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5745-atomics-semaphore-stress.ts.json new file mode 100644 index 0000000000..82ba497d20 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5745-atomics-semaphore-stress.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5745-atomics-semaphore-stress.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5746-shared-bulk-operations.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5746-shared-bulk-operations.ts.json new file mode 100644 index 0000000000..9b66663981 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5746-shared-bulk-operations.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5746-shared-bulk-operations.ts" + ], + "diags": [] +} diff --git a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs index 08dfb667cc..2bb811e892 100644 --- a/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs +++ b/packages/runtime-darwin-arm64/runtime-pack-matrix.mjs @@ -89,6 +89,8 @@ const optional = [ ["scr_util_style.c", all("parseArgs", "inspect", "dynAsync")], ["scr_util_callable.c", all("parseArgs", "inspect", "dynAsync", "dynInvoke", "regex")], ["scr_stream.c", "stream"], + // The zlib stream classes reference both the zlib engine and the stream unit. + ["scr_zlib_stream.c", all("zlib", "stream")], // The Readable operators await through the dyn-async unit. ["scr_stream_ops.c", all("stream", "dynAsync")], ["scr_loop_kqueue.c", any("netEffective", "dgram")], diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 6799b7384a..edf525a4c1 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -79,6 +79,8 @@ const OPTIONAL = [ ["scr_util_style.c", all("parseArgs", "inspect", "dynAsync")], ["scr_util_callable.c", all("parseArgs", "inspect", "dynAsync", "dynInvoke", "regex")], ["scr_stream.c", "stream"], + // The zlib stream classes reference both the zlib engine and the stream unit. + ["scr_zlib_stream.c", all("zlib", "stream")], // The Readable operators await through the dyn-async unit. ["scr_stream_ops.c", all("stream", "dynAsync")], ["scr_loop_kqueue.c", any("netEffective", "dgram")], diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index d9dcff40ed..12eb75cb48 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -3139,7 +3139,14 @@ bool scr_bytes_sort_numeric(ScrBytes *b, int32_t order) { size_t n = b->len; if (n < 2) return true; if (order < -1 || order > 1) scr_trap("scriptc: invalid typed array sort order\n"); - SCR_SHARED_GUARD(b, NULL); + if (b->shared) { + /* Sort a private copy and publish every element. */ + ScrBytes *copy = scr_bytes_snapshot(b); + bool sorted = scr_bytes_sort_numeric(copy, order); + if (sorted) scr_shared_store(b->data, copy->data, n * scr_bytes_elem_size(b->elem)); + scr_bytes_release(copy); + return sorted; + } size_t width = scr_bytes_elem_size(b->elem); uint8_t *data = b->data; /* Integer elements have no NaN or -0: every order is a value order. */ @@ -3234,18 +3241,23 @@ void scr_bytes_sort_compare(ScrBytes *b, ScrClosure *f, uint32_t arity) { if (n < 2) return; if (arity > 2) scr_trap("scriptc: invalid typed array comparator\n"); if (n > SIZE_MAX / 2 / sizeof(uint64_t)) scr_arr_oom(); + if (b->shared) { + /* The comparator runs on a private copy; a completed sort publishes + * every element. */ + ScrBytes *copy = scr_bytes_snapshot(b); + scr_bytes_sort_compare(copy, f, arity); + if (!scr_exc_pending()) scr_shared_store(b->data, copy->data, n * scr_bytes_elem_size(b->elem)); + scr_bytes_release(copy); + return; + } uint64_t *work1 = malloc(n * sizeof(*work1)), *work2 = malloc(n * sizeof(*work2)); if (!work1 || !work2) scr_arr_oom(); - { - SCR_SHARED_GUARD(b, NULL); - for (size_t i = 0; i < n; i++) work1[i] = work2[i] = scr_slot_from_f64(scr_bytes_sort_load(b, i)); - } + for (size_t i = 0; i < n; i++) work1[i] = work2[i] = scr_slot_from_f64(scr_bytes_sort_load(b, i)); ScrSortCompare c = {f, NULL, arity, true, false, 0, NULL}; scr_dyn_this_push_dyn(NULL); bool sorted = scr_bytes_merge_sort(work2, 0, n, work1, &c); scr_dyn_this_pop(); if (sorted) { - SCR_SHARED_GUARD(b, NULL); for (size_t i = 0; i < n; i++) scr_bytes_sort_store(b, i, scr_slot_to_f64(work1[i])); } free(work1); diff --git a/packages/runtime/src/scr_assert.c b/packages/runtime/src/scr_assert.c index 2a5df375fa..9e2e79418e 100644 --- a/packages/runtime/src/scr_assert.c +++ b/packages/runtime/src/scr_assert.c @@ -481,7 +481,8 @@ void scr_assert_deep_result(bool equal, bool negated, ScrStr *msg, bool has_msg) * by construction (the frontend's same-static-type gate), but the elem * check stays: a defensive answer beats a miscompare. */ static bool scr_assert_bytes_content_eq(const ScrBytes *a, const ScrBytes *b) { - SCR_SHARED_GUARD(a, b); + SCR_BYTES_SNAPSHOT(a); + SCR_BYTES_SNAPSHOT(b); if (a->elem != b->elem || a->len != b->len) return false; size_t nbytes = a->len * scr_bytes_elem_size(a->elem); return nbytes == 0 || memcmp(a->data, b->data, nbytes) == 0; @@ -1974,7 +1975,8 @@ static bool scr_assert_partial_bytes(const ScrBytes *x, const ScrBytes *y) { size_t la = x->len * scr_bytes_elem_size(x->elem), lb = y->len * scr_bytes_elem_size(y->elem); if (la == lb) return scr_assert_bytes_content_eq(x, y); if (la < lb) return false; - SCR_SHARED_GUARD(x, y); + SCR_BYTES_SNAPSHOT(x); + SCR_BYTES_SNAPSHOT(y); const uint8_t *pa = x->data, *pb = y->data; size_t offset_a = 0; for (size_t offset_b = 0; offset_b < lb; offset_b++) { diff --git a/packages/runtime/src/scr_bytes.c b/packages/runtime/src/scr_bytes.c index ed8642e19a..e6085b4ded 100644 --- a/packages/runtime/src/scr_bytes.c +++ b/packages/runtime/src/scr_bytes.c @@ -199,10 +199,29 @@ ScrBytes *scr_bytes_from_data(const uint8_t *data, size_t len) { return b; } +/* Raw bytes of a view: shared storage through relaxed atomic copies + * (scr_shared_load / scr_shared_store), other storage directly. */ +static void scr_bytes_raw_in(const ScrBytes *b, void *dst, const uint8_t *src, size_t n) { + if (b->shared) scr_shared_load(dst, src, n); + else if (n) memcpy(dst, src, n); +} + +static void scr_bytes_raw_out(ScrBytes *b, uint8_t *dst, const void *src, size_t n) { + if (b->shared) scr_shared_store(dst, src, n); + else if (n) memmove(dst, src, n); +} + +/* A private buffer for n bytes that a shared writer fills before + * publishing them with scr_bytes_raw_out. */ +static uint8_t *scr_bytes_scratch(size_t n) { + uint8_t *scratch = malloc(n ? n : 1); + if (!scratch) scr_bytes_oom(); + return scratch; +} + ScrBytes *scr_bytes_copy(const ScrBytes *src) { - SCR_SHARED_GUARD(src, NULL); ScrBytes *b = scr_bytes_alloc_private(src->elem, src->len); - memcpy(b->data, src->data, src->len * scr_bytes_elem_size(src->elem)); + scr_bytes_raw_in(src, b->data, src->data, src->len * scr_bytes_elem_size(src->elem)); return b; } @@ -241,11 +260,18 @@ void scr_bytes_release_v(void *b) { scr_bytes_release((ScrBytes *)b); } double scr_bytes_len(const ScrBytes *b) { return (double)b->len; } void scr_bytes_copy_contents(ScrBytes *dst, const ScrBytes *src) { - SCR_SHARED_GUARD(dst, src); if (dst->elem != src->elem || dst->len != src->len) { scr_trap("scriptc: typed array snapshot shape changed\n"); } - memcpy(dst->data, src->data, dst->len * scr_bytes_elem_size(dst->elem)); + size_t n = dst->len * scr_bytes_elem_size(dst->elem); + if (!dst->shared && !src->shared) { + memcpy(dst->data, src->data, n); + return; + } + uint8_t *scratch = scr_bytes_scratch(n); + scr_bytes_raw_in(src, scratch, src->data, n); + scr_bytes_raw_out(dst, dst->data, scratch, n); + free(scratch); } double scr_bytes_byte_len(const ScrBytes *b) { @@ -272,52 +298,146 @@ static uint32_t scr_bytes_to_u32(double v) { return scr_numeric_to_u32(v); } -/* The element at a valid index, as a number. */ -static double scr_bytes_load(const ScrBytes *b, size_t idx) { - switch (b->elem) { +/* An element's value from its bytes. */ +static double scr_bytes_decode(ScrBytesElem elem, const uint8_t *p) { + switch (elem) { case SCR_BYTES_U8: case SCR_BYTES_U8C: - return (double)b->data[idx]; + return (double)p[0]; case SCR_BYTES_I8: { int8_t v; - memcpy(&v, b->data + idx, 1); + memcpy(&v, p, 1); return (double)v; } case SCR_BYTES_U16: { uint16_t v; - memcpy(&v, b->data + idx * 2, 2); + memcpy(&v, p, 2); return (double)v; } case SCR_BYTES_I16: { int16_t v; - memcpy(&v, b->data + idx * 2, 2); + memcpy(&v, p, 2); return (double)v; } case SCR_BYTES_U32: { uint32_t v; - memcpy(&v, b->data + idx * 4, 4); + memcpy(&v, p, 4); return (double)v; } case SCR_BYTES_F32: { float v; - memcpy(&v, b->data + idx * 4, 4); + memcpy(&v, p, 4); return (double)v; } case SCR_BYTES_F64: { double v; - memcpy(&v, b->data + idx * 8, 8); + memcpy(&v, p, 8); return v; } case SCR_BYTES_I32: { int32_t v; - memcpy(&v, b->data + idx * 4, 4); + memcpy(&v, p, 4); return (double)v; } } return 0; /* unreachable */ } +/* An element's bytes for v, with the element type's JS conversion. */ +static void scr_bytes_encode(ScrBytesElem elem, uint8_t *p, double v) { + switch (elem) { + case SCR_BYTES_U8: case SCR_BYTES_I8: + p[0] = (uint8_t)scr_bytes_to_u32(v); + break; + case SCR_BYTES_U8C: + p[0] = (uint8_t)scr_bytes_to_u8_clamp(v); + break; + case SCR_BYTES_U16: case SCR_BYTES_I16: { + uint16_t u = (uint16_t)scr_bytes_to_u32(v); + memcpy(p, &u, 2); + break; + } + case SCR_BYTES_U32: case SCR_BYTES_I32: { + /* ToInt32 is ToUint32 reinterpreted signed (same 2^32 residue). */ + uint32_t u = scr_bytes_to_u32(v); + memcpy(p, &u, 4); + break; + } + case SCR_BYTES_F32: { + float f = (float)v; /* round-to-nearest-even, exactly Float32Array */ + memcpy(p, &f, 4); + break; + } + case SCR_BYTES_F64: + memcpy(p, &v, 8); + break; + } +} + +/* Shared elements are read and written as one relaxed atomic access of the + * element's width. Atomics operations on the same elements use atomic + * instructions without a lock, and an ordinary access of an aligned element + * never tears (ECMA-262 shared memory model). Views of shared storage are + * aligned to their element size. */ +static void scr_bytes_shared_read(const ScrBytes *b, size_t idx, uint8_t out[8]) { + size_t width = scr_bytes_elem_size(b->elem); + const uint8_t *p = b->data + idx * width; + switch (width) { + case 1: out[0] = __atomic_load_n(p, __ATOMIC_RELAXED); break; + case 2: { + uint16_t v = __atomic_load_n((const uint16_t *)p, __ATOMIC_RELAXED); + memcpy(out, &v, 2); + break; + } + case 4: { + uint32_t v = __atomic_load_n((const uint32_t *)p, __ATOMIC_RELAXED); + memcpy(out, &v, 4); + break; + } + default: { + uint64_t v = __atomic_load_n((const uint64_t *)p, __ATOMIC_RELAXED); + memcpy(out, &v, 8); + break; + } + } +} + +static void scr_bytes_shared_write(ScrBytes *b, size_t idx, const uint8_t in[8]) { + size_t width = scr_bytes_elem_size(b->elem); + uint8_t *p = b->data + idx * width; + switch (width) { + case 1: __atomic_store_n(p, in[0], __ATOMIC_RELAXED); break; + case 2: { + uint16_t v; + memcpy(&v, in, 2); + __atomic_store_n((uint16_t *)p, v, __ATOMIC_RELAXED); + break; + } + case 4: { + uint32_t v; + memcpy(&v, in, 4); + __atomic_store_n((uint32_t *)p, v, __ATOMIC_RELAXED); + break; + } + default: { + uint64_t v; + memcpy(&v, in, 8); + __atomic_store_n((uint64_t *)p, v, __ATOMIC_RELAXED); + break; + } + } +} + +/* The element at a valid index, as a number. */ +static double scr_bytes_load(const ScrBytes *b, size_t idx) { + if (b->shared) { + uint8_t bits[8]; + scr_bytes_shared_read(b, idx, bits); + return scr_bytes_decode(b->elem, bits); + } + return scr_bytes_decode(b->elem, b->data + idx * scr_bytes_elem_size(b->elem)); +} + double scr_bytes_get(const ScrBytes *b, double i) { - SCR_SHARED_GUARD(b, NULL); return scr_bytes_load(b, scr_bytes_check_index(b, i)); } @@ -331,89 +451,35 @@ double scr_bytes_get_or_nan(const ScrBytes *b, double i) { /* Store v at a valid index with the element type's JS conversion. */ static void scr_bytes_store(ScrBytes *b, size_t idx, double v) { - switch (b->elem) { - case SCR_BYTES_U8: case SCR_BYTES_I8: - b->data[idx] = (uint8_t)scr_bytes_to_u32(v); - break; - case SCR_BYTES_U8C: - b->data[idx] = (uint8_t)scr_bytes_to_u8_clamp(v); - break; - case SCR_BYTES_U16: case SCR_BYTES_I16: { - uint16_t u = (uint16_t)scr_bytes_to_u32(v); - memcpy(b->data + idx * 2, &u, 2); - break; - } - case SCR_BYTES_U32: { - uint32_t u = scr_bytes_to_u32(v); - memcpy(b->data + idx * 4, &u, 4); - break; - } - case SCR_BYTES_F32: { - float f = (float)v; /* round-to-nearest-even, exactly Float32Array */ - memcpy(b->data + idx * 4, &f, 4); - break; - } - case SCR_BYTES_F64: - memcpy(b->data + idx * 8, &v, 8); - break; - case SCR_BYTES_I32: { - /* ToInt32 is ToUint32 reinterpreted signed (same 2^32 residue). */ - uint32_t u = scr_bytes_to_u32(v); - int32_t s; - memcpy(&s, &u, 4); - memcpy(b->data + idx * 4, &s, 4); - break; - } + if (b->shared) { + uint8_t bits[8]; + scr_bytes_encode(b->elem, bits, v); + scr_bytes_shared_write(b, idx, bits); + return; } + scr_bytes_encode(b->elem, b->data + idx * scr_bytes_elem_size(b->elem), v); } void scr_bytes_set(ScrBytes *b, double i, double v) { - SCR_SHARED_GUARD(b, NULL); if (!(i >= 0) || i != trunc(i) || i >= (double)b->len) return; scr_bytes_store(b, (size_t)i, v); } /* Compiled code validates the index against the view's length, which never - * changes, before it takes these shared-view paths; they only add the - * shared-memory lock around the element access. */ + * changes, before it takes these shared-view paths. */ int32_t scr_bytes_shared_get_int(const ScrBytes *b, size_t i) { - SCR_SHARED_GUARD(b, NULL); - switch (b->elem) { - case SCR_BYTES_U8: case SCR_BYTES_U8C: - return b->data[i]; - case SCR_BYTES_I8: { - int8_t v; - memcpy(&v, b->data + i, 1); - return v; - } - case SCR_BYTES_U16: { - uint16_t v; - memcpy(&v, b->data + i * 2, 2); - return v; - } - case SCR_BYTES_I16: { - int16_t v; - memcpy(&v, b->data + i * 2, 2); - return v; - } - case SCR_BYTES_U32: case SCR_BYTES_I32: { - int32_t v; - memcpy(&v, b->data + i * 4, 4); - return v; - } - case SCR_BYTES_F32: case SCR_BYTES_F64: - break; - } - scr_trap("scriptc: integer read of a float typed array\n"); + if (b->elem == SCR_BYTES_F32 || b->elem == SCR_BYTES_F64) + scr_trap("scriptc: integer read of a float typed array\n"); + /* Uint32 elements read as their bit pattern, every other kind exactly. */ + double v = scr_bytes_load(b, i); + return b->elem == SCR_BYTES_U32 ? (int32_t)(uint32_t)v : (int32_t)v; } double scr_bytes_shared_get_f64(const ScrBytes *b, size_t i) { - SCR_SHARED_GUARD(b, NULL); return scr_bytes_load(b, i); } void scr_bytes_shared_set(ScrBytes *b, size_t i, double v) { - SCR_SHARED_GUARD(b, NULL); scr_bytes_store(b, i, v); } @@ -491,7 +557,7 @@ static void scr_bytes_convert_span(uint8_t *dst, ScrBytesElem dest_elem, } ScrBytes *scr_bytes_convert(ScrBytesElem elem, const ScrBytes *src) { - SCR_SHARED_GUARD(src, NULL); + SCR_BYTES_SNAPSHOT(src); if (elem == src->elem) return scr_bytes_copy(src); ScrBytes *out = scr_bytes_alloc_private(elem, src->len); scr_bytes_convert_span(out->data, elem, src->data, src->elem, src->len); @@ -511,25 +577,28 @@ static size_t scr_bytes_rel_index(double i, size_t len) { } ScrBytes *scr_bytes_slice(const ScrBytes *b, double start, double end) { - SCR_SHARED_GUARD(b, NULL); size_t s = scr_bytes_rel_index(start, b->len); size_t e = scr_bytes_rel_index(end, b->len); size_t count = e > s ? e - s : 0; size_t esize = scr_bytes_elem_size(b->elem); ScrBytes *out = scr_bytes_alloc_private(b->elem, count); - memcpy(out->data, b->data + s * esize, count * esize); + scr_bytes_raw_in(b, out->data, b->data + s * esize, count * esize); return out; } ScrBytes *scr_bytes_copy_within(ScrBytes *b, double target, double start, double end) { - SCR_SHARED_GUARD(b, NULL); size_t t = scr_bytes_rel_index(target, b->len); size_t s = scr_bytes_rel_index(start, b->len); size_t e = scr_bytes_rel_index(end, b->len); size_t count = e > s ? e - s : 0; if (count > b->len - t) count = b->len - t; size_t width = scr_bytes_elem_size(b->elem); - memmove(b->data + t * width, b->data + s * width, count * width); + if (b->shared) { + uint8_t *scratch = scr_bytes_scratch(count * width); + scr_shared_load(scratch, b->data + s * width, count * width); + scr_shared_store(b->data + t * width, scratch, count * width); + free(scratch); + } else memmove(b->data + t * width, b->data + s * width, count * width); return scr_bytes_retain(b); } @@ -538,15 +607,14 @@ ScrBytes *scr_bytes_copy_within(ScrBytes *b, double target, double start, double * rounding), slice-clamped relative indices; answers the receiver +1 * (chaining). Never throws — Buffer's throwing fill family is separate. */ ScrBytes *scr_bytes_fill_elem(ScrBytes *b, double v, double start, double end) { - SCR_SHARED_GUARD(b, NULL); size_t s = scr_bytes_rel_index(start, b->len); size_t e = scr_bytes_rel_index(end, b->len); if (e > s) { size_t width = scr_bytes_elem_size(b->elem); uint8_t pattern[8]; scr_bytes_write_numbers(pattern, b->elem, &v, 1); - uint8_t *dst = b->data + s * width; size_t bytes = (e - s) * width; + uint8_t *dst = b->shared ? scr_bytes_scratch(bytes) : b->data + s * width; if (width == 1) memset(dst, pattern[0], bytes); else { memcpy(dst, pattern, width); @@ -557,6 +625,10 @@ ScrBytes *scr_bytes_fill_elem(ScrBytes *b, double v, double start, double end) { filled += copy; } } + if (b->shared) { + scr_shared_store(b->data + s * width, dst, bytes); + free(dst); + } } return scr_bytes_retain(b); } @@ -590,7 +662,6 @@ ScrBytes *scr_bytes_subarray(ScrBytes *b, double start, double end) { } void scr_bytes_set_from(ScrBytes *dst, const ScrBytes *src, double offset) { - SCR_SHARED_GUARD(dst, src); double t = (offset != offset) ? 0 : trunc(offset); if (!(t >= 0) || (double)src->len + t > (double)dst->len) { static const char msg[] = "offset is out of bounds"; @@ -598,6 +669,16 @@ void scr_bytes_set_from(ScrBytes *dst, const ScrBytes *src, double offset) { return; } size_t width = scr_bytes_elem_size(dst->elem); + if (dst->shared || src->shared) { + /* Convert a private copy of the source, then publish the target range. */ + SCR_BYTES_SNAPSHOT(src); + size_t bytes = src->len * width; + uint8_t *scratch = scr_bytes_scratch(bytes); + scr_bytes_convert_span(scratch, dst->elem, src->data, src->elem, src->len); + scr_bytes_raw_out(dst, dst->data + (size_t)t * width, scratch, bytes); + free(scratch); + return; + } uint8_t *target = dst->data + (size_t)t * width; if (dst->elem == src->elem) { memmove(target, src->data, src->len * width); @@ -858,7 +939,6 @@ static size_t scr_dataview_get_size(ScrDataViewGet kind) { } double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind, bool le) { - SCR_SHARED_GUARD(b, NULL); size_t width = scr_dataview_get_size(kind); /* ToIndex, then the view-relative bounds check — Node's ONE constant * message for every failure mode (negative, NaN is fine, too large). */ @@ -868,7 +948,8 @@ double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind, scr_throw_error_msg(SCR_ERR_RANGE, msg, sizeof msg - 1); return 0; } - const uint8_t *p = b->data + (size_t)off; + uint8_t p[8]; + scr_bytes_raw_in(b, p, b->data + (size_t)off, width); /* Assemble host-independently; le false is the JS big-endian default. */ uint64_t u = 0; for (size_t i = 0; i < width; i++) { @@ -907,7 +988,6 @@ double scr_dataview_get(const ScrBytes *b, double byte_off, ScrDataViewGet kind, } void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet kind, bool le) { - SCR_SHARED_GUARD(b, NULL); size_t width = scr_dataview_get_size(kind); /* ToIndex + the view-relative bounds check — the getters' ONE message. */ double off = (byte_off != byte_off) ? 0 : trunc(byte_off); @@ -944,10 +1024,11 @@ void scr_dataview_set(ScrBytes *b, double byte_off, double value, ScrDataViewGet return; /* BigInt setters use scr_dataview_write_u64_raw. */ } /* Scatter host-independently; le false is the JS big-endian default. */ - uint8_t *p = b->data + (size_t)off; + uint8_t p[8]; for (size_t i = 0; i < width; i++) { p[le ? i : width - 1 - i] = (uint8_t)(u >> (8 * i)); } + scr_bytes_raw_out(b, b->data + (size_t)off, p, width); } /* ── encodings (u8 only — the compiler routes only u8 receivers here) ──── */ @@ -1058,7 +1139,7 @@ ScrStr *scr_str_from_utf8_lossy(const uint8_t *bytes, size_t len) { * difference — a leading UTF-8 BOM is stripped (ignoreBOM defaults to * false in the spec; Buffer.toString keeps the BOM as U+FEFF). */ ScrStr *scr_text_decode_options(const ScrBytes *b, bool fatal, bool ignore_bom) { - SCR_SHARED_GUARD(b, NULL); + SCR_BYTES_SNAPSHOT(b); const uint8_t *in = b->data; size_t n = b->len; if (!ignore_bom && n >= 3 && in[0] == 0xef && in[1] == 0xbb && in[2] == 0xbf) { @@ -1252,14 +1333,14 @@ static ScrStr *scr_strdec_step(const ScrStr *enc, double pending, const ScrBytes /* decoder.write(chunk): the decoded complete prefix (+1). */ ScrStr *scr_strdec_write(const ScrStr *enc, double pending, const ScrBytes *chunk) { - SCR_SHARED_GUARD(chunk, NULL); + SCR_BYTES_SNAPSHOT(chunk); double hold; return scr_strdec_step(enc, pending, chunk, true, &hold); } /* The decoder state AFTER a write: the packed trailing partial sequence. */ double scr_strdec_next(const ScrStr *enc, double pending, const ScrBytes *chunk) { - SCR_SHARED_GUARD(chunk, NULL); + SCR_BYTES_SNAPSHOT(chunk); double hold = 0; scr_strdec_step(enc, pending, chunk, false, &hold); return hold; @@ -1300,7 +1381,7 @@ static void scr_bytes_to_str_bounds(const ScrBytes *b, double start, double end, * form). Delegates to the whole-buffer decoder * through a stack view — no allocation, no rc traffic. */ ScrStr *scr_bytes_to_str_range(const ScrBytes *b, const ScrStr *enc, double start, double end) { - SCR_SHARED_GUARD(b, NULL); + SCR_BYTES_SNAPSHOT(b); size_t s0, e0; scr_bytes_to_str_bounds(b, start, end, &s0, &e0); ScrBytes view = { .rc = 1, .len = e0 - s0, .elem = b->elem, .data = b->data + s0, .backing = NULL }; @@ -1904,7 +1985,7 @@ static ScrStr *scr_td_iso_2022_jp_decode(const ScrBytes *b, bool fatal) { } ScrStr *scr_text_decode_legacy_options(const ScrBytes *b, double encoding_value, bool fatal, bool ignore_bom) { - SCR_SHARED_GUARD(b, NULL); + SCR_BYTES_SNAPSHOT(b); unsigned encoding = (unsigned)encoding_value; if (encoding == 36) encoding = 7; /* ISO-8859-8-I */ if (encoding == 37) encoding = SCR_TD_GB18030; /* GBK */ @@ -1945,7 +2026,7 @@ static size_t scr_td_utf8_tail(const uint8_t *bytes, size_t n) { ScrStr *scr_text_decode_stream(ScrDyn *state, const ScrBytes *input, double encoding, bool fatal, bool ignore_bom, bool stream) { - SCR_SHARED_GUARD(input, NULL); + SCR_BYTES_SNAPSHOT(input); if (encoding == 36) encoding = 7; /* ISO-8859-8-I is a single-byte decoder. */ ScrDyn *saved = scr_dyn_obj_read(state, "pending", 7); size_t pending = saved->kind == SCR_DYN_BYTES ? saved->v.bytes->len : 0; @@ -2033,7 +2114,6 @@ ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination) { } const ScrStr *text = source->v.str; ScrBytes *dest = destination->v.bytes; - SCR_SHARED_GUARD(dest, NULL); size_t read = 0, written = 0; size_t limit = text->len < dest->len ? text->len : dest->len; while (written < limit) { @@ -2048,8 +2128,13 @@ ScrDyn *scr_text_encode_into(const ScrDyn *source, const ScrDyn *destination) { written = end; } if (written) { - memcpy(dest->data, text->data, written); - if (!text->ascii) scr_wtf8_to_utf8((char *)dest->data, written); + uint8_t *out = dest->shared ? scr_bytes_scratch(written) : dest->data; + memcpy(out, text->data, written); + if (!text->ascii) scr_wtf8_to_utf8((char *)out, written); + if (dest->shared) { + scr_shared_store(dest->data, out, written); + free(out); + } } ScrDyn *result = scr_dyn_new_obj(); scr_dyn_obj_set(result, "read", 4, scr_dyn_new_num((double)read)); @@ -2073,8 +2158,9 @@ static bool scr_buffer_utf8_valid(const uint8_t *data, size_t len) { bool scr_buffer_is_ascii(const ScrDyn *input) { ScrBytes *bytes = scr_buffer_validation_input(input); if (!bytes) return false; - SCR_SHARED_GUARD(bytes, NULL); - bool result = scr_bytes_ascii_prefix(bytes->data, bytes->len) == bytes->len; + ScrBytes *view = bytes; + SCR_BYTES_SNAPSHOT(view); + bool result = scr_bytes_ascii_prefix(view->data, view->len) == view->len; scr_bytes_release(bytes); return result; } @@ -2082,8 +2168,9 @@ bool scr_buffer_is_ascii(const ScrDyn *input) { bool scr_buffer_is_utf8(const ScrDyn *input) { ScrBytes *bytes = scr_buffer_validation_input(input); if (!bytes) return false; - SCR_SHARED_GUARD(bytes, NULL); - bool result = scr_buffer_utf8_valid(bytes->data, bytes->len); + ScrBytes *view = bytes; + SCR_BYTES_SNAPSHOT(view); + bool result = scr_buffer_utf8_valid(view->data, view->len); scr_bytes_release(bytes); return result; } @@ -2255,7 +2342,7 @@ static ScrStr *scr_bytes_decode_utf16le(const uint8_t *in, size_t n) { } ScrStr *scr_bytes_to_str(const ScrBytes *b, const ScrStr *enc) { - SCR_SHARED_GUARD(b, NULL); + SCR_BYTES_SNAPSHOT(b); const uint8_t *in = b->data; size_t n = b->len; /* u8: len == byte length */ /* Implicit native string/number coercions have no encoding argument. */ @@ -2587,7 +2674,8 @@ bool scr_bytes_validate_off(const char *name, double value, double max) { } bool scr_bytes_equals(const ScrBytes *a, const ScrBytes *b) { - SCR_SHARED_GUARD(a, b); + SCR_BYTES_SNAPSHOT(a); + SCR_BYTES_SNAPSHOT(b); return a->len == b->len && memcmp(a->data, b->data, a->len) == 0; } @@ -2597,7 +2685,8 @@ bool scr_bytes_equals(const ScrBytes *a, const ScrBytes *b) { * Node's exactly. */ double scr_bytes_compare(const ScrBytes *src, const ScrBytes *target, double nargs, double ts, double te, double ss, double se) { - SCR_SHARED_GUARD(src, target); + SCR_BYTES_SNAPSHOT(src); + SCR_BYTES_SNAPSHOT(target); size_t n = (size_t)nargs; if (n < 1) ts = 0; else if (!scr_bytes_validate_off("targetStart", ts, 9007199254740991.0)) return 0; @@ -2626,7 +2715,8 @@ double scr_bytes_compare(const ScrBytes *src, const ScrBytes *target, double nar * count from the end, and a still-negative backward search answers -1), * and the utf16le alignment stride. */ double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, double align, bool fwd) { - SCR_SHARED_GUARD(b, needle); + SCR_BYTES_SNAPSHOT(b); + SCR_BYTES_SNAPSHOT(needle); size_t len = b->len, nlen = needle->len; size_t step = align == 2 ? 2 : 1; double o = (off != off) ? (fwd ? 0 : (double)len) : trunc(off); @@ -2657,7 +2747,6 @@ double scr_bytes_index_of(const ScrBytes *b, const ScrBytes *needle, double off, } double scr_bytes_index_of_num(const ScrBytes *b, double v, double off, bool fwd) { - SCR_SHARED_GUARD(b, NULL); uint8_t byte = (uint8_t)scr_bytes_to_u32(v); ScrBytes needle = { .rc = 1, .len = 1, .elem = SCR_BYTES_U8, .data = &byte, .backing = NULL }; return scr_bytes_index_of(b, &needle, off, 1, fwd); @@ -2682,31 +2771,34 @@ static ScrBytes *scr_bytes_fill_core(ScrBytes *b, const uint8_t *pat, size_t pat if (offset < end) { size_t o = (size_t)offset > b->len ? b->len : (size_t)offset; size_t e = (size_t)end; + uint8_t *dst = b->shared ? scr_bytes_scratch(e - o) : b->data + o; if (patn == 0) { - memset(b->data + o, 0, e - o); + memset(dst, 0, e - o); } else if (patn == 1) { - memset(b->data + o, pat[0], e - o); + memset(dst, pat[0], e - o); } else { - for (size_t i = o; i < e; i++) b->data[i] = pat[(i - o) % patn]; + for (size_t i = o; i < e; i++) dst[i - o] = pat[(i - o) % patn]; + } + if (b->shared) { + scr_shared_store(b->data + o, dst, e - o); + free(dst); } } return scr_bytes_retain(b); } ScrBytes *scr_bytes_fill(ScrBytes *b, const ScrBytes *pattern, double nargs, double offset, double end) { - SCR_SHARED_GUARD(b, pattern); + SCR_BYTES_SNAPSHOT(pattern); return scr_bytes_fill_core(b, pattern->data, pattern->len, false, nargs, offset, end); } ScrBytes *scr_bytes_fill_num(ScrBytes *b, double v, double nargs, double offset, double end) { - SCR_SHARED_GUARD(b, NULL); uint8_t byte = (uint8_t)scr_bytes_to_u32(v); return scr_bytes_fill_core(b, &byte, 1, false, nargs, offset, end); } ScrBytes *scr_bytes_fill_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc, double nargs, double offset, double end) { - SCR_SHARED_GUARD(b, NULL); ScrBytes *pat = scr_bytes_from_str(s, enc); ScrBytes *r = scr_bytes_fill_core(b, pat->data, pat->len, true, nargs, offset, end); scr_bytes_release(pat); @@ -2719,7 +2811,6 @@ ScrBytes *scr_bytes_fill_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc, * bounded by the source length. Returns the byte count copied. */ double scr_bytes_copy_into(const ScrBytes *src, ScrBytes *dst, double nargs, double ts, double ss, double se) { - SCR_SHARED_GUARD(src, dst); size_t n = (size_t)nargs; ts = n < 1 ? 0 : trunc(ts); ss = n < 2 ? 0 : trunc(ss); @@ -2734,7 +2825,12 @@ double scr_bytes_copy_into(const ScrBytes *src, ScrBytes *dst, double nargs, if (s0 >= e0) return 0; size_t count = e0 - s0; if (count > dst->len - t0) count = dst->len - t0; - memmove(dst->data + t0, src->data + s0, count); + if (src->shared || dst->shared) { + uint8_t *scratch = scr_bytes_scratch(count); + scr_bytes_raw_in(src, scratch, src->data + s0, count); + scr_bytes_raw_out(dst, dst->data + t0, scratch, count); + free(scratch); + } else memmove(dst->data + t0, src->data + s0, count); return (double)count; } @@ -2742,7 +2838,17 @@ double scr_bytes_copy_into(const ScrBytes *src, ScrBytes *dst, double nargs, * (+1, chaining like fill). A length off the width is Node's constant * ERR_INVALID_BUFFER_SIZE RangeError. */ ScrBytes *scr_bytes_swap(ScrBytes *b, double width) { - SCR_SHARED_GUARD(b, NULL); + if (b->shared) { + /* Swap a private copy and publish every byte. */ + ScrBytes *copy = scr_bytes_snapshot(b); + ScrBytes *swapped = scr_bytes_swap(copy, width); + if (swapped) { + scr_shared_store(b->data, copy->data, b->len); + scr_bytes_release(swapped); + } + scr_bytes_release(copy); + return swapped ? scr_bytes_retain(b) : NULL; + } size_t w = (size_t)width; if (b->len % w != 0) { char msg[64]; @@ -2768,7 +2874,14 @@ ScrBytes *scr_bytes_swap(ScrBytes *b, double width) { * lone lead), byte-level for the rest. Returns the bytes written. */ double scr_bytes_write_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc, double offset, double len, bool has_len) { - SCR_SHARED_GUARD(b, NULL); + if (b->shared) { + /* Encode into a private copy and publish the bytes written. */ + ScrBytes *copy = scr_bytes_snapshot(b); + double written = scr_bytes_write_str(copy, s, enc, offset, len, has_len); + if (written > 0) scr_shared_store(b->data + (size_t)offset, copy->data + (size_t)offset, (size_t)written); + scr_bytes_release(copy); + return written; + } if (!scr_bytes_validate_off("offset", offset, (double)b->len)) return 0; size_t o = (size_t)offset; size_t remaining = b->len - o; @@ -3120,10 +3233,10 @@ static size_t scr_bytes_num_width(ScrBytesNumKind kind) { } double scr_bytes_read_num(const ScrBytes *b, double offset, ScrBytesNumKind kind, bool le) { - SCR_SHARED_GUARD(b, NULL); size_t width = scr_bytes_num_width(kind); if (!scr_bytes_rw_check(b, offset, width)) return 0; - const uint8_t *p = b->data + (size_t)offset; + uint8_t p[8]; + scr_bytes_raw_in(b, p, b->data + (size_t)offset, width); uint64_t u = 0; for (size_t i = 0; i < width; i++) { u |= (uint64_t)p[le ? i : width - 1 - i] << (8 * i); @@ -3155,7 +3268,6 @@ double scr_bytes_read_num(const ScrBytes *b, double offset, ScrBytesNumKind kind } double scr_bytes_write_num(ScrBytes *b, double value, double offset, ScrBytesNumKind kind, bool le) { - SCR_SHARED_GUARD(b, NULL); size_t width = scr_bytes_num_width(kind); uint64_t u; if (kind == SCR_BN_F32 || kind == SCR_BN_F64) { @@ -3177,24 +3289,25 @@ double scr_bytes_write_num(ScrBytes *b, double value, double offset, ScrBytesNum if (t != t) t = 0; /* NaN passed the range gate: zeros, like Node */ u = (uint64_t)(int64_t)t; /* two's complement of the truncation */ } - uint8_t *p = b->data + (size_t)offset; + uint8_t p[8]; for (size_t i = 0; i < width; i++) { p[le ? i : width - 1 - i] = (uint8_t)(u >> (8 * i)); } + scr_bytes_raw_out(b, b->data + (size_t)offset, p, width); return offset + (double)width; } /* readUIntLE/BE / readIntLE/BE — the variable-width family: byteLength * validates FIRST (1-6, its own error ladder), then the offset gate. */ double scr_bytes_read_var(const ScrBytes *b, double offset, double byte_length, bool sign, bool le) { - SCR_SHARED_GUARD(b, NULL); if (floor(byte_length) != byte_length || byte_length < 1 || byte_length > 6) { scr_bytes_bounds_error(byte_length, 6, "byteLength"); return 0; } size_t width = (size_t)byte_length; if (!scr_bytes_rw_check(b, offset, width)) return 0; - const uint8_t *p = b->data + (size_t)offset; + uint8_t p[8]; + scr_bytes_raw_in(b, p, b->data + (size_t)offset, width); uint64_t u = 0; for (size_t i = 0; i < width; i++) { u |= (uint64_t)p[le ? i : width - 1 - i] << (8 * i); @@ -3208,7 +3321,6 @@ double scr_bytes_read_var(const ScrBytes *b, double offset, double byte_length, /* writeUIntLE/BE / writeIntLE/BE: byteLength, then value, then offset — * Node's exact check order. Returns offset + byteLength. */ double scr_bytes_write_var(ScrBytes *b, double value, double offset, double byte_length, bool sign, bool le) { - SCR_SHARED_GUARD(b, NULL); if (floor(byte_length) != byte_length || byte_length < 1 || byte_length > 6) { scr_bytes_bounds_error(byte_length, 6, "byteLength"); return 0; @@ -3218,17 +3330,18 @@ double scr_bytes_write_var(ScrBytes *b, double value, double offset, double byte double t = trunc(value); if (t != t) t = 0; uint64_t u = (uint64_t)(int64_t)t; - uint8_t *p = b->data + (size_t)offset; + uint8_t p[8]; for (size_t i = 0; i < width; i++) { p[le ? i : width - 1 - i] = (uint8_t)(u >> (8 * i)); } + scr_bytes_raw_out(b, b->data + (size_t)offset, p, width); return offset + (double)width; } bool scr_bytes_read_u64_raw(const ScrBytes *b, double offset, bool le, uint64_t *out) { - SCR_SHARED_GUARD(b, NULL); if (!scr_bytes_rw_check(b, offset, 8)) return false; - const uint8_t *p = b->data + (size_t)offset; + uint8_t p[8]; + scr_bytes_raw_in(b, p, b->data + (size_t)offset, 8); uint64_t value = 0; for (size_t i = 0; i < 8; i++) value |= (uint64_t)p[le ? i : 7 - i] << (8 * i); *out = value; @@ -3236,10 +3349,10 @@ bool scr_bytes_read_u64_raw(const ScrBytes *b, double offset, bool le, uint64_t } bool scr_bytes_write_u64_raw(ScrBytes *b, double offset, bool le, uint64_t value) { - SCR_SHARED_GUARD(b, NULL); if (!scr_bytes_rw_check(b, offset, 8)) return false; - uint8_t *p = b->data + (size_t)offset; + uint8_t p[8]; for (size_t i = 0; i < 8; i++) p[le ? i : 7 - i] = (uint8_t)(value >> (8 * i)); + scr_bytes_raw_out(b, b->data + (size_t)offset, p, 8); return true; } @@ -3254,10 +3367,10 @@ static bool scr_dataview_u64_check(const ScrBytes *b, double byte_off) { } bool scr_dataview_read_u64_raw(const ScrBytes *b, double offset, bool le, uint64_t *out) { - SCR_SHARED_GUARD(b, NULL); double off = (offset != offset) ? 0 : trunc(offset); if (!scr_dataview_u64_check(b, offset)) return false; - const uint8_t *p = b->data + (size_t)off; + uint8_t p[8]; + scr_bytes_raw_in(b, p, b->data + (size_t)off, 8); uint64_t value = 0; for (size_t i = 0; i < 8; i++) value |= (uint64_t)p[le ? i : 7 - i] << (8 * i); *out = value; @@ -3265,11 +3378,11 @@ bool scr_dataview_read_u64_raw(const ScrBytes *b, double offset, bool le, uint64 } bool scr_dataview_write_u64_raw(ScrBytes *b, double offset, bool le, uint64_t value) { - SCR_SHARED_GUARD(b, NULL); double off = (offset != offset) ? 0 : trunc(offset); if (!scr_dataview_u64_check(b, offset)) return false; - uint8_t *p = b->data + (size_t)off; + uint8_t p[8]; for (size_t i = 0; i < 8; i++) p[le ? i : 7 - i] = (uint8_t)(value >> (8 * i)); + scr_bytes_raw_out(b, b->data + (size_t)off, p, 8); return true; } diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 68e7c21e77..69fb7dd940 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -57,17 +57,14 @@ typedef struct ScrBytes { ScrSharedBytes *shared; /* Neutral backing, borrowed by views from their local owner. */ } ScrBytes; -/* Internal scoped locking is only for leaf memory operations. Never hold this - * guard across a script callback, blocking I/O, or a fiber suspension. */ -void scr_shared_enter(void); -void scr_shared_leave(void); -bool scr_shared_guard(const ScrBytes *, const ScrBytes *); -void scr_shared_guard_release(bool *); -#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) -#define SCR_SHARED_GUARD(a, b) bool scr_shared_held __attribute__((cleanup(scr_shared_guard_release))) = scr_shared_guard(a, b) -#else -#define SCR_SHARED_GUARD(a, b) ((void)0) -#endif +/* Copies out of and into shared storage, made of relaxed atomic accesses + * (aligned 8-byte words, single bytes at unaligned edges). Lock-free + * Atomics operations and element accesses on the same bytes stay free of + * data races; a copy that overlaps a concurrent write may observe either + * value of each word or byte, which ECMA-262 permits for non-atomic + * accesses. */ +void scr_shared_load(void *dst, const void *src, size_t n); +void scr_shared_store(void *dst, const void *src, size_t n); ScrSharedBytes *scr_shared_retain(ScrSharedBytes *); void scr_shared_release(ScrSharedBytes *); void scr_bytes_make_shared(ScrBytes *); @@ -79,8 +76,9 @@ ScrBytes *scr_bytes_snapshot(const ScrBytes *); void scr_bytes_snapshot_release(ScrBytes **); ScrBytes *scr_bytes_local_copy(ScrBytes *); /* +1; shared inputs are copied */ void scr_bytes_require_unshared(const ScrBytes *); -/* Snapshot before passing byte pointers to I/O or callback-capable consumers. - * The copy is local, so neither a lock nor shared raw memory escapes. */ +/* Snapshot before reading shared bytes in bulk or passing byte pointers to + * I/O or callback-capable consumers. The copy is local and read with + * scr_shared_load, so no shared raw memory escapes. */ #if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) #define SCR_BYTES_SNAPSHOT(name) \ ScrBytes *scr_snapshot_##name __attribute__((cleanup(scr_bytes_snapshot_release))) = scr_bytes_snapshot(name); \ @@ -222,8 +220,8 @@ double scr_bytes_get(const ScrBytes *b, double i); double scr_bytes_get_or_nan(const ScrBytes *b, double i); /* NaN when the index is invalid */ void scr_bytes_set(ScrBytes *b, double i, double v); /* Shared-view element access at an index the caller has checked against - * the length (0 <= i < len). Each takes the shared-memory lock around the - * access. Integer elements read as their value in 32 bits, sign- or + * the length (0 <= i < len). Each accesses the element as one relaxed + * atomic access of its width. Integer elements read as their value in 32 bits, sign- or * zero-extended, with Uint32 elements as their bit pattern; float elements * read as a double. Writes coerce like scr_bytes_set. Compiled code calls * these only from a view's shared branch. */ diff --git a/packages/runtime/src/scr_copying.c b/packages/runtime/src/scr_copying.c index 063536b8f8..6864a62623 100644 --- a/packages/runtime/src/scr_copying.c +++ b/packages/runtime/src/scr_copying.c @@ -123,7 +123,7 @@ ScrArr *scr_arr_with_undefined(ScrArr *a, double index) { } ScrBytes *scr_bytes_to_reversed(const ScrBytes *b) { - SCR_SHARED_GUARD(b, NULL); + SCR_BYTES_SNAPSHOT(b); ScrBytes *out = scr_bytes_new(b->elem, (double)b->len); size_t width = scr_bytes_elem_size(b->elem); /* Copy the stored representation without numeric conversion. Views may diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index 754157ac8f..fd75333f34 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -101,7 +101,8 @@ typedef struct ScrError { bool name_enumerable; ScrStr *stack_frames; /* captured native source frames */ ScrStr *stack; /* lazily formatted, cached stack */ - double system_errno; /* present exactly when system_call is non-NULL */ + double system_errno; /* present when system_call is non-NULL or this is + * nonzero (zlib errors carry errno without syscall) */ ScrStr *system_call; ScrStr *system_path; ScrStr *system_dest; diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 1b98e5a875..e0615635a4 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -695,7 +695,7 @@ ScrStr *scr_insp_regex(ScrRegex *re) { * runs before the depth check in Node). u8 element kind only (the * frontend fences the rest). */ ScrStr *scr_insp_buffer(ScrBytes *b) { - SCR_SHARED_GUARD(b, NULL); + SCR_BYTES_SNAPSHOT(b); InspBuf out = {0}; ib_cstr(&out, "len < 50 ? b->len : 50; diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index ac53c5689c..69ca83f540 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -5171,7 +5171,9 @@ ScrDyn *scr_error_dyn_fields(const ScrError *e) { d->v.obj.entries[d->v.obj.len - 1].enumerable = e->message_enumerable; } if (e->stack) scr_dyn_obj_set(d, "stack", 5, scr_dyn_new_str(e->stack)); - if (e->system_call) scr_dyn_obj_set(d, "errno", 5, scr_dyn_new_num(e->system_errno)); + /* errno rides fs errors (with syscall) and zlib errors (without). */ + if (e->system_call || e->system_errno != 0) + scr_dyn_obj_set(d, "errno", 5, scr_dyn_new_num(e->system_errno)); if (e->code) scr_dyn_obj_set(d, "code", 4, scr_dyn_new_str(e->code)); if (e->system_call) scr_dyn_obj_set(d, "syscall", 7, scr_dyn_new_str(e->system_call)); if (e->system_path) scr_dyn_obj_set(d, "path", 4, scr_dyn_new_str(e->system_path)); diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index dbdcced062..fd25cbc1f7 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -983,7 +983,7 @@ void scr_fs_invalid_named_path(const ScrDyn *value, const ScrStr *path, const ch scr_jb_puts(&b, "' must be a string, Uint8Array, or URL without null bytes. Received "); if (value->kind == SCR_DYN_BYTES) { const ScrBytes *bytes = value->v.bytes; - SCR_SHARED_GUARD(bytes, NULL); + SCR_BYTES_SNAPSHOT(bytes); if (value->buffer) { scr_jb_puts(&b, "len; i++) { @@ -8286,7 +8286,8 @@ ScrDyn *scr_crypto_hash_oneshot(ScrStr *alg, ScrDyn *input, ScrDyn *options) { } bool scr_crypto_timing_safe_equal(ScrBytes *left, ScrBytes *right) { - SCR_SHARED_GUARD(left, right); + SCR_BYTES_SNAPSHOT(left); + SCR_BYTES_SNAPSHOT(right); size_t left_len = left->len * scr_bytes_elem_size(left->elem); size_t right_len = right->len * scr_bytes_elem_size(right->elem); if (left_len != right_len) { diff --git a/packages/runtime/src/scr_region.c b/packages/runtime/src/scr_region.c index 35098ebec5..cb34f8a0e9 100644 --- a/packages/runtime/src/scr_region.c +++ b/packages/runtime/src/scr_region.c @@ -141,17 +141,21 @@ enum { }; typedef struct { - size_t entry; + size_t entry; /* SCR_RGN_SINGLE: walked like the region's own objects */ + void *obj; int kind; void (*walk)(void *, ScrRgnWalk *); } ScrRgnWork; +#define SCR_RGN_SINGLE SIZE_MAX + struct ScrRgnWalk { ScrRegion *region; int64_t internal; /* references found from the region's own objects */ size_t src; /* entry index + 1 being walked; 0 = a region object */ ScrRgnEntry *ents; size_t nents, cap_ents; + size_t singles; /* owned objects walked without an entry */ size_t *hash; /* open addressing: entry index + 1 */ size_t cap_hash; ScrRgnEdge *edges; @@ -478,10 +482,30 @@ static void scr_rgn_rehash(ScrRgnWalk *w) { for (size_t i = 0; i < w->nents; i++) w->hash[scr_rgn_find(w, w->ents[i].obj)] = i + 1; } +static void scr_rgn_queue(ScrRgnWalk *w, size_t entry, void *p, int kind, void (*walk)(void *, ScrRgnWalk *)) { + if (w->nwork == w->cap_work) w->work = scr_rgn_grow(w->work, &w->cap_work, sizeof *w->work, 4096); + w->work[w->nwork++] = (ScrRgnWork){entry, p, kind, walk}; +} + /* One more reference to the ordinary object `p`, from the object being - * walked; queues `p`'s own walk when it is first reached. */ + * walked; queues `p`'s own walk when it is first reached. + * + * An object with a single reference, found from one of the region's own + * objects (or from an object walked like them), has no other referrer, so + * it can never be referenced from outside: it gets no entry and no edges, + * and it is walked like the region's own objects, its references into the + * region counted as explained. RC-audit builds still enter it, and stop if + * the walk reaches it twice. */ static void scr_rgn_enter(ScrRgnWalk *w, void *p, int kind, void (*walk)(void *, ScrRgnWalk *)) { if (!p || *(size_t *)p == SIZE_MAX) return; /* immortal: never freed, not walked */ + bool single = w->src == 0 && *(size_t *)p == 1; +#ifndef SCR_RC_AUDIT + if (single) { + w->singles++; + scr_rgn_queue(w, SCR_RGN_SINGLE, p, kind, walk); + return; + } +#endif if ((w->nents + 1) * 2 > w->cap_hash) scr_rgn_rehash(w); size_t j = scr_rgn_find(w, p); size_t idx = w->hash[j]; @@ -489,8 +513,10 @@ static void scr_rgn_enter(ScrRgnWalk *w, void *p, int kind, void (*walk)(void *, if (w->nents == w->cap_ents) w->ents = scr_rgn_grow(w->ents, &w->cap_ents, sizeof *w->ents, 4096); w->ents[w->nents] = (ScrRgnEntry){p, 0, 0, false}; idx = w->hash[j] = ++w->nents; - if (w->nwork == w->cap_work) w->work = scr_rgn_grow(w->work, &w->cap_work, sizeof *w->work, 4096); - w->work[w->nwork++] = (ScrRgnWork){idx - 1, kind, walk}; + scr_rgn_queue(w, single ? SCR_RGN_SINGLE : idx - 1, p, kind, walk); + } else if (single) { + fprintf(stderr, "scriptc: internal error: the region walk reached an object with one reference twice\n"); + abort(); } w->ents[idx - 1].found++; if (w->src) { @@ -541,19 +567,39 @@ static void scr_rgn_index_build(void) { atomic_store_explicit(&scr_rgn_index_state, 2, memory_order_release); } +/* How the walk treats a value stored with a release entry point: a + * container kind, an ordinary object with an emitted walker, a reference + * into a region (SCR_RGN_S_REF), or nothing it can walk (SCR_RGN_S_NONE, + * its references stay unexplained). Containers resolve it once for all + * their elements. */ +enum { SCR_RGN_S_NONE = -1, SCR_RGN_S_REF = -2 }; +typedef struct { + int kind; + void (*walk)(void *, ScrRgnWalk *); +} ScrRgnSlotKind; + +static ScrRgnSlotKind scr_rgn_slot_kind(void (*release)(void *)) { + if (!release) return (ScrRgnSlotKind){SCR_RGN_S_NONE, NULL}; + if (release == scr_arr_release_v) return (ScrRgnSlotKind){SCR_RGN_W_ARR, NULL}; + if (release == scr_map_release_v) return (ScrRgnSlotKind){SCR_RGN_W_MAP, NULL}; + if (release == scr_union_release_v) return (ScrRgnSlotKind){SCR_RGN_W_UNION, NULL}; + if (release == scr_closure_release_v) return (ScrRgnSlotKind){SCR_RGN_W_CLOSURE, NULL}; + if (release == scr_dyn_release_v) return (ScrRgnSlotKind){SCR_RGN_W_DYN, NULL}; + const ScrRgnType *ty = scr_rgn_type_of(release); + if (!ty) return (ScrRgnSlotKind){SCR_RGN_S_NONE, NULL}; + if (!ty->walk) return (ScrRgnSlotKind){SCR_RGN_S_REF, NULL}; + return (ScrRgnSlotKind){SCR_RGN_W_EMITTED, ty->walk}; +} + +static void scr_rgn_slot_as(ScrRgnWalk *w, void *obj, ScrRgnSlotKind k) { + if (!obj || k.kind == SCR_RGN_S_NONE) return; + if (k.kind == SCR_RGN_S_REF) scr_region_ref(w, obj); + else scr_rgn_enter(w, obj, k.kind, k.walk); +} + void scr_region_slot(ScrRgnWalk *w, void *obj, void (*release)(void *)) { if (!obj || !release) return; - if (release == scr_arr_release_v) scr_rgn_enter(w, obj, SCR_RGN_W_ARR, NULL); - else if (release == scr_map_release_v) scr_rgn_enter(w, obj, SCR_RGN_W_MAP, NULL); - else if (release == scr_union_release_v) scr_rgn_enter(w, obj, SCR_RGN_W_UNION, NULL); - else if (release == scr_closure_release_v) scr_rgn_enter(w, obj, SCR_RGN_W_CLOSURE, NULL); - else if (release == scr_dyn_release_v) scr_rgn_enter(w, obj, SCR_RGN_W_DYN, NULL); - else { - const ScrRgnType *ty = scr_rgn_type_of(release); - if (!ty) return; /* not walkable: its references stay unexplained */ - if (!ty->walk) scr_region_ref(w, obj); - else scr_rgn_enter(w, obj, SCR_RGN_W_EMITTED, ty->walk); - } + scr_rgn_slot_as(w, obj, scr_rgn_slot_kind(release)); } static void *scr_rgn_slot_ptr(uint64_t slot) { @@ -562,21 +608,18 @@ static void *scr_rgn_slot_ptr(uint64_t slot) { return p; } -static void scr_rgn_walk_elem(ScrRgnWalk *w, const ScrArr *a, uint64_t slot) { - if (a->elem == SCR_ELEM_REF) scr_region_slot(w, scr_rgn_slot_ptr(slot), a->ops ? a->ops->release : NULL); - else if (a->elem == SCR_ELEM_ARR) scr_rgn_enter(w, scr_rgn_slot_ptr(slot), SCR_RGN_W_ARR, NULL); -} - static void scr_rgn_walk_arr(ScrRgnWalk *w, ScrArr *a) { if (a->elem != SCR_ELEM_REF && a->elem != SCR_ELEM_ARR) return; + ScrRgnSlotKind k = a->elem == SCR_ELEM_ARR ? (ScrRgnSlotKind){SCR_RGN_W_ARR, NULL} + : scr_rgn_slot_kind(a->ops ? a->ops->release : NULL); size_t dense = a->len < a->cap ? a->len : a->cap; for (size_t i = 0; i < dense; i++) - if (!a->present || a->present[i] == SCR_ARR_VALUE) scr_rgn_walk_elem(w, a, a->data[i]); + if (!a->present || a->present[i] == SCR_ARR_VALUE) scr_rgn_slot_as(w, scr_rgn_slot_ptr(a->data[i]), k); if (!a->ext) return; for (size_t i = 0; i < a->ext->sparse_len; i++) - if (a->ext->sparse[i].state == SCR_ARR_VALUE) scr_rgn_walk_elem(w, a, a->ext->sparse[i].slot); + if (a->ext->sparse[i].state == SCR_ARR_VALUE) scr_rgn_slot_as(w, scr_rgn_slot_ptr(a->ext->sparse[i].slot), k); for (size_t i = 0; i < a->ext->prop_len; i++) - if (a->ext->props[i].state == SCR_ARR_VALUE) scr_rgn_walk_elem(w, a, a->ext->props[i].slot); + if (a->ext->props[i].state == SCR_ARR_VALUE) scr_rgn_slot_as(w, scr_rgn_slot_ptr(a->ext->props[i].slot), k); scr_rgn_enter(w, a->ext->metadata, SCR_RGN_W_DYN, NULL); } @@ -586,12 +629,15 @@ static void scr_rgn_walk_map(ScrRgnWalk *w, ScrMap *m) { bool keydyn = m->key_kind == SCR_MAP_KEY_DYN; bool valref = m->val_kind == SCR_MAP_VAL_REF; if (!keyref && !keydyn && !valref) return; + ScrRgnSlotKind kk = keydyn ? (ScrRgnSlotKind){SCR_RGN_W_DYN, NULL} + : keyref ? scr_rgn_slot_kind(m->key_release) + : (ScrRgnSlotKind){SCR_RGN_S_NONE, NULL}; + ScrRgnSlotKind vk = valref ? scr_rgn_slot_kind(m->val_release) : (ScrRgnSlotKind){SCR_RGN_S_NONE, NULL}; for (size_t i = 0; i < m->nentries; i++) { const ScrMapEntry *e = &m->entries[i]; if (!e->hash) continue; /* tombstone */ - if (keyref) scr_region_slot(w, scr_rgn_slot_ptr(e->key), m->key_release); - else if (keydyn) scr_rgn_enter(w, scr_rgn_slot_ptr(e->key), SCR_RGN_W_DYN, NULL); - if (valref) scr_region_slot(w, scr_rgn_slot_ptr(e->val), m->val_release); + scr_rgn_slot_as(w, scr_rgn_slot_ptr(e->key), kk); + scr_rgn_slot_as(w, scr_rgn_slot_ptr(e->val), vk); } } @@ -648,8 +694,8 @@ static void scr_rgn_walk_dyn(ScrRgnWalk *w, ScrDyn *d) { static void scr_rgn_drain(ScrRgnWalk *w) { while (w->nwork) { ScrRgnWork job = w->work[--w->nwork]; - void *p = w->ents[job.entry].obj; - w->src = job.entry + 1; + void *p = job.obj; + w->src = job.entry == SCR_RGN_SINGLE ? 0 : job.entry + 1; switch (job.kind) { case SCR_RGN_W_EMITTED: job.walk(p, w); break; case SCR_RGN_W_ARR: scr_rgn_walk_arr(w, p); break; @@ -755,7 +801,7 @@ static bool scr_rgn_try_free(ScrRgnThread *t, ScrRegion *r) { w->region = r; w->internal = 0; w->src = 0; - w->nents = w->nedges = w->nwork = 0; + w->nents = w->nedges = w->nwork = w->singles = 0; if (w->hash) memset(w->hash, 0, w->cap_hash * sizeof *w->hash); for (ScrRgnChunk *c = r->chunks; c; c = c->next) c->found = 0; size_t objects = 0; @@ -785,7 +831,7 @@ static bool scr_rgn_try_free(ScrRgnThread *t, ScrRegion *r) { if (stats) fprintf(stderr, "scriptc region %llu: kept: %lld references counted, %lld explained; %zu of %zu owned objects referenced from outside; cycles %.1f ms, walk %.1f ms\n", - (unsigned long long)r->id, (long long)counted, (long long)explained, black, w->nents, cycles, scr_rgn_ms(t0, t1)); + (unsigned long long)r->id, (long long)counted, (long long)explained, black, w->nents + w->singles, cycles, scr_rgn_ms(t0, t1)); return false; } /* Free: drop weak entries keyed by the instances, then release every @@ -820,7 +866,7 @@ static bool scr_rgn_try_free(ScrRgnThread *t, ScrRegion *r) { if (stats) fprintf(stderr, "scriptc region %llu: freed %zu objects, %zu owned objects, %zu chunks (%.1f MiB); cycles %.1f ms, walk %.1f ms, free %.1f ms\n", - (unsigned long long)id, objects, w->nents, nchunks, (double)bytes / 1048576.0, cycles, scr_rgn_ms(t0, t1), + (unsigned long long)id, objects, w->nents + w->singles, nchunks, (double)bytes / 1048576.0, cycles, scr_rgn_ms(t0, t1), scr_rgn_ms(t1, scr_hrtime_ns())); return true; } diff --git a/packages/runtime/src/scr_shared.c b/packages/runtime/src/scr_shared.c index 82e33b6cad..2dac5a4d15 100644 --- a/packages/runtime/src/scr_shared.c +++ b/packages/runtime/src/scr_shared.c @@ -1,5 +1,8 @@ /* Shared storage is neutral: only its backing allocation and synchronization - * cross contexts. ScrBytes wrappers and their reference counts remain local. */ + * cross contexts. ScrBytes wrappers and their reference counts remain local. + * Every access to shared bytes is atomic: Atomics operations as sequentially + * consistent instructions, other element and bulk accesses as relaxed ones + * (scr_shared_load, scr_shared_store). No lock orders them. */ #include "scr_runtime.h" #include "scr_numeric.h" #include @@ -21,49 +24,29 @@ struct ScrSharedBytes { uint8_t *data; }; -#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) -#ifdef _WIN32 -static SRWLOCK scr_shared_mutex = SRWLOCK_INIT; -static CONDITION_VARIABLE scr_shared_condition = CONDITION_VARIABLE_INIT; -#else -static pthread_mutex_t scr_shared_mutex = PTHREAD_MUTEX_INITIALIZER; -static pthread_cond_t scr_shared_condition = PTHREAD_COND_INITIALIZER; -#endif -static SCR_TL unsigned scr_shared_depth; -#endif - -/* One ordering domain covers overlapping views of different widths. Recursive - * runtime helpers may nest, but no caller may execute script while holding it. */ -void scr_shared_enter(void) { -#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) - if (scr_shared_depth++ != 0) return; -#ifdef _WIN32 - AcquireSRWLockExclusive(&scr_shared_mutex); -#else - pthread_mutex_lock(&scr_shared_mutex); -#endif -#endif +void scr_shared_load(void *dst, const void *src, size_t n) { + uint8_t *out = dst; + const uint8_t *in = src; + for (; n && ((uintptr_t)in & 7); n--) *out++ = __atomic_load_n(in++, __ATOMIC_RELAXED); + for (; n >= 8; n -= 8, in += 8, out += 8) { + uint64_t word = __atomic_load_n((const uint64_t *)(const void *)in, __ATOMIC_RELAXED); + memcpy(out, &word, 8); + } + for (; n; n--) *out++ = __atomic_load_n(in++, __ATOMIC_RELAXED); } -void scr_shared_leave(void) { -#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) - if (--scr_shared_depth != 0) return; -#ifdef _WIN32 - ReleaseSRWLockExclusive(&scr_shared_mutex); -#else - pthread_mutex_unlock(&scr_shared_mutex); -#endif -#endif +void scr_shared_store(void *dst, const void *src, size_t n) { + uint8_t *out = dst; + const uint8_t *in = src; + for (; n && ((uintptr_t)out & 7); n--) __atomic_store_n(out++, *in++, __ATOMIC_RELAXED); + for (; n >= 8; n -= 8, in += 8, out += 8) { + uint64_t word; + memcpy(&word, in, 8); + __atomic_store_n((uint64_t *)(void *)out, word, __ATOMIC_RELAXED); + } + for (; n; n--) __atomic_store_n(out++, *in++, __ATOMIC_RELAXED); } -bool scr_shared_guard(const ScrBytes *a, const ScrBytes *b) { - bool shared = (a && a->shared) || (b && b->shared); - if (shared) scr_shared_enter(); - return shared; -} - -void scr_shared_guard_release(bool *held) { if (*held) scr_shared_leave(); } - ScrSharedBytes *scr_shared_retain(ScrSharedBytes *storage) { atomic_fetch_add_explicit(&storage->references, 1, memory_order_relaxed); return storage; @@ -93,18 +76,25 @@ ScrBytes *scr_shared_wrap(ScrSharedBytes *storage) { } void scr_bytes_read(const ScrBytes *bytes, size_t offset, void *out, size_t length) { - SCR_SHARED_GUARD(bytes, NULL); - if (length) memcpy(out, bytes->data + offset, length); + if (!length) return; + if (bytes->shared) scr_shared_load(out, bytes->data + offset, length); + else memcpy(out, bytes->data + offset, length); } void scr_bytes_write(ScrBytes *bytes, size_t offset, const void *data, size_t length) { - SCR_SHARED_GUARD(bytes, NULL); - if (length) memcpy(bytes->data + offset, data, length); + if (!length) return; + if (bytes->shared) scr_shared_store(bytes->data + offset, data, length); + else memcpy(bytes->data + offset, data, length); } ScrStr *scr_bytes_string(const ScrBytes *bytes, size_t offset, size_t length) { - SCR_SHARED_GUARD(bytes, NULL); - return scr_str_new((const char *)bytes->data + offset, length); + if (!bytes->shared) return scr_str_new((const char *)bytes->data + offset, length); + char *copy = malloc(length ? length : 1); + if (!copy) scr_trap("scriptc: out of memory\n"); + scr_shared_load(copy, bytes->data + offset, length); + ScrStr *text = scr_str_new(copy, length); + free(copy); + return text; } ScrBytes *scr_bytes_snapshot(const ScrBytes *bytes) { @@ -147,62 +137,153 @@ static bool scr_atomics_index(ScrBytes *bytes, double index, bool waitable, size return true; } -/* Operations: load, store, exchange, compareExchange, add, sub, and, or, xor. - * Unsigned arithmetic avoids signed overflow; the element store supplies the - * selected width and signedness. The lock gives one sequentially consistent - * order shared with ordinary accesses, including overlapping DataViews. */ +/* Atomics operations are single atomic instructions on the element, with + * sequentially consistent ordering (ECMA-262 Atomics), and take no lock. + * Operands arrive as the element type's ToUint32 residue; unsigned + * arithmetic wraps at the element width. Each returns the element's + * previous bits. */ +#define SCR_ATOMICS_RMW(type) \ + static type scr_atomics_rmw_##type(type *p, int operation, type operand, type replacement) { \ + switch (operation) { \ + case 0: return __atomic_load_n(p, __ATOMIC_SEQ_CST); \ + case 2: return __atomic_exchange_n(p, operand, __ATOMIC_SEQ_CST); \ + case 3: { \ + type expected = operand; \ + /* A failed exchange leaves the current value in expected. */ \ + __atomic_compare_exchange_n(p, &expected, replacement, false, __ATOMIC_SEQ_CST, \ + __ATOMIC_SEQ_CST); \ + return expected; \ + } \ + case 4: return __atomic_fetch_add(p, operand, __ATOMIC_SEQ_CST); \ + case 5: return __atomic_fetch_sub(p, operand, __ATOMIC_SEQ_CST); \ + case 6: return __atomic_fetch_and(p, operand, __ATOMIC_SEQ_CST); \ + case 7: return __atomic_fetch_or(p, operand, __ATOMIC_SEQ_CST); \ + case 8: return __atomic_fetch_xor(p, operand, __ATOMIC_SEQ_CST); \ + } \ + scr_trap("scriptc: invalid atomic operation\n"); \ + } +SCR_ATOMICS_RMW(uint8_t) +SCR_ATOMICS_RMW(uint16_t) +SCR_ATOMICS_RMW(uint32_t) + +/* Operations: load, store, exchange, compareExchange, add, sub, and, or, xor. */ double scr_atomics_op(ScrBytes *bytes, double index, double value, double replacement, double operation) { size_t i; if (!scr_atomics_index(bytes, index, false, &i)) return 0; - SCR_SHARED_GUARD(bytes, NULL); - double previous = scr_bytes_get(bytes, (double)i); - uint32_t before = scr_numeric_to_u32(previous), operand = scr_numeric_to_u32(value); - uint32_t after; - switch ((int)operation) { - case 0: return previous; + int op = (int)operation; + uint32_t operand = scr_numeric_to_u32(value), swap = scr_numeric_to_u32(replacement); + uint8_t *p = bytes->data + i * scr_bytes_elem_size(bytes->elem); + uint32_t previous = 0; + switch (scr_bytes_elem_size(bytes->elem)) { case 1: - scr_bytes_set(bytes, (double)i, value); - return isnan(value) || value == 0 ? 0 : trunc(value); - case 2: after = operand; break; - case 3: { - unsigned bits = (unsigned)scr_bytes_elem_size(bytes->elem) * 8; - uint32_t mask = bits == 32 ? UINT32_MAX : (UINT32_C(1) << bits) - 1; - if ((before & mask) != (operand & mask)) return previous; - after = scr_numeric_to_u32(replacement); + if (op == 1) __atomic_store_n(p, (uint8_t)operand, __ATOMIC_SEQ_CST); + else previous = scr_atomics_rmw_uint8_t(p, op, (uint8_t)operand, (uint8_t)swap); + break; + case 2: + if (op == 1) __atomic_store_n((uint16_t *)p, (uint16_t)operand, __ATOMIC_SEQ_CST); + else previous = scr_atomics_rmw_uint16_t((uint16_t *)p, op, (uint16_t)operand, (uint16_t)swap); + break; + default: + if (op == 1) __atomic_store_n((uint32_t *)p, operand, __ATOMIC_SEQ_CST); + else previous = scr_atomics_rmw_uint32_t((uint32_t *)p, op, operand, swap); break; } - case 4: after = before + operand; break; - case 5: after = before - operand; break; - case 6: after = before & operand; break; - case 7: after = before | operand; break; - case 8: after = before ^ operand; break; - default: scr_trap("scriptc: invalid atomic operation\n"); + /* store answers ToIntegerOrInfinity of the value, not the stored element + * (NaN and both zeros, including a truncated -0.5, are +0). */ + if (op == 1) { + double whole = trunc(value); + return isnan(whole) || whole == 0 ? 0 : whole; + } + switch (bytes->elem) { + case SCR_BYTES_I8: return (double)(int8_t)previous; + case SCR_BYTES_I16: return (double)(int16_t)previous; + case SCR_BYTES_I32: return (double)(int32_t)previous; + default: return (double)previous; } - scr_bytes_set(bytes, (double)i, (double)after); - return previous; } +/* Waiters queue per bucket, chosen by storage and byte offset. A bucket's + * lock orders a wait's value check and registration against notify for the + * same location; other Atomics operations never take it. */ typedef struct ScrAtomicWaiter { ScrSharedBytes *storage; size_t offset; bool notified; struct ScrAtomicWaiter *next; } ScrAtomicWaiter; -static ScrAtomicWaiter *scr_atomic_waiters; -static void scr_atomics_pause(double ms) { +#define SCR_WAIT_BUCKETS 64 #if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) +#define SCR_WAIT_THREADS 1 +#endif + +typedef struct ScrWaitBucket { +#ifdef SCR_WAIT_THREADS #ifdef _WIN32 - SleepConditionVariableSRW(&scr_shared_condition, &scr_shared_mutex, (DWORD)ceil(ms), 0); + SRWLOCK lock; + CONDITION_VARIABLE condition; +#else + pthread_mutex_t lock; + pthread_cond_t condition; +#endif +#endif + ScrAtomicWaiter *waiters; +} ScrWaitBucket; + +static ScrWaitBucket scr_wait_buckets[SCR_WAIT_BUCKETS]; +#if defined(SCR_WAIT_THREADS) && !defined(_WIN32) +static pthread_once_t scr_wait_once = PTHREAD_ONCE_INIT; +static void scr_wait_init(void) { + for (size_t b = 0; b < SCR_WAIT_BUCKETS; b++) { + pthread_mutex_init(&scr_wait_buckets[b].lock, NULL); + pthread_cond_init(&scr_wait_buckets[b].condition, NULL); + } +} +#endif + +/* Locked bucket for a location (SRWLOCK and CONDITION_VARIABLE are valid + * when zero-initialized). */ +static ScrWaitBucket *scr_wait_lock(ScrSharedBytes *storage, size_t offset) { + uintptr_t key = (uintptr_t)storage ^ (uintptr_t)(offset / 4) * 0x9E3779B1u; + ScrWaitBucket *bucket = &scr_wait_buckets[(key ^ (key >> 7)) % SCR_WAIT_BUCKETS]; +#ifdef SCR_WAIT_THREADS +#ifdef _WIN32 + AcquireSRWLockExclusive(&bucket->lock); +#else + pthread_once(&scr_wait_once, scr_wait_init); + pthread_mutex_lock(&bucket->lock); +#endif +#endif + return bucket; +} + +static void scr_wait_unlock(ScrWaitBucket *bucket) { +#ifdef SCR_WAIT_THREADS +#ifdef _WIN32 + ReleaseSRWLockExclusive(&bucket->lock); +#else + pthread_mutex_unlock(&bucket->lock); +#endif +#else + (void)bucket; +#endif +} + +/* Sleep up to ms with the bucket locked; returns with it locked. */ +static void scr_atomics_pause(ScrWaitBucket *bucket, double ms) { +#ifdef SCR_WAIT_THREADS +#ifdef _WIN32 + SleepConditionVariableSRW(&bucket->condition, &bucket->lock, (DWORD)ceil(ms), 0); #else struct timespec until; clock_gettime(CLOCK_REALTIME, &until); until.tv_nsec += (long)(ms * 1000000.0); until.tv_sec += until.tv_nsec / 1000000000; until.tv_nsec %= 1000000000; - pthread_cond_timedwait(&scr_shared_condition, &scr_shared_mutex, &until); + pthread_cond_timedwait(&bucket->condition, &bucket->lock, &until); #endif #else + (void)bucket; struct timespec duration = {(time_t)(ms / 1000), (long)(fmod(ms, 1000) * 1000000)}; nanosleep(&duration, NULL); #endif @@ -219,38 +300,38 @@ ScrStr *scr_atomics_wait(ScrBytes *bytes, double index, double expected, double int32_t compare; uint32_t bits = scr_numeric_to_u32(expected); memcpy(&compare, &bits, sizeof compare); - scr_shared_enter(); - if (scr_bytes_get(bytes, (double)i) != compare) { - scr_shared_leave(); + size_t offset = (size_t)(bytes->data - bytes->shared->data) + i * 4; + int32_t *cell = (int32_t *)(bytes->data + i * 4); + ScrWaitBucket *bucket = scr_wait_lock(bytes->shared, offset); + /* Under the bucket lock, a notify for this location cannot fall between + * this check and the registration below. */ + if (__atomic_load_n(cell, __ATOMIC_SEQ_CST) != compare) { + scr_wait_unlock(bucket); return scr_str_new("not-equal", 9); } double duration = isnan(timeout) ? INFINITY : fmax(timeout, 0); double end = scr_perf_now() + duration; - ScrAtomicWaiter waiter = {bytes->shared, (size_t)(bytes->data - bytes->shared->data) + i * 4, false, NULL}; - ScrAtomicWaiter **tail = &scr_atomic_waiters; + ScrAtomicWaiter waiter = {bytes->shared, offset, false, NULL}; + ScrAtomicWaiter **tail = &bucket->waiters; while (*tail) tail = &(*tail)->next; *tail = &waiter; bool stopped = false; while (!waiter.notified) { double remaining = end - scr_perf_now(); if (!(remaining > 0)) break; - /* A bounded wait gives cooperative termination a safe cancellation point. - * Registration and condition release share the notify lock: no wakeup can - * fall between the expected-value check and entering the wait. */ - scr_atomics_pause(fmin(remaining, 10)); -#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) - scr_shared_leave(); + /* A bounded wait gives cooperative termination a safe cancellation point. */ + scr_atomics_pause(bucket, fmin(remaining, 10)); #ifdef SCR_WORKERS + scr_wait_unlock(bucket); stopped = scr_context_checkpoint(); -#endif - scr_shared_enter(); + bucket = scr_wait_lock(bytes->shared, offset); if (stopped) break; #endif } - ScrAtomicWaiter **slot = &scr_atomic_waiters; + ScrAtomicWaiter **slot = &bucket->waiters; while (*slot != &waiter) slot = &(*slot)->next; *slot = waiter.next; - scr_shared_leave(); + scr_wait_unlock(bucket); return stopped ? NULL : scr_str_new(waiter.notified ? "ok" : "timed-out", waiter.notified ? 2 : 9); } @@ -261,20 +342,21 @@ double scr_atomics_notify(ScrBytes *bytes, double index, double count) { double limit = isnan(count) ? 0 : fmax(trunc(count), 0); size_t offset = (size_t)(bytes->data - bytes->shared->data) + i * 4; double notified = 0; - scr_shared_enter(); - for (ScrAtomicWaiter *waiter = scr_atomic_waiters; waiter && notified < limit; waiter = waiter->next) { + ScrWaitBucket *bucket = scr_wait_lock(bytes->shared, offset); + /* Waiters are woken in the order they arrived (ECMA-262 WaiterList). */ + for (ScrAtomicWaiter *waiter = bucket->waiters; waiter && notified < limit; waiter = waiter->next) { if (waiter->storage == bytes->shared && waiter->offset == offset && !waiter->notified) { waiter->notified = true; notified++; } } -#if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) +#ifdef SCR_WAIT_THREADS #ifdef _WIN32 - if (notified) WakeAllConditionVariable(&scr_shared_condition); + if (notified) WakeAllConditionVariable(&bucket->condition); #else - if (notified) pthread_cond_broadcast(&scr_shared_condition); + if (notified) pthread_cond_broadcast(&bucket->condition); #endif #endif - scr_shared_leave(); + scr_wait_unlock(bucket); return notified; } diff --git a/packages/runtime/src/scr_shared.test.c b/packages/runtime/src/scr_shared.test.c index 4de3d33008..4391f8f957 100644 --- a/packages/runtime/src/scr_shared.test.c +++ b/packages/runtime/src/scr_shared.test.c @@ -21,6 +21,7 @@ static void *run(void *ptr) { scr_shared_release(task->storage); ScrBytes *values = scr_bytes_buffer_view(storage, SCR_BYTES_I32, 4, true, 2); ScrBytes *view = scr_dataview_new(storage, 12, true, 4); + ScrBytes *counter_view = scr_dataview_new(storage, 4, true, 4); atomic_fetch_add_explicit(&ready, 1, memory_order_release); if (task->waiting) { scr_context_stop_flag(&cancel_wait); @@ -46,11 +47,20 @@ static void *run(void *ptr) { ScrStr *text = scr_bytes_string(storage, 0, storage->len); assert(text->len == storage->len); scr_str_release(text); + /* Bulk reads and writes race with the lock-free Atomics updates of + * the counter's bytes; each access is atomic, so none is a data race. */ + ScrBytes *slice = scr_bytes_slice(storage, 0, (double)storage->len); + assert(slice->len == storage->len && !slice->shared); + scr_bytes_release(slice); + (void)scr_dataview_get(counter_view, 0, SCR_DV_U32, true); + scr_bytes_release(scr_bytes_fill_num(storage, task->id, 2, 0, 4)); + scr_bytes_release(scr_bytes_copy_within(storage, 0, 2, 4)); } } } scr_bytes_release(values); scr_bytes_release(view); + scr_bytes_release(counter_view); scr_bytes_release(storage); scr_context_cleanup(); return NULL; diff --git a/packages/runtime/src/scr_shared.test.ts b/packages/runtime/src/scr_shared.test.ts index 2b6e8e9f15..bdb2d0a749 100644 --- a/packages/runtime/src/scr_shared.test.ts +++ b/packages/runtime/src/scr_shared.test.ts @@ -8,9 +8,14 @@ import { EXECUTABLE_RUNTIME_SOURCES } from "../../compiler/src/backend/native-to const exec = promisify(execFile); -test.skipIf(process.platform === "win32")( - "shared views serialize concurrent accesses and cancel parked waiters", - async () => { +// Atomics operations take no lock, and every other access to shared bytes +// is a relaxed atomic one; the ThreadSanitizer build checks that concurrent +// element, bulk and Atomics accesses to the same bytes are race-free. +test + .skipIf(process.platform === "win32") + .each([["-fsanitize=address,undefined"], ["-fsanitize=thread"]])( + "shared views keep concurrent accesses atomic and cancel parked waiters (%s)", + async (sanitize) => { const dir = await mkdtemp(join(tmpdir(), "scriptc-shared-")); try { const bin = join(dir, "shared"); @@ -20,7 +25,7 @@ test.skipIf(process.platform === "win32")( "-pthread", "-DSCR_WORKERS", "-DSCR_RC_AUDIT", - "-fsanitize=address,undefined", + sanitize, "-Wno-deprecated-declarations", ...(process.platform === "linux" ? ["-D_GNU_SOURCE"] : []), ...["scr_shared.test.c", ...EXECUTABLE_RUNTIME_SOURCES].map((name) => @@ -31,8 +36,8 @@ test.skipIf(process.platform === "win32")( bin, ]); const result = await exec(bin, [], { - timeout: 10000, - env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1" }, + timeout: 30000, + env: { ...process.env, UBSAN_OPTIONS: "halt_on_error=1", TSAN_OPTIONS: "halt_on_error=1" }, }); expect(result.stdout).toBe( "shared views preserve atomic updates, wait queues and cancellation ownership\n", diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index fdb7b1710f..5911c75d63 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -174,6 +174,14 @@ struct ScrStreamState { ScrStreamPlainInv flush_inv; bool flush_called; bool flush_pending; /* _flush called, its callback not yet run */ + /* A _final that completes at once (zlib's ZlibBase): 'prefinish' starts + * the flush and 'finish' does not wait for it. */ + bool flush_on_prefinish; + /* A runtime-built stream's own state (scr_zlib_stream.c): one owned + * reference, kept past 'close' until the stream is freed. The release + * function doubles as the state's type tag. */ + void *native; + void (*native_release)(void *); /* destroy */ ScrClosure *destroy_cb; @@ -262,6 +270,7 @@ static void scr_stream_entry_release_kind(void *e, bool is_str) { static void scr_stream_state_drop(ScrStreamState *st, bool gc) { if (!st) return; if (st->errored) scr_error_release(st->errored); + if (st->native) st->native_release(st->native); for (size_t i = 0; i < st->r.n; i++) scr_stream_entry_release(st, st->r.buf[i]); free(st->r.buf); if (st->r.enc) scr_str_release(st->r.enc); @@ -1748,6 +1757,18 @@ static void scr_stream_finish_maybe(ScrStream *s) { * pendingcb) */ return; } + if (!st->w.prefinished && st->flush_on_prefinish) { + /* The Transform's own 'prefinish' listener (registered first) runs + * the flush; 'finish' follows on a tick. */ + st->w.prefinished = true; + if (!st->flush_called) { + st->flush_called = true; + st->flush_inv(st->flush_cb, s); + if (scr_exc_pending()) return; + } + scr_stream_emit0(s, "prefinish"); + if (scr_exc_pending()) return; + } if (!st->w.prefinished) { if ((st->is_transform || st->passthrough) && !st->flush_called) { st->flush_called = true; @@ -2089,6 +2110,20 @@ void scr_stream_set_flush(ScrStream *s, ScrClosure *cb /*moves*/, ScrStreamPlain st->flush_inv = inv; } +void scr_stream_set_flush_on_prefinish(ScrStream *s) { s->st->flush_on_prefinish = true; } + +void scr_stream_set_native(ScrStream *s, void *state /*moves*/, void (*release)(void *)) { + ScrStreamState *st = s->st; + if (st->native) st->native_release(st->native); + st->native = state; + st->native_release = release; +} + +void *scr_stream_native(ScrStream *s, void (*release)(void *)) { + ScrStreamState *st = s->st; + return st != NULL && st->native != NULL && st->native_release == release ? st->native : NULL; +} + /* ── dyn options (a checked-dynamic options record at construction) ───── * * The JS lane's `constructor(options) { super(options); }` forwarding and diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 8b8f30a8df..055d42d7bf 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -412,6 +412,14 @@ void scr_stream_set_final(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); void scr_stream_set_destroy(ScrStream *s, ScrClosure *cb, ScrStreamErrInv inv); void scr_stream_set_transform(ScrStream *s, ScrClosure *cb, ScrStreamChunkInv inv); void scr_stream_set_flush(ScrStream *s, ScrClosure *cb, ScrStreamPlainInv inv); +/* Runtime-built Transforms (scr_zlib_stream.c): the flush starts at + * 'prefinish' and 'finish' does not wait for it (a _final that completes + * at once). set_native moves one reference to the stream's own state, + * released when the stream is freed; scr_stream_native answers it + * (borrowed) when it was set with `release`, else NULL. */ +void scr_stream_set_flush_on_prefinish(ScrStream *s); +void scr_stream_set_native(ScrStream *s, void *state, void (*release)(void *)); +void *scr_stream_native(ScrStream *s, void (*release)(void *)); /* Completion entries for the compiler-emitted done closures (err/data * MOVE; NULLs = absent). */ diff --git a/packages/runtime/src/scr_test.c b/packages/runtime/src/scr_test.c index aa51af08d5..6f9fbbbcdb 100644 --- a/packages/runtime/src/scr_test.c +++ b/packages/runtime/src/scr_test.c @@ -97,7 +97,10 @@ struct ScrTestCtx { struct ScrTestCtx *parent; /* borrowed back-edge */ struct ScrTestCtx **children; /* owned refs */ size_t nchildren, ccap; - ScrTestHooks hooks[4]; /* suites and root only */ + ScrTestHooks hooks[4]; /* suites and root; tests through t.after/t.beforeEach/t.afterEach */ + ScrCaught *before_err; /* owned; a throwing t.before, which fails the test and later subtests */ + int plan; /* t.plan count, -1 without a plan */ + int plan_count; /* t.assert calls and subtests counted against the plan */ }; static SCR_TL ScrTestCtx *scr_test_root = NULL; @@ -135,6 +138,7 @@ void scr_testctx_release(ScrTestCtx *t) { scr_str_release(t->at); scr_closure_release(t->cb); scr_caught_release(t->err); + scr_caught_release(t->before_err); for (size_t i = 0; i < t->ndiags; i++) scr_str_release(t->diags[i]); free(t->diags); for (size_t i = 0; i < t->nchildren; i++) scr_testctx_release(t->children[i]); @@ -164,6 +168,7 @@ static ScrTestCtx *scr_test_node_new(ScrStr *name /*borrowed*/, int mode, t->mode = mode; t->flags = flags; t->is_suite = is_suite; + t->plan = -1; #ifdef SCR_RC_AUDIT scr_testctx_live++; #endif @@ -177,6 +182,10 @@ static void scr_test_child_add(ScrTestCtx *parent, ScrTestCtx *child /*moves*/) if (!parent->children) scr_test_oom(); } child->parent = parent; + /* Node's todo inheritance: tests and suites under a todo suite or test + * are todo themselves, with a bare TODO directive, unless they carry + * their own skip or todo. */ + if (child->mode == SCR_TEST_RUN && parent->mode == SCR_TEST_TODO) child->mode = SCR_TEST_TODO; parent->children[parent->nchildren++] = child; } @@ -308,9 +317,18 @@ static void scr_test_run_each_hooks(ScrTestCtx *t, int which, bool outermost_fir } } +/* A test or suite marked only, or one with a marked descendant. */ +static bool scr_test_has_only(const ScrTestCtx *t) { + if (t->flags & SCR_TEST_F_ONLY) return true; + for (size_t i = 0; i < t->nchildren; i++) { + if (scr_test_has_only(t->children[i])) return true; + } + return false; +} + static bool scr_test_any_only(ScrTestCtx *parent) { for (size_t i = 0; i < parent->nchildren; i++) { - if (parent->children[i]->flags & SCR_TEST_F_ONLY) return true; + if (scr_test_has_only(parent->children[i])) return true; } return false; } @@ -318,12 +336,13 @@ static bool scr_test_any_only(ScrTestCtx *parent) { static void scr_test_run_node(ScrTestCtx *t); static void scr_test_run_children(ScrTestCtx *t) { - /* `only: true` among the siblings: run only the marked ones — the - * others are dropped entirely (Node reports nothing for them). */ + /* `only` at this level: when a child is marked, or contains a marked + * test or suite, only those children run. The others are dropped entirely + * (Node reports nothing for them). */ bool only = scr_test_any_only(t); for (size_t i = 0; i < t->nchildren; i++) { ScrTestCtx *c = t->children[i]; - if (only && !(c->flags & SCR_TEST_F_ONLY)) { + if (only && !scr_test_has_only(c)) { continue; /* filtered: never run, never reported, never counted */ } /* Subtests t.test already ran INLINE at their call site. */ @@ -362,9 +381,14 @@ static void scr_test_run_node(ScrTestCtx *t) { } t->dur_ms = scr_now_ms() - start; if (t->err) scr_test_suite_hook_failed = true; - t->state = t->err ? SCR_TS_FAIL - : scr_test_subtree_failed(t) ? SCR_TS_CHILDFAIL - : SCR_TS_PASS; + if (t->mode == SCR_TEST_TODO) { + /* A todo suite reports its directive; its children are todo too. */ + t->state = t->err ? SCR_TS_TODO_FAIL : SCR_TS_TODO_PASS; + } else { + t->state = t->err ? SCR_TS_FAIL + : scr_test_subtree_failed(t) ? SCR_TS_CHILDFAIL + : SCR_TS_PASS; + } scr_test_report_result(t); return; } @@ -389,6 +413,20 @@ static void scr_test_run_node(ScrTestCtx *t) { * the body): run them now, before the parent completes — Node holds * the parent open for its pending subtests. */ if (!failed) scr_test_run_children(t); + if (!failed && t->before_err) { + t->err = scr_caught_retain(t->before_err); + failed = true; + } + /* t.plan: the t.assert calls and subtests the test made. Node reports the + * mismatch as a string failure. */ + if (!failed && t->plan >= 0 && t->plan_count != t->plan) { + char buf[96]; + int n = snprintf(buf, sizeof buf, "plan expected %d assertions but received %d", t->plan, + t->plan_count); + scr_throw_str(scr_str_new(buf, (size_t)n)); + t->err = scr_exc_take(); + failed = true; + } scr_test_run_each_hooks(t, SCR_HOOK_AFTER_EACH, false); if (scr_exc_pending()) { if (!failed && !t->err) { @@ -398,6 +436,16 @@ static void scr_test_run_node(ScrTestCtx *t) { scr_exc_clear(); } } + /* t.after hooks run once the test and its afterEach chain finish. */ + scr_test_run_hooks_of(t, SCR_HOOK_AFTER); + if (scr_exc_pending()) { + if (!failed && !t->err) { + t->err = scr_exc_take(); + failed = true; + } else { + scr_exc_clear(); + } + } scr_test_reg_parent = prev_parent; t->dur_ms = scr_now_ms() - start; if (t->mode == SCR_TEST_SKIP) { @@ -482,7 +530,9 @@ static void scr_test_print_failure(ScrTestCtx *t) { } fputc('\n', stdout); if (t->err) { - ScrStr *msg = scr_caught_to_string(t->err); + /* Node prints the failure inspected: a thrown string is quoted. */ + ScrStr *msg = t->err->kind == SCR_EXC_STR ? scr_insp_str(t->err->payload) + : scr_caught_to_string(t->err); if (msg) { const char *p = msg->data; const char *end = msg->data + msg->len; @@ -504,8 +554,11 @@ static void scr_test_print_failure(ScrTestCtx *t) { * bodies threw (todo throwers included); parents failed by descendants * and suites never appear. */ static void scr_test_print_failures(ScrTestCtx *t, bool *any) { + /* Completion order, as Node lists them: a test completes after its + * subtests, so they print first. */ for (size_t i = 0; i < t->nchildren; i++) { ScrTestCtx *c = t->children[i]; + scr_test_print_failures(c, any); if (!c->is_suite && (c->state == SCR_TS_FAIL || c->state == SCR_TS_TODO_FAIL) && c->err) { if (!*any) { *any = true; @@ -513,7 +566,6 @@ static void scr_test_print_failures(ScrTestCtx *t, bool *any) { } scr_test_print_failure(c); } - scr_test_print_failures(c, any); } } @@ -572,14 +624,15 @@ void scr_test_register(ScrStr *name /*borrowed*/, double mode, ScrStr *msg /*bor } void scr_test_suite(ScrStr *name /*borrowed*/, double mode, ScrStr *msg /*borrowed*/, - ScrClosure *cb /*moves, may be NULL*/, ScrStr *at /*borrowed*/) { + ScrClosure *cb /*moves, may be NULL*/, ScrStr *at /*borrowed*/, + double flags) { scr_test_ensure_root(); scr_test_ensure_runner(); if (scr_test_run_done) { scr_closure_release(cb); return; } - ScrTestCtx *t = scr_test_node_new(name, (int)mode, msg, NULL, 0, at, true); + ScrTestCtx *t = scr_test_node_new(name, (int)mode, msg, NULL, (int)flags, at, true); scr_test_child_add(scr_test_reg_parent, t); /* The suite body runs NOW (Node's collection phase) — its test()/it() * calls attach here. Skipped suites keep their bodies unrun. */ @@ -615,6 +668,17 @@ ScrPromise *scr_test_sub(ScrTestCtx *t, ScrStr *name /*borrowed*/, double mode, double flags, ScrStr *at /*borrowed*/) { ScrTestCtx *sub = scr_test_node_new(name, (int)mode, msg, cb, (int)flags, at, false); scr_test_child_add(t, sub); + t->plan_count++; + if (t->before_err) { + /* A throwing t.before fails the subtests that follow: their bodies and + * beforeEach hooks do not run, their afterEach hooks do. */ + sub->err = scr_caught_retain(t->before_err); + sub->state = SCR_TS_FAIL; + scr_test_run_each_hooks(sub, SCR_HOOK_AFTER_EACH, false); + if (scr_exc_pending()) scr_exc_clear(); + scr_test_report_result(sub); + return scr_promise_settled_void(); + } scr_test_run_node(sub); return scr_promise_settled_void(); } @@ -635,6 +699,33 @@ void scr_test_ctx_todo(ScrTestCtx *t, ScrStr *msg /*borrowed*/) { } } +/* t.before/t.after/t.beforeEach/t.afterEach. t.before runs at once (the + * test is already running); a throw fails the test and the subtests that + * follow. The others attach to the test: beforeEach and afterEach wrap + * each descendant subtest, and after runs once the test finishes. */ +void scr_test_ctx_hook(ScrTestCtx *t, double which, ScrClosure *cb /*moves*/, double flags) { + if ((int)which == SCR_HOOK_BEFORE) { + scr_test_call(cb, (int)flags, NULL); + scr_closure_release(cb); + if (scr_exc_pending() && !t->before_err) t->before_err = scr_exc_take(); + else if (scr_exc_pending()) scr_exc_clear(); + return; + } + ScrTestHooks *h = &t->hooks[(int)which]; + if (h->n == h->cap) { + h->cap = h->cap ? h->cap * 2 : 2; + h->v = realloc(h->v, h->cap * sizeof *h->v); + if (!h->v) scr_test_oom(); + } + h->v[h->n].cb = cb; + h->v[h->n].flags = (int)flags; + h->n++; +} + +void scr_test_ctx_plan(ScrTestCtx *t, double count) { t->plan = (int)count; } + +void scr_test_ctx_count(ScrTestCtx *t) { t->plan_count++; } + void scr_test_ctx_diagnostic(ScrTestCtx *t, ScrStr *msg /*borrowed*/) { if (t->ndiags == t->dcap) { t->dcap = t->dcap ? t->dcap * 2 : 2; @@ -648,6 +739,32 @@ ScrStr *scr_test_ctx_name(ScrTestCtx *t) { return t->name ? scr_str_retain(t->name) : scr_str_new("", 0); } +/* t.fullName: the names from the outermost suite or test down to this one, + * joined with " > ". */ +ScrStr *scr_test_ctx_full_name(ScrTestCtx *t) { + const ScrTestCtx *chain[64]; + int n = 0; + for (const ScrTestCtx *p = t; p && p->parent && n < 64; p = p->parent) chain[n++] = p; + size_t len = 0; + for (int i = 0; i < n; i++) len += (chain[i]->name ? chain[i]->name->len : 0) + (i ? 3 : 0); + char *buf = malloc(len ? len : 1); + if (!buf) scr_test_oom(); + size_t at = 0; + for (int i = n - 1; i >= 0; i--) { + if (i != n - 1) { + memcpy(buf + at, " > ", 3); + at += 3; + } + if (chain[i]->name) { + memcpy(buf + at, chain[i]->name->data, chain[i]->name->len); + at += chain[i]->name->len; + } + } + ScrStr *out = scr_str_new(buf, at); + free(buf); + return out; +} + int scr_test_exit_code(void) { /* The runner never finished: a test awaited a promise nobody settles * and the loop exhausted (Node cancels the test and exits 1 — the diff --git a/packages/runtime/src/scr_test.h b/packages/runtime/src/scr_test.h index 667b96bb8f..7d64145f4e 100644 --- a/packages/runtime/src/scr_test.h +++ b/packages/runtime/src/scr_test.h @@ -24,7 +24,8 @@ void scr_testctx_release(ScrTestCtx *t); void *scr_testctx_retain_v(void *t); void scr_testctx_release_v(void *t); void scr_test_register(ScrStr *name, double mode, ScrStr *msg, ScrClosure *cb, double flags, ScrStr *at); -void scr_test_suite(ScrStr *name, double mode, ScrStr *msg, ScrClosure *cb, ScrStr *at); +void scr_test_suite(ScrStr *name, double mode, ScrStr *msg, ScrClosure *cb, ScrStr *at, + double flags); /* before/after/beforeEach/afterEach on the CURRENT registration parent * (`which`: 0 before, 1 after, 2 beforeEach, 3 afterEach). */ void scr_test_hook(double which, ScrClosure *cb, double flags); @@ -34,6 +35,10 @@ ScrPromise *scr_test_sub(ScrTestCtx *t, ScrStr *name, double mode, ScrStr *msg, void scr_test_ctx_skip(ScrTestCtx *t, ScrStr *msg); void scr_test_ctx_todo(ScrTestCtx *t, ScrStr *msg); void scr_test_ctx_diagnostic(ScrTestCtx *t, ScrStr *msg); +void scr_test_ctx_hook(ScrTestCtx *t, double which, ScrClosure *cb, double flags); +void scr_test_ctx_plan(ScrTestCtx *t, double count); +void scr_test_ctx_count(ScrTestCtx *t); +ScrStr *scr_test_ctx_full_name(ScrTestCtx *t); ScrStr *scr_test_ctx_name(ScrTestCtx *t); /* +1 */ /* Main's epilogue (emitted when moduleUsesNodeTest): 1 when any non-todo * test failed, Node's exit contract. */ diff --git a/packages/runtime/src/scr_util_compare.c b/packages/runtime/src/scr_util_compare.c index bb7cd4c419..5a15b662f1 100644 --- a/packages/runtime/src/scr_util_compare.c +++ b/packages/runtime/src/scr_util_compare.c @@ -181,7 +181,8 @@ static bool util_deep_equal(const ScrDyn *a, const ScrDyn *b, bool skip, if (a->kind == SCR_DYN_BYTES) { const ScrBytes *x = a->v.bytes, *y = b->v.bytes; if (x->elem != y->elem || x->len != y->len) return false; - SCR_SHARED_GUARD(x, y); + SCR_BYTES_SNAPSHOT(x); + SCR_BYTES_SNAPSHOT(y); size_t length = x->len * scr_bytes_elem_size(x->elem); if (length && memcmp(x->data, y->data, length)) return false; } diff --git a/packages/runtime/src/scr_zlib.c b/packages/runtime/src/scr_zlib.c index 87a7b1524e..c93e7f5746 100644 --- a/packages/runtime/src/scr_zlib.c +++ b/packages/runtime/src/scr_zlib.c @@ -350,3 +350,235 @@ bool scr_zlib_inflate_exact(const unsigned char *src, size_t src_len, inflateEnd(&zs); return ok; } + +/* ── the stream engine (Node's ZlibContext, node_zlib.cc) ─────────────── + * One z_stream per zlib.Gzip/Deflate/... instance. scr_zlib_stream.c owns + * the Transform side and submits every write step to the worker pool; + * scr_zlib_engine_work runs on the worker and touches only the z_stream. + * Modes are Node's node_zlib_mode numbers (DEFLATE 1 ... UNZIP 7). */ +struct ScrZlibEngine { + z_stream strm; + int mode; + int level, window_bits, mem_level, strategy; + int flush; + int err; + int gzip_id_bytes_read; + bool init_done; + bool reject_garbage; + unsigned char *dict; + size_t dict_len; +}; + +enum { SCR_Z_DEFLATE = 1, SCR_Z_INFLATE, SCR_Z_GZIP, SCR_Z_GUNZIP, SCR_Z_DEFLATERAW, + SCR_Z_INFLATERAW, SCR_Z_UNZIP }; + +ScrZlibEngine *scr_zlib_engine_new(int mode, int level, int window_bits, int mem_level, + int strategy, const unsigned char *dict, size_t dict_len, + bool reject_garbage) { + ScrZlibEngine *e = calloc(1, sizeof *e); + if (!e) scr_zlib_oom(); + e->mode = mode; + e->level = level; + e->mem_level = mem_level; + e->strategy = strategy; + e->reject_garbage = reject_garbage; + e->flush = Z_NO_FLUSH; + e->err = Z_OK; + e->window_bits = window_bits; + if (mode == SCR_Z_GZIP || mode == SCR_Z_GUNZIP) e->window_bits += 16; + if (mode == SCR_Z_UNZIP) e->window_bits += 32; + if (mode == SCR_Z_DEFLATERAW || mode == SCR_Z_INFLATERAW) e->window_bits *= -1; + if (dict && dict_len > 0) { + e->dict = malloc(dict_len); + if (!e->dict) scr_zlib_oom(); + memcpy(e->dict, dict, dict_len); + e->dict_len = dict_len; + } + return e; +} + +static bool scr_zlib_engine_deflating(const ScrZlibEngine *e) { + return e->mode == SCR_Z_DEFLATE || e->mode == SCR_Z_GZIP || e->mode == SCR_Z_DEFLATERAW; +} + +/* SetDictionary: deflate modes and raw inflate load it up front; the + * other inflate modes load it when inflate() asks (Z_NEED_DICT). */ +static bool scr_zlib_engine_set_dict(ScrZlibEngine *e) { + if (!e->dict) return true; + e->err = Z_OK; + if (e->mode == SCR_Z_DEFLATE || e->mode == SCR_Z_DEFLATERAW) { + e->err = deflateSetDictionary(&e->strm, e->dict, (uInt)e->dict_len); + } else if (e->mode == SCR_Z_INFLATERAW) { + e->err = inflateSetDictionary(&e->strm, e->dict, (uInt)e->dict_len); + } + return e->err == Z_OK; +} + +/* InitZlib: lazy, on the first step. True when this call initialized. */ +static bool scr_zlib_engine_init(ScrZlibEngine *e) { + if (e->init_done) return false; + e->strm.msg = NULL; + if (scr_zlib_engine_deflating(e)) { + e->err = deflateInit2(&e->strm, e->level, Z_DEFLATED, e->window_bits, e->mem_level, + e->strategy); + } else { + e->err = inflateInit2(&e->strm, e->window_bits); + } + if (e->err != Z_OK) { + free(e->dict); + e->dict = NULL; + e->dict_len = 0; + e->mode = 0; + return true; + } + scr_zlib_engine_set_dict(e); + e->init_done = true; + return true; +} + +static int scr_zlib_engine_reset_stream(ScrZlibEngine *e) { + if (scr_zlib_engine_init(e) && e->err != Z_OK) return 1; + e->err = Z_OK; + switch (e->mode) { + case SCR_Z_DEFLATE: + case SCR_Z_DEFLATERAW: + case SCR_Z_GZIP: e->err = deflateReset(&e->strm); break; + case SCR_Z_INFLATE: + case SCR_Z_INFLATERAW: + case SCR_Z_GUNZIP: e->err = inflateReset(&e->strm); break; + default: break; + } + if (e->err != Z_OK) return 2; + return scr_zlib_engine_set_dict(e) ? 0 : 3; +} + +void scr_zlib_engine_work(ScrZlibEngine *e, int flush, const unsigned char *in, uint32_t in_len, + unsigned char *out, uint32_t out_len) { + e->strm.avail_in = in_len; + e->strm.next_in = (Bytef *)(uintptr_t)in; + e->strm.avail_out = out_len; + e->strm.next_out = out; + e->flush = flush; + if (scr_zlib_engine_init(e) && e->err != Z_OK) return; + switch (e->mode) { + case SCR_Z_DEFLATE: + case SCR_Z_GZIP: + case SCR_Z_DEFLATERAW: + e->err = deflate(&e->strm, flush); + return; + case SCR_Z_UNZIP: { + /* The gzip magic decides GUNZIP vs INFLATE, possibly across steps. */ + const Bytef *next = e->strm.avail_in > 0 ? e->strm.next_in : NULL; + if (e->gzip_id_bytes_read == 0 && next != NULL) { + if (*next == 0x1f) { + e->gzip_id_bytes_read = 1; + next++; + if (e->strm.avail_in == 1) next = NULL; + } else { + e->mode = SCR_Z_INFLATE; + next = NULL; + } + } + if (e->gzip_id_bytes_read == 1 && next != NULL && e->mode == SCR_Z_UNZIP) { + if (*next == 0x8b) { + e->gzip_id_bytes_read = 2; + e->mode = SCR_Z_GUNZIP; + } else { + e->mode = SCR_Z_INFLATE; + } + } + } + /* fallthrough */ + case SCR_Z_INFLATE: + case SCR_Z_GUNZIP: + case SCR_Z_INFLATERAW: + e->err = inflate(&e->strm, flush); + if (e->mode != SCR_Z_INFLATERAW && e->err == Z_NEED_DICT && e->dict) { + e->err = inflateSetDictionary(&e->strm, e->dict, (uInt)e->dict_len); + if (e->err == Z_OK) e->err = inflate(&e->strm, flush); + else if (e->err == Z_DATA_ERROR) e->err = Z_NEED_DICT; + } + /* Another gzip member may follow; trailing zero bytes are padding. */ + while (e->strm.avail_in > 0 && e->mode == SCR_Z_GUNZIP && e->err == Z_STREAM_END && + !e->reject_garbage && e->strm.next_in[0] != 0x00) { + scr_zlib_engine_reset_stream(e); + e->err = inflate(&e->strm, flush); + } + return; + default: + return; + } +} + +void scr_zlib_engine_offsets(const ScrZlibEngine *e, uint32_t *avail_in, uint32_t *avail_out) { + *avail_in = e->strm.avail_in; + *avail_out = e->strm.avail_out; +} + +static const char *scr_zlib_strerror(int err) { + switch (err) { + case Z_OK: return "Z_OK"; + case Z_STREAM_END: return "Z_STREAM_END"; + case Z_NEED_DICT: return "Z_NEED_DICT"; + case Z_ERRNO: return "Z_ERRNO"; + case Z_STREAM_ERROR: return "Z_STREAM_ERROR"; + case Z_DATA_ERROR: return "Z_DATA_ERROR"; + case Z_MEM_ERROR: return "Z_MEM_ERROR"; + case Z_BUF_ERROR: return "Z_BUF_ERROR"; + case Z_VERSION_ERROR: return "Z_VERSION_ERROR"; + default: return "Z_UNKNOWN_ERROR"; + } +} + +static bool scr_zlib_engine_fail(const ScrZlibEngine *e, const char *message, ScrZlibError *out) { + out->message = e->strm.msg ? e->strm.msg : message; + out->code = scr_zlib_strerror(e->err); + out->err = e->err; + return true; +} + +bool scr_zlib_engine_error(const ScrZlibEngine *e, ScrZlibError *out) { + switch (e->err) { + case Z_OK: + case Z_BUF_ERROR: + if (e->strm.avail_out != 0 && e->flush == Z_FINISH) + return scr_zlib_engine_fail(e, "unexpected end of file", out); + return false; + case Z_STREAM_END: return false; + case Z_NEED_DICT: + return scr_zlib_engine_fail(e, e->dict ? "Bad dictionary" : "Missing dictionary", out); + default: return scr_zlib_engine_fail(e, "Zlib error", out); + } +} + +bool scr_zlib_engine_reset(ScrZlibEngine *e, ScrZlibError *out) { + switch (scr_zlib_engine_reset_stream(e)) { + case 0: return false; + case 1: return scr_zlib_engine_fail(e, "Failed to init stream before reset", out); + case 2: return scr_zlib_engine_fail(e, "Failed to reset stream", out); + default: return scr_zlib_engine_fail(e, "Failed to set dictionary", out); + } +} + +/* SetParams: deflateParams for DEFLATE and DEFLATERAW only (Node's + * switch leaves GZIP's level and strategy unchanged). */ +bool scr_zlib_engine_params(ScrZlibEngine *e, int level, int strategy, ScrZlibError *out) { + if (scr_zlib_engine_init(e) && e->err != Z_OK) + return scr_zlib_engine_fail(e, "Failed to init stream before set parameters", out); + e->err = Z_OK; + if (e->mode == SCR_Z_DEFLATE || e->mode == SCR_Z_DEFLATERAW) + e->err = deflateParams(&e->strm, level, strategy); + if (e->err != Z_OK && e->err != Z_BUF_ERROR) + return scr_zlib_engine_fail(e, "Failed to set parameters", out); + return false; +} + +void scr_zlib_engine_free(ScrZlibEngine *e) { + if (!e) return; + if (e->init_done) { + if (scr_zlib_engine_deflating(e)) deflateEnd(&e->strm); + else inflateEnd(&e->strm); + } + free(e->dict); + free(e); +} diff --git a/packages/runtime/src/scr_zlib.h b/packages/runtime/src/scr_zlib.h index bd374b1dac..be9475a99f 100644 --- a/packages/runtime/src/scr_zlib.h +++ b/packages/runtime/src/scr_zlib.h @@ -21,5 +21,37 @@ typedef void (*ScrZlibBytesFn)(ScrClosure *cb, ScrError *err, ScrBytes *value /* moves, NULL on error */); void scr_zlib_codec_async(const ScrBytes *data, double mode, bool compressing, ScrClosure *cb /* moves */, ScrZlibBytesFn fn); +/* The stream engine (Node's ZlibContext): one z_stream per zlib stream + * object, initialized on its first step. scr_zlib_engine_work runs on a + * worker thread and touches only the engine; every other entry runs on the + * main thread. The error entries answer true and fill *out (static or + * engine-owned strings) when Node would emit an error. */ +typedef struct ScrZlibEngine ScrZlibEngine; +typedef struct { + const char *message; + const char *code; + int err; +} ScrZlibError; +ScrZlibEngine *scr_zlib_engine_new(int mode, int level, int window_bits, int mem_level, + int strategy, const unsigned char *dict, size_t dict_len, + bool reject_garbage); +void scr_zlib_engine_work(ScrZlibEngine *e, int flush, const unsigned char *in, uint32_t in_len, + unsigned char *out, uint32_t out_len); +void scr_zlib_engine_offsets(const ScrZlibEngine *e, uint32_t *avail_in, uint32_t *avail_out); +bool scr_zlib_engine_error(const ScrZlibEngine *e, ScrZlibError *out); +bool scr_zlib_engine_reset(ScrZlibEngine *e, ScrZlibError *out); +bool scr_zlib_engine_params(ScrZlibEngine *e, int level, int strategy, ScrZlibError *out); +void scr_zlib_engine_free(ScrZlibEngine *e); +/* zlib.Deflate, Inflate, Gzip, Gunzip, DeflateRaw, InflateRaw and Unzip + * (scr_zlib_stream.c — linked when the program uses both zlib and the + * stream surface): Transform streams over the engine. mode is Node's + * node_zlib_mode, opts the options value (undefined for none), validated + * with Node's ladder. The callbacks MOVE in and may be NULL. */ +ScrStream *scr_zlib_stream_new(double mode, ScrDyn *opts); +double scr_zlib_stream_bytes_written(ScrStream *s); +void scr_zlib_stream_flush(ScrStream *s, ScrDyn *kind, ScrClosure *cb); +void scr_zlib_stream_params(ScrStream *s, ScrDyn *level, ScrDyn *strategy, ScrClosure *cb); +void scr_zlib_stream_close(ScrStream *s, ScrClosure *cb); +void scr_zlib_stream_reset(ScrStream *s); #endif /* SCR_ZLIB_H */ diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c new file mode 100644 index 0000000000..43bf940373 --- /dev/null +++ b/packages/runtime/src/scr_zlib_stream.c @@ -0,0 +1,768 @@ +/* node:zlib's stream classes — Deflate, Inflate, Gzip, Gunzip, DeflateRaw, + * InflateRaw and Unzip (createDeflate() et al) — as runtime Transform + * streams over the scr_zlib.c engine. Linked only when the program uses + * both zlib and the stream surface (the scr_util_style.c pairing rule), so + * zlib's one-shot users keep their link line. + * + * The write path follows lib/zlib.js: every chunk runs as one or more + * engine steps on the shared worker pool (Node's threadpool write), each + * step filling the chunkSize output buffer from the current offset; a + * non-empty step pushes one 'data' chunk, a full buffer starts the next + * step (after a _read when push() answered false), and the transform + * callback runs once the input is consumed. The last chunk written after + * end() runs with finishFlush, the 'prefinish' flush writes an empty chunk + * with it, and flush() writes a zero-length sentinel chunk carrying its + * flush kind. Engine errors destroy the stream with Node's Error (message, + * errno and code). */ +#include "scr_runtime.h" + +#include +#include +#include +#include + +/* zlib.h values (the engine unit owns the zlib include). */ +enum { SCR_ZF_NO_FLUSH, SCR_ZF_PARTIAL, SCR_ZF_SYNC, SCR_ZF_FULL, SCR_ZF_FINISH, SCR_ZF_BLOCK }; +enum { SCR_ZM_DEFLATE = 1, SCR_ZM_INFLATE, SCR_ZM_GZIP, SCR_ZM_GUNZIP, SCR_ZM_DEFLATERAW, + SCR_ZM_INFLATERAW, SCR_ZM_UNZIP }; +#define SCR_Z_DEFAULT_CHUNK 16384.0 +#define SCR_Z_MIN_CHUNK 64.0 +#define SCR_K_MAX_LENGTH 9007199254740991.0 + +typedef struct ScrZlibStream { + size_t rc; + ScrZlibEngine *eng; /* freed at close (deferred while a step runs) */ + bool closed; /* Node's `_handle === null` */ + bool pending_close; + bool write_in_progress; + ScrStream *s; /* borrowed: the stream owns this state through its hooks */ + int mode; + uint32_t chunk_size; + int default_flush, finish_flush, full_flush; + double level, strategy; /* Node's _level/_strategy (params() compares them) */ + bool reject_garbage; + double bytes_written; + /* _outBuffer/_outOffset: steps write at out_offset, pushes copy slices */ + unsigned char *out_buf; + uint32_t out_offset; + /* the chunk in flight (handle.buffer/inOff/availInBefore/availOutBefore) */ + unsigned char *in_buf; + uint32_t in_off, avail_in_before, avail_out_before; + int flush_flag; + bool flushing; /* the 'prefinish' flush: completion is flush_done */ + bool wait_read; /* push() answered false with output pending */ + bool done_pending; /* Transform's kCallback: completion waits for _read */ + double rlen_before; /* readableLength when the chunk's transform began */ + ScrBytes *flush_bufs[6]; /* zero-length sentinels, one per flush kind */ +} ScrZlibStream; + +static void scr_zlib_stream_oom(void) { + scr_trap("scriptc: out of memory\n"); +} + +static void *scr_zs_retain_v(void *p) { + ((ScrZlibStream *)p)->rc++; + return p; +} + +static void scr_zs_close_engine(ScrZlibStream *zs) { + if (zs->write_in_progress) { + zs->pending_close = true; + return; + } + zs->pending_close = false; + scr_zlib_engine_free(zs->eng); + zs->eng = NULL; +} + +static void scr_zs_release_v(void *p) { + ScrZlibStream *zs = p; + if (--zs->rc > 0) return; + scr_zlib_engine_free(zs->eng); + free(zs->out_buf); + free(zs->in_buf); + for (size_t i = 0; i < 6; i++) { + if (zs->flush_bufs[i]) scr_bytes_release(zs->flush_bufs[i]); + } + free(zs); +} + +static ScrClosure *scr_zs_hook(ScrZlibStream *zs, void *fn) { + ScrClosure *cb = scr_closure_new(fn, 1); + cb->caps[0] = scr_box_new_obj(&scr_zs_retain_v, &scr_zs_release_v, NULL); + scr_box_set_ref(cb->caps[0], scr_zs_retain_v(zs)); + return cb; +} + +static ScrZlibStream *scr_zs_of(ScrClosure *cb) { + ScrZlibStream *zs = scr_box_get_ref(cb->caps[0]); + scr_zs_release_v(zs); /* the stream's hooks keep it alive */ + return zs; +} + +/* ── completion ───────────────────────────────────────────────────────── */ + +static void scr_zs_finish(ScrZlibStream *zs, ScrStream *s) { + if (zs->flushing) { + zs->flushing = false; + scr_stream_flush_done(s, NULL, NULL, NULL); + } else { + scr_stream_transform_done(s, NULL, NULL, NULL); + } +} + +static void scr_zs_tick_finish(ScrClosure *cb) { + ScrStream *s = scr_box_get_ref(cb->caps[0]); + ScrZlibStream *zs = scr_box_get_ref(cb->caps[1]); + scr_zs_finish(zs, s); + scr_zs_release_v(zs); + scr_stream_release(s); +} + +static void scr_zs_finish_on_tick(ScrZlibStream *zs, ScrStream *s) { + ScrClosure *cb = scr_closure_new((void *)&scr_zs_tick_finish, 2); + cb->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); + scr_box_set_ref(cb->caps[0], scr_stream_retain(s)); + cb->caps[1] = scr_box_new_obj(&scr_zs_retain_v, &scr_zs_release_v, NULL); + scr_box_set_ref(cb->caps[1], scr_zs_retain_v(zs)); + scr_next_tick(cb); +} + +/* Transform._write's callback: a write that leaves the readable buffer + * grown past its high-water mark completes at the next _read; a readable + * side that already ended completes on a tick. The flush completes at + * once. */ +static void scr_zs_complete(ScrZlibStream *zs, ScrStream *s) { + if (zs->flushing) { + scr_zs_finish(zs, s); + return; + } + if (scr_stream_prop(s, "rs:ended")) { + scr_zs_finish_on_tick(zs, s); + return; + } + double rlen = scr_stream_prop(s, "readableLength"); + if (scr_stream_prop(s, "writableEnded") || rlen == zs->rlen_before || + rlen < scr_stream_prop(s, "readableHighWaterMark")) { + scr_zs_finish(zs, s); + return; + } + zs->done_pending = true; +} + +/* ── the engine steps ─────────────────────────────────────────────────── */ + +typedef struct { + ScrZlibStream *zs; + ScrStream *s; +} ScrZlibStep; + +static void scr_zs_submit(ScrZlibStream *zs, ScrStream *s); + +static void scr_zs_step_work(void *payload) { + ScrZlibStep *op = payload; + ScrZlibStream *zs = op->zs; + scr_zlib_engine_work(zs->eng, zs->flush_flag, zs->in_buf + zs->in_off, zs->avail_in_before, + zs->out_buf + zs->out_offset, zs->avail_out_before); +} + +static ScrError *scr_zs_engine_error(const ScrZlibError *ze) { + ScrStr *message = scr_str_new(ze->message, strlen(ze->message)); + ScrError *err = scr_error_new(SCR_ERR_ERROR, message); + scr_str_release(message); + err->system_errno = ze->err; + scr_error_set_code(err, ze->code); + return err; +} + +static void scr_zs_destroy_with(ScrStream *s, ScrError *err /*moves*/) { + scr_stream_release(scr_stream_destroy(s, err)); + scr_error_release(err); +} + +/* processCallback: account the step, push its output, then either run + * the next step (the output buffer filled) or complete the chunk. */ +static void scr_zs_process(ScrZlibStream *zs, ScrStream *s) { + if (scr_stream_prop(s, "destroyed")) { + scr_zs_finish(zs, s); + return; + } + uint32_t avail_in_after, avail_out_after; + scr_zlib_engine_offsets(zs->eng, &avail_in_after, &avail_out_after); + uint32_t in_delta = zs->avail_in_before - avail_in_after; + zs->bytes_written += in_delta; + uint32_t have = zs->avail_out_before - avail_out_after; + bool full = false; + if (have > 0) { + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)have); + out->is_buffer = true; /* Node pushes Buffer slices of its output buffer */ + memcpy(out->data, zs->out_buf + zs->out_offset, have); + zs->out_offset += have; + full = !scr_stream_push(s, out); + scr_bytes_release(out); + if (scr_exc_pending()) return; + } + if (scr_stream_prop(s, "destroyed")) { + scr_zs_finish(zs, s); + return; + } + if (avail_out_after == 0 || zs->out_offset >= zs->chunk_size) { + zs->avail_out_before = zs->chunk_size; + zs->out_offset = 0; + } + if (avail_out_after == 0) { + zs->in_off += in_delta; + zs->avail_in_before = avail_in_after; + if (full) zs->wait_read = true; + else scr_zs_submit(zs, s); + return; + } + if (avail_in_after > 0) { + /* The decoder stopped before the input did (trailing data). */ + if (zs->reject_garbage) { + static const char msg[] = "Trailing junk found after the end of the compressed stream"; + ScrStr *m = scr_str_new(msg, sizeof msg - 1); + ScrError *err = scr_error_new(SCR_ERR_TYPE, m); + scr_str_release(m); + scr_error_set_code(err, "ERR_TRAILING_JUNK_AFTER_STREAM_END"); + scr_stream_release(scr_stream_destroy(s, err)); + if (zs->flushing) { + zs->flushing = false; + scr_stream_flush_done(s, err, NULL, NULL); + } else { + scr_stream_transform_done(s, err, NULL, NULL); + } + return; + } + scr_stream_push_null(s); + if (scr_exc_pending()) return; + } + scr_zs_complete(zs, s); +} + +static void scr_zs_step_after(void *payload) { + ScrZlibStep *op = payload; + ScrZlibStream *zs = op->zs; + ScrStream *s = op->s; + zs->write_in_progress = false; + ScrZlibError ze; + if (scr_zlib_engine_error(zs->eng, &ze)) { + ScrError *err = scr_zs_engine_error(&ze); + zs->closed = true; + scr_zs_destroy_with(s, err); + } else { + scr_zs_process(zs, s); + } + if (zs->pending_close) scr_zs_close_engine(zs); +} + +static void scr_zs_step_destroy(void *payload) { + ScrZlibStep *op = payload; + scr_zs_release_v(op->zs); + scr_stream_release(op->s); + free(op); +} + +static void scr_zs_submit(ScrZlibStream *zs, ScrStream *s) { + ScrZlibStep *op = malloc(sizeof *op); + if (!op) scr_zlib_stream_oom(); + op->zs = scr_zs_retain_v(zs); + op->s = scr_stream_retain(s); + zs->write_in_progress = true; + scr_work_submit(op, &scr_zs_step_work, &scr_zs_step_after, &scr_zs_step_destroy); +} + +/* processChunk: a closed handle completes on a tick; otherwise the chunk + * is copied (the worker reads it) and its first step starts. */ +static void scr_zs_process_chunk(ScrZlibStream *zs, ScrStream *s, const ScrBytes *chunk, int flag) { + if (zs->closed) { + scr_zs_finish_on_tick(zs, s); + return; + } + size_t len = chunk->len; + free(zs->in_buf); + zs->in_buf = malloc(len ? len : 1); + if (!zs->in_buf) scr_zlib_stream_oom(); + if (len) scr_bytes_read(chunk, 0, zs->in_buf, len); + zs->in_off = 0; + zs->avail_in_before = (uint32_t)len; + zs->avail_out_before = zs->chunk_size - zs->out_offset; + zs->flush_flag = flag; + zs->rlen_before = scr_stream_prop(s, "readableLength"); + scr_zs_submit(zs, s); +} + +/* Node's flushiness order: NO_FLUSH < BLOCK < PARTIAL < SYNC < FULL < FINISH. */ +static int scr_zs_flushiness(int f) { + static const int rank[6] = { 0, 2, 3, 4, 5, 1 }; + return f >= 0 && f < 6 ? rank[f] : 0; +} + +static int scr_zs_max_flush(int a, int b) { + return scr_zs_flushiness(a) > scr_zs_flushiness(b) ? a : b; +} + +static void scr_zs_transform_inv(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + ScrZlibStream *zs = scr_zs_of(cb); + int flag = zs->default_flush; + for (int k = 0; k < 6; k++) { + if (zs->flush_bufs[k] != NULL && chunk == zs->flush_bufs[k]) flag = k; + } + if (scr_stream_prop(s, "writableEnded") && + scr_stream_prop(s, "writableLength") == (double)chunk->len) { + flag = scr_zs_max_flush(flag, zs->finish_flush); + } + zs->flushing = false; + scr_zs_process_chunk(zs, s, chunk, flag); +} + +/* _flush (run from the Transform's 'prefinish' listener): an empty chunk + * through _transform — the stream has ended with nothing buffered, so the + * finish flush applies. */ +static void scr_zs_flush_inv(ScrClosure *cb, ScrStream *s) { + ScrZlibStream *zs = scr_zs_of(cb); + ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0); + zs->flushing = true; + scr_zs_process_chunk(zs, s, empty, scr_zs_max_flush(zs->default_flush, zs->finish_flush)); + scr_bytes_release(empty); +} + +/* _read: the step a full push() paused resumes; a completion that waited + * on the readable buffer (Transform's kCallback) runs. */ +static void scr_zs_read_inv(ScrClosure *cb, ScrStream *s, double size) { + (void)size; + ScrZlibStream *zs = scr_zs_of(cb); + if (zs->wait_read) { + zs->wait_read = false; + scr_zs_submit(zs, s); + } + if (zs->done_pending) { + zs->done_pending = false; + scr_zs_finish(zs, s); + } +} + +static void scr_zs_destroy_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + ScrZlibStream *zs = scr_zs_of(cb); + zs->closed = true; + scr_zs_close_engine(zs); + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); +} + +/* ── option validation (lib/internal/validators.js) ───────────────────── */ + +static void scr_zs_range_fail(const char *name, const char *range, double v) { + char recv[48], msg[256]; + scr_num_received(v, recv); + int len = snprintf(msg, sizeof msg, "The value of \"%s\" is out of range. It must be %s. Received %s", + name, range, recv); + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, (size_t)len, "ERR_OUT_OF_RANGE"); +} + +/* validateFiniteNumber: false for undefined/NaN, true for finite numbers, + * throws otherwise. `prop` picks the property/argument message flavor. */ +static bool scr_zs_finite(const ScrDyn *v, const char *name, bool prop, bool *ok) { + *ok = true; + if (v == NULL || v->kind == SCR_DYN_UNDEF) return false; + if (v->kind == SCR_DYN_NUM) { + if (isfinite(v->v.num)) return true; + if (isnan(v->v.num)) return false; + scr_zs_range_fail(name, "a finite number", v->v.num); + *ok = false; + return false; + } + if (prop) scr_dyn_prop_type_fail(name, "of type number", v); + else scr_dyn_arg_type_fail(name, "of type number", v); + *ok = false; + return false; +} + +/* checkRangesOrGetDefault */ +static bool scr_zs_ranged(const ScrDyn *v, const char *name, bool prop, double lower, double upper, + double def, double *out) { + bool ok; + if (!scr_zs_finite(v, name, prop, &ok)) { + *out = def; + return ok; + } + if (v->v.num < lower || v->v.num > upper) { + char range[96]; + snprintf(range, sizeof range, ">= %.17g and <= %.17g", lower, upper); + scr_zs_range_fail(name, range, v->v.num); + return false; + } + *out = v->v.num; + return true; +} + +static const ScrDyn *scr_zs_opt(const ScrDyn *opts, const char *key) { + if (opts == NULL || opts->kind != SCR_DYN_OBJ) return NULL; + return scr_dyn_obj_get((ScrDyn *)opts, key, strlen(key)); +} + +static bool scr_zs_is_null(const ScrDyn *v) { + return v == NULL || v->kind == SCR_DYN_UNDEF || v->kind == SCR_DYN_NULL; +} + +/* getHighWaterMark (lib/internal/streams/state.js): highWaterMark wins + * over the sided key; a non-integer or negative value throws with the + * sided name. -1 = the default. */ +static bool scr_zs_hwm_value(const ScrDyn *v, const char *name, double *out) { + if (v->kind != SCR_DYN_NUM || !isfinite(v->v.num) || trunc(v->v.num) != v->v.num || + v->v.num < 0) { + scr_dyn_arg_value_fail(name, NULL, v); + return false; + } + *out = v->v.num; + return true; +} + +static bool scr_zs_hwm(const ScrDyn *opts, const char *sided, const char *name, double *out) { + const ScrDyn *v = scr_zs_opt(opts, "highWaterMark"); + if (scr_zs_is_null(v)) v = scr_zs_opt(opts, sided); + *out = -1; + if (scr_zs_is_null(v)) return true; + return scr_zs_hwm_value(v, name, out); +} + +static bool scr_zs_false(const ScrDyn *v) { + return v != NULL && v->kind == SCR_DYN_BOOL && !v->v.b; +} + +static bool scr_zs_unsupported(const char *msg) { + scr_throw_error_msg(SCR_ERR_ERROR, msg, strlen(msg)); + return false; +} + +/* The Transform options Node would consume with no lowering here. */ +static bool scr_zs_check_unlowered(const ScrDyn *opts) { + if (opts == NULL || opts->kind != SCR_DYN_OBJ) return true; + const ScrDyn *v = scr_zs_opt(opts, "readableObjectMode"); + if (v != NULL && scr_dyn_truthy(v)) + return scr_zs_unsupported("objectMode streams are not supported yet (chunks are Buffers or utf8 strings)"); + if (scr_zs_false(scr_zs_opt(opts, "decodeStrings"))) + return scr_zs_unsupported("decodeStrings: false is not supported yet (chunks always decode to Buffers)"); + v = scr_zs_opt(opts, "signal"); + if (!scr_zs_is_null(v)) + return scr_zs_unsupported("the stream option 'signal' is not supported yet (AbortSignal-driven destruction has no lowering)"); + v = scr_zs_opt(opts, "defaultEncoding"); + if (v != NULL && v->kind == SCR_DYN_STR && + !(v->v.str->len == 4 && memcmp(v->v.str->data, "utf8", 4) == 0) && + !(v->v.str->len == 5 && memcmp(v->v.str->data, "utf-8", 5) == 0)) + return scr_zs_unsupported("the stream option 'defaultEncoding' is not supported yet (utf8, the default, is the write-side string encoding)"); + static const char *const fns[] = { "construct", "read", "write", "writev", "final", "destroy", + "transform" }; + for (size_t i = 0; i < sizeof fns / sizeof fns[0]; i++) { + v = scr_zs_opt(opts, fns[i]); + if (v != NULL && v->kind == SCR_DYN_FUNC) { + char msg[160]; + snprintf(msg, sizeof msg, + "the zlib stream option '%s' is not supported yet (it would replace the zlib transform)", + fns[i]); + return scr_zs_unsupported(msg); + } + } + return true; +} + +static const char *scr_zs_class_name(int mode) { + switch (mode) { + case SCR_ZM_DEFLATE: return "Deflate"; + case SCR_ZM_INFLATE: return "Inflate"; + case SCR_ZM_GZIP: return "Gzip"; + case SCR_ZM_GUNZIP: return "Gunzip"; + case SCR_ZM_DEFLATERAW: return "DeflateRaw"; + case SCR_ZM_INFLATERAW: return "InflateRaw"; + default: return "Unzip"; + } +} + +ScrStream *scr_zlib_stream_new(double mode_d, ScrDyn *opts) { + int mode = (int)mode_d; + bool have_opts = opts != NULL && scr_dyn_truthy(opts); + const ScrDyn *o = have_opts ? opts : NULL; + /* Zlib(): windowBits, level, memLevel, strategy, dictionary. */ + double window_bits = 15, level = -1, mem_level = 8, strategy = 0; + const unsigned char *dict = NULL; + size_t dict_len = 0; + ScrBytes *dict_view = NULL; + if (have_opts) { + const ScrDyn *wb = scr_zs_opt(o, "windowBits"); + if (mode == SCR_ZM_DEFLATERAW && wb != NULL && wb->kind == SCR_DYN_NUM && wb->v.num == 8) { + window_bits = 9; /* DeflateRaw rewrites windowBits 8 as 9 */ + } else if ((scr_zs_is_null(wb) || (wb->kind == SCR_DYN_NUM && wb->v.num == 0)) && + (mode == SCR_ZM_INFLATE || mode == SCR_ZM_GUNZIP || mode == SCR_ZM_UNZIP)) { + window_bits = 0; + } else if (!scr_zs_ranged(wb, "options.windowBits", true, mode == SCR_ZM_GZIP ? 9 : 8, 15, 15, + &window_bits)) { + return NULL; + } + if (!scr_zs_ranged(scr_zs_opt(o, "level"), "options.level", true, -1, 9, -1, &level) || + !scr_zs_ranged(scr_zs_opt(o, "memLevel"), "options.memLevel", true, 1, 9, 8, &mem_level) || + !scr_zs_ranged(scr_zs_opt(o, "strategy"), "options.strategy", true, 0, 4, 0, &strategy)) { + return NULL; + } + const ScrDyn *d = scr_zs_opt(o, "dictionary"); + if (d != NULL && d->kind != SCR_DYN_UNDEF) { + if (d->kind != SCR_DYN_BYTES) { + scr_dyn_prop_type_fail("options.dictionary", + "an instance of Buffer, TypedArray, DataView, or ArrayBuffer", d); + return NULL; + } + dict_view = scr_bytes_raw_view(d->v.bytes); + dict = dict_view->data; + dict_len = dict_view->len; + } + } + const ScrDyn *rg = scr_zs_opt(o, "rejectGarbageAfterEnd"); + bool reject_garbage = rg != NULL && rg->kind == SCR_DYN_BOOL && rg->v.b; + /* ZlibBase(): chunkSize, flush, finishFlush, maxOutputLength, + * rejectGarbageAfterEnd. */ + double chunk_size = SCR_Z_DEFAULT_CHUNK, flush = SCR_ZF_NO_FLUSH, finish_flush = SCR_ZF_FINISH; + if (have_opts) { + const ScrDyn *cs = scr_zs_opt(o, "chunkSize"); + bool ok; + if (scr_zs_finite(cs, "options.chunkSize", true, &ok)) { + if (cs->v.num < SCR_Z_MIN_CHUNK) { + scr_zs_range_fail("options.chunkSize", ">= 64", cs->v.num); + ok = false; + } else { + chunk_size = floor(cs->v.num); + } + } + double max_output; + if (!ok || + !scr_zs_ranged(scr_zs_opt(o, "flush"), "options.flush", true, 0, 5, SCR_ZF_NO_FLUSH, &flush) || + !scr_zs_ranged(scr_zs_opt(o, "finishFlush"), "options.finishFlush", true, 0, 5, + SCR_ZF_FINISH, &finish_flush) || + !scr_zs_ranged(scr_zs_opt(o, "maxOutputLength"), "options.maxOutputLength", true, 1, + SCR_K_MAX_LENGTH, SCR_K_MAX_LENGTH, &max_output)) { + if (dict_view) scr_bytes_release(dict_view); + return NULL; + } + if (rg != NULL && rg->kind != SCR_DYN_UNDEF && rg->kind != SCR_DYN_BOOL) { + if (dict_view) scr_bytes_release(dict_view); + scr_dyn_prop_type_fail("options.rejectGarbageAfterEnd", "of type boolean", rg); + return NULL; + } + if (chunk_size > 4294967295.0) { + if (dict_view) scr_bytes_release(dict_view); + scr_zs_unsupported("a zlib chunkSize above 4294967295 is not supported yet"); + return NULL; + } + } + /* Transform(): the high-water marks, then the unlowered options. */ + double rhwm = -1, whwm = -1; + bool hwm_ok = scr_zs_hwm(o, "readableHighWaterMark", "options.readableHighWaterMark", &rhwm); + if (hwm_ok && rhwm == 0) { + /* A zero readable mark: Transform drops highWaterMark and the writable + * side reads `writableHighWaterMark || 0`. */ + const ScrDyn *w = scr_zs_opt(o, "writableHighWaterMark"); + whwm = 0; + if (w != NULL && scr_dyn_truthy(w)) + hwm_ok = scr_zs_hwm_value(w, "options.writableHighWaterMark", &whwm); + } else if (hwm_ok) { + hwm_ok = scr_zs_hwm(o, "writableHighWaterMark", "options.writableHighWaterMark", &whwm); + } + if (!hwm_ok || !scr_zs_check_unlowered(o)) { + if (dict_view) scr_bytes_release(dict_view); + return NULL; + } + ScrZlibStream *zs = calloc(1, sizeof *zs); + if (!zs) scr_zlib_stream_oom(); + zs->rc = 1; + zs->mode = mode; + zs->eng = scr_zlib_engine_new(mode, (int)level, (int)window_bits, (int)mem_level, (int)strategy, + dict, dict_len, reject_garbage); + if (dict_view) scr_bytes_release(dict_view); + zs->chunk_size = (uint32_t)chunk_size; + zs->out_buf = malloc(zs->chunk_size); + if (!zs->out_buf) scr_zlib_stream_oom(); + zs->default_flush = (int)flush; + zs->finish_flush = (int)finish_flush; + zs->full_flush = SCR_ZF_FULL; + zs->level = level; + zs->strategy = strategy; + zs->reject_garbage = reject_garbage; + bool auto_destroy = !scr_zs_false(scr_zs_opt(o, "autoDestroy")); + bool emit_close = !scr_zs_false(scr_zs_opt(o, "emitClose")); + bool allow_half_open = !scr_zs_false(scr_zs_opt(o, "allowHalfOpen")); + bool readable_side = !scr_zs_false(scr_zs_opt(o, "readable")); + bool writable_side = !scr_zs_false(scr_zs_opt(o, "writable")); + ScrStream *s = scr_stream_new_transform(rhwm, whwm, auto_destroy, emit_close, allow_half_open, + readable_side, writable_side, + scr_zs_hook(zs, (void *)&scr_zs_transform_inv), &scr_zs_transform_inv, + scr_zs_hook(zs, (void *)&scr_zs_flush_inv), &scr_zs_flush_inv, + scr_zs_hook(zs, (void *)&scr_zs_destroy_inv), &scr_zs_destroy_inv); + scr_stream_set_read(s, scr_zs_hook(zs, (void *)&scr_zs_read_inv), &scr_zs_read_inv); + scr_stream_set_flush_on_prefinish(s); + s->cls = scr_zs_class_name(mode); + zs->s = s; + scr_stream_set_native(s, zs, &scr_zs_release_v); /* moves the creation reference */ + return s; +} + +/* ── the instance surface ─────────────────────────────────────────────── */ + +static ScrZlibStream *scr_zs_from_stream(ScrStream *s) { + ScrZlibStream *zs = scr_stream_native(s, &scr_zs_release_v); + if (zs == NULL) { + static const char msg[] = "the receiver is not a zlib stream"; + scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1); + } + return zs; +} + +double scr_zlib_stream_bytes_written(ScrStream *s) { + ScrZlibStream *zs = scr_zs_from_stream(s); + return zs ? zs->bytes_written : 0; +} + +static void scr_zs_call0(ScrClosure *cb) { + ((void (*)(ScrClosure *))cb->fn)(cb); +} + +static void scr_zs_end_inv(ScrClosure *cb, va_list ap) { + (void)ap; + scr_zs_call0(cb); +} + +/* flush(kind?, callback?) — kind arrives undefined when absent; the + * frontend has already moved a function first argument into cb. */ +void scr_zlib_stream_flush(ScrStream *s, ScrDyn *kind, ScrClosure *cb) { + ScrZlibStream *zs = scr_zs_from_stream(s); + if (!zs) { + if (cb) scr_closure_release(cb); + return; + } + double k; + if (!scr_zs_ranged(kind, "kind", false, 0, 5, zs->full_flush, &k)) { + if (cb) scr_closure_release(cb); + return; + } + if (scr_stream_prop(s, "writableFinished")) { + if (cb) scr_next_tick(cb); + return; + } + if (scr_stream_prop(s, "writableEnded")) { + if (cb) { + ScrStr *name = scr_str_new("end", 3); + scr_emitter_release(scr_emitter_on((ScrEmitter *)s, name, cb, &scr_zs_end_inv, true, false)); + scr_str_release(name); + } + return; + } + int ki = (int)k; + if (zs->flush_bufs[ki] == NULL) zs->flush_bufs[ki] = scr_bytes_new(SCR_BYTES_U8, 0); + scr_stream_write(s, zs->flush_bufs[ki], cb); +} + +/* params(level, strategy, callback): a sync flush first when either + * changes, then the engine's deflateParams; the callback runs unless the + * stream was destroyed meanwhile. */ +static void scr_zs_params_after(ScrClosure *cb) { + ScrStream *s = scr_box_get_ref(cb->caps[0]); + double level = scr_box_get_f64(cb->caps[1]); + double strategy = scr_box_get_f64(cb->caps[2]); + ScrClosure *user = cb->ncaps > 3 ? scr_box_get_ref(cb->caps[3]) : NULL; + ScrZlibStream *zs = scr_zs_from_stream(s); + if (zs && zs->closed) { + static const char msg[] = + "zlib binding closed\nThis is caused by either a bug in Node.js or incorrect usage of " + "Node.js internals.\nPlease open an issue with this stack trace at " + "https://github.com/nodejs/node/issues\n"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "ERR_INTERNAL_ASSERTION"); + } else if (zs) { + ScrZlibError ze; + if (scr_zlib_engine_params(zs->eng, isnan(level) ? 0 : (int)level, + isnan(strategy) ? 0 : (int)strategy, &ze)) { + zs->closed = true; + scr_zs_destroy_with(s, scr_zs_engine_error(&ze)); + } + if (!scr_stream_prop(s, "destroyed")) { + zs->level = level; + zs->strategy = strategy; + if (user) scr_zs_call0(user); + } + } + if (user) scr_closure_release(user); + scr_stream_release(s); +} + +void scr_zlib_stream_params(ScrStream *s, ScrDyn *level_d, ScrDyn *strategy_d, ScrClosure *cb) { + ScrZlibStream *zs = scr_zs_from_stream(s); + double level, strategy; + if (!zs || !scr_zs_ranged(level_d, "level", false, -1, 9, NAN, &level) || + !scr_zs_ranged(strategy_d, "strategy", false, 0, 4, NAN, &strategy)) { + if (cb) scr_closure_release(cb); + return; + } + bool same_level = level == zs->level || (isnan(level) && isnan(zs->level)); + bool same_strategy = strategy == zs->strategy || (isnan(strategy) && isnan(zs->strategy)); + if (same_level && same_strategy) { + if (cb == NULL) { + ScrDyn *undef = scr_dyn_undefined(); + scr_dyn_arg_type_fail("callback", "of type function", undef); + return; + } + scr_next_tick(cb); + return; + } + /* caps: the stream, level, strategy, then the callback when given */ + ScrClosure *after = scr_closure_new((void *)&scr_zs_params_after, cb ? 4 : 3); + after->caps[0] = scr_box_new_obj(&scr_stream_retain_v, &scr_stream_release_v, &scr_stream_trace); + scr_box_set_ref(after->caps[0], scr_stream_retain(s)); + after->caps[1] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(after->caps[1], level); + after->caps[2] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(after->caps[2], strategy); + if (cb) { + after->caps[3] = scr_box_new(SCR_BOX_FUNC); + scr_box_set_ref(after->caps[3], cb); + } + ScrDyn *sync = scr_dyn_new_num(SCR_ZF_SYNC); + scr_zlib_stream_flush(s, sync, after); + scr_dyn_release(sync); +} + +static void scr_zs_close_inv(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)s; + (void)err; + scr_zs_call0(cb); +} + +/* close(callback?): finished(this, callback), then destroy(). */ +void scr_zlib_stream_close(ScrStream *s, ScrClosure *cb) { + if (!scr_zs_from_stream(s)) { + if (cb) scr_closure_release(cb); + return; + } + if (cb) { + ScrClosure *cleanup = scr_stream_finished(s, cb, &scr_zs_close_inv); + if (cleanup) scr_closure_release(cleanup); + } + scr_stream_release(scr_stream_destroy(s, NULL)); +} + +void scr_zlib_stream_reset(ScrStream *s) { + ScrZlibStream *zs = scr_zs_from_stream(s); + if (!zs) return; + if (zs->closed) { + static const char msg[] = + "zlib binding closed\nThis is caused by either a bug in Node.js or incorrect usage of " + "Node.js internals.\nPlease open an issue with this stack trace at " + "https://github.com/nodejs/node/issues\n"; + scr_throw_error_msg_code(SCR_ERR_ERROR, msg, sizeof msg - 1, "ERR_INTERNAL_ASSERTION"); + return; + } + if (zs->write_in_progress) { + static const char msg[] = "Cannot reset zlib stream while a write is in progress"; + scr_throw_error_msg(SCR_ERR_ERROR, msg, sizeof msg - 1); + return; + } + ScrZlibError ze; + if (scr_zlib_engine_reset(zs->eng, &ze)) { + zs->closed = true; + scr_zs_destroy_with(s, scr_zs_engine_error(&ze)); + } +} diff --git a/scripts/ci-test-costs.json b/scripts/ci-test-costs.json index 5ea6000a0b..949e80dc85 100644 --- a/scripts/ci-test-costs.json +++ b/scripts/ci-test-costs.json @@ -361,7 +361,6 @@ "tests/harness/node-compatibility.test.ts": 3, "tests/harness/node-test.test.ts": 14918, "tests/harness/npm-fetch-defaults.test.ts": 2062, - "tests/harness/npm-static.test.ts": 406491, "tests/harness/oci-manifest.test.ts": 3, "tests/harness/opaque-class-view.test.ts": 1743, "tests/harness/process-argv.test.ts": 1923, @@ -387,7 +386,9 @@ "tests/harness/self-hosting-llvm-emitter.test.ts": 297588, "tests/harness/self-hosting-lowering-context.test.ts": 54669, "tests/harness/self-hosting-native-cache.test.ts": 19497, - "tests/harness/self-hosting-native-frontend.test.ts": 576883, + "tests/harness/self-hosting-native-frontend-checks.test.ts": 363000, + "tests/harness/self-hosting-native-frontend-programs.test.ts": 331000, + "tests/harness/self-hosting-native-frontend.test.ts": 334000, "tests/harness/self-hosting-native-toolchain.test.ts": 54572, "tests/harness/self-hosting-program-host.test.ts": 32496, "tests/harness/self-hosting-runtime-features.test.ts": 40264, @@ -777,7 +778,6 @@ "tests/harness/node-compatibility.test.ts": 5, "tests/harness/node-test.test.ts": 17953, "tests/harness/npm-fetch-defaults.test.ts": 5674, - "tests/harness/npm-static.test.ts": 518221, "tests/harness/oci-manifest.test.ts": 3, "tests/harness/opaque-class-view.test.ts": 2545, "tests/harness/process-argv.test.ts": 2583, @@ -804,7 +804,9 @@ "tests/harness/self-hosting-llvm-emitter.test.ts": 429842, "tests/harness/self-hosting-lowering-context.test.ts": 47647, "tests/harness/self-hosting-native-cache.test.ts": 29299, - "tests/harness/self-hosting-native-frontend.test.ts": 606506, + "tests/harness/self-hosting-native-frontend-checks.test.ts": 381000, + "tests/harness/self-hosting-native-frontend-programs.test.ts": 347000, + "tests/harness/self-hosting-native-frontend.test.ts": 351000, "tests/harness/self-hosting-native-toolchain.test.ts": 58186, "tests/harness/self-hosting-program-host.test.ts": 36298, "tests/harness/self-hosting-runtime-features.test.ts": 34461, diff --git a/scripts/ci-test-costs.mjs b/scripts/ci-test-costs.mjs index 661d0a8591..c227424d0f 100644 --- a/scripts/ci-test-costs.mjs +++ b/scripts/ci-test-costs.mjs @@ -1,21 +1,27 @@ #!/usr/bin/env node -// Refreshes scripts/ci-test-costs.json from the logs of the Linux test shards of green CI runs on main. +// Refreshes scripts/ci-test-costs.json and tests/harness/npm-static-costs.json from the logs of the Linux test +// shards of green CI runs on main. // // Usage: node scripts/ci-test-costs.mjs ... // // Each log is a downloaded "test (Linux, plain|san, i/n)" job log. vitest prints every test file it ran with its -// duration; for each file that runs in the side queue (not the case-sharded corpus, Effect, coverage and cache -// suites) this stores the median over the logs of its flavor. Files without a measurement keep their current -// value. +// duration, and every test slower than its threshold with its duration. ci-test-costs.json stores, for each file +// that runs whole in the side queue (not the case-sharded corpus, Effect, coverage, cache and npm-static suites), +// the median over the logs of its flavor; npm-static-costs.json stores the median of each npm-static test. Entries +// without a new measurement keep their value; files the side queue no longer runs whole are removed. import { readFileSync, writeFileSync } from "node:fs"; import { fileExclusions, isolatedFiles } from "./ci-test-plan.mjs"; -const path = new URL("./ci-test-costs.json", import.meta.url); -const table = JSON.parse(readFileSync(path, "utf8")); +const filePath = new URL("./ci-test-costs.json", import.meta.url); +const npmStaticPath = new URL("../tests/harness/npm-static-costs.json", import.meta.url); +const tables = { + files: JSON.parse(readFileSync(filePath, "utf8")), + npmStatic: JSON.parse(readFileSync(npmStaticPath, "utf8")), +}; const skipped = new Set( fileExclusions.filter((file) => !isolatedFiles.some(([, isolated]) => isolated === file)), ); -const samples = { plain: {}, san: {} }; +const samples = { files: { plain: {}, san: {} }, npmStatic: { plain: {}, san: {} } }; for (const log of process.argv.slice(2)) { // Logs keep the terminal color codes (ESC [ ... m); drop them before matching. const text = readFileSync(log, "utf8") @@ -23,21 +29,26 @@ for (const log of process.argv.slice(2)) { .replace(/\[[0-9;]*m/g, ""); const flavor = /SCRIPTC_SAN: 1\b/.test(text) ? "san" : "plain"; for (const line of text.split("\n")) { - const m = /^\S+ {2}✓ (\S+\.test\.ts) \(\d+ tests?[^)]*\) (\d+)ms$/.exec(line); - if (!m || skipped.has(m[1])) continue; - (samples[flavor][m[1]] ??= []).push(Number(m[2])); + let m = /^\S+ {2}✓ (\S+\.test\.ts) \(\d+ tests?[^)]*\) (\d+)ms$/.exec(line); + if (m && !skipped.has(m[1])) (samples.files[flavor][m[1]] ??= []).push(Number(m[2])); + m = /^\S+ {4}✓ npm-static pilots(?: \(sanitized\))? > (.*?) {2}(\d+)ms$/.exec(line); + if (m) (samples.npmStatic[flavor][m[1]] ??= []).push(Number(m[2])); } } const median = (values) => [...values].sort((a, b) => a - b)[Math.floor(values.length / 2)]; let updated = 0; -for (const flavor of ["plain", "san"]) - for (const [file, values] of Object.entries(samples[flavor])) { - table[flavor][file] = median(values); - updated++; +for (const [name, table] of Object.entries(tables)) + for (const flavor of ["plain", "san"]) { + for (const [key, values] of Object.entries(samples[name][flavor])) { + table[flavor][key] = median(values); + updated++; + } + table[flavor] = Object.fromEntries( + Object.entries(table[flavor]) + .filter(([key]) => name !== "files" || !skipped.has(key)) + .sort(([a], [b]) => a.localeCompare(b)), + ); } -for (const flavor of ["plain", "san"]) - table[flavor] = Object.fromEntries( - Object.entries(table[flavor]).sort(([a], [b]) => a.localeCompare(b)), - ); -writeFileSync(path, JSON.stringify(table, null, 2) + "\n"); -console.log(`updated ${updated} file costs`); +writeFileSync(filePath, JSON.stringify(tables.files, null, 2) + "\n"); +writeFileSync(npmStaticPath, JSON.stringify(tables.npmStatic, null, 2) + "\n"); +console.log(`updated ${updated} costs`); diff --git a/scripts/ci-test-plan.mjs b/scripts/ci-test-plan.mjs index af716c3c5e..d922b53db9 100644 --- a/scripts/ci-test-plan.mjs +++ b/scripts/ci-test-plan.mjs @@ -19,15 +19,19 @@ export const isolatedFiles = [ ["self-hosting", "tests/harness/self-hosting.test.ts"], ["self-hosting LLVM emitter", "tests/harness/self-hosting-llvm-emitter.test.ts"], ["fetch", "tests/harness/fetch.test.ts"], - ["npm static", "tests/harness/npm-static.test.ts"], ["surface manifest", "tests/harness/surface-manifest.test.ts"], ]; +// Suites that split their own tests across the shards (SCRIPTC_TEST_SHARD, +// skipUnownedTest in tests/harness/shard.ts): every shard runs its share in +// its own vitest process. +export const caseShardedSideFiles = [["npm static", "tests/harness/npm-static.test.ts"]]; export const fileExclusions = [ ...corpusFiles, ...effectFiles, coverageFile, cacheFile, ...isolatedFiles.map(([, file]) => file), + ...caseShardedSideFiles.map(([, file]) => file), "tests/harness/self-hosting-native-driver.test.ts", ]; @@ -152,6 +156,8 @@ export function ciTestPlan({ shard, flavor, workers = 4, suite = "full" }) { if (owners.get(file) === index) side.push({ name, args: ["test", file], env: { ...env, SCRIPTC_TEST_WORKERS: "1" } }); } + for (const [name, file] of caseShardedSideFiles) + side.push({ name, args: ["test", file], env: { ...env, SCRIPTC_TEST_WORKERS: "1" } }); // Frontend coverage and cache invalidation do not consume sanitizer state. if (flavor === "plain") { side.push({ diff --git a/tests/corpus/5370-zlib-stream-roundtrip.ts b/tests/corpus/5370-zlib-stream-roundtrip.ts new file mode 100644 index 0000000000..12bf0a8114 --- /dev/null +++ b/tests/corpus/5370-zlib-stream-roundtrip.ts @@ -0,0 +1,84 @@ +// The zlib stream classes round-trip through pipeline(): Deflate/Inflate, +// DeflateRaw/InflateRaw, Gzip/Gunzip, and Unzip over both gzip and zlib +// input, with the factory and constructor spellings. Compressed bytes are +// zlib-version-dependent, so only decompressed results, input byte counts, +// and the format markers are printed. +import * as zlib from "node:zlib"; +import { pipeline, Readable, Writable } from "node:stream"; + +const lines: string[] = []; +for (let i = 0; i < 1500; i++) lines.push(`line ${i} ${"abc".repeat(i % 7)}`); +const input = Buffer.from(lines.join("\n")); + +function source(): Readable { + let offset = 0; + return new Readable({ + read() { + if (offset >= input.length) { + this.push(null); + return; + } + this.push(input.subarray(offset, offset + 4000)); + offset += 4000; + }, + }); +} + +function sink(parts: Buffer[]): Writable { + return new Writable({ + write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void) { + parts.push(chunk); + callback(); + }, + }); +} + +function roundTrip(label: string, encoder: zlib.Gzip, decoder: zlib.Gunzip): Promise { + return new Promise((resolve) => { + const parts: Buffer[] = []; + pipeline(source(), encoder, decoder, sink(parts), (error: Error | null | undefined) => { + const out = Buffer.concat(parts); + console.log(label, error === undefined || error === null, out.equals(input), out.length); + console.log(" bytesWritten", encoder.bytesWritten === input.length, decoder.bytesWritten > 0); + resolve(); + }); + }); +} + +function header(label: string, encoder: zlib.Gzip): Promise { + return new Promise((resolve) => { + const parts: Buffer[] = []; + encoder.on("data", (chunk: Buffer) => parts.push(chunk)); + encoder.on("end", () => { + console.log(label, Buffer.concat(parts).subarray(0, 2).toString("hex")); + resolve(); + }); + encoder.end("header check"); + }); +} + +// Decoded chunks are Buffers: string conversion gives their text. +function text(): Promise { + return new Promise((resolve) => { + const decoder = zlib.createGunzip(); + decoder.on("data", (chunk: Buffer) => { + console.log("chunk text", `${chunk}`, String(chunk) + "!", chunk + "", chunk.toString("hex")); + }); + decoder.on("end", () => resolve()); + decoder.end(zlib.gzipSync("plain text")); + }); +} + +async function main(): Promise { + await roundTrip("deflate", zlib.createDeflate(), zlib.createInflate()); + await roundTrip("raw", zlib.createDeflateRaw(), zlib.createInflateRaw()); + await roundTrip("gzip", zlib.createGzip(), zlib.createGunzip()); + await roundTrip("unzip gzip", new zlib.Gzip(), new zlib.Unzip()); + await roundTrip("unzip deflate", new zlib.Deflate(), zlib.createUnzip()); + await roundTrip("level 1", zlib.createGzip({ level: 1 }), zlib.createGunzip({ chunkSize: 1024 })); + await header("gzip header", zlib.createGzip()); + await header("zlib header", zlib.createDeflate()); + await text(); +} + +main(); diff --git a/tests/corpus/5371-zlib-stream-chunking.cjs b/tests/corpus/5371-zlib-stream-chunking.cjs new file mode 100644 index 0000000000..53899d835e --- /dev/null +++ b/tests/corpus/5371-zlib-stream-chunking.cjs @@ -0,0 +1,49 @@ +// zlib stream output chunking and event order: one compressed write +// inflates into chunkSize pieces (the output buffer fills, a new one +// starts), 'data' follows the synchronous code and its nextTick, the end() +// callback runs before 'finish', and 'end' then 'close' follow the +// flush. Each chunk is a Buffer. The write and flush callbacks, +// writableFinished, and the readable/writable flags settle in Node's order. +const zlib = require("node:zlib"); + +const input = Buffer.from(Array.from({ length: 2500 }, (_, i) => `row ${i * 7919}`).join("\n")); + +function run(label, options, next) { + const packed = zlib.deflateSync(input); + const stream = zlib.createInflate(options); + const order = []; + const sizes = []; + stream.on("data", (chunk) => { + sizes.push(chunk.length); + if (sizes.length === 1) order.push(`data ${Buffer.isBuffer(chunk)} ${JSON.stringify(String(chunk.subarray(0, 8)))}`); + }); + stream.on("finish", () => order.push(`finish ${stream.writableFinished}`)); + stream.on("end", () => order.push(`end ${stream.readableEnded}`)); + stream.on("close", () => { + order.push("close"); + console.log(label, order.join(" ")); + console.log(" sizes", sizes.length, sizes.slice(0, 3).join(","), sizes[sizes.length - 1]); + console.log(" total", sizes.reduce((a, b) => a + b, 0) === input.length, stream.bytesWritten === packed.length); + console.log(" flags", stream.destroyed, stream.writable, stream.readable); + next(); + }); + process.nextTick(() => order.push("tick")); + stream.write(packed, () => order.push("write cb")); + stream.end(() => order.push("end cb")); + order.push("sync"); +} + +run("default", undefined, () => + run("chunk 1000", { chunkSize: 1000 }, () => + run("chunk 64", { chunkSize: 64 }, () => + run("chunk 70000", { chunkSize: 70000 }, () => { + const gz = zlib.createGzip(); + const events = []; + gz.on("data", () => events.push("data")); + gz.on("end", () => events.push("end")); + gz.on("close", () => console.log("empty gzip", events.join(" "))); + gz.end(); + }), + ), + ), +); diff --git a/tests/corpus/5372-zlib-stream-errors.cjs b/tests/corpus/5372-zlib-stream-errors.cjs new file mode 100644 index 0000000000..0865b60aa5 --- /dev/null +++ b/tests/corpus/5372-zlib-stream-errors.cjs @@ -0,0 +1,56 @@ +// zlib stream errors: corrupt input destroys the stream with Node's Error +// (zlib's message, errno and code; 'error' then 'close'), truncated input +// fails at the final flush with "unexpected end of file" (a stored, level +// 0 prefix decodes to the same bytes under every zlib build), a missing or +// wrong dictionary reports Z_NEED_DICT, Gunzip continues into a second +// gzip member and ignores trailing zero padding, other decoders stop at +// the stream end, and rejectGarbageAfterEnd turns trailing data, including +// a second gzip member, into ERR_TRAILING_JUNK_AFTER_STREAM_END. The dictionary blob is fixed (Node's +// deflate of the text with the dictionary "fox dog"). +const zlib = require("node:zlib"); + +const text = "the quick brown fox jumps over the lazy dog ".repeat(40); +const gz = zlib.gzipSync(text); +const deflated = zlib.deflateSync(text); +const withDictionary = Buffer.from( + "78bb0ab402a82bc94855282ccd4cce56482aca2fcf5348cbaf50c82acd2d2856c82f4b2d520049e724565582942a8caa1d553baa7654eda8da51b543452d004c808407", + "hex", +); + +function run(label, kind, options, data) { + return new Promise((resolve) => { + const stream = kind === "gunzip" ? zlib.createGunzip(options) : zlib.createInflate(options); + const events = []; + let length = 0; + stream.on("data", (chunk) => (length += chunk.length)); + stream.on("error", (error) => { + events.push(`error ${error.name} ${error.code} ${error.errno} ${JSON.stringify(error.message)}`); + events.push(`destroyed ${stream.destroyed}`); + }); + stream.on("end", () => events.push("end")); + stream.on("finish", () => events.push("finish")); + stream.on("close", () => { + console.log(label, length, events.join(" | ")); + resolve(undefined); + }); + stream.end(data); + }); +} + +async function main() { + await run("corrupt inflate", "inflate", undefined, Buffer.from("not zlib data at all")); + await run("corrupt gunzip", "gunzip", undefined, Buffer.from([0x1f, 0x8b, 9, 9, 9, 9, 9, 9, 9, 9])); + await run("truncated gunzip", "gunzip", undefined, gz.subarray(0, gz.length - 6)); + await run("truncated inflate", "inflate", undefined, zlib.deflateSync(text, { level: 0 }).subarray(0, 40)); + await run("two members", "gunzip", undefined, Buffer.concat([gz, zlib.gzipSync("second")])); + await run("zero padding", "gunzip", undefined, Buffer.concat([gz, Buffer.alloc(8)])); + await run("gunzip junk", "gunzip", undefined, Buffer.concat([gz, Buffer.from("junk")])); + await run("inflate junk", "inflate", undefined, Buffer.concat([deflated, Buffer.from("junk")])); + await run("reject junk", "inflate", { rejectGarbageAfterEnd: true }, Buffer.concat([deflated, Buffer.from("junk")])); + await run("reject gunzip", "gunzip", { rejectGarbageAfterEnd: true }, Buffer.concat([gz, zlib.gzipSync("x")])); + await run("missing dictionary", "inflate", undefined, withDictionary); + await run("bad dictionary", "inflate", { dictionary: Buffer.from("other") }, withDictionary); + await run("dictionary", "inflate", { dictionary: Buffer.from("fox dog") }, withDictionary); +} + +main(); diff --git a/tests/corpus/5373-zlib-stream-options.cjs b/tests/corpus/5373-zlib-stream-options.cjs new file mode 100644 index 0000000000..3505122e5f --- /dev/null +++ b/tests/corpus/5373-zlib-stream-options.cjs @@ -0,0 +1,61 @@ +// zlib stream options: Node's validation order and messages for the zlib +// options (windowBits, level, memLevel, strategy, dictionary), the stream +// base's (chunkSize, flush, finishFlush, maxOutputLength, +// rejectGarbageAfterEnd) and the Transform high-water marks, plus the +// options that change behavior: encoding and objectMode are ignored, +// highWaterMark sets both sides, DeflateRaw takes windowBits 8 as 9, and +// inflate accepts windowBits 0. +const zlib = require("node:zlib"); + +function attempt(label, make) { + try { + const stream = make(); + console.log(label, "ok", stream.readableHighWaterMark, stream.writableHighWaterMark); + } catch (error) { + console.log(label, error.name, error.code, JSON.stringify(error.message)); + } +} +attempt("chunkSize 10", () => zlib.createGzip({ chunkSize: 10 })); +attempt("chunkSize NaN", () => zlib.createGzip({ chunkSize: NaN })); +attempt("chunkSize string", () => zlib.createGzip({ chunkSize: "big" })); +attempt("level 10", () => zlib.createDeflate({ level: 10 })); +attempt("level string", () => zlib.createDeflate({ level: "x" })); +attempt("level Infinity", () => zlib.createDeflate({ level: Infinity })); +attempt("windowBits 8 gzip", () => zlib.createGzip({ windowBits: 8 })); +attempt("windowBits 16", () => zlib.createInflate({ windowBits: 16 })); +attempt("memLevel 0", () => zlib.createDeflate({ memLevel: 0 })); +attempt("strategy 5", () => zlib.createDeflate({ strategy: 5 })); +attempt("dictionary string", () => zlib.createDeflate({ dictionary: "abc" })); +attempt("flush 6", () => zlib.createGzip({ flush: 6 })); +attempt("finishFlush -1", () => zlib.createGzip({ finishFlush: -1 })); +attempt("maxOutputLength 0", () => zlib.createGunzip({ maxOutputLength: 0 })); +attempt("rejectGarbageAfterEnd 1", () => zlib.createGunzip({ rejectGarbageAfterEnd: 1 })); +attempt("highWaterMark -1", () => zlib.createGzip({ highWaterMark: -1 })); +attempt("writableHighWaterMark 1.5", () => zlib.createGzip({ writableHighWaterMark: 1.5 })); +attempt("level and chunkSize", () => zlib.createGzip({ level: 99, chunkSize: 1 })); +attempt("chunkSize and flush", () => zlib.createGzip({ chunkSize: 1, flush: 99 })); +attempt("null options", () => zlib.createGzip(null)); +attempt("windowBits 8 raw", () => zlib.createDeflateRaw({ windowBits: 8 })); +attempt("windowBits 0 inflate", () => zlib.createInflate({ windowBits: 0 })); + +const marks = zlib.createGzip({ highWaterMark: 100 }); +console.log("marks", marks.readableHighWaterMark, marks.writableHighWaterMark); +const sided = zlib.createGzip({ readableHighWaterMark: 10, writableHighWaterMark: 20 }); +console.log("sided", sided.readableHighWaterMark, sided.writableHighWaterMark); + +const input = "options keep the data intact ".repeat(30); +const packed = zlib.deflateRawSync(input); +const decoded = []; +const raw = zlib.createInflateRaw({ windowBits: 8, encoding: "utf8", objectMode: true }); +raw.on("data", (chunk) => decoded.push(Buffer.isBuffer(chunk))); +raw.on("end", () => { + console.log("encoding ignored", decoded.every(Boolean)); + let text = ""; + const pair = zlib.createDeflate({ level: 9, memLevel: 9, strategy: zlib.constants.Z_FILTERED, windowBits: 9 }); + const back = zlib.createInflate({ windowBits: 0 }); + pair.pipe(back); + back.on("data", (chunk) => (text += chunk.toString())); + back.on("end", () => console.log("tuned", text === input)); + pair.end(input); +}); +raw.end(packed); diff --git a/tests/corpus/5374-zlib-stream-flush-params.ts b/tests/corpus/5374-zlib-stream-flush-params.ts new file mode 100644 index 0000000000..89897557ed --- /dev/null +++ b/tests/corpus/5374-zlib-stream-flush-params.ts @@ -0,0 +1,73 @@ +// The zlib stream members: flush() emits everything written so far (a +// Z_SYNC_FLUSH prefix decodes on its own), flush(kind) range-checks its +// kind and flush() after end() waits for 'end', params() flushes and switches +// the level for later data, close(callback) destroys and reports through +// finished(), reset() restarts a decoder mid-stream, and bytesWritten +// counts input. +import * as zlib from "node:zlib"; + +const deflate = zlib.createDeflate(); +const parts: Buffer[] = []; +deflate.on("data", (chunk: Buffer) => parts.push(chunk)); + +function decodePrefix(label: string, next: () => void): void { + const inflate = zlib.createInflate({ finishFlush: zlib.constants.Z_SYNC_FLUSH }); + const text: string[] = []; + inflate.on("data", (chunk: Buffer) => text.push(chunk.toString())); + inflate.on("end", () => { + console.log(label, JSON.stringify(text.join(""))); + next(); + }); + inflate.end(Buffer.concat(parts)); +} + +deflate.write("hello "); +deflate.flush(() => { + console.log("flushed", deflate.bytesWritten); + decodePrefix("prefix", () => { + deflate.write("world"); + deflate.flush(zlib.constants.Z_SYNC_FLUSH, () => { + decodePrefix("prefix 2", () => { + try { + deflate.flush(9); + } catch (error) { + console.log("bad kind", (error as Error).message); + } + deflate.params(9, zlib.constants.Z_DEFAULT_STRATEGY, () => { + console.log("params done", deflate.bytesWritten); + deflate.end(" again".repeat(50)); + deflate.flush(() => { + const all = zlib.inflateSync(Buffer.concat(parts)).toString(); + console.log("after end", all === "hello world" + " again".repeat(50), deflate.writableFinished); + closing(); + }); + }); + }); + }); + }); +}); + +function closing(): void { + const gzip = zlib.createGzip(); + gzip.on("close", () => console.log("close event", gzip.destroyed)); + gzip.close(() => console.log("close callback")); + console.log("closed sync", gzip.destroyed); + resetting(); +} + +function resetting(): void { + // A stored (level 0) stream decodes the same under every zlib build, so + // half of it yields a known prefix before reset() starts over. + const first = zlib.deflateSync("first stream text ".repeat(4), { level: 0 }); + const inflate = zlib.createInflate(); + const out: string[] = []; + inflate.on("data", (chunk: Buffer) => out.push(chunk.toString())); + const second = zlib.deflateSync("after reset"); + inflate.on("end", () => + console.log("reset", JSON.stringify(out.join("")), inflate.bytesWritten === 40 + second.length), + ); + inflate.write(first.subarray(0, 40), () => { + inflate.reset(); + inflate.end(second); + }); +} diff --git a/tests/corpus/5375-zlib-stream-promises.ts b/tests/corpus/5375-zlib-stream-promises.ts new file mode 100644 index 0000000000..1b3602b962 --- /dev/null +++ b/tests/corpus/5375-zlib-stream-promises.ts @@ -0,0 +1,66 @@ +// zlib streams with stream/promises and async iteration: a promise +// pipeline from Readable.from() through Gzip and Gunzip into a Writable, +// a rejected pipeline when the decoder sees corrupt input, and a +// for-await loop over a decoder fed in pieces. +import { createGunzip, createGzip, createInflate, deflateSync, gzipSync } from "node:zlib"; +import { pipeline } from "node:stream/promises"; +import { Readable, Writable } from "node:stream"; + +const words = ["alpha ", "beta ", "gamma ", "delta "]; + +async function main(): Promise { + const parts: Buffer[] = []; + const sink = new Writable({ + write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void) { + parts.push(chunk); + callback(); + }, + }); + await pipeline(Readable.from(words.map((w) => w.repeat(200))), createGzip(), createGunzip(), sink); + const text = Buffer.concat(parts).toString(); + console.log("pipeline", text.length, text.startsWith("alpha alpha"), text.endsWith("delta ")); + + try { + await pipeline( + Readable.from([Buffer.from("this is not gzip")]), + createGunzip(), + new Writable({ + write(_chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void) { + callback(); + }, + }), + ); + console.log("unexpected success"); + } catch (error) { + const e = error as Error & { code?: string }; + console.log("rejected", e.code, e.message); + } + + const packed = gzipSync(words.join("").repeat(500)); + const decoder = createGunzip({ chunkSize: 4096 }); + let offset = 0; + const feed = (): void => { + if (offset >= packed.length) { + decoder.end(); + return; + } + decoder.write(packed.subarray(offset, offset + 1000), feed); + offset += 1000; + }; + feed(); + let total = 0; + let largest = 0; + for await (const chunk of decoder) { + total += (chunk as Buffer).length; + largest = Math.max(largest, (chunk as Buffer).length); + } + console.log("for await", total, largest <= 4096); + + const inflate = createInflate(); + inflate.end(deflateSync("tail")); + const chunks: string[] = []; + for await (const chunk of inflate) chunks.push((chunk as Buffer).toString()); + console.log("inflate", chunks.join("")); +} + +main(); diff --git a/tests/corpus/5376-zlib-constants.cjs b/tests/corpus/5376-zlib-constants.cjs new file mode 100644 index 0000000000..d748dbd432 --- /dev/null +++ b/tests/corpus/5376-zlib-constants.cjs @@ -0,0 +1,19 @@ +// zlib.constants and the module's own copies of its non-Brotli members: +// flush levels, return codes, option bounds, the stream mode numbers, and +// Brotli/Zstd parameter values, read through the namespace, a destructured +// binding, and the module properties. +const zlib = require("node:zlib"); +const { constants } = require("node:zlib"); + +console.log(zlib.constants.Z_NO_FLUSH, zlib.constants.Z_PARTIAL_FLUSH, zlib.constants.Z_SYNC_FLUSH); +console.log(zlib.constants.Z_FULL_FLUSH, zlib.constants.Z_FINISH, zlib.constants.Z_BLOCK); +console.log(constants.Z_OK, constants.Z_STREAM_END, constants.Z_NEED_DICT, constants.Z_ERRNO); +console.log(constants.Z_STREAM_ERROR, constants.Z_DATA_ERROR, constants.Z_MEM_ERROR, constants.Z_BUF_ERROR); +console.log(constants.Z_VERSION_ERROR, constants.Z_DEFAULT_COMPRESSION, constants.Z_BEST_SPEED); +console.log(constants.Z_MIN_WINDOWBITS, constants.Z_MAX_WINDOWBITS, constants.Z_DEFAULT_CHUNK, constants.Z_MIN_CHUNK); +console.log(constants.DEFLATE, constants.INFLATE, constants.GZIP, constants.GUNZIP, constants.UNZIP); +console.log(constants.BROTLI_OPERATION_FINISH, constants.BROTLI_PARAM_QUALITY, constants.BROTLI_MAX_QUALITY); +console.log(constants.ZSTD_e_end, constants.ZSTD_c_compressionLevel); +console.log(zlib.Z_SYNC_FLUSH, zlib.Z_DEFAULT_STRATEGY, zlib.Z_FIXED, zlib.DEFLATERAW, zlib.ZSTD_e_continue); +console.log(zlib.Z_SYNC_FLUSH === zlib.constants.Z_SYNC_FLUSH, zlib.constants.ZLIB_VERNUM === zlib.ZLIB_VERNUM); +console.log(zlib.constants.ZLIB_VERNUM); diff --git a/tests/corpus/5377-zlib-stream-backpressure.cjs b/tests/corpus/5377-zlib-stream-backpressure.cjs new file mode 100644 index 0000000000..90840155d2 --- /dev/null +++ b/tests/corpus/5377-zlib-stream-backpressure.cjs @@ -0,0 +1,64 @@ +// zlib stream backpressure: a decoder whose output far exceeds its +// readable high-water mark pauses between output buffers until the +// consumer reads ('readable' and read() drive it), write() answers false +// while a large input waits, and a slow piped destination keeps the +// decoder's buffer near its mark. +const zlib = require("node:zlib"); +const { Writable } = require("node:stream"); + +const input = Buffer.alloc(300000); +let seed = 12345; +for (let i = 0; i < input.length; i++) { + seed = (seed * 1103515245 + 12345) % 2147483648; + input[i] = seed >> 23; +} +const packed = zlib.deflateSync(input); + +const inflate = zlib.createInflate({ + readableHighWaterMark: 20000, + writableHighWaterMark: 4096, + chunkSize: 8192, +}); +const reads = []; +let total = 0; +let largest = 0; +inflate.on("readable", () => { + let chunk; + const before = inflate.readableLength; + while ((chunk = inflate.read()) !== null) total += chunk.length; + reads.push(before); + if (before > largest) largest = before; +}); +inflate.on("end", () => { + console.log("total", total === input.length); + console.log("readable events", reads.length, "max buffered", largest); + console.log("bounded", reads.every((n) => n <= 20000 + 8192)); + slowPipe(); +}); +const ok = inflate.write(packed, () => console.log("write callback")); +console.log("write returned", ok, inflate.writableNeedDrain); +inflate.on("drain", () => console.log("drain", inflate.writableLength)); +inflate.end(); + +// A slow destination: each write completes on a later turn, so the decoder +// stops between output buffers until the pipe reads again. +function slowPipe() { + const decoder = zlib.createInflate({ chunkSize: 4096, readableHighWaterMark: 4096 }); + let writes = 0; + let received = 0; + let largestBuffer = 0; + const sink = new Writable({ + highWaterMark: 4096, + write(chunk, encoding, callback) { + writes++; + received += chunk.length; + if (decoder.readableLength > largestBuffer) largestBuffer = decoder.readableLength; + setImmediate(callback); + }, + }); + sink.on("finish", () => { + console.log("slow pipe", writes, received === input.length, largestBuffer <= 4096 + 4096); + }); + decoder.pipe(sink); + decoder.end(packed); +} diff --git a/tests/corpus/5621-util-format-float.ts b/tests/corpus/5621-util-format-float.ts new file mode 100644 index 0000000000..b91a9fc597 --- /dev/null +++ b/tests/corpus/5621-util-format-float.ts @@ -0,0 +1,28 @@ +// util.format and console.log %f: parseFloat(String(arg)) formatted, for +// numbers (-0 prints 0), strings, booleans, unit values and checked-dynamic values. +// Boolean and unit arguments still evaluate once, for their effects. +import { format } from "node:util"; +const n: number = Number(process.argv.length > 99 ? "1" : "-0"); +const s: string = process.argv[2] ?? "3.5kg"; +const flag = process.argv.length > 99; +const u: unknown = JSON.parse('{"a":1}'); +const v: unknown = JSON.parse('"2.5e1x"'); +console.log(format("%f|%f|%f|%f|%f|%f", 1.5, n, -0, s, "abc", flag)); +console.log(format("%f %f %f %f", Infinity, NaN, undefined, null)); +console.log(format("%f %f", u, v)); +console.log(format("%s:%d:%i:%f:%j:%O %%", "a", 1.5, 2.7, "3.5", { a: 1 }, [1])); +console.log(format("%f", 1, "extra"), format("%f")); +console.log("%f and %f", "1.25e2px", 0.1 + 0.2); +let calls = 0; +const hit = (): boolean => ++calls > 0; +const touch = (): undefined => { + calls += 10; + return undefined; +}; +function log(): void { + calls += 100; +} +let un: undefined; +const nu: null | undefined = process.argv.length > 99 ? null : undefined; +console.log(format("%f|%f|%f|%f|%f", hit(), touch(), log(), un, nu), calls); +console.log(format("%f %f", [1, 2].some((x) => x > 1), [1].includes(1) && hit()), calls); diff --git a/tests/corpus/5740-atomics-operation-semantics.ts b/tests/corpus/5740-atomics-operation-semantics.ts new file mode 100644 index 0000000000..35b110dd6e --- /dev/null +++ b/tests/corpus/5740-atomics-operation-semantics.ts @@ -0,0 +1,80 @@ +// Atomics operations on every integer typed array kind, shared and not: +// results, wraparound at the element width, sign extension, store's integer +// result, index conversion, errors, and overlapping views of other widths. +function run( + name: string, + a: T, +): void { + const out: number[] = []; + out.push(Atomics.store(a, 0, 5)); + out.push(Atomics.add(a, 0, 3), Atomics.load(a, 0)); + out.push(Atomics.sub(a, 0, 10), Atomics.load(a, 0)); + out.push(Atomics.and(a, 0, 0x0f), Atomics.or(a, 0, 0x70), Atomics.xor(a, 0, 0xff)); + out.push(Atomics.exchange(a, 0, 300), Atomics.load(a, 0)); + out.push(Atomics.compareExchange(a, 0, 1, 7), Atomics.load(a, 0)); + out.push(Atomics.compareExchange(a, 0, Atomics.load(a, 0), 9), Atomics.load(a, 0)); + // Expected values compare at the element width. + out.push(Atomics.compareExchange(a, 1, 256 + 0, 4), Atomics.load(a, 1)); + out.push(Atomics.store(a, 2, -1), Atomics.load(a, 2), Atomics.add(a, 2, 1), Atomics.load(a, 2)); + out.push(Atomics.store(a, 3, 2 ** 32 + 6), Atomics.load(a, 3), Atomics.sub(a, 3, 2 ** 31), Atomics.load(a, 3)); + console.log(name, out.join(" ")); +} + +const shared = (bytes: number): SharedArrayBuffer => new SharedArrayBuffer(bytes); +run("Int8Array", new Int8Array(4)); +run("Int8Array/shared", new Int8Array(shared(4))); +run("Uint8Array", new Uint8Array(4)); +run("Uint8Array/shared", new Uint8Array(shared(4))); +run("Int16Array", new Int16Array(4)); +run("Int16Array/shared", new Int16Array(shared(8))); +run("Uint16Array", new Uint16Array(4)); +run("Uint16Array/shared", new Uint16Array(shared(8))); +run("Int32Array", new Int32Array(4)); +run("Int32Array/shared", new Int32Array(shared(16))); +run("Uint32Array", new Uint32Array(4)); +run("Uint32Array/shared", new Uint32Array(shared(16))); + +// store answers ToIntegerOrInfinity of its value. +const i32 = new Int32Array(new SharedArrayBuffer(16)); +console.log( + [NaN, -0, 0.5, -0.5, 1.9, -1.9, Infinity, -Infinity, 2 ** 40, -(2 ** 31) - 1].map((v) => { + const r = Atomics.store(i32, 0, v); + return `${Object.is(r, -0) ? "-0" : r}:${Atomics.load(i32, 0)}`; + }).join(" "), +); + +// A fractional index converts first; store's result is still an integer. +console.log(Atomics.store(i32, 2.5, -0.5), Atomics.store(i32, 2.5, -7.25), Atomics.load(i32, 2)); + +// Index conversion: fractions truncate, NaN is 0; out of range throws. +Atomics.store(i32, 1, 11); +Atomics.store(i32, 0, 22); +console.log(Atomics.load(i32, 1.7), Atomics.load(i32, NaN), Atomics.add(i32, 3.999, 1), Atomics.load(i32, 3)); +for (const index of [-1, 4, 4.5, Infinity, -Infinity, 2 ** 53]) { + try { + console.log("no error", index, Atomics.add(i32, index, 1)); + } catch (error) { + console.log(index, (error as Error).name, (error as Error).message); + } +} + +// Overlapping views of different widths address the same bytes. +const buffer = new SharedArrayBuffer(8); +const words = new Uint32Array(buffer); +const halves = new Uint16Array(buffer); +const bytes = new Uint8Array(buffer); +const signedBytes = new Int8Array(buffer, 4, 4); +Atomics.store(words, 0, 0x11223344); +console.log(Atomics.load(halves, 0), Atomics.load(halves, 1), Atomics.load(bytes, 0), Atomics.load(bytes, 3)); +Atomics.add(bytes, 0, 0xcc); +Atomics.or(halves, 1, 0x8000); +console.log(Atomics.load(words, 0).toString(16), words[0]!.toString(16)); +Atomics.store(words, 1, 0xff80ff7f); +console.log(Atomics.load(signedBytes, 0), Atomics.load(signedBytes, 1), Atomics.exchange(signedBytes, 3, -2), Atomics.load(words, 1).toString(16)); + +// Ordinary element reads and writes agree with Atomics on shared storage. +const plain = new Int16Array(new SharedArrayBuffer(8)); +plain[0] = 40000; +plain[1] = -3; +Atomics.add(plain, 1, 1); +console.log(plain[0], plain[1], Atomics.load(plain, 0), plain.join(",")); diff --git a/tests/corpus/5741-atomics-contended-counters.ts b/tests/corpus/5741-atomics-contended-counters.ts new file mode 100644 index 0000000000..2ba951c61b --- /dev/null +++ b/tests/corpus/5741-atomics-contended-counters.ts @@ -0,0 +1,58 @@ +// Workers race on shared counters with every Atomics read-modify-write: +// one counter for all workers, one counter each, bit sets and toggles, +// compare-exchange loops and narrow counters that wrap. Each final value +// has one correct answer. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +const WORKERS = 4; +const ROUNDS = 20000; + +type Job = { buffer: SharedArrayBuffer; id: number }; + +if (isMainThread) { + const buffer = new SharedArrayBuffer(256); + const words = new Int32Array(buffer); + Atomics.store(words, 12, -1); + let exited = 0; + for (let id = 0; id < WORKERS; id++) { + const worker = new Worker(new URL(import.meta.url), { workerData: { buffer, id } }); + worker.on("exit", (code: number) => { + if (code !== 0) console.log("worker exit", code); + exited++; + if (exited < WORKERS) return; + const bytes = new Uint8Array(buffer); + const halves = new Int16Array(buffer); + console.log("shared add", words[0], "sub", words[1], "cas", words[2]); + console.log("own", [words[4], words[5], words[6], words[7]].join(",")); + console.log("bits", Atomics.load(words, 8).toString(2), "xor", Atomics.load(words, 9)); + console.log("u8 wrap", Atomics.load(bytes, 40), "i16 wrap", Atomics.load(halves, 21)); + console.log("max", Atomics.load(words, 11), "and", (Atomics.load(words, 12) >>> 0).toString(16)); + }); + } +} else { + const { buffer, id } = workerData as Job; + const words = new Int32Array(buffer); + const bytes = new Uint8Array(buffer); + const halves = new Int16Array(buffer); + for (let i = 0; i < ROUNDS; i++) { + Atomics.add(words, 0, 1); + Atomics.sub(words, 1, 2); + for (;;) { + const seen = Atomics.load(words, 2); + if (Atomics.compareExchange(words, 2, seen, seen + 3) === seen) break; + } + Atomics.add(words, 4 + id, id + 1); + Atomics.xor(words, 9, 1 << id); + Atomics.add(bytes, 40, 1); + Atomics.sub(halves, 21, 7); + // A compare-exchange maximum. + const candidate = id * ROUNDS + i; + for (;;) { + const current = Atomics.load(words, 11); + if (current >= candidate || Atomics.compareExchange(words, 11, current, candidate) === current) break; + } + } + Atomics.or(words, 8, 1 << (id * 3)); + Atomics.and(words, 12, ~(0xf << (id * 4))); + parentPort!.postMessage("done"); +} diff --git a/tests/corpus/5742-atomics-lock-ordering.ts b/tests/corpus/5742-atomics-lock-ordering.ts new file mode 100644 index 0000000000..9ac16f1293 --- /dev/null +++ b/tests/corpus/5742-atomics-lock-ordering.ts @@ -0,0 +1,62 @@ +// A lock built from Atomics.compareExchange, Atomics.wait and Atomics.notify +// guards ordinary reads and writes of shared Int32 and Float64 elements. +// The sequentially consistent operations order the plain accesses, so +// every increment under the lock survives. +import { Worker, isMainThread, workerData } from "node:worker_threads"; + +const WORKERS = 4; +const ROUNDS = 5000; + +// 0 unlocked, 1 locked, 2 locked with waiters. +function lock(state: Int32Array): void { + let c = Atomics.compareExchange(state, 0, 0, 1); + if (c === 0) return; + if (c !== 2) c = Atomics.exchange(state, 0, 2); + while (c !== 0) { + Atomics.wait(state, 0, 2); + c = Atomics.exchange(state, 0, 2); + } +} + +function unlock(state: Int32Array): void { + if (Atomics.sub(state, 0, 1) !== 1) { + Atomics.store(state, 0, 0); + Atomics.notify(state, 0, 1); + } +} + +type Job = { control: SharedArrayBuffer; data: SharedArrayBuffer; id: number }; + +if (isMainThread) { + const control = new SharedArrayBuffer(16); + const data = new SharedArrayBuffer(64); + let exited = 0; + for (let id = 0; id < WORKERS; id++) { + const worker = new Worker(new URL(import.meta.url), { workerData: { control, data, id } }); + worker.on("exit", () => { + exited++; + if (exited < WORKERS) return; + const ints = new Int32Array(data); + const floats = new Float64Array(data, 32, 4); + console.log("count", ints[0], "pairs", ints[1] === ints[2], ints[1], ints[2]); + console.log("sum", floats[0], "last writer seen", ints[3] >= 0 && ints[3] < WORKERS); + console.log("lock", Atomics.load(new Int32Array(control), 0)); + }); + } +} else { + const { control, data, id } = workerData as Job; + const state = new Int32Array(control); + const ints = new Int32Array(data); + const floats = new Float64Array(data, 32, 4); + for (let i = 0; i < ROUNDS; i++) { + lock(state); + // Plain read-modify-write sequences: correct only under the lock. + ints[0] = ints[0]! + 1; + const pair = ints[1]!; + ints[1] = pair + 1; + ints[2] = ints[2]! + 1; + floats[0] = floats[0]! + 0.5; + ints[3] = id; + unlock(state); + } +} diff --git a/tests/corpus/5743-atomics-wait-notify.ts b/tests/corpus/5743-atomics-wait-notify.ts new file mode 100644 index 0000000000..d988f8ca19 --- /dev/null +++ b/tests/corpus/5743-atomics-wait-notify.ts @@ -0,0 +1,66 @@ +// Atomics.wait and Atomics.notify results: not-equal, timed-out and ok; +// notify counts with and without waiters, on other indexes and views of the +// same storage, on unshared arrays; waiters on many indexes released one +// index at a time. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +const INDEXES = 48; + +if (isMainThread) { + const buffer = new SharedArrayBuffer(4 * (INDEXES + 8)); + const cells = new Int32Array(buffer); + console.log(Atomics.wait(cells, 0, 1), Atomics.wait(cells, 0, 0, 0), Atomics.wait(cells, 0, 0, 1)); + console.log(Atomics.notify(cells, 0), Atomics.notify(cells, 0, 0), Atomics.notify(new Int32Array(4), 0, 1)); + try { + Atomics.wait(new Int32Array(4), 0, 0, 0); + } catch (error) { + console.log((error as Error).name, (error as Error).message); + } + try { + Atomics.notify(cells, INDEXES + 8); + } catch (error) { + console.log((error as Error).name); + } + + // One worker waits on each of many indexes; release them in order. + const control = new Int32Array(new SharedArrayBuffer(16)); + const worker = new Worker(new URL(import.meta.url), { workerData: { buffer, control: control.buffer } }); + const order: number[] = []; + worker.on("message", (message: unknown) => { + if (typeof message === "number") order.push(message); + else console.log("worker", JSON.stringify(message)); + }); + worker.on("exit", (code: number) => { + console.log("released", order.join(","), "exit", code); + }); + // Wait until the worker is parked on index 0, then release each index. + while (Atomics.load(control, 0) !== 1) Atomics.wait(control, 0, 0, 5); + const view = new Int32Array(buffer, 4 * 8); + // The worker can only be waiting on index 0: notifying any other index, + // through either view, wakes nothing. + let others = 0; + for (let index = 1; index < INDEXES; index++) others += Atomics.notify(view, index) + Atomics.notify(cells, 8 + index); + console.log("other indexes woke", others); + for (let index = 0; index < INDEXES; index += 1) { + for (;;) { + Atomics.store(view, index, 1); + if (Atomics.notify(cells, 8 + index, 1) === 1 || Atomics.load(control, 1) > index) break; + Atomics.wait(control, 0, 1, 1); + } + } +} else { + const { buffer, control } = workerData as { buffer: SharedArrayBuffer; control: SharedArrayBuffer }; + const cells = new Int32Array(buffer); + const flags = new Int32Array(control); + const results: string[] = []; + Atomics.store(flags, 0, 1); + Atomics.notify(flags, 0); + for (let index = 0; index < INDEXES; index++) { + let result = Atomics.wait(cells, 8 + index, 0); + while (result === "timed-out") result = Atomics.wait(cells, 8 + index, 0); + results.push(result === "not-equal" ? "ok" : result); + Atomics.store(flags, 1, index + 1); + parentPort!.postMessage(index); + } + parentPort!.postMessage({ results: [...new Set(results)], last: Atomics.load(cells, 8 + INDEXES - 1) }); +} diff --git a/tests/corpus/5744-atomics-ring-buffer.ts b/tests/corpus/5744-atomics-ring-buffer.ts new file mode 100644 index 0000000000..bfce00ae18 --- /dev/null +++ b/tests/corpus/5744-atomics-ring-buffer.ts @@ -0,0 +1,65 @@ +// A single-producer, single-consumer ring buffer: the producer writes each +// slot with ordinary stores to Uint8, Int16 and Float64 views and publishes +// it with Atomics.store; the consumer sees the slot after Atomics.load and +// sleeps in Atomics.wait while the ring is empty or full. +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +const SLOTS = 8; +const ITEMS = 3000; + +type Ring = { buffer: SharedArrayBuffer }; + +function views(buffer: SharedArrayBuffer) { + return { + head: new Int32Array(buffer, 0, 2), // [written, read] + tags: new Uint8Array(buffer, 8, SLOTS), + small: new Int16Array(buffer, 16, SLOTS), + values: new Float64Array(buffer, 32, SLOTS), + }; +} + +if (isMainThread) { + const buffer = new SharedArrayBuffer(32 + 8 * SLOTS); + const ring = views(buffer); + const worker = new Worker(new URL(import.meta.url), { workerData: { buffer } }); + worker.on("message", (message: unknown) => console.log("consumer", JSON.stringify(message))); + worker.on("exit", (code: number) => console.log("exit", code, Atomics.load(ring.head, 0), Atomics.load(ring.head, 1))); + for (let item = 0; item < ITEMS; item++) { + for (;;) { + const read = Atomics.load(ring.head, 1); + if (item - read < SLOTS) break; + Atomics.wait(ring.head, 1, read, 50); + } + const slot = item % SLOTS; + ring.tags[slot] = item & 0xff; + ring.small[slot] = -item; + ring.values[slot] = item / 4; + Atomics.store(ring.head, 0, item + 1); + Atomics.notify(ring.head, 0); + } +} else { + const { buffer } = workerData as Ring; + const ring = views(buffer); + let tagSum = 0; + let smallSum = 0; + let valueSum = 0; + let mismatches = 0; + for (let item = 0; item < ITEMS; item++) { + for (;;) { + const written = Atomics.load(ring.head, 0); + if (written > item) break; + Atomics.wait(ring.head, 0, written, 50); + } + const slot = item % SLOTS; + const tag = ring.tags[slot]!; + const small = ring.small[slot]!; + const value = ring.values[slot]!; + if (tag !== (item & 0xff) || small !== ((-item << 16) >> 16) || value !== item / 4) mismatches++; + tagSum += tag; + smallSum += small; + valueSum += value; + Atomics.store(ring.head, 1, item + 1); + Atomics.notify(ring.head, 1); + } + parentPort!.postMessage({ tagSum, smallSum, valueSum, mismatches }); +} diff --git a/tests/corpus/5745-atomics-semaphore-stress.ts b/tests/corpus/5745-atomics-semaphore-stress.ts new file mode 100644 index 0000000000..2e6ab9cd9c --- /dev/null +++ b/tests/corpus/5745-atomics-semaphore-stress.ts @@ -0,0 +1,69 @@ +// Counting semaphores on many indexes of one Int32Array, shared by eight +// workers that acquire with Atomics.wait and release with Atomics.notify, +// while other workers hammer neighboring indexes with Atomics.add. +import { Worker, isMainThread, workerData } from "node:worker_threads"; + +const WAITERS = 6; +const ADDERS = 2; +const SEMAPHORES = 16; +const ROUNDS = 400; + +function acquire(cells: Int32Array, index: number): void { + for (;;) { + const available = Atomics.load(cells, index); + if (available > 0) { + if (Atomics.compareExchange(cells, index, available, available - 1) === available) return; + } else Atomics.wait(cells, index, available, 20); + } +} + +function release(cells: Int32Array, index: number): void { + Atomics.add(cells, index, 1); + Atomics.notify(cells, index, 1); +} + +type Job = { buffer: SharedArrayBuffer; id: number; kind: string }; + +if (isMainThread) { + const buffer = new SharedArrayBuffer(4 * (SEMAPHORES * 4 + 4)); + const cells = new Int32Array(buffer); + // Each semaphore starts with two permits; its usage counter sits next to it. + for (let s = 0; s < SEMAPHORES; s++) Atomics.store(cells, s * 4, 2); + let exited = 0; + const total = WAITERS + ADDERS; + for (let id = 0; id < total; id++) { + const kind = id < WAITERS ? "waiter" : "adder"; + const worker = new Worker(new URL(import.meta.url), { workerData: { buffer, id, kind } }); + worker.on("exit", () => { + exited++; + if (exited < total) return; + const permits: number[] = []; + let uses = 0; + let peak = 0; + let noise = 0; + for (let s = 0; s < SEMAPHORES; s++) { + permits.push(Atomics.load(cells, s * 4)); + uses += Atomics.load(cells, s * 4 + 1); + peak = Math.max(peak, Atomics.load(cells, s * 4 + 2)); + noise += Atomics.load(cells, s * 4 + 3); + } + console.log("permits", permits.join(","), "uses", uses, "within limit", peak <= 2, "noise", noise); + }); + } +} else { + const { buffer, id, kind } = workerData as Job; + const cells = new Int32Array(buffer); + for (let round = 0; round < ROUNDS; round++) { + const s = (round * 7 + id * 3) % SEMAPHORES; + if (kind === "adder") { + for (let k = 0; k < 20; k++) Atomics.add(cells, s * 4 + 3, 1); + continue; + } + acquire(cells, s * 4); + const inside = Atomics.add(cells, s * 4 + 2, 1) + 1; + if (inside > 2) Atomics.store(cells, s * 4 + 2, 99); + Atomics.add(cells, s * 4 + 1, 1); + Atomics.sub(cells, s * 4 + 2, 1); + release(cells, s * 4); + } +} diff --git a/tests/corpus/5746-shared-bulk-operations.ts b/tests/corpus/5746-shared-bulk-operations.ts new file mode 100644 index 0000000000..051e016f2b --- /dev/null +++ b/tests/corpus/5746-shared-bulk-operations.ts @@ -0,0 +1,45 @@ +// Bulk operations on SharedArrayBuffer-backed views: fill, copyWithin, +// slice, set across element kinds, sort with and without a comparator, +// toReversed, DataView and Buffer number reads and writes, string +// conversions, searches and comparisons, and Buffer.write. +const shared = new SharedArrayBuffer(64); +const bytes = new Uint8Array(shared); +const ints = new Int32Array(shared, 16, 8); +const floats = new Float64Array(shared, 48, 2); + +bytes.fill(7, 0, 8); +bytes.copyWithin(8, 0, 4); +console.log(bytes.slice(0, 16).join(","), bytes.subarray(4, 10).join(",")); + +ints.set([5, -3, 9, 1, 0, -8, 4, 2]); +ints.sort(); +console.log(ints.join(","), ints.toReversed().join(",")); +ints.sort((a, b) => b - a); +console.log(ints.join(","), Array.from(ints.slice(2, 5)).join(",")); + +const halves = new Int16Array(shared, 0, 8); +halves.set(ints.subarray(0, 4)); +console.log(halves.join(","), Atomics.load(halves, 1)); + +floats.set([1.5, -2.25]); +const view = new DataView(shared); +view.setUint16(2, 0xbeef, true); +view.setFloat32(4, 3.5); +console.log(view.getUint16(2, true).toString(16), view.getFloat32(4), view.getFloat64(48, true), floats[1]); + +const buf = Buffer.from(shared, 0, 16); +buf.write("héllo", 0, "utf8"); +console.log(buf.toString("utf8", 0, 6), buf.toString("hex", 0, 8), buf.toString("base64", 0, 6)); +buf.writeInt32BE(-123456, 8); +buf.writeUInt16LE(0x1234, 12); +console.log(buf.readInt32BE(8), buf.readUInt16LE(12).toString(16), buf.readUIntBE(8, 3), buf.readBigUInt64LE(8)); +console.log(buf.indexOf("llo"), buf.indexOf(0x34), buf.includes("hé"), buf.lastIndexOf(0x12)); + +const other = Buffer.alloc(16); +buf.copy(other, 0, 0, 16); +console.log(other.equals(buf), Buffer.compare(other, buf), other.compare(buf, 0, 8, 0, 8)); +other[0] = 0; +console.log(other.equals(buf), Buffer.compare(other, buf), buf.compare(other)); +buf.fill("ab", 0, 5); +console.log(buf.toString("latin1", 0, 6), buf.swap16().toString("hex", 0, 6)); +console.log(new Uint8Array(shared, 0, 8).join(" ")); diff --git a/tests/fixtures/node-test/cases/ctx-hooks.ts b/tests/fixtures/node-test/cases/ctx-hooks.ts new file mode 100644 index 0000000000..9136ccf960 --- /dev/null +++ b/tests/fixtures/node-test/cases/ctx-hooks.ts @@ -0,0 +1,107 @@ +// @exit: 1 +// TestContext hooks: t.before runs when it is called; t.beforeEach and +// t.afterEach wrap every descendant subtest, inside the enclosing suite's +// afterEach; t.after runs after the test's own afterEach chain, also when +// the body throws. A throwing t.before fails the test and the subtests that +// follow (their afterEach hooks still run); a throwing t.after fails the +// test. The last test checks the order the hooks ran in. +import assert from "node:assert"; +import { afterEach, beforeEach, describe, it } from "node:test"; + +const log: string[] = []; + +describe("outer", () => { + afterEach(() => { + log.push("outer afterEach"); + }); + it("with hooks", async (t) => { + t.after(() => { + log.push("t.after"); + }); + t.before(() => { + log.push("t.before"); + }); + t.beforeEach(() => { + log.push("t.beforeEach"); + }); + t.afterEach(() => { + log.push("t.afterEach"); + }); + log.push("body"); + await t.test("a", () => { + log.push("a"); + }); + await t.test("b", async (st) => { + log.push("b"); + await st.test("b1", () => { + log.push("b1"); + }); + }); + log.push("body end"); + }); + it("no subtests", (t) => { + t.before(() => { + log.push("before without subtests"); + }); + t.after(() => { + log.push("after without subtests"); + }); + }); + it("after on failure", (t) => { + t.after(() => { + log.push("after on failure"); + }); + throw new Error("fails"); + }); + it("before throws", async (t) => { + t.beforeEach(() => { + log.push("never: beforeEach"); + }); + t.before(() => { + throw new Error("before boom"); + }); + await t.test("sub after a throwing before", () => { + log.push("never: sub body"); + }); + log.push("after the failed sub"); + }); + it("after throws", (t) => { + t.after(() => { + throw new Error("after boom"); + }); + }); +}); + +describe("order", () => { + beforeEach(() => {}); + it("matches Node", () => { + assert.deepStrictEqual(log, [ + "t.before", + "body", + "t.beforeEach", + "a", + "t.afterEach", + "outer afterEach", + "t.beforeEach", + "b", + "t.beforeEach", + "b1", + "t.afterEach", + "outer afterEach", + "t.afterEach", + "outer afterEach", + "body end", + "outer afterEach", + "t.after", + "before without subtests", + "outer afterEach", + "after without subtests", + "outer afterEach", + "after on failure", + "outer afterEach", + "after the failed sub", + "outer afterEach", + "outer afterEach", + ]); + }); +}); diff --git a/tests/fixtures/node-test/cases/ctx-plan.ts b/tests/fixtures/node-test/cases/ctx-plan.ts new file mode 100644 index 0000000000..4d856ccae8 --- /dev/null +++ b/tests/fixtures/node-test/cases/ctx-plan.ts @@ -0,0 +1,45 @@ +// @exit: 1 +// t.plan counts the test's t.assert calls and subtests; a mismatch fails +// the test with Node's string message, which the failing-tests section +// prints inspected. A failing assertion reports its own error. t.fullName +// joins the names from the outermost suite or test with " > ". +import { describe, it, test } from "node:test"; + +test("plan met", (t) => { + t.plan(2); + t.assert.ok(true); + t.assert.strictEqual(1, 1); +}); +test("plan short", (t) => { + t.plan(3); + t.assert.ok(true); +}); +test("plan over", (t) => { + t.plan(1); + t.assert.ok(true); + t.assert.ok(true); +}); +test("plan counts subtests", async (t) => { + t.plan(2); + await t.test("a", () => {}); + t.assert.ok(true); +}); +test("plan with a failing assertion", (t) => { + t.plan(2); + t.assert.ok(true); + t.assert.strictEqual(1, 2); +}); + +describe("outer", () => { + describe("mid", () => { + it("leaf", (t) => { + t.assert.strictEqual(t.fullName, "outer > mid > leaf"); + }); + }); +}); +test("top", async (t) => { + t.assert.strictEqual(t.fullName, "top"); + await t.test("child", (c) => { + c.assert.strictEqual(c.fullName, "top > child"); + }); +}); diff --git a/tests/fixtures/node-test/cases/only-nested.ts b/tests/fixtures/node-test/cases/only-nested.ts new file mode 100644 index 0000000000..0e6b2f66de --- /dev/null +++ b/tests/fixtures/node-test/cases/only-nested.ts @@ -0,0 +1,28 @@ +// The only twins of describe, suite and it inside a suite: the marked +// tests and suites run, and their unmarked siblings, and the unmarked +// top-level suites, do not. +import { describe, it, suite } from "node:test"; + +describe("not marked", () => { + it("dropped with its suite", () => { + throw new Error("an unmarked top-level suite does not run"); + }); +}); + +describe("only among siblings", () => { + it.only("it.only runs", () => {}); + it("dropped", () => { + throw new Error("an unmarked sibling of it.only does not run"); + }); + describe.only("describe.only", () => { + it("inside", () => {}); + }); + suite.only("suite.only", () => { + it("inside", () => {}); + }); + describe("unmarked suite", () => { + it("dropped", () => { + throw new Error("an unmarked suite next to describe.only does not run"); + }); + }); +}); diff --git a/tests/fixtures/node-test/cases/suite-variants.ts b/tests/fixtures/node-test/cases/suite-variants.ts new file mode 100644 index 0000000000..4e32ce3fa9 --- /dev/null +++ b/tests/fixtures/node-test/cases/suite-variants.ts @@ -0,0 +1,20 @@ +// The skip and todo twins of describe, suite and it: a skipped suite does +// not run its body, and a todo suite's tests are todo. +import { describe, it, suite } from "node:test"; + +describe("variants", () => { + it("runs", () => {}); + it.skip("it.skip", () => { + throw new Error("a skipped test does not run"); + }); + it.todo("it.todo", () => {}); + describe.skip("describe.skip", () => { + it("inside a skipped suite", () => {}); + }); + suite.skip("suite.skip", () => { + it("inside a skipped suite", () => {}); + }); + suite.todo("suite.todo", () => { + it("inherits todo", () => {}); + }); +}); diff --git a/tests/fixtures/node-test/cases/thrown-values.ts b/tests/fixtures/node-test/cases/thrown-values.ts new file mode 100644 index 0000000000..2793b62b54 --- /dev/null +++ b/tests/fixtures/node-test/cases/thrown-values.ts @@ -0,0 +1,17 @@ +// @exit: 1 +// Failures from thrown non-Error values print as Node inspects them: a +// string quoted, a number and a boolean as written. +import { test } from "node:test"; + +test("throws a string", () => { + throw "plain string"; +}); +test("throws a string with a quote", () => { + throw "it's quoted"; +}); +test("throws a number", () => { + throw 42; +}); +test("throws a boolean", () => { + throw false; +}); diff --git a/tests/fixtures/node-test/cases/todo-suites.ts b/tests/fixtures/node-test/cases/todo-suites.ts new file mode 100644 index 0000000000..5a645bd544 --- /dev/null +++ b/tests/fixtures/node-test/cases/todo-suites.ts @@ -0,0 +1,29 @@ +// describe.todo and a todo option on a suite: the tests and suites under +// it are todo, with a bare TODO directive, unless they are skipped or carry +// their own todo message. Subtests of an inherited-todo test are todo too. +// A failing inherited-todo test prints ⚠, is listed under failing tests, +// and leaves the exit code 0. +import { describe, it, test } from "node:test"; + +describe.todo("todo suite", () => { + it("passes", () => {}); + it("fails", () => { + throw new Error("boom"); + }); + it.skip("skipped", () => {}); + it("own todo", { todo: "later" }, () => {}); + describe("nested", () => { + it("deep", () => {}); + }); + test("with sub", async (t) => { + await t.test("sub", () => {}); + }); +}); + +describe("todo message", { todo: "not yet" }, () => { + it("inherits", () => {}); +}); + +describe("plain", () => { + it("runs", () => {}); +}); diff --git a/tests/harness/__snapshots__/node-fallback-fence.ts.txt b/tests/harness/__snapshots__/node-fallback-fence.ts.txt index 54a3b3e1a0..0f6f31bf9a 100644 --- a/tests/harness/__snapshots__/node-fallback-fence.ts.txt +++ b/tests/harness/__snapshots__/node-fallback-fence.ts.txt @@ -41,4 +41,4 @@ node-fallback-fence.ts:37:16 - error SC2020: 'zlib.brotliCompressSync' is part o | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 38 | // The remaining imports continue the declared-but-not-lowered surface. - hint: the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms plus crc32 are the lowered zlib surface \ No newline at end of file + hint: Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface \ No newline at end of file diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 68cc920ae3..2e55219c14 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -104,7 +104,7 @@ fenced.ts:54:1 - error SC2020: 'zlib.brotliCompressSync' is typed by @types/node | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 55 | /* The http2 compatibility slice's @types/node-world fences (divergence - hint: the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms plus crc32 are the lowered zlib surface + hint: Brotli needs a Brotli codec, which the static runtime does not vendor — the default-options deflate/inflate, raw, gzip/gunzip, and unzip sync/callback forms, crc32, and the Deflate/Inflate/Gzip/Gunzip/DeflateRaw/InflateRaw/Unzip streams are the lowered zlib surface fenced.ts:63:71 - error SC2020: 'a createSecureServer SNICallback of 'undefined' values' is part of the standard library types but has no scriptc lowering yet diff --git a/tests/harness/ci-test-plan.test.ts b/tests/harness/ci-test-plan.test.ts index 7925d799b6..76f7f0bce9 100644 --- a/tests/harness/ci-test-plan.test.ts +++ b/tests/harness/ci-test-plan.test.ts @@ -8,6 +8,7 @@ import { darwinFiles, effectFiles, fileExclusions, + caseShardedSideFiles, isolatedFiles, testCosts, } from "../../scripts/ci-test-plan.mjs"; @@ -31,10 +32,12 @@ test.for([1, 3, 5, 10, 20])( !file.includes("coverage") && !file.includes("native-toolchain"), )) { + // A suite that splits its own tests runs on every shard; the others on one. + const everyShard = caseShardedSideFiles.some(([, sharded]) => sharded === file); expect( sideFiles.filter((selected) => selected === file), file, - ).toHaveLength(1); + ).toHaveLength(everyShard ? count : 1); } // Each measured file runs on exactly one shard, outside vitest's file shard. const isolated = new Set(isolatedFiles.map(([, file]) => file)); diff --git a/tests/harness/native-frontend.ts b/tests/harness/native-frontend.ts new file mode 100644 index 0000000000..feee2f5911 --- /dev/null +++ b/tests/harness/native-frontend.ts @@ -0,0 +1,400 @@ +/* The native frontend checks: the complete compiler frontend built to a native + * executable, which lowers programs without Node and must produce the IR and + * reports of the Node pipeline it was built from. The checks are split across + * the self-hosting-native-frontend*.test.ts files so that CI runs them on + * different shards; each file builds the executable once (useNativeFrontend). */ +import { execFile, execFileSync, spawnSync } from "node:child_process"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { promisify } from "node:util"; +import { afterAll, beforeAll, expect } from "vitest"; +import { compileC, deserializeModule, emitLlvmModule, validateModule } from "@scriptc/compiler"; +import type { compile } from "@scriptc/compiler"; +import type { IrModule } from "../../packages/compiler/src/ir/ir.js"; +import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; + +import { + moduleUsesAssert, + moduleUsesBigInt, + moduleUsesCopying, + moduleUsesDynInvoke, + moduleUsesInspect, + moduleUsesRegex, + moduleUsesSymbol, + moduleUsesZlib, +} from "../../packages/compiler/src/ir/runtime-features.js"; + +const root = join(import.meta.dirname, "../.."); +const entry = join(root, "tests/fixtures/self-hosting/frontend-pipeline.ts"); +const oracle = join(root, "tests/fixtures/self-hosting/frontend-pipeline-node.ts"); +const execFileAsync = promisify(execFile); +const sanitize = process.env["SCRIPTC_SAN"] === "1"; +const runOptions = { + cwd: root, + timeout: 120_000, + maxBuffer: 16 * 1024 * 1024, + encoding: "utf8" as const, +}; + +function comparableStderr(text: string): string { + return sanitize + ? text.replace( + /^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext functions and may produce false positives in some cases!\n/gm, + "", + ) + : text; +} + +export const programs = [ + "001-hello.ts", + "101-arithmetic.ts", + "600-closures-basic.ts", + "711-inheritance-dispatch.ts", + "1005-json-nested.ts", + "1452-return-through-finally.ts", + "2012-generators-return-throw.ts", + "3089-array-find-narrowing.ts", + "tuple-array-union.ts", + "array-refined-storage.ts", + "typed-rest-filtered-return.ts", + "rest-optional-elements.ts", + "1300-errors-basics.ts", + "error-tostring-overrides.ts", + "nullish-long-chain.ts", + "class-array-optional-return.ts", + "contextual-array-union-write.ts", +]; + +interface LowerRequest { + entry: string; + npmStatic?: string[] | "auto" | "lib"; + externalTypes?: Record; + ffiProfile?: string; + contract?: boolean; +} + +export interface NativeFrontend { + /** The build directory; checks write their scratch files here. */ + directory: string; + executable(name: string): string; + /** Lowers one request with the Node pipeline and the native frontend, and checks that their reports and + * IR agree. */ + lowerRequest(file: string, request: LowerRequest): { module: IrModule | null; report: unknown[] }; +} + +/** Builds the native frontend in a beforeAll hook of the calling suite (and removes it afterwards). The + * returned function gives the built frontend to the suite's tests. */ +export function useNativeFrontend(backend: "llvm" = "llvm"): () => NativeFrontend { + let frontend: NativeFrontend | undefined; + let directory: string | undefined; + beforeAll(async () => { + const dir = mkdtempSync( + join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-frontend-"), + ); + directory = dir; + const executable = (name: string) => + join(dir, name + (process.platform === "win32" ? ".exe" : "")); + const nativeSources = join(root, "packages/compiler/native"); + const object = join(dir, "ts7-process.o"); + execFileSync("clang", [ + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + ...(sanitize ? ["-fsanitize=address"] : []), + "-c", + join(nativeSources, "ts7-process.c"), + "-o", + object, + ]); + const profile = join(dir, "ffi.json"); + writeFileSync( + profile, + JSON.stringify({ + ...JSON.parse(readFileSync(join(nativeSources, "ts7-process.ffi.json"), "utf8")), + libraries: [object], + }), + ); + // Production lowering can monopolize a worker's synchronous RPC loop. + // Build the actual entry in a child while keeping Vitest responsive. + const api = pathToFileURL(join(root, "packages/compiler/src/index.ts")).href; + const { stdout, stderr } = await execFileAsync( + process.execPath, + [ + "--max-old-space-size=8192", + "--import", + "tsx", + "--input-type=module", + "--eval", + `import { compile } from ${JSON.stringify(api)}; + const result = await compile(process.argv[1], { + outDir: process.argv[2], outPath: process.argv[3], backend: process.argv[4], + optimization: 'dev', sanitize: process.argv[5] === '1', + ffiProfilePath: process.argv[6], + }); + console.log(JSON.stringify({ ...result, sourceTexts: undefined }));`, + entry, + dir, + executable("frontend"), + backend, + sanitize ? "1" : "0", + profile, + ], + // Under the sanitizer this compile alone can take most of 15 minutes + // on a loaded macOS runner. + { cwd: root, timeout: 1_500_000, maxBuffer: 16 * 1024 * 1024 }, + ); + expect(stderr).toBe(""); + const built = JSON.parse(stdout) as Awaited>; + if (!built.ok) + throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + if (!("binaryPath" in built)) throw new Error("expected a native frontend executable"); + expect(built.backend).toBe(backend); + + const lowerRequest = ( + file: string, + request: LowerRequest, + ): { module: IrModule | null; report: unknown[] } => { + const requestPath = join(dir, "request.json"); + writeFileSync(requestPath, JSON.stringify(request)); + const expectedPath = join(dir, "node.json"); + const actualPath = join(dir, "native.json"); + for (const output of [expectedPath, actualPath]) rmSync(output, { force: true }); + const expected = spawnSync( + process.execPath, + ["--import", "tsx", oracle, ts7Executable(), requestPath, expectedPath], + runOptions, + ); + // The native client launches the selected TS7 executable directly. + // Removing PATH makes an accidental Node subprocess dependency fail. + const actual = spawnSync(built.binaryPath, [ts7Executable(), requestPath, actualPath], { + ...runOptions, + env: { ...process.env, PATH: "" }, + }); + for (const result of [expected, actual]) { + expect(result.error, file + "\n" + result.stderr).toBeUndefined(); + expect(result.signal, file + "\n" + result.stderr).toBeNull(); + expect(result.status, file + "\n" + result.stderr).toBe(0); + expect(comparableStderr(result.stderr), file).toBe(""); + } + const expectedReport = expected.stdout + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + const actualReport = actual.stdout + .trim() + .split("\n") + .map((line) => JSON.parse(line)); + expect(actualReport, file).toEqual(expectedReport); + expect(existsSync(actualPath), file).toBe(existsSync(expectedPath)); + if (!existsSync(actualPath)) return { module: null, report: actualReport }; + const module = deserializeModule(readFileSync(actualPath, "utf8")); + expect(module, file).toEqual(deserializeModule(readFileSync(expectedPath, "utf8"))); + expect(validateModule(module), file).toEqual([]); + + return { module, report: actualReport }; + }; + frontend = { directory: dir, executable, lowerRequest }; + }, 1_800_000); + afterAll(() => { + if (directory !== undefined) rmSync(directory, { recursive: true, force: true }); + }); + return () => { + if (frontend === undefined) throw new Error("the native frontend was not built"); + return frontend; + }; +} + +/** A corpus program lowered by the native frontend compiles and prints what Node prints. */ +export async function checkProgram(frontend: NativeFrontend, file: string): Promise { + const source = join(root, "tests/corpus", file); + const { module, report } = frontend.lowerRequest(file, { entry: source }); + expect(report[0], file).toEqual({ preflight: [], npmStatic: [] }); + if (!module) throw new Error(file + ": missing native IR"); + const cPath = join(frontend.directory, "program.ll"); + writeFileSync(cPath, emitLlvmModule(module)); + await compileC({ + cPath, + outPath: frontend.executable("program"), + sanitize, + regex: moduleUsesRegex(module), + copying: moduleUsesCopying(module), + inspect: moduleUsesInspect(module), + dynInvoke: moduleUsesDynInvoke(module), + symbol: moduleUsesSymbol(module), + bigint: moduleUsesBigInt(module), + zlib: moduleUsesZlib(module), + assert: moduleUsesAssert(module), + }); + const node = spawnSync(process.execPath, [source], runOptions); + const native = spawnSync(frontend.executable("program"), [], runOptions); + for (const result of [node, native]) { + expect(result.error, file).toBeUndefined(); + expect(result.signal, file + "\n" + result.stderr).toBeNull(); + expect(result.status, file + "\n" + result.stderr).toBe(0); + } + expect(native.stdout, file).toBe(node.stdout); + expect(comparableStderr(native.stderr), file).toBe(node.stderr); +} + +/** Opted-in npm packages lower statically, and a deviant __toESM package is refused. */ +export function checkNpmStatic(frontend: NativeFrontend): void { + for (const [file, npmStatic, packages] of [ + ["escape-cli.ts", "auto", ["escape-string-regexp"]], + ["escape-barrel.ts", "auto", ["escape-string-regexp"]], + ["nested/main.ts", "auto", ["shouty"]], + ["slash-cli.ts", ["slash"], ["slash"]], + ["inferred-returns.mjs", ["inferred-returns"], ["inferred-returns"]], + ] as const) { + const result = frontend.lowerRequest(file, { + entry: join(root, "tests/fixtures/npm-static", file), + npmStatic: typeof npmStatic === "string" ? npmStatic : [...npmStatic], + }); + expect(result.report[0], file).toEqual({ + preflight: [], + npmStatic: packages.map((name) => ({ package: name, status: "static" })), + }); + expect(result.report[1], file).toMatchObject({ + stats: { statementsFailed: 0 }, + diagnostics: [], + validation: [], + }); + expect(result.module, file).not.toBeNull(); + } + const fallback = frontend.lowerRequest("package fallback", { + entry: join(root, "tests/fixtures/npm/cases/2557-toesm-drift/main.ts"), + npmStatic: "auto", + }); + expect(fallback.report[0]).toMatchObject({ + npmStatic: [ + { package: "gtdrift", status: "refused", detail: expect.stringContaining("__toESM") }, + ], + }); +} + +/** A module project in a directory of its own: returns its main.ts path. */ +function project(frontend: NativeFrontend): string { + const directory = mkdtempSync(join(frontend.directory, "project-")); + writeFileSync(join(directory, "package.json"), JSON.stringify({ type: "module" })); + writeFileSync( + join(directory, "tsconfig.json"), + JSON.stringify({ + compilerOptions: { + target: "ES2023", + module: "NodeNext", + strict: true, + noEmit: true, + }, + }), + ); + return join(directory, "main.ts"); +} + +/** A type error and a static refusal produce the Node pipeline's diagnostics and no IR. */ +export function checkDiagnostics(frontend: NativeFrontend): void { + const source = project(frontend); + writeFileSync(source, "const value: string = 1; console.log(value);\n"); + const badType = frontend.lowerRequest("type error", { entry: source }); + expect(badType.module).toBeNull(); + expect(badType.report[0]).toMatchObject({ + preflight: [expect.objectContaining({ code: "SC0001" })], + }); + writeFileSync( + source, + ` + const values: (string | number)[] = ["wrong"]; + console.log(values.find((value): value is number => true)); + `, + ); + const refusal = frontend.lowerRequest("static refusal", { entry: source }); + expect(refusal.module).toBeNull(); + expect(refusal.report[1]).toMatchObject({ + diagnostics: [expect.objectContaining({ code: "SC1090" })], + }); +} + +/** The same production pipeline supplies the library's syntax contract. Local type names and convention + * constants exercise its scoped cache and the heterogeneous destination fields used by the projection. */ +export function checkLibraryContract(frontend: NativeFrontend): void { + const source = project(frontend); + writeFileSync( + source, + ` + export interface Model { zebra: string; alpha: readonly number[]; } + export type Msg = { kind: "paint"; payload: string } | { kind: "data"; payload: Uint8Array }; + export type ReadonlyArray = { item: T }; + export type Local = ReadonlyArray; + export const modelUnbound = ["zebra"] as const; + export const msgUnbound = ["paint"] as const; + export const appearanceMsg = "paint"; + export const chromeMsg = "paint"; + export const envMsgs = [{ env: "DATA", msg: "data" }] as const; + export function count(value: Model): number { return value.alpha.length; } + `, + ); + const contract = frontend.lowerRequest("library contract", { + entry: source, + npmStatic: "lib", + contract: true, + }); + expect(contract.report[0]).toEqual({ preflight: [], npmStatic: [] }); + expect(contract.report[1]).toMatchObject({ + modelUnbound: { value: ["zebra"] }, + msgUnbound: { value: ["paint"] }, + appearanceMsg: { value: "paint" }, + chromeMsg: { value: "paint" }, + envMsgs: { value: [{ env: "DATA", msg: "data" }] }, + types: expect.arrayContaining([ + expect.objectContaining({ + name: "Local", + shape: { k: "unsupported", text: "ReadonlyArray" }, + }), + ]), + }); +} + +/** A declared native function bound through an FFI profile lowers, links and runs. */ +export async function checkNativeBindings(frontend: NativeFrontend): Promise { + const source = project(frontend); + const directory = join(source, ".."); + writeFileSync( + source, + "declare function nativeAdd(a: number, b: number): number; console.log(nativeAdd(20, 22));\n", + ); + const inputFfi = join(directory, "input-ffi.json"); + writeFileSync( + inputFfi, + JSON.stringify({ + ffi_format: 1, + functions: [ + { name: "nativeAdd", symbol: "native_add", params: ["f64", "f64"], returns: "f64" }, + ], + }), + ); + const ffiResult = frontend.lowerRequest("native bindings", { + entry: source, + ffiProfile: inputFfi, + }); + expect(ffiResult.report[1]).toMatchObject({ diagnostics: [], validation: [] }); + if (!ffiResult.module) throw new Error("missing FFI IR"); + const helper = join(directory, "add.c"); + const helperObject = join(directory, "add.o"); + writeFileSync(helper, "double native_add(double a, double b) { return a + b; }\n"); + execFileSync("clang", ["-c", helper, "-o", helperObject]); + const ffiC = join(directory, "ffi.ll"); + writeFileSync(ffiC, emitLlvmModule(ffiResult.module)); + await compileC({ + cPath: ffiC, + outPath: frontend.executable("ffi"), + sanitize, + inspect: true, + linkInputs: [helperObject], + }); + const ffiRun = spawnSync(frontend.executable("ffi"), [], runOptions); + expect(ffiRun.error).toBeUndefined(); + expect(ffiRun.status, ffiRun.stderr).toBe(0); + expect(ffiRun.stdout).toBe("42\n"); + expect(comparableStderr(ffiRun.stderr)).toBe(""); +} diff --git a/tests/harness/npm-static-costs.json b/tests/harness/npm-static-costs.json new file mode 100644 index 0000000000..2f1cdde1e3 --- /dev/null +++ b/tests/harness/npm-static-costs.json @@ -0,0 +1,176 @@ +{ + "plain": { + "__toESM dependencies compile recursively without explicit selection": 626, + "--npm-static=auto degrades a deviant __toESM package with the same note": 496, + "--npm-static=auto opts a getter-table bundle in": 442, + "--npm-static=auto opts the eligible pilot in": 476, + "--npm-static=auto probes runtime JS from the importing file's realm": 1819, + "--npm-static=auto refuses a package with no own .d.ts": 314, + "a cyclic package tree retains an impure dependency through an unused namespace re-export": 1517, + "a deviant __toESM helper refuses the package with a construct-naming note": 532, + "a package's computed specifier preserves createRequire fallback": 708, + "a package's destructured reassignment preserves createRequire fallback": 769, + "a package's early declarator call preserves createRequire fallback": 722, + "a package's early hoisted call preserves createRequire fallback": 754, + "a package's early let call preserves createRequire fallback": 733, + "a package's early shorthand call preserves createRequire fallback": 713, + "a package's early var call preserves createRequire fallback": 768, + "a package's escaped mutable shorthand preserves createRequire fallback": 701, + "a package's escaped require preserves createRequire fallback": 698, + "a package's exported mutable alias preserves createRequire fallback": 659, + "a package's exported mutable require preserves createRequire fallback": 756, + "a package's foreign base preserves createRequire fallback": 718, + "a package's literal createRequire calls compile without an engine (llvm)": 2137, + "a package's optional resolution preserves createRequire fallback": 738, + "a package's reassigned require preserves createRequire fallback": 718, + "a package's redeclared require preserves createRequire fallback": 729, + "a package's shadowed filename preserves createRequire fallback": 730, + "a preflight-refused package reports its native refusal": 608, + "a workspace-linked package classifies as an npm import without flags": 342, + "a workspace-linked package compiles statically under --npm-static": 1490, + "an ineligible untyped package is refused alongside a compiled dependency": 566, + "auto preserves safe declaration overloads while compiling the JavaScript body": 2164, + "bundled class aliases preserve declared methods and callback fields (llvm)": 5433, + "bundled CommonJS function imports compile without an engine (llvm)": 2234, + "bundler-emitted CJS 2465-getter-table compiles statically and byte-matches Node": 1713, + "bundler-emitted CJS 2466-getter-star compiles statically and byte-matches Node": 1570, + "bundler-emitted CJS 2467-star-barrel compiles statically and byte-matches Node": 1587, + "bundler-emitted CJS 2468-defineprop-exports compiles statically and byte-matches Node": 1484, + "bundler-emitted CJS 2556-toesm-external compiles statically and byte-matches Node": 1645, + "commander compiles statically and byte-matches Node": 23670, + "commander computed option listener registration compiles statically and byte-matches Node": 7303, + "conditional exports keep distinct import and require behavior": 4421, + "conflicting public class aliases keep the JavaScript return inference": 598, + "createRequire loads an opted-in package through its require entry": 3154, + "dualist compiles statically and byte-matches Node": 1896, + "effect4-shape-match-metadata.ts preserves native object layout and module initialization": 1990, + "effect4-shape-prototype-factory.ts preserves native object layout and module initialization": 1790, + "effect4-shape-semantics.ts preserves native object layout and module initialization": 9277, + "effect4-shape-string-constructor.ts preserves native object layout and module initialization": 2844, + "escape-string-regexp compiles statically and byte-matches Node": 1619, + "hoisted CommonJS function exports stay static (auto)": 2139, + "hoisted CommonJS function exports stay static (explicit)": 2187, + "inferred class methods dispatch through inherited checked receivers (dev)": 1752, + "inferred class methods dispatch through inherited checked receivers (release)": 2240, + "inferred helpers preserve native array storage (dev)": 1171, + "inferred helpers preserve native array storage (release)": 1217, + "inferred variadic factories preserve checked callable results": 1231, + "ms compiles statically and byte-matches Node": 2306, + "ms supports invalid duration strings without runtime fences": 691, + "opted-in npm packages retain imports deeper than four JavaScript modules": 1799, + "picocolors compiles fully statically and byte-matches both color branches": 3698, + "picocolors inherited hasOwnProperty matches Node": 3008, + "picocolors inherited valueOf retains the prototype-method fence": 708, + "published type-only declarations coexist with a static runtime bundle": 1829, + "purebarrel namespace re-exports byte-match Node": 1449, + "recursive inferred JavaScript results retain their promised ABI": 1369, + "renderer startup 'async-backend-factory.mjs' compiles from shipped JavaScript ('llvm')": 2230, + "renderer startup 'checked-renderer-defaults.js' compiles from shipped JavaScript ('llvm')": 2359, + "renderer startup 'destructure-inferred-row-defaults.js' compiles from shipped JavaScript ('llvm')": 2313, + "renderer startup 'fresh-array-union-layout.js' compiles from shipped JavaScript ('llvm')": 2260, + "renderer startup 'frozen-checked-dictionaries.mjs' compiles from shipped JavaScript ('llvm')": 2579, + "renderer startup 'object-from-checked-entries.js' compiles from shipped JavaScript ('llvm')": 3447, + "renderer startup 'regex-checked-storage.js' compiles from shipped JavaScript ('llvm')": 2648, + "renderer startup 'renderer-export-dictionaries.js' compiles from shipped JavaScript ('llvm')": 2405, + "renderer startup 'renderer-specializations.js' compiles from shipped JavaScript ('llvm')": 2578, + "renderer startup 'set-checked-methods.js' compiles from shipped JavaScript ('llvm')": 2375, + "renderer startup 'stored-native-builtins.js' compiles from shipped JavaScript ('llvm')": 2495, + "renderer startup 'stored-object-helpers.js' compiles from shipped JavaScript ('llvm')": 2509, + "renderer values and callbacks compile from shipped JavaScript (llvm)": 5773, + "selectivebarrel-cli.ts preserves native object layout and module initialization": 1772, + "slash compiles statically and byte-matches Node": 1646, + "statefulbarrel namespace re-exports byte-match Node": 1427, + "the crypto utility package compiles fully statically and byte-matches Node": 2554, + "the published Effect Function subpath initializes and runs statically": 9097, + "the published Effect generator has deterministic native results": 105577, + "the published Effect generator logs and completes statically": 112814, + "untyped package methods preserve virtual overrides (llvm)": 3065 + }, + "san": { + "__toESM dependencies compile recursively without explicit selection": 458, + "--npm-static=auto degrades a deviant __toESM package with the same note": 370, + "--npm-static=auto opts a getter-table bundle in": 317, + "--npm-static=auto opts the eligible pilot in": 524, + "--npm-static=auto probes runtime JS from the importing file's realm": 2349, + "--npm-static=auto refuses a package with no own .d.ts": 424, + "a cyclic package tree retains an impure dependency through an unused namespace re-export": 1505, + "a deviant __toESM helper refuses the package with a construct-naming note": 427, + "a package's computed specifier preserves createRequire fallback": 623, + "a package's destructured reassignment preserves createRequire fallback": 585, + "a package's early declarator call preserves createRequire fallback": 559, + "a package's early hoisted call preserves createRequire fallback": 531, + "a package's early let call preserves createRequire fallback": 599, + "a package's early shorthand call preserves createRequire fallback": 545, + "a package's early var call preserves createRequire fallback": 586, + "a package's escaped mutable shorthand preserves createRequire fallback": 596, + "a package's escaped require preserves createRequire fallback": 566, + "a package's exported mutable alias preserves createRequire fallback": 565, + "a package's exported mutable require preserves createRequire fallback": 515, + "a package's foreign base preserves createRequire fallback": 551, + "a package's literal createRequire calls compile without an engine (llvm)": 2051, + "a package's optional resolution preserves createRequire fallback": 605, + "a package's reassigned require preserves createRequire fallback": 535, + "a package's redeclared require preserves createRequire fallback": 644, + "a package's shadowed filename preserves createRequire fallback": 626, + "a preflight-refused package reports its native refusal": 659, + "a workspace-linked package classifies as an npm import without flags": 340, + "a workspace-linked package compiles statically under --npm-static": 1446, + "an ineligible untyped package is refused alongside a compiled dependency": 552, + "auto preserves safe declaration overloads while compiling the JavaScript body": 2370, + "bundled class aliases preserve declared methods and callback fields (llvm)": 4728, + "bundled CommonJS function imports compile without an engine (llvm)": 2088, + "bundler-emitted CJS 2465-getter-table compiles statically and byte-matches Node": 1490, + "bundler-emitted CJS 2466-getter-star compiles statically and byte-matches Node": 1325, + "bundler-emitted CJS 2467-star-barrel compiles statically and byte-matches Node": 1370, + "bundler-emitted CJS 2468-defineprop-exports compiles statically and byte-matches Node": 1361, + "bundler-emitted CJS 2556-toesm-external compiles statically and byte-matches Node": 1608, + "commander compiles statically and byte-matches Node": 32614, + "commander computed option listener registration compiles statically and byte-matches Node": 7719, + "conditional exports keep distinct import and require behavior": 5017, + "conflicting public class aliases keep the JavaScript return inference": 482, + "createRequire loads an opted-in package through its require entry": 2581, + "dualist compiles statically and byte-matches Node": 1864, + "effect4-shape-match-metadata.ts preserves native object layout and module initialization": 2106, + "effect4-shape-prototype-factory.ts preserves native object layout and module initialization": 1612, + "effect4-shape-semantics.ts preserves native object layout and module initialization": 23336, + "effect4-shape-string-constructor.ts preserves native object layout and module initialization": 2806, + "escape-string-regexp compiles statically and byte-matches Node": 1816, + "hoisted CommonJS function exports stay static (auto)": 1955, + "hoisted CommonJS function exports stay static (explicit)": 1983, + "inferred class methods dispatch through inherited checked receivers (dev)": 1954, + "inferred class methods dispatch through inherited checked receivers (release)": 2111, + "inferred helpers preserve native array storage (dev)": 1189, + "inferred helpers preserve native array storage (release)": 1079, + "inferred variadic factories preserve checked callable results": 1157, + "ms compiles statically and byte-matches Node": 2387, + "ms supports invalid duration strings without runtime fences": 669, + "opted-in npm packages retain imports deeper than four JavaScript modules": 2301, + "picocolors compiles fully statically and byte-matches both color branches": 2954, + "picocolors inherited hasOwnProperty matches Node": 2682, + "picocolors inherited valueOf retains the prototype-method fence": 535, + "published type-only declarations coexist with a static runtime bundle": 1684, + "purebarrel namespace re-exports byte-match Node": 1308, + "recursive inferred JavaScript results retain their promised ABI": 1628, + "renderer startup 'async-backend-factory.mjs' compiles from shipped JavaScript ('llvm')": 2063, + "renderer startup 'checked-renderer-defaults.js' compiles from shipped JavaScript ('llvm')": 2539, + "renderer startup 'destructure-inferred-row-defaults.js' compiles from shipped JavaScript ('llvm')": 2301, + "renderer startup 'fresh-array-union-layout.js' compiles from shipped JavaScript ('llvm')": 2117, + "renderer startup 'frozen-checked-dictionaries.mjs' compiles from shipped JavaScript ('llvm')": 2414, + "renderer startup 'object-from-checked-entries.js' compiles from shipped JavaScript ('llvm')": 2968, + "renderer startup 'regex-checked-storage.js' compiles from shipped JavaScript ('llvm')": 2997, + "renderer startup 'renderer-export-dictionaries.js' compiles from shipped JavaScript ('llvm')": 2440, + "renderer startup 'renderer-specializations.js' compiles from shipped JavaScript ('llvm')": 2435, + "renderer startup 'set-checked-methods.js' compiles from shipped JavaScript ('llvm')": 2176, + "renderer startup 'stored-native-builtins.js' compiles from shipped JavaScript ('llvm')": 2283, + "renderer startup 'stored-object-helpers.js' compiles from shipped JavaScript ('llvm')": 2164, + "renderer values and callbacks compile from shipped JavaScript (llvm)": 4668, + "selectivebarrel-cli.ts preserves native object layout and module initialization": 1725, + "slash compiles statically and byte-matches Node": 2173, + "statefulbarrel namespace re-exports byte-match Node": 1246, + "the crypto utility package compiles fully statically and byte-matches Node": 3064, + "the published Effect Function subpath initializes and runs statically": 8280, + "the published Effect generator has deterministic native results": 136502, + "the published Effect generator logs and completes statically": 181378, + "untyped package methods preserve virtual overrides (llvm)": 2730 + } +} diff --git a/tests/harness/npm-static.test.ts b/tests/harness/npm-static.test.ts index b6e4513148..8de3886c8d 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -12,8 +12,9 @@ import { import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; -import { describe, expect, test } from "vitest"; +import { beforeEach, describe, expect, test } from "vitest"; import { analyze, compile } from "@scriptc/compiler"; +import { skipUnownedTest } from "./shard.js"; const execFileAsync = promisify(execFile); const repoRoot = join(import.meta.dirname, "../.."); @@ -21,6 +22,13 @@ const fixturesRoot = join(repoRoot, "tests/fixtures"); const pilotRoot = join(fixturesRoot, "npm-static"); const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); const sanitize = process.env["SCRIPTC_SAN"] === "1"; +// Measured milliseconds per test on a CI runner (scripts/ci-test-costs.mjs). +const testCosts = ( + JSON.parse(readFileSync(new URL("./npm-static-costs.json", import.meta.url), "utf8")) as { + plain: Record; + san: Record; + } +)[sanitize ? "san" : "plain"]; interface RunResult { stdout: Buffer; @@ -99,6 +107,9 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise } describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { + // CI runs this file on every Linux shard; each runs its share of the tests. + beforeEach((context) => skipUnownedTest(context, testCosts)); + test.each([ ["effect4-shape-string-constructor.ts", "effect4-shapes"], ["effect4-shape-prototype-factory.ts", "effect4-shapes"], diff --git a/tests/harness/self-hosting-native-frontend-checks.test.ts b/tests/harness/self-hosting-native-frontend-checks.test.ts new file mode 100644 index 0000000000..9aed511505 --- /dev/null +++ b/tests/harness/self-hosting-native-frontend-checks.test.ts @@ -0,0 +1,28 @@ +import { describe, test } from "vitest"; +import { + checkDiagnostics, + checkLibraryContract, + checkNativeBindings, + checkNpmStatic, + checkProgram, + programs, + useNativeFrontend, +} from "./native-frontend.js"; + +// The native frontend checks are split across self-hosting-native-frontend.test.ts, +// self-hosting-native-frontend-programs.test.ts and self-hosting-native-frontend-checks.test.ts, +// so that CI runs them on different shards; each file builds the frontend once +// (about 4 minutes on a CI runner). +describe("the complete frontend produces executable IR without Node (llvm)", () => { + const frontend = useNativeFrontend(); + test.for(programs.slice(12))("%s runs as under Node", async (file) => + checkProgram(frontend(), file), + ); + test("opted-in npm packages lower statically and a deviant __toESM package is refused", () => + checkNpmStatic(frontend())); + test("a type error and a static refusal report the Node pipeline's diagnostics", () => + checkDiagnostics(frontend())); + test("the library syntax contract matches the Node pipeline", () => + checkLibraryContract(frontend())); + test("FFI-bound native functions lower, link and run", () => checkNativeBindings(frontend())); +}); diff --git a/tests/harness/self-hosting-native-frontend-programs.test.ts b/tests/harness/self-hosting-native-frontend-programs.test.ts new file mode 100644 index 0000000000..bf53e8f077 --- /dev/null +++ b/tests/harness/self-hosting-native-frontend-programs.test.ts @@ -0,0 +1,13 @@ +import { describe, test } from "vitest"; +import { checkProgram, programs, useNativeFrontend } from "./native-frontend.js"; + +// The native frontend checks are split across self-hosting-native-frontend.test.ts, +// self-hosting-native-frontend-programs.test.ts and self-hosting-native-frontend-checks.test.ts, +// so that CI runs them on different shards; each file builds the frontend once +// (about 4 minutes on a CI runner). +describe("the complete frontend produces executable IR without Node (llvm)", () => { + const frontend = useNativeFrontend(); + test.for(programs.slice(6, 12))("%s runs as under Node", async (file) => + checkProgram(frontend(), file), + ); +}); diff --git a/tests/harness/self-hosting-native-frontend.test.ts b/tests/harness/self-hosting-native-frontend.test.ts index 8cb5dc7c15..94b6077536 100644 --- a/tests/harness/self-hosting-native-frontend.test.ts +++ b/tests/harness/self-hosting-native-frontend.test.ts @@ -1,352 +1,13 @@ -import { execFile, execFileSync, spawnSync } from "node:child_process"; -import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { pathToFileURL } from "node:url"; -import { promisify } from "node:util"; -import { expect, test } from "vitest"; -import { compileC, deserializeModule, emitLlvmModule, validateModule } from "@scriptc/compiler"; -import type { compile } from "@scriptc/compiler"; -import type { IrModule } from "../../packages/compiler/src/ir/ir.js"; -import { ts7Executable } from "../../packages/compiler/src/frontend/ts7/rpc-api.js"; +import { describe, test } from "vitest"; +import { checkProgram, programs, useNativeFrontend } from "./native-frontend.js"; -import { - moduleUsesAssert, - moduleUsesBigInt, - moduleUsesCopying, - moduleUsesDynInvoke, - moduleUsesInspect, - moduleUsesRegex, - moduleUsesSymbol, - moduleUsesZlib, -} from "../../packages/compiler/src/ir/runtime-features.js"; - -const root = join(import.meta.dirname, "../.."); -const entry = join(root, "tests/fixtures/self-hosting/frontend-pipeline.ts"); -const oracle = join(root, "tests/fixtures/self-hosting/frontend-pipeline-node.ts"); -const execFileAsync = promisify(execFile); -const sanitize = process.env["SCRIPTC_SAN"] === "1"; -const runOptions = { - cwd: root, - timeout: 120_000, - maxBuffer: 16 * 1024 * 1024, - encoding: "utf8" as const, -}; - -function comparableStderr(text: string): string { - return sanitize - ? text.replace( - /^==\d+==WARNING: ASan doesn't fully support makecontext\/swapcontext functions and may produce false positives in some cases!\n/gm, - "", - ) - : text; -} - -const programs = [ - "001-hello.ts", - "101-arithmetic.ts", - "600-closures-basic.ts", - "711-inheritance-dispatch.ts", - "1005-json-nested.ts", - "1452-return-through-finally.ts", - "2012-generators-return-throw.ts", - "3089-array-find-narrowing.ts", - "tuple-array-union.ts", - "array-refined-storage.ts", - "typed-rest-filtered-return.ts", - "rest-optional-elements.ts", - "1300-errors-basics.ts", - "error-tostring-overrides.ts", - "nullish-long-chain.ts", - "class-array-optional-return.ts", - "contextual-array-union-write.ts", -]; - -for (const backend of ["llvm"] as const) { - test(`the complete frontend produces executable IR without Node (${backend})`, async () => { - const directory = mkdtempSync( - join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-frontend-"), - ); - const executable = (name: string) => - join(directory, name + (process.platform === "win32" ? ".exe" : "")); - try { - const nativeSources = join(root, "packages/compiler/native"); - const object = join(directory, "ts7-process.o"); - execFileSync("clang", [ - "-std=c11", - "-Wall", - "-Wextra", - "-Werror", - ...(sanitize ? ["-fsanitize=address"] : []), - "-c", - join(nativeSources, "ts7-process.c"), - "-o", - object, - ]); - const profile = join(directory, "ffi.json"); - writeFileSync( - profile, - JSON.stringify({ - ...JSON.parse(readFileSync(join(nativeSources, "ts7-process.ffi.json"), "utf8")), - libraries: [object], - }), - ); - // Production lowering can monopolize a worker's synchronous RPC loop. - // Build the actual entry in a child while keeping Vitest responsive. - const api = pathToFileURL(join(root, "packages/compiler/src/index.ts")).href; - const { stdout, stderr } = await execFileAsync( - process.execPath, - [ - "--max-old-space-size=8192", - "--import", - "tsx", - "--input-type=module", - "--eval", - `import { compile } from ${JSON.stringify(api)}; - const result = await compile(process.argv[1], { - outDir: process.argv[2], outPath: process.argv[3], backend: process.argv[4], - optimization: 'dev', sanitize: process.argv[5] === '1', - ffiProfilePath: process.argv[6], - }); - console.log(JSON.stringify({ ...result, sourceTexts: undefined }));`, - entry, - directory, - executable("frontend"), - backend, - sanitize ? "1" : "0", - profile, - ], - // Under the sanitizer this compile alone can take most of 15 minutes - // on a loaded macOS runner. - { cwd: root, timeout: 1_500_000, maxBuffer: 16 * 1024 * 1024 }, - ); - expect(stderr).toBe(""); - const built = JSON.parse(stdout) as Awaited>; - if (!built.ok) - throw new Error(built.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); - if (!("binaryPath" in built)) throw new Error("expected a native frontend executable"); - expect(built.backend).toBe(backend); - - const lowerRequest = ( - file: string, - request: { - entry: string; - npmStatic?: string[] | "auto" | "lib"; - externalTypes?: Record; - ffiProfile?: string; - contract?: boolean; - }, - ): { module: IrModule | null; report: unknown[] } => { - const requestPath = join(directory, "request.json"); - writeFileSync(requestPath, JSON.stringify(request)); - const expectedPath = join(directory, "node.json"); - const actualPath = join(directory, "native.json"); - for (const output of [expectedPath, actualPath]) rmSync(output, { force: true }); - const expected = spawnSync( - process.execPath, - ["--import", "tsx", oracle, ts7Executable(), requestPath, expectedPath], - runOptions, - ); - // The native client launches the selected TS7 executable directly. - // Removing PATH makes an accidental Node subprocess dependency fail. - const actual = spawnSync(built.binaryPath, [ts7Executable(), requestPath, actualPath], { - ...runOptions, - env: { ...process.env, PATH: "" }, - }); - for (const result of [expected, actual]) { - expect(result.error, file + "\n" + result.stderr).toBeUndefined(); - expect(result.signal, file + "\n" + result.stderr).toBeNull(); - expect(result.status, file + "\n" + result.stderr).toBe(0); - expect(comparableStderr(result.stderr), file).toBe(""); - } - const expectedReport = expected.stdout - .trim() - .split("\n") - .map((line) => JSON.parse(line)); - const actualReport = actual.stdout - .trim() - .split("\n") - .map((line) => JSON.parse(line)); - expect(actualReport, file).toEqual(expectedReport); - expect(existsSync(actualPath), file).toBe(existsSync(expectedPath)); - if (!existsSync(actualPath)) return { module: null, report: actualReport }; - const module = deserializeModule(readFileSync(actualPath, "utf8")); - expect(module, file).toEqual(deserializeModule(readFileSync(expectedPath, "utf8"))); - expect(validateModule(module), file).toEqual([]); - - return { module, report: actualReport }; - }; - - for (const file of programs) { - const source = join(root, "tests/corpus", file); - const { module, report } = lowerRequest(file, { entry: source }); - expect(report[0], file).toEqual({ preflight: [], npmStatic: [] }); - if (!module) throw new Error(file + ": missing native IR"); - const cPath = join(directory, "program.ll"); - writeFileSync(cPath, emitLlvmModule(module)); - await compileC({ - cPath, - outPath: executable("program"), - sanitize, - regex: moduleUsesRegex(module), - copying: moduleUsesCopying(module), - inspect: moduleUsesInspect(module), - dynInvoke: moduleUsesDynInvoke(module), - symbol: moduleUsesSymbol(module), - bigint: moduleUsesBigInt(module), - zlib: moduleUsesZlib(module), - assert: moduleUsesAssert(module), - }); - const node = spawnSync(process.execPath, [source], runOptions); - const native = spawnSync(executable("program"), [], runOptions); - for (const result of [node, native]) { - expect(result.error, file).toBeUndefined(); - expect(result.signal, file + "\n" + result.stderr).toBeNull(); - expect(result.status, file + "\n" + result.stderr).toBe(0); - } - expect(native.stdout, file).toBe(node.stdout); - expect(comparableStderr(native.stderr), file).toBe(node.stderr); - } - - for (const [file, npmStatic, packages] of [ - ["escape-cli.ts", "auto", ["escape-string-regexp"]], - ["escape-barrel.ts", "auto", ["escape-string-regexp"]], - ["nested/main.ts", "auto", ["shouty"]], - ["slash-cli.ts", ["slash"], ["slash"]], - ["inferred-returns.mjs", ["inferred-returns"], ["inferred-returns"]], - ] as const) { - const result = lowerRequest(file, { - entry: join(root, "tests/fixtures/npm-static", file), - npmStatic: typeof npmStatic === "string" ? npmStatic : [...npmStatic], - }); - expect(result.report[0], file).toEqual({ - preflight: [], - npmStatic: packages.map((name) => ({ package: name, status: "static" })), - }); - expect(result.report[1], file).toMatchObject({ - stats: { statementsFailed: 0 }, - diagnostics: [], - validation: [], - }); - expect(result.module, file).not.toBeNull(); - } - const fallback = lowerRequest("package fallback", { - entry: join(root, "tests/fixtures/npm/cases/2557-toesm-drift/main.ts"), - npmStatic: "auto", - }); - expect(fallback.report[0]).toMatchObject({ - npmStatic: [ - { package: "gtdrift", status: "refused", detail: expect.stringContaining("__toESM") }, - ], - }); - - const source = join(directory, "main.ts"); - writeFileSync(join(directory, "package.json"), JSON.stringify({ type: "module" })); - writeFileSync( - join(directory, "tsconfig.json"), - JSON.stringify({ - compilerOptions: { - target: "ES2023", - module: "NodeNext", - strict: true, - noEmit: true, - }, - }), - ); - writeFileSync(source, "const value: string = 1; console.log(value);\n"); - const badType = lowerRequest("type error", { entry: source }); - expect(badType.module).toBeNull(); - expect(badType.report[0]).toMatchObject({ - preflight: [expect.objectContaining({ code: "SC0001" })], - }); - writeFileSync( - source, - ` - const values: (string | number)[] = ["wrong"]; - console.log(values.find((value): value is number => true)); - `, - ); - const refusal = lowerRequest("static refusal", { entry: source }); - expect(refusal.module).toBeNull(); - expect(refusal.report[1]).toMatchObject({ - diagnostics: [expect.objectContaining({ code: "SC1090" })], - }); - - // The same production pipeline supplies the library's syntax contract. - // Local type names and convention constants exercise its scoped cache - // and the heterogeneous destination fields used by the projection. - writeFileSync( - source, - ` - export interface Model { zebra: string; alpha: readonly number[]; } - export type Msg = { kind: "paint"; payload: string } | { kind: "data"; payload: Uint8Array }; - export type ReadonlyArray = { item: T }; - export type Local = ReadonlyArray; - export const modelUnbound = ["zebra"] as const; - export const msgUnbound = ["paint"] as const; - export const appearanceMsg = "paint"; - export const chromeMsg = "paint"; - export const envMsgs = [{ env: "DATA", msg: "data" }] as const; - export function count(value: Model): number { return value.alpha.length; } - `, - ); - const contract = lowerRequest("library contract", { - entry: source, - npmStatic: "lib", - contract: true, - }); - expect(contract.report[0]).toEqual({ preflight: [], npmStatic: [] }); - expect(contract.report[1]).toMatchObject({ - modelUnbound: { value: ["zebra"] }, - msgUnbound: { value: ["paint"] }, - appearanceMsg: { value: "paint" }, - chromeMsg: { value: "paint" }, - envMsgs: { value: [{ env: "DATA", msg: "data" }] }, - types: expect.arrayContaining([ - expect.objectContaining({ - name: "Local", - shape: { k: "unsupported", text: "ReadonlyArray" }, - }), - ]), - }); - - writeFileSync( - source, - "declare function nativeAdd(a: number, b: number): number; console.log(nativeAdd(20, 22));\n", - ); - const inputFfi = join(directory, "input-ffi.json"); - writeFileSync( - inputFfi, - JSON.stringify({ - ffi_format: 1, - functions: [ - { name: "nativeAdd", symbol: "native_add", params: ["f64", "f64"], returns: "f64" }, - ], - }), - ); - const ffiResult = lowerRequest("native bindings", { entry: source, ffiProfile: inputFfi }); - expect(ffiResult.report[1]).toMatchObject({ diagnostics: [], validation: [] }); - if (!ffiResult.module) throw new Error("missing FFI IR"); - const helper = join(directory, "add.c"); - const helperObject = join(directory, "add.o"); - writeFileSync(helper, "double native_add(double a, double b) { return a + b; }\n"); - execFileSync("clang", ["-c", helper, "-o", helperObject]); - const ffiC = join(directory, "ffi.ll"); - writeFileSync(ffiC, emitLlvmModule(ffiResult.module)); - await compileC({ - cPath: ffiC, - outPath: executable("ffi"), - sanitize, - inspect: true, - linkInputs: [helperObject], - }); - const ffiRun = spawnSync(executable("ffi"), [], runOptions); - expect(ffiRun.error).toBeUndefined(); - expect(ffiRun.status, ffiRun.stderr).toBe(0); - expect(ffiRun.stdout).toBe("42\n"); - expect(comparableStderr(ffiRun.stderr)).toBe(""); - } finally { - rmSync(directory, { recursive: true, force: true }); - } - }, 1_800_000); -} +// The native frontend checks are split across self-hosting-native-frontend.test.ts, +// self-hosting-native-frontend-programs.test.ts and self-hosting-native-frontend-checks.test.ts, +// so that CI runs them on different shards; each file builds the frontend once +// (about 4 minutes on a CI runner). +describe("the complete frontend produces executable IR without Node (llvm)", () => { + const frontend = useNativeFrontend(); + test.for(programs.slice(0, 6))("%s runs as under Node", async (file) => + checkProgram(frontend(), file), + ); +}); diff --git a/tests/harness/shard.test.ts b/tests/harness/shard.test.ts index ea1c10e4d8..e645f2cacf 100644 --- a/tests/harness/shard.test.ts +++ b/tests/harness/shard.test.ts @@ -13,6 +13,7 @@ import { shardOf, shardSelect, shardSuffix, + skipUnownedTest, } from "./shard.js"; import corpusCosts from "./corpus-costs.json"; import fixtureCosts from "./effect4-costs.json"; @@ -208,6 +209,41 @@ describe("balancedShardSelect", () => { }); }); +test("skipUnownedTest runs each test of a suite on exactly one shard, longest apart", () => { + // Two tests share a title, as test.each titles without parameters do. + const names = [...Array.from({ length: 39 }, (_, index) => `case ${index}`), "case 0"]; + // Most tests measured at a second, two long ones, and the last five unmeasured. + const costs: Record = Object.fromEntries( + names.slice(0, 35).map((name) => [name, 1_000]), + ); + costs["case 3"] = 200_000; + costs["case 7"] = 150_000; + // One suite per process, as in a CI shard: the owners are computed once per suite. + const runs = (spec: { index: number; count: number } | undefined) => { + const suite = { tasks: [] as { name: string }[] }; + suite.tasks = names.map((name) => ({ type: "test", name, suite })); + return suite.tasks + .filter((task) => { + let skipped = false; + skipUnownedTest( + { task, skip: () => (skipped = true) } as unknown as Parameters< + typeof skipUnownedTest + >[0], + costs, + spec, + ); + return !skipped; + }) + .map((task) => task.name); + }; + expect(runs(undefined)).toEqual(names); + const shards = Array.from({ length: 5 }, (_, index) => runs({ index: index + 1, count: 5 })); + expect(shards.flat().toSorted()).toEqual(names.toSorted()); + // The two long tests hold shards of their own. + expect(shards.find((shard) => shard.includes("case 3"))).toEqual(["case 3"]); + expect(shards.find((shard) => shard.includes("case 7"))).toEqual(["case 7"]); +}); + describe("corpusSelect", () => { const programs = ["ts", "js", "mjs", "cjs"] .flatMap((ext) => [ diff --git a/tests/harness/shard.ts b/tests/harness/shard.ts index 460a78df39..4c3b9c2448 100644 --- a/tests/harness/shard.ts +++ b/tests/harness/shard.ts @@ -17,6 +17,7 @@ */ import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; +import type { Suite, Test, TestContext } from "vitest"; export interface ShardSpec { /** 1-based shard index. */ @@ -122,3 +123,44 @@ export function corpusSelect( export function shardSuffix(spec: ShardSpec | undefined = parseShardSpec()): string { return spec === undefined ? "" : `, shard ${spec.index}/${spec.count}`; } + +const ownedTests = new WeakMap>(); + +/** For a suite of independent tests, called from beforeEach: skips the tests + * this shard does not own. The suite's tests are balanced across the shards + * by their measured cost (`costs`: milliseconds by test name; a test without + * a measurement weighs as much as the median measured one), so every shard + * runs a similar share and the union of the shards runs each test once. */ +export function skipUnownedTest( + context: TestContext, + costs: Readonly>, + spec: ShardSpec | undefined = parseShardSpec(), +): void { + const suite = context.task.suite; + if (spec === undefined || spec.count === 1 || suite === undefined) return; + let owned = ownedTests.get(suite); + if (owned === undefined) { + const known = Object.values(costs).sort((a, b) => a - b); + const median = known.length === 0 ? 1 : known[Math.floor(known.length / 2)]!; + const tests = suite.tasks.filter((task): task is Test => task.type === "test"); + // test.each titles without their parameters repeat; later repeats get "#2", "#3"... as their key. + const seen = new Map(); + const keys = new Map( + tests.map((test) => { + const count = (seen.get(test.name) ?? 0) + 1; + seen.set(test.name, count); + return [test, count === 1 ? test.name : `${test.name} #${count}`]; + }), + ); + owned = new Set( + balancedShardSelect( + tests, + (test) => keys.get(test)!, + (test) => Math.max(1, costs[test.name] ?? median), + spec, + ), + ); + ownedTests.set(suite, owned); + } + if (!owned.has(context.task)) context.skip(); +} diff --git a/vitest.config.ts b/vitest.config.ts index 16cbf40c5c..b729acd036 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,6 @@ import { chmodSync, mkdirSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; -import { BaseSequencer, type TestSpecification } from "vitest/node"; // Compiled-binary + oracle caches for the test lanes (see cc.ts's cache block // and tests/harness/README.md). Workers inherit SCRIPTC_CACHE_DIR from here; @@ -32,20 +31,6 @@ if (effectConcurrency !== undefined && effectConcurrency !== "1" && effectConcur throw new Error("SCRIPTC_EFFECT_TEST_CONCURRENCY must be 1 or 2"); } -// Without recorded results vitest starts larger files first. three.test.ts is -// small but its shard can hold one long case (the "nodes" fixture takes 4-6 -// minutes in CI), so it starts first and overlaps the corpus files instead of -// running after them. -const longCaseFiles = ["tests/harness/three.test.ts"]; -class LongCasesFirst extends BaseSequencer { - override async sort(files: TestSpecification[]): Promise { - const sorted = await super.sort(files); - const first = (file: TestSpecification) => - longCaseFiles.some((name) => file.moduleId.endsWith("/" + name)); - return [...sorted.filter(first), ...sorted.filter((file) => !first(file))]; - } -} - export default defineConfig({ resolve: { alias: { @@ -96,6 +81,5 @@ export default defineConfig({ ? { maxWorkers: Number(workers), minWorkers: 1 } : {}), globalSetup: ["./tests/harness/suite-lock.mjs"], - sequence: { sequencer: LongCasesFirst }, }, });