diff --git a/benchmarks/runtime/README.md b/benchmarks/runtime/README.md index 1acdc9ce7..cbf47675a 100644 --- a/benchmarks/runtime/README.md +++ b/benchmarks/runtime/README.md @@ -26,7 +26,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea | `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 | | `template-render` | cpu | template literals, escaping, `map`/`join` | | `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread | -| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, RGBA to gray | +| `typed-array-kernels` | cpu | element-wise loops that run to one typed array's length and index others at the same position: `Float64Array` axpy, `Float32Array` blend, `Uint8Array` mask, wrapping `Int32Array` add, `Uint8ClampedArray` brighten, RGBA to gray | | `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 | | `unicode-lexer` | cpu | a hand-written lexer over a 2.7M-unit bundled module whose comments and strings are in several languages: `charCodeAt` scans, line starts | | `validate-errors` | cpu | throw/catch on a fraction of inputs, small call chains | diff --git a/benchmarks/runtime/workloads/typed-array-kernels.ts b/benchmarks/runtime/workloads/typed-array-kernels.ts index 87c227bbc..ba68f7241 100644 --- a/benchmarks/runtime/workloads/typed-array-kernels.ts +++ b/benchmarks/runtime/workloads/typed-array-kernels.ts @@ -17,6 +17,10 @@ function addWrap(a: Int32Array, b: Int32Array, out: Int32Array): void { for (let i = 0; i < out.length; i++) out[i] = (a[i]! + b[i]!) | 0; } +function brighten(src: Uint8ClampedArray, gain: number, out: Uint8ClampedArray): void { + for (let i = 0; i < out.length; i++) out[i] = src[i]! * gain + 8; +} + function gray(rgba: Uint8Array, out: Uint8Array): void { for (let i = 0; i < out.length; i++) { const j = i * 4; @@ -38,6 +42,8 @@ const masked = new Uint8Array(n); const ia = new Int32Array(n); const ib = new Int32Array(n); const io = new Int32Array(n); +const pixels = new Uint8ClampedArray(n); +const bright = new Uint8ClampedArray(n); const rgba = new Uint8Array(n * 4); const luma = new Uint8Array(n); let state = 2463534242; @@ -56,6 +62,7 @@ for (let i = 0; i < n; i++) { key[i] = next() & 255; ia[i] = next() | 0; ib[i] = next() | 0; + pixels[i] = next() & 255; } for (let i = 0; i < rgba.length; i++) rgba[i] = next() & 255; @@ -64,6 +71,7 @@ let f64Sum = 0; let f32Sum = 0; let byteSum = 0; let intSum = 0; +let brightSum = 0; let lumaSum = 0; for (let r = 0; r < rounds; r++) { // Change one input element per round so no round repeats the last. @@ -72,21 +80,25 @@ for (let r = 0; r < rounds; r++) { fa[at] = r; data[at] = r & 255; ia[at] = r * 65537; + pixels[at] = r & 255; rgba[at * 4] = r & 255; axpy(1.5, x, y, d); blend((r & 15) / 16, fa, fb, fo); mask(data, key, masked); addWrap(ia, ib, io); + brighten(pixels, 0.75 + (r & 7) / 8, bright); gray(rgba, luma); const probe = (r * 104729) & (n - 1); f64Sum += d[probe]!; f32Sum += fo[probe]!; byteSum += masked[probe]!; intSum = (intSum + io[probe]!) | 0; + brightSum += bright[probe]!; lumaSum += luma[probe]!; } console.log(`float64 ${f64Sum.toFixed(3)}`); console.log(`float32 ${f32Sum.toFixed(3)}`); console.log(`uint8 ${byteSum}`); console.log(`int32 ${intSum}`); +console.log(`clamped ${brightSum}`); console.log(`gray ${lumaSum}`); diff --git a/docs/content/limitations/builtin-apis/crypto.mdx b/docs/content/limitations/builtin-apis/crypto.mdx index 3c830c63d..641d0d8f9 100644 --- a/docs/content/limitations/builtin-apis/crypto.mdx +++ b/docs/content/limitations/builtin-apis/crypto.mdx @@ -8,7 +8,10 @@ `randomFill`, `randomFillSync` (every view kind and ArrayBuffer), `randomInt`, `randomUUID`, and `randomUUIDv7` follow Node 24.22, including callback timing; a `randomInt` callback whose error parameter is typed `Error | null` receives `null` where Node passes `undefined`. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. - `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers, cipher streams, and wasm32-wasi builds are refused. + `createCipheriv` and `createDecipheriv` support aes-128, aes-192, and aes-256 in CBC, CTR, and GCM modes and chacha20-poly1305, on native targets, through `update()`, `final()`, `setAAD()`, `setAutoPadding()`, `getAuthTag()`, and `setAuthTag()`; other ciphers and wasm32-wasi builds are refused. + Cipheriv and Decipheriv values are also Transforms: they can be written, piped into, used as `pipeline()` stages, read, iterated with `for await`, and listened to, and their stream properties read as in Node; written chunks go through `update()`, and the end pushes `final()` or reports its error through `'error'`. + Through `unknown` and `any` slots and untyped JavaScript, in a program that uses streams, their stream properties and their `on`, `once`, `off`, `pipe`, `unpipe`, `read`, `pause`, `resume`, `setEncoding`, `write`, `end`, `destroy`, `cork`, and `uncork` members work; `write` and `end` take no encoding or callback there, and the other stream members throw a coded SC2020 error. + Hash, Hmac, Cipheriv, and Decipheriv streams count a written string's bytes in `writableLength`, where Node counts the string's length. Decrypting chacha20-poly1305 without an authentication tag fails in `final()`; Node 24.22 returns unverified plaintext. After `update()` fails, for example past the AES-GCM limit of 2^36 - 32 bytes per message, every later call on that cipher throws; Node still lets `final()` run. An output encoding that is a function or an instance of a class defining `toString` throws a coded SC2020 error. @@ -16,4 +19,7 @@ A call on an ECDH value whose arguments no @types/node overload accepts (`getPublicKey("HEX")`) is refused at compile time, as is `computeSecret(view, encoding)`: Node reads that encoding as the input encoding and answers a Buffer where @types/node declares a string. ECDH property reads and writes throw a coded SC2020 error. Other public-key operations remain unsupported. + The WebCrypto `crypto` global, which node:crypto also exports as `webcrypto`, supports `randomUUID()` and `getRandomValues()`, also read as values from untyped JavaScript. + Its `subtle` object, also node:crypto's `subtle` export, supports `generateKey()` and `exportKey()` for HMAC and AES keys, `importKey()` for HMAC, AES, PBKDF2, and HKDF keys, `encrypt()` and `decrypt()` with AES-GCM, AES-CBC, and AES-CTR on native targets, `deriveBits()` and `deriveKey()` with PBKDF2 and HKDF, HMAC `sign()` and `verify()`, and SHA-1 and SHA-2 `digest()`. + Secret keys import and export in the `raw`, `raw-secret`, and `jwk` formats; other SubtleCrypto methods, algorithms, and key formats are refused. Native derivations finish before their deferred callbacks, so computation blocks the caller. diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx index a21f7a617..4e0832c26 100644 --- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx +++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx @@ -3,14 +3,16 @@ `readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream. When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects. Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol. - A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams at run time; other values throw a not-supported error. + A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams, or `Cipheriv` and `Decipheriv` values, at run time; other values throw a not-supported error. Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one. `Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported. In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported. Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported. - `pipeline` accepts one run of function stages: async generator functions between streams (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument. + `pipeline` accepts function stages anywhere among its streams: async generator functions (`async function* (source)`, iterating `source` with `for await`), an async generator source function (`async function* ({ signal })`), and an async function destination whose value resolves the promise form and reaches the callback's second argument. + A stage is written inline, or named by a function declaration or a `const` bound to a function expression that nothing reassigns. + The source may also be an array, a string, a synchronous generator object, or a synchronous generator function without parameters; their elements reach the next stage on the same ticks as in Node. Stages may take the `{ signal }` argument. - An async generator function as the destination, using the PassThrough that `pipeline()` returns for a function destination, several runs of function stages, and function stages together with pipeline options are unsupported. + An async generator function as the destination, using the PassThrough that `pipeline()` returns for a function destination, and function stages or iterable sources together with pipeline options are unsupported. `Readable.from` accepts `objectMode` and `highWaterMark` options. The `Readable` operators (`map`, `filter`, `forEach`, `toArray`, `some`, `find`, `every`, `flatMap`, `drop`, `take`, and `reduce`) accept `concurrency`, `highWaterMark`, and `signal` options; their callbacks take only the chunk, `map` returns strings or Buffers, and `flatMap` returns strings or arrays of them. `readable.compose` is unsupported. @@ -19,6 +21,7 @@ Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream. A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error. `node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile. + `on`, `once`, and `addListener` return the interface, so registrations chain. The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node. With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output. The output's `'resize'` event does not redraw the line. diff --git a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx index 8b653b25e..173bdc0c6 100644 --- a/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx +++ b/docs/content/limitations/types-and-class-definitions/local-class-expressions.mdx @@ -1,4 +1,5 @@ -- **Local class expressions:** Static members, decorators, private brands, generic members, and references through the class's internal name are unsupported. +- **Local class expressions:** Static fields and static blocks in TypeScript files, decorators, private brands, generic members, and references through the class's internal name are unsupported. + Static methods read the enclosing function's bindings and are inherited by subclasses. Supported expressions preserve fresh constructor identity and shared captures across constructors, methods, accessors, and arrow fields. Local inheritance requires a compiled base class. Constructors retain identity, `typeof`, `name`, and `length` through `unknown`; cast back to their exact type before construction. diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 66b0ba3dd..682e68f99 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -12442,9 +12442,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 by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -12465,9 +12465,9 @@ "inherited": true }, "static": { - "status": "refused", - "detail": "Explicitly rejected by a named compiler diagnostic.", - "verification": "explicit-refusal" + "status": "partial", + "detail": "Implemented by a dedicated static compiler/runtime path.", + "verification": "test-backed" } }, { @@ -12581,7 +12581,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12604,7 +12604,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12627,7 +12627,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12650,7 +12650,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12673,7 +12673,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12696,7 +12696,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12719,7 +12719,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12742,7 +12742,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12765,7 +12765,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12788,7 +12788,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12811,7 +12811,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -12834,7 +12834,7 @@ }, "static": { "status": "partial", - "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused.", + "detail": "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'.", "verification": "test-backed" } }, @@ -79656,9 +79656,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" } }, { @@ -79680,7 +79680,7 @@ }, "static": { "status": "supported", - "detail": "Implemented by a dedicated static compiler/runtime path.", + "detail": "Implemented for the documented scriptc module-loader subset.", "verification": "test-backed" } }, @@ -79703,7 +79703,7 @@ }, "static": { "status": "supported", - "detail": "Implemented by a dedicated static compiler/runtime path.", + "detail": "Implemented for the documented scriptc module-loader subset.", "verification": "test-backed" } }, @@ -80001,9 +80001,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" } }, { @@ -80024,9 +80024,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" } }, { @@ -80047,9 +80047,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" } }, { @@ -80139,9 +80139,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" } }, { @@ -80162,9 +80162,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" } }, { @@ -80208,9 +80208,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" } }, { diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index c6a7a102c..a86967ff3 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -19313,6 +19313,11 @@ "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", @@ -19700,6 +19705,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -19736,6 +19744,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -21215,8 +21226,12 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:crypto" + "status": "partial", + "evidence": "compiler-dedicated:subtle", + "tests": [ + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs" + ] }, "anchorSource": "ancestor" }, @@ -21238,8 +21253,12 @@ "inherited": true }, "static": { - "status": "refused", - "evidence": "surface-manifest:unsupported:node-builtin.crypto.webcrypto" + "status": "partial", + "evidence": "compiler-dedicated:crypto.webcrypto", + "tests": [ + "tests/corpus/7122-webcrypto-node-crypto.ts", + "tests/corpus/7123-webcrypto-node-crypto.cjs" + ] }, "anchorSource": "exact" }, @@ -21734,6 +21753,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -21770,6 +21792,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -21806,6 +21831,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -21842,6 +21870,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -21878,6 +21909,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -21914,6 +21948,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -21950,6 +21987,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -21986,6 +22026,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -22022,6 +22065,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -22058,6 +22104,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -22094,6 +22143,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -22130,6 +22182,9 @@ "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", + "tests/corpus/7055-cipher-streams.ts", + "tests/corpus/7056-cipher-stream-errors.ts", + "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts" ] }, @@ -106607,8 +106662,6 @@ "tests/corpus/1534-crypto-hash-chain.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", - "tests/corpus/5442-crypto-random-values.ts", - "tests/corpus/5443-crypto-random-arguments.js", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", @@ -106616,11 +106669,20 @@ "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/fixtures/node-types/webcrypto-hmac.ts", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/harness/npm-static.test.ts" ] }, @@ -110078,8 +110140,13 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.Crypto", + "tests": [ + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/6293-webcrypto-digest.ts" + ] }, "anchorSource": "exact" }, @@ -110102,8 +110169,10 @@ }, "static": { "status": "supported", - "evidence": "compiler-dedicated:crypto.getRandomValues", + "evidence": "compiler-feature:webcrypto.crypto.getRandomValues", "tests": [ + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js" ] @@ -110129,11 +110198,12 @@ }, "static": { "status": "supported", - "evidence": "compiler-dedicated:crypto.randomUUID", + "evidence": "compiler-feature:webcrypto.crypto.randomUUID", "tests": [ - "tests/corpus/5442-crypto-random-values.ts", - "tests/corpus/5443-crypto-random-arguments.js", - "tests/corpus/1358-crypto-random.ts" + "tests/corpus/7120-webcrypto-random-uuid.ts", + "tests/corpus/7121-webcrypto-crypto-object.mjs", + "tests/corpus/6204-crypto-get-random-values.ts", + "tests/corpus/6205-crypto-get-random-values.js" ] }, "anchorSource": "exact" @@ -110189,6 +110259,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.CryptoKey", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110217,6 +110288,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.cryptoKey.algorithm", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110245,6 +110317,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.extractable", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110273,6 +110346,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.type", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110301,6 +110375,7 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.usages", "tests": [ + "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs" @@ -110471,8 +110546,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.decrypt", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110494,8 +110584,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.deriveBits", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110517,8 +110622,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.deriveKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110543,6 +110663,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.digest", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110617,8 +110744,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.encrypt", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110640,8 +110782,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.exportKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110686,8 +110843,23 @@ "inherited": true }, "static": { - "status": "not-implemented", - "evidence": "compiler-unmatched:webcrypto" + "status": "partial", + "evidence": "compiler-feature:webcrypto.subtle.generateKey", + "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", + "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", + "tests/corpus/6292-webcrypto-hmac-errors.ts", + "tests/corpus/6293-webcrypto-digest.ts", + "tests/corpus/6294-webcrypto-js-values.mjs", + "tests/corpus/6295-jose-hs256/main.mjs", + "tests/fixtures/node-types/webcrypto-hmac.ts" + ] }, "anchorSource": "exact" }, @@ -110712,6 +110884,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.importKey", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110743,6 +110922,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.sign", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", @@ -110797,6 +110983,13 @@ "status": "partial", "evidence": "compiler-feature:webcrypto.subtle.verify", "tests": [ + "tests/corpus/7128-webcrypto-derive.ts", + "tests/corpus/7129-webcrypto-derive-errors.mjs", + "tests/corpus/7126-webcrypto-aes.ts", + "tests/corpus/7127-webcrypto-aes-errors.mjs", + "tests/corpus/7124-webcrypto-secret-keys.ts", + "tests/corpus/7125-webcrypto-key-errors.mjs", + "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", diff --git a/internal/compatibility/src/generate.mjs b/internal/compatibility/src/generate.mjs index 9bd3927b8..cf77c1f84 100644 --- a/internal/compatibility/src/generate.mjs +++ b/internal/compatibility/src/generate.mjs @@ -594,7 +594,7 @@ function publicDetail(tier) { return "Matches Node's key formats, point encodings, private-key range checks, key-pair checks and errors on the supported curves; ECDH.convertKey is supported, and property reads and writes are refused."; } if (/^compiler-dedicated:(?:Cipheriv|Decipheriv|cipher\.|decipher\.)/.test(source)) { - return "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers; the stream.Transform interface is refused."; + return "Matches Node's update() output lengths, padding, authentication tags, string encodings, and state errors for the supported ciphers, and its stream.Transform behavior: chunks written through update(), final() pushed at the end, and its errors reported through 'error'."; } if (source.startsWith("compiler-dedicated:")) return "Implemented by a dedicated static compiler/runtime path."; diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 23c7a159b..6b3eaab4c 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -42,8 +42,8 @@ "node-builtin.child_process": ["tests/corpus/1361-spawn-events.ts", "tests/corpus/1565-spawn-pipe-streams.ts", "tests/corpus/2911-execfile-callback.ts", "tests/corpus/2939-child-shell-text.ts", "tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/3127-spawn-runtime-options.ts", "tests/corpus/3130-child-generic-streams.ts", "tests/corpus/3131-child-rest-listeners.js", "tests/corpus/3132-child-generic-stream-completion.ts"], "node-builtin.cluster": ["tests/corpus/957-builtins-namespace.ts"], "node-builtin.console": ["tests/corpus/console-imports.cjs", "tests/corpus/2440-console-inspect-args.ts", "tests/corpus/console-native-value.js", "tests/harness/npm-static.test.ts"], - "node-builtin.crypto.createCipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"], - "node-builtin.crypto.createDecipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.createCipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], + "node-builtin.crypto.createDecipheriv": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"], "node-builtin.crypto.createECDH": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"], "node-builtin.crypto": ["tests/corpus/1358-crypto-random.ts", "tests/corpus/1534-crypto-hash-chain.ts", "tests/corpus/2556-crypto-introspection.ts", "tests/corpus/2890-crypto-static-utility.ts", "tests/harness/npm-static.test.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/node24-crypto-kdf.ts", "tests/corpus/node24-crypto-kdf-errors.ts", "tests/corpus/node24-crypto-kdf-options.ts"], "node-builtin.dgram": ["tests/corpus/2597-dgram-send-ladders.cjs"], @@ -187,16 +187,18 @@ "stdlib.set": ["tests/corpus/typed-collection-iterators.ts", "tests/corpus/typed-collection-iterator-references.ts", "tests/corpus/checked-collection-iterators.ts"], "stdlib.headers": ["tests/harness/fetch-conformance.test.ts"], "stdlib.request": ["tests/harness/fetch-conformance.test.ts"], - "stdlib.webcrypto.subtle": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], - "stdlib.webcrypto.CryptoKey": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.subtle": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], + "stdlib.webcrypto.CryptoKey": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"], "stdlib.response": ["tests/harness/fetch-conformance.test.ts"], "stdlib.readable-stream": ["tests/harness/fetch-conformance.test.ts"], "stdlib.abort": ["tests/harness/fetch-conformance.test.ts"] }, "features": [ - { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, + { "chapter": "webcrypto", "symbols": ["subtle.importKey", "subtle.generateKey", "subtle.exportKey", "subtle.encrypt", "subtle.decrypt", "subtle.deriveBits", "subtle.deriveKey", "subtle.sign", "subtle.verify", "subtle.digest"], "status": "partial", "evidence": ["tests/corpus/7128-webcrypto-derive.ts", "tests/corpus/7129-webcrypto-derive-errors.mjs", "tests/corpus/7126-webcrypto-aes.ts", "tests/corpus/7127-webcrypto-aes-errors.mjs", "tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/7125-webcrypto-key-errors.mjs", "tests/fixtures/node-types/webcrypto-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs", "tests/fixtures/node-types/webcrypto-hmac.ts"] }, { "chapter": "webcrypto", "signatures": ["Class: `SubtleCrypto`", "`subtle` Type: {SubtleCrypto}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, - { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `CryptoKey`", "`cryptoKey.algorithm`", "`extractable` Type: {boolean}", "`type` Type: {string} One of `'secret'`, `'private'`, or `'public'`.", "`usages` Type: {string\\[]}"], "status": "partial", "evidence": ["tests/corpus/7124-webcrypto-secret-keys.ts", "tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, + { "chapter": "webcrypto", "signatures": ["Class: `Crypto`"], "status": "partial", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6293-webcrypto-digest.ts"] }, + { "chapter": "webcrypto", "symbols": ["crypto.randomUUID", "crypto.getRandomValues"], "status": "supported", "evidence": ["tests/corpus/7120-webcrypto-random-uuid.ts", "tests/corpus/7121-webcrypto-crypto-object.mjs", "tests/corpus/6204-crypto-get-random-values.ts", "tests/corpus/6205-crypto-get-random-values.js"] }, { "chapter": "globals", "signatures": ["`navigator`"], "status": "partial", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts", "tests/corpus/6331-jose-root-entry/main.ts"] }, { "chapter": "globals", "signatures": ["`userAgent` Type: {string}"], "status": "supported", "evidence": ["tests/fixtures/node-types/navigator.ts", "tests/harness/project-config.test.ts"] }, { "chapter": "globals", "signatures": ["Class: `CryptoKey`", "Class: `SubtleCrypto`"], "status": "partial", "evidence": ["tests/corpus/6291-webcrypto-hmac-sign-verify.ts", "tests/corpus/6292-webcrypto-hmac-errors.ts", "tests/corpus/6293-webcrypto-digest.ts", "tests/corpus/6294-webcrypto-js-values.mjs", "tests/corpus/6295-jose-hs256/main.mjs"] }, @@ -412,7 +414,8 @@ { "symbols": ["assert.partialDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/5517-assert-partial-deep-equal.js", "tests/corpus/5518-assert-partial-deep-typed.ts", "tests/corpus/5519-assert-partial-deep-typed-edges.ts"] }, { "symbols": ["assert.deepEqual", "assert.notDeepEqual"], "status": "partial", "evidence": ["tests/corpus/5513-assert-deep-equal-loose.js", "tests/corpus/5514-assert-deep-equal-typed.ts", "tests/corpus/5516-assert-deep-equal-edges.js"] }, { "symbols": ["assert", "assert.ok", "assert.equal", "assert.notEqual", "assert.strictEqual", "assert.notStrictEqual", "assert.deepStrictEqual", "assert.notDeepStrictEqual", "assert.fail", "assert.match", "assert.doesNotMatch", "assert.throws", "assert.rejects", "assert.ifError"], "status": "partial", "evidence": ["tests/corpus/1600-assert-passing.ts", "tests/corpus/1604-assert-deep-structures.ts", "tests/corpus/1609-assert-async.ts", "tests/corpus/1721-assert-throws-regex-class.ts", "tests/corpus/2936-static-loose-equality.ts", "tests/corpus/5515-assert-deep-strict-typed-messages.ts"] }, - { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/fixtures/node-types/cipher.ts"] }, + { "chapter": "crypto", "symbols": ["Cipheriv", "cipher.update", "cipher.final", "cipher.setAAD", "cipher.getAuthTag", "cipher.setAutoPadding", "Decipheriv", "decipher.update", "decipher.final", "decipher.setAAD", "decipher.setAuthTag", "decipher.setAutoPadding"], "status": "partial", "evidence": ["tests/corpus/5250-cipher-aes-gcm.ts", "tests/corpus/5251-cipher-aes-cbc.ts", "tests/corpus/5252-cipher-aes-ctr.ts", "tests/corpus/5253-cipher-chacha20-poly1305.ts", "tests/corpus/5254-cipher-errors.js", "tests/corpus/5255-cipher-encodings.cjs", "tests/corpus/5256-cipher-deprecation.cjs", "tests/corpus/5257-cipher-vectors.ts", "tests/corpus/5258-cipher-handles.js", "tests/corpus/5259-cipher-chunked.ts", "tests/corpus/7055-cipher-streams.ts", "tests/corpus/7056-cipher-stream-errors.ts", "tests/corpus/7057-cipher-streams.mjs", "tests/fixtures/node-types/cipher.ts"] }, + { "chapter": "crypto", "symbols": ["crypto.webcrypto", "subtle"], "status": "partial", "evidence": ["tests/corpus/7122-webcrypto-node-crypto.ts", "tests/corpus/7123-webcrypto-node-crypto.cjs"] }, { "chapter": "crypto", "symbols": ["ECDH", "ecdh.computeSecret", "ecdh.generateKeys", "ecdh.getPrivateKey", "ecdh.getPublicKey", "ecdh.setPrivateKey", "ecdh.setPublicKey"], "status": "partial", "evidence": ["tests/corpus/6700-ecdh-shared-secret.ts", "tests/corpus/6701-ecdh-formats.ts", "tests/corpus/6702-ecdh-errors.js", "tests/corpus/6703-ecdh-private-keys.ts", "tests/corpus/6704-ecdh-set-public-key.cjs", "tests/corpus/6705-ecdh-handles.ts", "tests/fixtures/node-types/ecdh.ts"] }, { "chapter": "errors", "symbols": ["ERR_CRYPTO_ECDH_INVALID_FORMAT", "ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY"], "status": "partial", "evidence": ["tests/corpus/6702-ecdh-errors.js"] }, { "symbols": ["crypto.createHash", "Hash", "hash.update", "hash.digest", "hash.copy", "crypto.createHmac", "Hmac", "hmac.update", "hmac.digest", "crypto.hash", "crypto.pbkdf2", "crypto.pbkdf2Sync"], "status": "partial", "evidence": ["tests/corpus/5760-crypto-hash-stream.ts", "tests/corpus/5768-crypto-hash-unknown.ts", "tests/corpus/5769-crypto-hash-unknown.cjs", "tests/corpus/5761-crypto-hash-stream-states.ts", "tests/corpus/5762-crypto-hash-stream.cjs", "tests/corpus/2890-crypto-static-utility.ts", "tests/corpus/node24-crypto-sha2.ts", "tests/corpus/5435-crypto-hash-algorithms.ts", "tests/corpus/5436-crypto-hash-names.js", "tests/corpus/5437-crypto-hmac-algorithms.ts", "tests/corpus/5438-crypto-kdf-digests.ts", "tests/corpus/5440-crypto-hash-encodings.js", "tests/corpus/5441-crypto-hash-output-length.ts", "tests/harness/npm-static.test.ts"] }, diff --git a/package.json b/package.json index 5cc30d9f8..a3cc42fc7 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "test:test262": "tsx tests/test262/run.ts", "test:test262:fetch": "node tests/test262/fetch.mjs", "test:cache-identity": "node tests/harness/cache-identity.mjs", - "test:lowering-order": "node scripts/lowering-order.mjs --fail-on content", + "test:lowering-order": "node scripts/lowering-order.mjs --fail-on names", "test:sandbox": "node scripts/sandbox-test.mjs", "test:sandbox:image": "node scripts/sandbox-image.mjs", "test:watch": "vitest", @@ -40,10 +40,11 @@ "@scriptc/types": "workspace:*", "@types/node": "^24.0.0", "effect": "3.22.2", - "js-yaml": "4.3.0", + "js-yaml": "4.3.2", "oxfmt": "0.71.0", "oxlint": "1.86.0", "rollup": "4.62.2", + "smol-toml": "1.7.1", "three": "0.186.1", "tsx": "^4.19.0", "typescript": "5.9.3", diff --git a/packages/cli/test/native-link-info.test.ts b/packages/cli/test/native-link-info.test.ts index f5ff2642e..56ca46878 100644 --- a/packages/cli/test/native-link-info.test.ts +++ b/packages/cli/test/native-link-info.test.ts @@ -72,7 +72,7 @@ describe.runIf(supported)("macOS arm64 native link info", () => { minimum_os: "14.0", }, program: { object, entry_symbol: "main" }, - runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, runtime_pack: { kind: "precompiled", package: "@scriptc/runtime-darwin-arm64", @@ -165,6 +165,6 @@ describe.runIf(supported)("macOS arm64 native link info", () => { () => null, (failure: { stderr?: string }) => failure, ); - expect(error?.stderr).toContain("scr_runtime_abi_v24"); + expect(error?.stderr).toContain("scr_runtime_abi_v25"); }); }); diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index dbb3cd8fd..936283d40 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -544,12 +544,15 @@ declare var DOMException: { readonly prototype: DOMException; }; -/* WebCrypto's HMAC slice (scr_webcrypto.c): crypto.subtle's importKey for - * raw HMAC keys, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as +/* WebCrypto's secret-key slice (scr_webcrypto.c): crypto.subtle's + * importKey, generateKey and exportKey for HMAC and AES keys (importKey + * also for PBKDF2 and HKDF keys), AES-GCM/CBC/CTR encrypt and decrypt, + * PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign/verify and the SHA-1/SHA-2 digests, typed as * exactly that surface (the declarations mirror @types/node's webcrypto - * shapes). Every other SubtleCrypto method and crypto member is absent, - * so a use is an honest type error. CryptoKey values are native handles - * whose four members read through the handle. */ + * shapes), plus the global's randomUUID and getRandomValues. Every other + * SubtleCrypto method and crypto member is absent, so a use is an honest + * type error. CryptoKey values are native handles whose four members read + * through the handle. */ declare var crypto: Crypto; /* The Crypto and SubtleCrypto constructors (feature probes only) stay * `unknown`; the values keep this declaration order. */ @@ -582,9 +585,44 @@ interface HmacImportParams extends Algorithm { hash: HashAlgorithmIdentifier; length?: number; } +interface HmacKeyGenParams extends Algorithm { + hash: HashAlgorithmIdentifier; + length?: number; +} +interface AesKeyGenParams extends Algorithm { + length: number; +} +interface AesGcmParams extends Algorithm { + additionalData?: BufferSource; + iv: BufferSource; + tagLength?: number; +} +interface AesCbcParams extends Algorithm { + iv: BufferSource; +} +interface AesCtrParams extends Algorithm { + counter: BufferSource; + length: number; +} +interface Pbkdf2Params extends Algorithm { + hash: HashAlgorithmIdentifier; + iterations: number; + salt: BufferSource; +} +interface HkdfParams extends Algorithm { + hash: HashAlgorithmIdentifier; + info: BufferSource; + salt: BufferSource; +} +interface AesDerivedKeyParams extends Algorithm { + length: number; +} interface KeyAlgorithm { name: string; } +interface AesKeyAlgorithm extends KeyAlgorithm { + length: number; +} interface HmacKeyAlgorithm extends KeyAlgorithm { hash: KeyAlgorithm; length: number; @@ -623,10 +661,37 @@ interface JsonWebKey { x?: string; y?: string; } -/* The 'jwk' form type-checks like @types/node's; the compiler refuses it - * (the JsonWebKey import is not implemented). */ interface SubtleCrypto { + decrypt( + algorithm: AesGcmParams | AesCbcParams | AesCtrParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + deriveBits( + algorithm: Pbkdf2Params | HkdfParams, + baseKey: CryptoKey, + length?: number | null, + ): Promise; + deriveKey( + algorithm: Pbkdf2Params | HkdfParams, + baseKey: CryptoKey, + derivedKeyType: AlgorithmIdentifier | AesDerivedKeyParams | HmacImportParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise; + encrypt( + algorithm: AesGcmParams | AesCbcParams | AesCtrParams, + key: CryptoKey, + data: BufferSource, + ): Promise; + exportKey(format: "jwk", key: CryptoKey): Promise; + exportKey(format: Exclude, key: CryptoKey): Promise; + generateKey( + algorithm: AesKeyGenParams | HmacKeyGenParams, + extractable: boolean, + keyUsages: readonly KeyUsage[], + ): Promise; importKey( format: "jwk", keyData: JsonWebKey, @@ -651,6 +716,21 @@ interface SubtleCrypto { } interface Crypto { readonly subtle: SubtleCrypto; + getRandomValues< + T extends + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | BigInt64Array + | BigUint64Array, + >( + typedArray: T, + ): T; + randomUUID(): `${string}-${string}-${string}-${string}-${string}`; } /* Capability-gated globals harness code probes behind feature guards @@ -2784,8 +2864,7 @@ declare module "crypto" { b: ArrayBufferView | ArrayBufferLike, ): boolean; /* WebCrypto's getRandomValues (also the webcrypto object's): integer - * typed arrays, filled in place and returned. The crypto GLOBAL stays - * unknown in fallback builds. */ + * typed arrays, filled in place and returned. */ type RandomValuesArray = | Int8Array | Uint8Array @@ -2797,9 +2876,9 @@ declare module "crypto" { | BigInt64Array | BigUint64Array; export function getRandomValues(typedArray: T): T; - export const webcrypto: { - getRandomValues(typedArray: T): T; - }; + /* The WebCrypto global's object and its SubtleCrypto. */ + export const webcrypto: Crypto; + export const subtle: SubtleCrypto; export function randomFillSync( buffer: T, offset?: number, @@ -2852,7 +2931,8 @@ declare module "crypto" { ): void; export type BinaryLike = string | ArrayBuffer | ArrayBufferView; /* Cipheriv/Decipheriv: aes-128/192/256 in cbc, ctr, and gcm modes and - * chacha20-poly1305. The stream.Transform members are not declared. */ + * chacha20-poly1305. Both are Transforms: written chunks go through + * update(), and the end pushes final(). */ export type CipherGCMTypes = "aes-128-gcm" | "aes-192-gcm" | "aes-256-gcm"; export type CipherChaCha20Poly1305Types = "chacha20-poly1305"; export type CipherKey = BinaryLike | KeyObject; @@ -2862,7 +2942,7 @@ declare module "crypto" { export interface CipherChaCha20Poly1305Options { authTagLength?: number; } - export interface Cipheriv { + export interface Cipheriv extends Transform { update(data: string | ArrayBufferView): Buffer; update(data: string, inputEncoding: BufferEncoding): Buffer; update(data: ArrayBufferView, inputEncoding: undefined, outputEncoding: BufferEncoding): string; @@ -2883,7 +2963,7 @@ declare module "crypto" { setAAD(buffer: ArrayBufferView, options?: { plaintextLength: number }): this; getAuthTag(): Buffer; } - export interface Decipheriv { + export interface Decipheriv extends Transform { update(data: ArrayBufferView): Buffer; update(data: string, inputEncoding: BufferEncoding): Buffer; update(data: ArrayBufferView, inputEncoding: undefined, outputEncoding: BufferEncoding): string; diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts index ea568bc3f..28194c7b3 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.test.ts @@ -109,6 +109,8 @@ function match( ranges, initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(), proven: new Set(), + workers: false, + owned: new Set(), }); } @@ -250,6 +252,79 @@ test("loops to a typed array's length version the other typed arrays they index" expect(rebound).toContain("bytes.index.invalid"); }); +test("worker programs test each receiver's shared storage once in the guard", () => { + // for (let i = 0; i < n; i++) out[i] = src[i] * 2, where either view may + // be shared. + const loop = counted([ + set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2))), + ]); + const kernel = fn([loop]); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + workers: true, + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + kernel, + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const guard = body.slice(0, body.search(/^bytes\.version\.(fast|slow)\d+:$/m)); + expect(guard.match(/icmp eq ptr %\S+, null/g)).toHaveLength(2); + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + expect(fast).not.toContain("bytes.shared"); + expect(fast).not.toMatch(/(load|store) atomic double/); + // The checked copy keeps the shared-memory path for each access. + const slow = body.slice(body.search(/^bytes\.version\.slow\d+:$/m), body.indexOf(fast)); + expect(slow).toContain("load atomic double"); + expect(slow).toContain("store atomic double"); +}); + +test("index temps of a version's counter are integers in the fast copy", () => { + // for (let i = 0; i < out.length; i++) { const j = i * 4; out[i] = src[j] + src[j + 1] } + const length: IrExpr = { + kind: "bytesIntrinsic", + method: "length", + receiver: ref("out", f64s), + args: [], + type: F64, + loc, + }; + const loop = counted( + [ + { kind: "varDecl", localId: "at", init: bin("*", ref("i"), num(4)), loc }, + set( + ref("out", f64s), + ref("i"), + bin( + "+", + get(ref("src", f64s), ref("at")), + get(ref("src", f64s), bin("+", ref("at"), num(1))), + ), + ), + ], + length, + ); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + fn([loop], [local("at", F64)]), + ], + }; + const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0]; + expect(body).toContain("alloca i64 ; integer at"); + expect(fast).toMatch(/mul nsw i64 %\S+, 4/); + expect(fast).toMatch(/add nsw i64 %\S+, 1/); + expect(fast).not.toContain("fmul"); + expect(fast).not.toContain("fptosi"); + expect(fast).not.toContain("bytes.index"); +}); + test("Float16Array accesses version in elements and share the element tag", () => { const f16s: IrType = { kind: "bytes", elem: "f16" }; const withHalves = (body: IrStmt[]): IrFunction => { diff --git a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts index df8977426..8c7f668a0 100644 --- a/packages/compiler/src/backend/llvm/bytes-loop-versions.ts +++ b/packages/compiler/src/backend/llvm/bytes-loop-versions.ts @@ -6,7 +6,7 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js"; import { everyExprChild, everyStmtChild } from "../../ir/traverse.js"; import { exactInteger, widenInteger } from "./integer-values.js"; import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js"; -import { emitBytesHeaderLength } from "./expr-bytes.js"; +import { emitBytesHeaderLength, emitBytesSharedStorage } from "./expr-bytes.js"; type Node = IrExpr | IrStmt; type VarRef = IrExpr & { kind: "varRef" }; @@ -41,8 +41,13 @@ interface AffineCheck { export interface BytesLoopVersion { /** Typed-array accesses that are in bounds whenever the guard holds. */ bounds: ReadonlySet; + /** Worker programs: typed-array accesses whose views the guard proves + * thread-owned (no shared storage). */ + owned: ReadonlySet; /** Root receiver bindings, read once before the loop. */ receivers: { ref: VarRef; global: boolean }[]; + /** Receivers the guard requires to have no shared storage. */ + ownedReceivers: number[]; fixed: FixedCheck[]; affine: AffineCheck[]; /** The counted loop whose iteration space the affine checks cover. */ @@ -66,6 +71,11 @@ export interface BytesLoopFacts { initialized: ReadonlySet; /** Accesses already proved in bounds without a guard. */ proven: ReadonlySet; + /** Worker programs: an access of a view that may be shared tests the + * view's shared storage first. */ + workers: boolean; + /** Accesses already proved thread-owned by an enclosing version. */ + owned: ReadonlySet; } const MAX_COST = 400; @@ -87,6 +97,11 @@ const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]) * and last values in bounds, such as `out[base + i]` for `i < n` or * `view.getUint32(i * 4)`. * + * In worker programs, every access of a view that may be shared tests its + * shared storage. A view's storage is also fixed at construction, so the + * same guard tests each invariant receiver once, and the fast copy reads + * and writes thread-owned views directly. + * * Only small synchronous loops are versioned (nested loops included), and * the emitter bounds how deeply versions nest. The checked loop runs * whenever the guard fails, and keeps every out-of-bounds read, ignored @@ -314,10 +329,19 @@ export function matchBytesLoopVersion( } return index; }; + // Worker programs: accesses through invariant receivers, proven or not, + // become thread-owned when the guard finds no shared storage. + const owned = new Map(); const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => { - if (facts.proven.has(node)) return; const ref = root(array); if (!ref) return; + if (facts.workers && !facts.owned.has(node)) { + const receiver = receiverOf(ref); + const nodes = owned.get(receiver); + if (nodes) nodes.push(node); + else owned.set(receiver, [node]); + } + if (facts.proven.has(node)) return; const shape = counter !== null ? affine(index) : null; if (shape && shape.counter !== 0) { const operands = new Set(); @@ -383,9 +407,15 @@ export function matchBytesLoopVersion( }); const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS); const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length); - if (fixedKept.length === 0 && affineKept.length === 0) return null; + const ownedKept = [...owned.entries()].slice(0, MAX_CHECKS); + if (fixedKept.length === 0 && affineKept.length === 0 && ownedKept.length === 0) return null; const bounds = new Set(); + const ownedNodes = new Set(); const used = new Set(); + for (const [receiver, nodes] of ownedKept) { + used.add(receiver); + for (const node of nodes) ownedNodes.add(node); + } const ranges = new Map(); const counterLow = Math.max(counted?.range.min ?? 0, -bound), counterHigh = Math.min(counted?.range.max ?? 0, bound); @@ -397,7 +427,6 @@ export function matchBytesLoopVersion( used.add(group.check.receiver); for (const node of group.nodes) bounds.add(node); for (const operand of group.operands) { - if (facts.ranges.get(operand)) continue; const shape = shapes.get(operand)!; const a = shape.counter * counterLow, b = shape.counter * counterHigh; @@ -408,7 +437,14 @@ export function matchBytesLoopVersion( min += term.sign > 0 ? r.min : -r.max; max += term.sign > 0 ? r.max : -r.min; } - if (counterLow <= counterHigh) ranges.set(operand, { min, max }); + if (counterLow > counterHigh) continue; + // The guard's bound on the counter can narrow a range the operand + // already has (a loop to a length proves only `i < 2^53`), so that + // `i * 4` in the fast copy is exact integer arithmetic. + const known = facts.ranges.get(operand); + if (!known) ranges.set(operand, { min, max }); + else if (min > known.min || max < known.max) + ranges.set(operand, { min: Math.max(min, known.min), max: Math.min(max, known.max) }); } } // Renumber the receivers the kept checks use. @@ -420,7 +456,9 @@ export function matchBytesLoopVersion( (counted.range.min < -bound || counted.range.max > bound); return { bounds, + owned: ownedNodes, receivers: order.map((index) => receivers[index]!), + ownedReceivers: ownedKept.map(([receiver]) => renumber.get(receiver)!), fixed: fixedKept.map((g) => ({ receiver: renumber.get(g.check.receiver)!, end: g.check.end, @@ -439,7 +477,8 @@ export function matchBytesLoopVersion( } /** Branches to `fast` when every access the version proves is in bounds for - * every iteration, and to `slow` otherwise (including a counted loop whose + * every iteration and every view it proves thread-owned has no shared + * storage, and to `slow` otherwise (including a counted loop whose * integer form is not established, or a module receiver not yet set). */ export function emitBytesLoopVersionGuard( host: LlvmEmitterContext, @@ -473,6 +512,11 @@ export function emitBytesLoopVersionGuard( return wide; }); const conditions: string[] = []; + for (const index of version.ownedReceivers) { + const owned = B.tmp(); + B.line(`${owned} = icmp eq ptr ${emitBytesSharedStorage(host, receivers[index]!)}, null`); + conditions.push(owned); + } for (const check of version.fixed) { const ok = B.tmp(); B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`); diff --git a/packages/compiler/src/backend/llvm/emit-function.ts b/packages/compiler/src/backend/llvm/emit-function.ts index 0da08315f..b80162131 100644 --- a/packages/compiler/src/backend/llvm/emit-function.ts +++ b/packages/compiler/src/backend/llvm/emit-function.ts @@ -126,6 +126,8 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.stableCaptureIds = fn.captures !== undefined ? emitter.lazyCaptures.stableCaptures(fn.name) : new Set(); emitter.integerLoopBindings.clear(); + emitter.integerTemps.clear(); + emitter.integerTempsEnabled = emitter.debug === null && !fn.async && !fn.generator; beginFunctionLoops(emitter, fn, numericFn); emitter.speculativeLoopsEnabled = LOOPS.of(emitter).countedLoopsEnabled; emitter.speculativeFunction = fn.name; @@ -212,6 +214,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string { emitter.integerLoopRanges = rangeFacts.loopRanges; emitter.int32Elements = slotFacts.elements ?? null; emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges); + emitter.bytesOwned = new Set(); // Debug builds keep every number local a double for the debugger's // variable view. Lazily boxed captured parameters keep their double, // which the box is created from, and so do plain copies of a capture or diff --git a/packages/compiler/src/backend/llvm/emit-statements.ts b/packages/compiler/src/backend/llvm/emit-statements.ts index e2a744f07..dd5f864f9 100644 --- a/packages/compiler/src/backend/llvm/emit-statements.ts +++ b/packages/compiler/src/backend/llvm/emit-statements.ts @@ -50,6 +50,7 @@ import { emitBytesLoopVersionGuard } from "./bytes-loop-versions.js"; import { mangleLocal } from "../mangle.js"; import { LlvmUnsupportedError } from "./unsupported.js"; import type { LlValue } from "./expr-context.js"; +import { widenInteger } from "./integer-values.js"; import { SCALAR_UNION_RESULT } from "./int32-abi.js"; import { emitLiteralSwitch } from "./switch-dispatch.js"; import { @@ -268,6 +269,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { else emitter.toFrame(v); B.line(`store ${emitter.llType(b.type)} ${v.name}, ptr ${b.slot}`); emitter.storeIntegerView(s.localId, v, s.init); + storeIntegerTemp(emitter, s.localId, b, v); if (isRefCounted(b.type)) { emitter.scopes[emitter.scopes.length - 1]!.push({ slot: b.slot, type: b.type }); emitter.noteLastUseMove(s, b); @@ -518,6 +520,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { s.index, s.value, emitter.bytesBounds.has(s), + emitter.bytesOwned.has(s), ); break; } @@ -971,6 +974,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { done = B.newLabel("bytes.version.done"); emitBytesLoopVersionGuard(emitter, version, fast, slow); const originalBounds = emitter.bytesBounds, + originalOwned = emitter.bytesOwned, originalRanges = emitter.integerRanges; LOOPS.of(emitter).bytesLoopVersioning.add(s); try { @@ -979,6 +983,8 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.br(done); B.startBlock(fast); emitter.bytesBounds = new Set([...originalBounds, ...version.bounds]); + if (version.owned.size > 0) + emitter.bytesOwned = new Set([...originalOwned, ...version.owned]); if (version.ranges.size > 0) emitter.integerRanges = new Map([...originalRanges, ...version.ranges]); if (version.counted?.guarded) LOOPS.of(emitter).provenCountedGuards.add(s); @@ -986,6 +992,7 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { B.br(done); } finally { emitter.bytesBounds = originalBounds; + emitter.bytesOwned = originalOwned; emitter.integerRanges = originalRanges; LOOPS.of(emitter).provenCountedGuards.delete(s); LOOPS.of(emitter).bytesLoopVersioning.delete(s); @@ -1540,6 +1547,48 @@ export function emitStmtBody(emitter: LlEmitter, s: IrStmt): void { * Abrupt completions inside tryBody/catchBody ride the finallyStack: * returns inline copies at the return site, while break/continue use the * shared region walk before branching to their resolved target. */ +/** A constant number local whose initializer has an exact integer form + * (such as `const j = i * 4` in a typed-array loop version) keeps that + * integer in an i64 slot beside the double, so reads such as `px[j + 1]` + * index with integer arithmetic instead of converting the double back. + * Every other declaration of the local drops the integer: the same + * declaration is emitted once per loop copy, and a copy must not read the + * integer another copy stored. */ +function storeIntegerTemp( + emitter: LlEmitter, + localId: string, + b: ReturnType, + v: LlValue, +): void { + const local = b.local; + if ( + !emitter.integerTempsEnabled || + b.kind !== "local" || + !local || + local.type.kind !== "f64" || + local.mutable || + local.tdz === true || + emitter.captureIds.has(localId) || + !v.integer + ) { + emitter.integerTemps.delete(localId); + return; + } + const B = emitter.B; + let slot = emitter.integerTemps.get(localId)?.slot; + if (slot === undefined) { + slot = B.slot(); + B.entryAllocas.push(`${slot} = alloca i64 ; integer ${local.name}`); + } + B.line(`store i64 ${widenInteger(emitter, v.integer)}, ptr ${slot}`); + emitter.integerTemps.set(localId, { + slot, + type: "i64", + signed: true, + range: v.integer.range, + }); +} + export function emitTryCatch(emitter: LlEmitter, s: IrStmt & { kind: "tryCatch" }): void { const B = emitter.B; const hasCatch = s.catchBody !== null; diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 50add97c7..fdc93953e 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -1075,6 +1075,17 @@ export class LlEmitter { string, { slot: string; type: "i32" | "i64"; signed?: boolean; range?: { min: number; max: number } } >(); + /** Constant number locals whose latest emitted declaration stored an + * exact integer beside the double, such as `const j = i * 4` in a + * typed-array loop version: an i64 slot and the integer's range. */ + integerTemps = new Map< + string, + { slot: string; type: "i64"; signed: true; range: IntegerRange } + >(); + /** Integer temps are kept only in optimized synchronous functions: an + * async or generator body resumes with its locals in its frame, which + * an entry alloca is not part of. */ + integerTempsEnabled = false; /** Speculative integer loops (speculative-integers.ts) run in this function. */ speculativeLoopsEnabled = false; /** The fast copy of a speculative integer loop being emitted: its @@ -1105,6 +1116,9 @@ export class LlEmitter { /** Sums whose result only feeds 32-bit conversions (wrapping-sums.ts). */ wrappingSums = new Set(); bytesBounds: ReadonlySet = new Set(); + /** Worker programs: typed-array accesses whose views an enclosing version + * guard proved thread-owned. */ + bytesOwned: ReadonlySet = new Set(); integerViews = new Map(); /** Module globals the current function owns outright (initializer * bindings): their function-local slot, which stands in for the global. */ @@ -3816,6 +3830,8 @@ export class LlEmitter { ranges: this.integerRanges, initialized, proven: this.bytesBounds, + workers: this.mod.workers === true, + owned: this.bytesOwned, }); } @@ -6176,8 +6192,9 @@ export class LlEmitter { expr?: IrExpr, inBounds = false, invalidNaN = false, + owned = false, ): LlValue { - return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN); + return emitBytesGet(this, elem, receiver, index, expr, inBounds, invalidNaN, owned); } emitToUint32(value: string, expr?: IrExpr, uint32?: string): string { @@ -6240,8 +6257,9 @@ export class LlEmitter { indexExpr?: IrExpr, expr?: IrExpr, inBounds = false, + owned = false, ): void { - return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds); + return emitBytesSet(this, elem, receiver, index, value, indexExpr, expr, inBounds, owned); } emitBytesIntrinsic(e: IrExpr & { kind: "bytesIntrinsic" }): LlValue { diff --git a/packages/compiler/src/backend/llvm/expr-bytes.ts b/packages/compiler/src/backend/llvm/expr-bytes.ts index d36001bf6..062bfdfbe 100644 --- a/packages/compiler/src/backend/llvm/expr-bytes.ts +++ b/packages/compiler/src/backend/llvm/expr-bytes.ts @@ -176,6 +176,17 @@ export function emitBytesLength( return { name: out, type: F64 }; } +/** A view's shared backing (null for a buffer this thread owns). It is + * set at construction, before script code can see the view. */ +export function emitBytesSharedStorage(host: LlvmEmitterContext, receiver: string): string { + const B = host.B; + const slot = B.tmp(), + storage = B.tmp(); + B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`); + B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`); + return storage; +} + /** Worker programs: an element of a view of a SharedArrayBuffer (`shared` is * fixed before script code can see the view) is read and written with one * relaxed atomic access of its width, as the runtime's shared accessors do; @@ -186,12 +197,9 @@ export function emitBytesLength( function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): string { const B = host.B; host.declare(`declare i1 @llvm.expect.i1(i1, i1)`); - const slot = B.tmp(), - storage = B.tmp(), + const storage = emitBytesSharedStorage(host, receiver), isShared = B.tmp(), expected = B.tmp(); - B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`); - B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`); B.line(`${isShared} = icmp ne ptr ${storage}, null`); B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${isShared}, i1 false)`); const shared = B.newLabel("bytes.shared"), @@ -209,15 +217,17 @@ export function emitBytesGet( expr?: IrExpr, inBounds = false, invalidNaN = false, + owned = false, ): LlValue { const B = host.B; // Any invalid index traps (the array runtime's discipline). - if (!invalidNaN) return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined); + if (!invalidNaN) + return emitBytesLoad(host, elem, receiver, index, expr, inBounds, undefined, owned); // The numeric view of an ordinary read: an invalid index never touches // storage and reads as NaN — ToNumber of Node's undefined. const invalid = B.newLabel("bytes.read.invalid"); const done = B.newLabel("bytes.read.done"); - const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid); + const loaded = emitBytesLoad(host, elem, receiver, index, expr, inBounds, invalid, owned); const ok = B.newLabel("bytes.read.ok"); B.br(ok); B.startBlock(ok); @@ -260,8 +270,10 @@ function emitBytesLoad( expr: IrExpr | undefined, inBounds: boolean, invalid: string | undefined, + owned: boolean, ): LlValue { - if (host.mod.workers) { + // `owned`: an enclosing version guard proved the view thread-owned. + if (host.mod.workers && !owned) { const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds); return emitSharedOrLocalLoad(host, elem, receiver, idx); } @@ -515,13 +527,14 @@ export function emitBytesSet( indexExpr?: IrExpr, expr?: IrExpr, inBounds = false, + owned = false, ): void { const B = host.B; // Invalid integer-indexed writes are ignored after evaluating the RHS. const done = B.newLabel("bytes.store.done"); const data = host.emitBytesData(receiver); const idx = emitBytesIndex(host, receiver, index, indexExpr, done, inBounds); - if (host.mod.workers && elem === "f16") { + if (host.mod.workers && !owned && elem === "f16") { host.declare(`declare void @scr_bytes_shared_set(ptr, ${host.sizeType}, double)`); const local = emitSharedBytesSplit(host, receiver); B.line( @@ -532,10 +545,24 @@ export function emitBytesSet( } let stored: string | null; if (elem === "u8c") { - host.declare(`declare double @scr_bytes_to_u8_clamp(double)`); - const clamped = B.tmp(); + // ToUint8Clamp, as scr_bytes_to_u8_clamp computes it: NaN and values + // at most 0 store 0, values at least 255 store 255, and values between + // round to the nearest integer, ties to even. Adding and subtracting + // 2^52 rounds a value in (0, 255) that way in the default rounding mode, + // with no call or branch, so loops of clamped stores can vectorize. + const positive = B.tmp(), + high = B.tmp(), + shifted = B.tmp(), + rounded = B.tmp(), + upper = B.tmp(), + clamped = B.tmp(); stored = B.tmp(); - B.line(`${clamped} = call double @scr_bytes_to_u8_clamp(double ${value.name})`); + B.line(`${positive} = fcmp ogt double ${value.name}, 0.0`); + B.line(`${high} = fcmp oge double ${value.name}, 255.0`); + B.line(`${shifted} = fadd double ${value.name}, 0x4330000000000000`); + B.line(`${rounded} = fsub double ${shifted}, 0x4330000000000000`); + B.line(`${upper} = select i1 ${high}, double 255.0, double ${rounded}`); + B.line(`${clamped} = select i1 ${positive}, double ${upper}, double 0.0`); B.line(`${stored} = fptoui double ${clamped} to i32`); } else stored = @@ -572,7 +599,7 @@ export function emitBytesSet( ty = "i32"; operand = stored!; } - if (host.mod.workers) { + if (host.mod.workers && !owned) { // A shared view's element takes a relaxed atomic store of its width. const local = emitSharedBytesSplit(host, receiver); const p = B.tmp(); @@ -721,6 +748,7 @@ export function emitBytesIntrinsic( e.args[0], host.bytesBounds.has(e), e.invalidNaN === true, + host.bytesOwned.has(e), ); case "slice": return call( diff --git a/packages/compiler/src/backend/llvm/expr-primitives.ts b/packages/compiler/src/backend/llvm/expr-primitives.ts index 12e488b6d..354004e9e 100644 --- a/packages/compiler/src/backend/llvm/expr-primitives.ts +++ b/packages/compiler/src/backend/llvm/expr-primitives.ts @@ -59,7 +59,8 @@ export function emitLiteralExpr( throw new InternalCompilerError(`llvm emitter bug: bare unitLit '${e.unit}'`); case "varRef": { host.materializeSplitLocal(e.localId); - const integerSlot = host.integerLoopBindings.get(e.localId); + const integerSlot = + host.integerLoopBindings.get(e.localId) ?? host.integerTemps.get(e.localId); if (integerSlot !== undefined) { const integer = B.tmp(); const number = B.tmp(); diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index 8bb8bc3aa..c08d7b720 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -448,6 +448,7 @@ export const LIB_FN_SYMS: Record = { "buffer.poolFromArray": "scr_buffer_from_arr", "buffer.poolFromDyn": "scr_buffer_from_dyn_pooled", "buffer.poolFromDynRef": "scr_buffer_from_dyn_ref_pooled", + "buffer.poolFromDynOffset": "scr_buffer_from_dyn_offset_pooled", "buffer.poolSize": "scr_buffer_pool_size", "buffer.setPoolSize": "scr_buffer_set_pool_size", "buffer.byteLenDyn": "scr_buffer_byte_length_dyn", @@ -615,10 +616,17 @@ export const LIB_FN_SYMS: Record = { "crypto.keyObjectInput": "scr_crypto_key_object_input", "crypto.isKeyObject": "scr_crypto_is_key_object", "webcrypto.importKey": "scr_webcrypto_import_key", + "webcrypto.generateKey": "scr_webcrypto_generate_key", + "webcrypto.exportKey": "scr_webcrypto_export_key", + "webcrypto.encrypt": "scr_webcrypto_encrypt", + "webcrypto.decrypt": "scr_webcrypto_decrypt", + "webcrypto.deriveBits": "scr_webcrypto_derive_bits", + "webcrypto.deriveKey": "scr_webcrypto_derive_key", "webcrypto.sign": "scr_webcrypto_sign", "webcrypto.verify": "scr_webcrypto_verify", "webcrypto.digest": "scr_webcrypto_digest", "webcrypto.isCryptoKey": "scr_webcrypto_is_crypto_key", + "webcrypto.object": "scr_webcrypto_object", "crypto.randomUUIDv7": "scr_crypto_random_uuid_v7", "crypto.pbkdf2": "scr_crypto_pbkdf2", "crypto.hkdf": "scr_crypto_hkdf", @@ -754,6 +762,7 @@ export const LIB_FN_SYMS: Record = { "procStream.write": "scr_proc_stream_write", "stream.stdioWritable": "scr_stream_stdio_writable", "stream.unboxDyn": "scr_stream_unbox_dyn", + "stream.handleStream": "scr_stream_handle", "stream.pipeDyn": "scr_stream_pipe_dyn", "stream.unpipeDyn": "scr_stream_unpipe_dyn", "process.kill": "scr_process_kill_named", @@ -997,12 +1006,15 @@ export const LIB_FN_SYMS: Record = { "dyn.nativeSetIs": "scr_dyn_native_set_is", "dyn.nativeMapIs": "scr_dyn_native_map_is", "dyn.nativeUrlIs": "scr_dyn_native_url_is", - "dyn.nativeDateIs": "scr_dyn_native_date_is", + "dyn.nativeDateIs": "scr_dyn_date_instance_is", + "dyn.nativeDateHandleIs": "scr_dyn_native_date_is", "dyn.globalConstructor": "scr_dyn_global_constructor", "dyn.builtinFunction": "scr_dyn_builtin_function", "date.nativeNew": "scr_dyn_native_date_new", "date.nativeNewFields": "scr_dyn_native_date_new_fields", "date.nativeNewValue": "scr_dyn_native_date_new_value", + "date.argumentsTime": "scr_dyn_date_arguments_time", + "date.classOperation": "scr_date_class_operation_install", "date.checkedValue": "scr_dyn_native_date_value", "dyn.nativeRegexIs": "scr_dyn_native_regex_is", "dyn.toString": "scr_dyn_to_string_argument", @@ -1037,6 +1049,9 @@ export const LIB_FN_SYMS: Record = { "atomics.op": "scr_atomics_op", "atomics.notify": "scr_atomics_notify", "atomics.wait": "scr_atomics_wait", + "atomics.opDyn": "scr_atomics_op_dyn", + "atomics.waitDyn": "scr_atomics_wait_dyn", + "atomics.notifyDyn": "scr_atomics_notify_dyn", "worker.new": "scr_worker_new", "worker.isMainThread": "scr_context_is_main", "worker.threadId": "scr_context_thread_number", @@ -1478,6 +1493,10 @@ export const LIB_FN_SYMS: Record = { "dyn.arrayPrototype": "scr_dyn_array_prototype", "dyn.arrayConstructor": "scr_dyn_array_constructor", "dyn.objectPrototype": "scr_dyn_object_prototype", + "dyn.errorPrototype": "scr_dyn_error_prototype", + "dyn.inheritsErrorPrototype": "scr_dyn_inherits_error_prototype", + "dyn.errorCaptureStackTrace": "scr_dyn_error_capture_stack_trace", + "dyn.errorCaptureStackTraceFunction": "scr_dyn_error_capture_stack_trace_function", "dyn.functionApply": "scr_dyn_function_apply", "dyn.builtinMethod": "scr_dyn_builtin_method", "dyn.classPrototype": "scr_dyn_class_prototype", @@ -1573,6 +1592,11 @@ export const USES_TIMERS_LIB_FNS = new Set([ // ecdh.setPublicKey reports DEP0031 on the next tick. "crypto.ecdhNew", // WebCrypto results settle through the deferred crypto queue. + "webcrypto.generateKey", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.deriveBits", + "webcrypto.deriveKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest", diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts index d3b4b1f5b..5d198d306 100644 --- a/packages/compiler/src/backend/llvm/lib-stream.ts +++ b/packages/compiler/src/backend/llvm/lib-stream.ts @@ -2,6 +2,7 @@ import { InternalCompilerError } from "../../errors.js"; import type { LlvmEmitterContext, LibCallExpr, LlValue } from "./expr-context.js"; import { f64Lit } from "./common.js"; +import { pipelinePumpedRuns } from "../../ir/builtin-signatures.js"; const STREAM_CANONICAL_CBS: Record = { readable: ["r", "d"], @@ -662,14 +663,14 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV e.fn === "stream.pipelineStages" || e.fn === "stream.pipelineStagesDyn" ) { - // (n, at, k, flags, mask, s1..sn, stage1..stagek, (next, read, - // return)?, box?, (signal, abort)?[, cb]): the streams and the stage - // closures ride stack arrays; absent groups pass null. - const [nArg, atArg, kArg, flagsArg, maskArg] = e.args; + // (n, k, positions, flags, mask, s1..sn, stage1..stagek, helpers..., + // box?, (signal, abort)?[, cb]): the streams, the stage closures and + // the helpers ride stack arrays; absent groups pass null. + const [nArg, kArg, positionsArg, flagsArg, maskArg] = e.args; if ( nArg?.kind !== "numLit" || - atArg?.kind !== "numLit" || kArg?.kind !== "numLit" || + positionsArg?.kind !== "numLit" || flagsArg?.kind !== "numLit" || maskArg?.kind !== "numLit" ) @@ -677,6 +678,7 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV const n = nArg.value; const k = kArg.value; const flags = flagsArg.value; + const helperCount = 3 * pipelinePumpedRuns(n + k, positionsArg.value); const args = e.args.map((a) => host.emitExpr(a)); const packed = (from: number, count: number): string => { if (count === 0) return "null"; @@ -691,20 +693,17 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV }; const streams = packed(5, n); const stages = packed(5 + n, k); - let at = 5 + n + k; + const helpers = packed(5 + n + k, helperCount); + let at = 5 + n + k + helperCount; const take = (present: boolean): string => (present ? args[at++]!.name : "null"); - const hasHelpers = (flags & 8) !== 0; - const next = take(hasHelpers); - const read = take(hasHelpers); - const ret = take(hasHelpers); - const box = take((flags & 16) !== 0); + const box = take(((flags >> 1) & 3) === 2); const signal = take((flags & 32) !== 0); const abort = take((flags & 32) !== 0); const head = - `double ${f64Lit(n)}, ptr ${streams}, double ${f64Lit(atArg.value)}, double ${f64Lit(k)}, ` + - `ptr ${stages}, double ${f64Lit(flags)}, double ${f64Lit(maskArg.value)}, ` + - `ptr ${next}, ptr ${read}, ptr ${ret}, ptr ${box}, ptr ${signal}, ptr ${abort}`; - const params = "double, ptr, double, double, ptr, double, double, ptr, ptr, ptr, ptr, ptr, ptr"; + `double ${f64Lit(n)}, ptr ${streams}, double ${f64Lit(k)}, ptr ${stages}, ` + + `double ${f64Lit(positionsArg.value)}, double ${f64Lit(flags)}, double ${f64Lit(maskArg.value)}, ` + + `ptr ${helpers}, ptr ${box}, ptr ${signal}, ptr ${abort}`; + const params = "double, ptr, double, ptr, double, double, double, ptr, ptr, ptr, ptr"; const t = B.tmp(); if (e.fn === "sp.pipelineStages") { host.declare(`declare ptr @scr_sp_pipeline_stages(${params})`); diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts index a476e1f7f..55301948b 100644 --- a/packages/compiler/src/backend/native-link-info-core.ts +++ b/packages/compiler/src/backend/native-link-info-core.ts @@ -84,8 +84,9 @@ export interface NativeLinkFeatures { foreignFfi: boolean; nodeTest: boolean; tls: boolean; - /** crypto.createCipheriv/createDecipheriv: scr_cipher.c plus the mbedTLS - * archive. */ + /** crypto.createCipheriv/createDecipheriv and WebCrypto's encrypt and + * decrypt: scr_cipher.c plus the mbedTLS archive (and, with webcrypto, + * scr_webcrypto_aes.c). */ cipher: boolean; /** crypto.createECDH and ECDH.convertKey: scr_ecdh.c plus the mbedTLS * archive. */ diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts index 06a41c9b2..8df233585 100644 --- a/packages/compiler/src/backend/native/contracts.ts +++ b/packages/compiler/src/backend/native/contracts.ts @@ -243,8 +243,9 @@ export interface CcOptions { * The unit also compiles whenever `tls` does — scr_tls.c consults its * default-set override and shared Windows-certificate enumerator. */ tlsCa?: boolean; - /** crypto.createCipheriv/createDecipheriv: compiles scr_cipher.c and links - * the mbedTLS archive (shared with tls). */ + /** crypto.createCipheriv/createDecipheriv and WebCrypto's encrypt and + * decrypt: compiles scr_cipher.c (and, with webcrypto, + * scr_webcrypto_aes.c) and links the mbedTLS archive (shared with tls). */ cipher?: boolean; /** crypto.createECDH and ECDH.convertKey: compiles scr_ecdh.c and links * the mbedTLS archive (shared with tls). */ diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts index a551ad600..229b2f967 100644 --- a/packages/compiler/src/backend/native/executable.ts +++ b/packages/compiler/src/backend/native/executable.ts @@ -297,6 +297,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"), unit((u) => u.opts.nodeTest, "scr_test.c"), unit((u) => u.opts.webcrypto, "scr_webcrypto.c"), + // The crypto global's object extends scr_async_dyn.c's. + unit((u) => u.opts.webcrypto && u.opts.dynAsync, "scr_webcrypto_object.c"), // The CA-store unit rides its own gate OR the tls one: scr_tls.c // references its default-set override unconditionally. unit((u) => u.tlsCa, "scr_tls_ca.c"), @@ -308,6 +310,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [ ...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []), ...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []), ...(u.ecdh ? [u.rt(join(u.rtDir, "scr_ecdh.c"))] : []), + ...(u.cipher && u.opts.webcrypto ? [u.rt(join(u.rtDir, "scr_webcrypto_aes.c"))] : []), u.tlsArchive, // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h). // The unconditional win32 libs above do not carry it. diff --git a/packages/compiler/src/backend/runtime-abi.ts b/packages/compiler/src/backend/runtime-abi.ts index 5eb24482b..f02510338 100644 --- a/packages/compiler/src/backend/runtime-abi.ts +++ b/packages/compiler/src/backend/runtime-abi.ts @@ -2,8 +2,8 @@ * of the link contract: a program object keeps this symbol undefined and the * matching runtime defines it, so incompatible manual links fail before the * program can start. */ -export const RUNTIME_ABI_VERSION = 24 as const; -export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v24" as const; +export const RUNTIME_ABI_VERSION = 25 as const; +export const RUNTIME_ABI_MARKER = "scr_runtime_abi_v25" as const; /** Object consumption is useful today, but the complete scr_* surface may * still change before 1.0. The versioned marker prevents accidental mixing; diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts index fc7ed9efa..8bdbc8492 100644 --- a/packages/compiler/src/backend/runtime-pack-targets.test.ts +++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts @@ -52,7 +52,7 @@ async function fixture(target: NativeTargetSpec, workers = false) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: ["SCR_LIB"], sanitizer: "external-toolchain-required" }, flavors: { diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts index 5ffb4797d..a3ac9fe5d 100644 --- a/packages/compiler/src/backend/runtime-pack.test.ts +++ b/packages/compiler/src/backend/runtime-pack.test.ts @@ -124,7 +124,7 @@ async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, compiler: { command: "clang", identity: "fixture clang", diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts index b7049f6fe..eb70a0d2c 100644 --- a/packages/compiler/src/backend/target-diagnostics.ts +++ b/packages/compiler/src/backend/target-diagnostics.ts @@ -208,6 +208,14 @@ export function moduleWasiUnavailableSurface( }; return false; } + if (node.fn === "webcrypto.encrypt" || node.fn === "webcrypto.decrypt") { + found = { + surface: + "WebCrypto encrypt and decrypt (the wasm32-wasi runtime does not include the mbedTLS library)", + loc, + }; + return false; + } if (node.fn === "crypto.ecdhNew" || node.fn === "crypto.ecdhConvertKey") { found = { surface: "ECDH (the wasm32-wasi runtime does not include the mbedTLS library)", diff --git a/packages/compiler/src/contracts/check.ts b/packages/compiler/src/contracts/check.ts index 5934624bf..c80468961 100644 --- a/packages/compiler/src/contracts/check.ts +++ b/packages/compiler/src/contracts/check.ts @@ -174,7 +174,8 @@ class ContractChecker { return `${owner}.${method[2]}`; } if (fn?.sourceName !== undefined && fn.sourceName !== "") return fn.sourceName; - if (fn !== undefined && /^%fn\d+$/.test(fnName)) return `the function at ${this.where(fn.loc)}`; + if (fn !== undefined && /^%fn\.[0-9a-f]{12}$/.test(fnName)) + return `the function at ${this.where(fn.loc)}`; return fnName.replace(/^%/, ""); } diff --git a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts index 6d539212e..0ded460da 100644 --- a/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts +++ b/packages/compiler/src/frontend/lowering/array-indexed-mutation.ts @@ -9,6 +9,7 @@ import { type SrcLoc, typeEquals, } from "../../ir/ir.js"; +import { keyedHelperName } from "./stable-names.js"; import { numLit, varRef } from "../../ir/build.js"; import { typeKey } from "../type-mapper.js"; import type { Lowerer } from "./lowerer.js"; @@ -125,7 +126,7 @@ export function lowerArrayFill( writeNull; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = "%arr.fill." + lowerer.arrHofHelpers.size; + name = keyedHelperName(lowerer, "%arr.fill", key); lowerer.arrHofHelpers.set(key, name); const a = varRef("a.0", arrType, loc); const n = varRef("n.0", F64, loc); diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index 0ca8503df..314cdfd24 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; import { dynUndefinedExpr, helperLoc, varRef } from "../../ir/build.js"; import { @@ -252,7 +253,7 @@ export function lowerSafeIndexRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = `%arr.idxOr.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.idxOr", key); lowerer.arrHofHelpers.set(key, name); const arrT = arr.type; lowerer.liftedFns.push({ @@ -298,7 +299,7 @@ export function lowerSafeBytesRead( let name = lowerer.arrHofHelpers.get(key); if (!name) { const at = helperLoc(loc); - name = `%bytes.idxOr.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%bytes.idxOr", key); lowerer.arrHofHelpers.set(key, name); const b = varRef("b.0", bytesT, at); const i = varRef("i.0", F64, at); diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts index 83b9aa6f9..1987c86ba 100644 --- a/packages/compiler/src/frontend/lowering/binding-analysis.ts +++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts @@ -10,7 +10,13 @@ import { isGenericCallableMemberType } from "../type-mapper.js"; * Sound file-locally for module-scope bindings because ESM import * bindings are read-only — no other file can write one. Cached per * symbol (the scan walks the whole file once). */ -export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): boolean { +export function bindingNeverReassigned( + lowerer: Lowerer, + sym: ts.Symbol, + decl: ts.Node, + resolve: (ident: ts.Identifier) => ts.Symbol | null | undefined = (ident) => + lowerer.resolveValueSymbol(ident), +): boolean { const cached = lowerer.neverReassignedCache.get(sym); if (cached !== undefined) return cached; let written = false; @@ -18,7 +24,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t // identifiers pay a symbol resolution. const symText = ts.symbolName(sym); const namesSym = (e: ts.Node): boolean => - ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym; + ts.isIdentifier(e) && e.text === symText && resolve(e) === sym; const scanTarget = (t: ts.Expression): void => { let e: ts.Expression = t; while (ts.isParenthesizedExpression(e)) e = e.expression; diff --git a/packages/compiler/src/frontend/lowering/builtins/arguments.ts b/packages/compiler/src/frontend/lowering/builtins/arguments.ts index 7f862f603..d2fc430dd 100644 --- a/packages/compiler/src/frontend/lowering/builtins/arguments.ts +++ b/packages/compiler/src/frontend/lowering/builtins/arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { BYTES_U8, BOOL, @@ -72,7 +73,7 @@ export function lowerOptionalNumberPredicate( const key = `number.optionalPredicate:${fn}:${typeKey(widened)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.optionalPredicate.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.optionalPredicate", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/atomics.ts b/packages/compiler/src/frontend/lowering/builtins/atomics.ts index 41e35a511..5c72ed08c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/atomics.ts +++ b/packages/compiler/src/frontend/lowering/builtins/atomics.ts @@ -60,10 +60,14 @@ export function lowerAtomicsCall( // has a concrete typed-array binding. Validate the actual lowered storage. const array = lowerer.lowerExpr(source); const type = array.type; + // An untyped array (a JavaScript `workerData` or field): the runtime + // checks its kind and throws Node's TypeError for any other value. + const dynamic = type?.kind === "dyn"; if ( - type?.kind !== "bytes" || - !["i8", "u8", "i16", "u16", "i32", "u32"].includes(type.elem) || - ((member === "wait" || member === "notify") && type.elem !== "i32") + !dynamic && + (type?.kind !== "bytes" || + !["i8", "u8", "i16", "u16", "i32", "u32"].includes(type.elem) || + ((member === "wait" || member === "notify") && type.elem !== "i32")) ) { lowerer.noLowering( `Atomics.${member} over this array`, @@ -82,7 +86,7 @@ export function lowerAtomicsCall( if (member === "wait") return { kind: "libCall", - fn: "atomics.wait", + fn: dynamic ? "atomics.waitDyn" : "atomics.wait", args: [array, index, arg(2, 0), arg(3, Infinity)], type: STRING, loc, @@ -90,14 +94,14 @@ export function lowerAtomicsCall( if (member === "notify") return { kind: "libCall", - fn: "atomics.notify", + fn: dynamic ? "atomics.notifyDyn" : "atomics.notify", args: [array, index, arg(2, Infinity)], type: F64, loc, }; return { kind: "libCall", - fn: "atomics.op", + fn: dynamic ? "atomics.opDyn" : "atomics.op", args: [array, index, arg(2, 0), arg(3, 0), number(operation)], type: F64, loc, diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts index 5590fbe9f..d0991dce5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-async.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; @@ -218,7 +219,7 @@ function execFileAsyncHelper( const key = shell ? "execAsync" : "execFileAsync"; const existing = lowerer.valueHelpers.get(key); if (existing) return { name: existing, shapeId, envShapeId }; - const name = `%${key}.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%${key}`, key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const envRecT: IrType = { kind: "record", shapeId: envShapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts index 24adcc1ba..b9858e768 100644 --- a/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts +++ b/packages/compiler/src/frontend/lowering/builtins/child-process-sync.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { recordToEnvPairs } from "./environment.js"; import { lowerChildArgsArg } from "./child-process.js"; import { boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -541,7 +542,7 @@ function execStdioModeHelper( const key = `cp.stdiomode:${shapeId}:${fd}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%cp.stdioMode.${fd}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%cp.stdioMode.${fd}`, key); lowerer.arrHofHelpers.set(key, name); const optsT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts index f4270dbe8..93c8498ef 100644 --- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts @@ -7,6 +7,7 @@ import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; import { CIPHER_TYPE_NAMES } from "../../type-mapper.js"; import { CRYPTO_CIPHERS, CRYPTO_CURVES, CRYPTO_HASHES } from "../crypto-tables.js"; +import { stdlibGlobalNameOf } from "../surfaces.js"; import { BOOL, BYTES_U8, @@ -907,7 +908,8 @@ export function v8CalleeText(node: ts.Expression): string { /** Calls on native Cipheriv/Decipheriv handles dispatch through the * handle's methods with checked-dynamic arguments. The stream.Transform - * members the classes inherit are refused. */ + * members the classes inherit lower over the cipher's Transform + * (lowerHashStreamCall, which runs first); other members are refused. */ export function lowerCipherMethodCall( lowerer: Lowerer, call: ts.CallExpression, @@ -920,7 +922,7 @@ export function lowerCipherMethodCall( lowerer.noLowering( `Cipheriv/Decipheriv.${name}`, call, - "update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not", + "update(), final(), setAAD(), setAutoPadding(), getAuthTag(), setAuthTag(), and the stream members are supported", ); } if (call.arguments.nodes.some(ts.isSpreadElement)) @@ -1316,8 +1318,10 @@ export function lowerCryptoGetRandomValues( } /** Whether `receiver` names a WebCrypto Crypto object: the `crypto` - * global (also through globalThis) or node:crypto's `webcrypto`. */ + * global (also through globalThis or an alias of it) or node:crypto's + * `webcrypto`. */ export function isWebCryptoReceiver(lowerer: Lowerer, receiver: ts.Expression): boolean { + if (stdlibGlobalNameOf(lowerer, receiver) === "crypto") return true; if (ts.isIdentifier(receiver)) { if (receiver.text === "crypto" && lowerer.isStdlibGlobal(receiver, "crypto")) return true; const imported = lowerer.builtinImportOf(receiver); diff --git a/packages/compiler/src/frontend/lowering/builtins/date-classes.ts b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts new file mode 100644 index 000000000..a2b0fc20a --- /dev/null +++ b/packages/compiler/src/frontend/lowering/builtins/date-classes.ts @@ -0,0 +1,552 @@ +import * as ts from "../../ts7/adapter.js"; +import { type Lowerer } from "../lowerer.js"; +import { locOf } from "../../program.js"; +import { type ClassInfo } from "../lower-classes.js"; +import { DATE_METHOD_HINT, dateJsonType, dateValueMethod } from "./dates.js"; +import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; +import { + BOOL, + DATE_T, + DYN, + F64, + NULL_T, + STRING, + VOID, + typeKey, + type IrExpr, + type IrStmt, + type IrType, + type SrcLoc, +} from "../../../ir/ir.js"; + +/** The hidden field holding the time value of a class declared `extends + * Date`, the first field of its layout. */ +export const DATE_TIME_FIELD = "%time"; + +/** The class declared `extends Date` at the root of `info`'s base chain, or + * null. */ +export function dateRootOf(info: ClassInfo | null | undefined): ClassInfo | null { + for (let c = info ?? null; c; c = c.base) if (c.extendsDate) return c; + return null; +} + +/** Whether the program declares a class that extends Date. */ +export function programDeclaresDateClass(lowerer: Lowerer): boolean { + for (const info of lowerer.classes.values()) if (info.extendsDate) return true; + return false; +} + +/** The Date.prototype methods a class declared `extends Date` inherits. + * Those without a lowering refuse at the call. */ +const DATE_PROTOTYPE_METHODS: ReadonlySet = new Set([ + "getTime", + "valueOf", + "toISOString", + "toUTCString", + "toJSON", + "toString", + "toDateString", + "toTimeString", + "toGMTString", + "toLocaleString", + "toLocaleDateString", + "toLocaleTimeString", + "getFullYear", + "getUTCFullYear", + "getYear", + "getMonth", + "getUTCMonth", + "getDate", + "getUTCDate", + "getDay", + "getUTCDay", + "getHours", + "getUTCHours", + "getMinutes", + "getUTCMinutes", + "getSeconds", + "getUTCSeconds", + "getMilliseconds", + "getUTCMilliseconds", + "getTimezoneOffset", + "setTime", + "setFullYear", + "setUTCFullYear", + "setYear", + "setMonth", + "setUTCMonth", + "setDate", + "setUTCDate", + "setHours", + "setUTCHours", + "setMinutes", + "setUTCMinutes", + "setSeconds", + "setUTCSeconds", + "setMilliseconds", + "setUTCMilliseconds", +]); + +export const DATE_PROTOTYPE_METHOD_NAMES: readonly string[] = [...DATE_PROTOTYPE_METHODS]; + +export function isDatePrototypeMethod(name: string): boolean { + return DATE_PROTOTYPE_METHODS.has(name); +} + +/** Whether `name` is a Date method an instance of `info` inherits from + * Date.prototype: its class chain is rooted at a class declared `extends + * Date` and declares no member of that name. The runtime's view of the + * instance answers it (scr_dyn_date_class_method). */ +export function dateInheritedMember(lowerer: Lowerer, info: ClassInfo, name: string): boolean { + return ( + dateRootOf(info) !== null && + DATE_PROTOTYPE_METHODS.has(name) && + !lowerer.findMethodOn(info, name) && + !info.fields.has(name) + ); +} + +/** super(...args) in a class declared `extends Date`: the time value the + * Date constructor computes from the argument vector `args` (one dyn + * array), stored in the hidden field. */ +export function dateSuperCall(thisRef: IrExpr, info: ClassInfo, args: IrExpr, loc: SrcLoc): IrStmt { + return { + kind: "fieldSet", + obj: thisRef, + className: info.def.name, + field: DATE_TIME_FIELD, + value: { kind: "libCall", fn: "date.argumentsTime", args: [args], type: DATE_T, loc }, + loc, + }; +} + +/** The time value of `receiver`, an instance of a class rooted at `root`. */ +function timeOf(lowerer: Lowerer, receiver: IrExpr, root: ClassInfo, loc: SrcLoc): IrExpr { + return { + kind: "fieldGet", + obj: lowerer.upcastTo(receiver, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + type: DATE_T, + loc, + }; +} + +/** `receiver.name()` with no arguments where the static class `info` does + * not declare `name`: the subclasses of `info` that declare it run theirs, + * any other instance the Date method. `receiver` is a variable read. */ +function instanceMethodCall( + lowerer: Lowerer, + blame: ts.Node, + info: ClassInfo, + receiver: IrExpr, + name: string, + inherited: IrExpr, + loc: SrcLoc, +): IrExpr { + const found = lowerer.findMethodOn(info, name); + if (found) { + if (found.sig.params.length > 0) + lowerer.unsupported( + "SC1090", + blame, + `a '${name}' declaration with parameters in a class that extends Date, called by an inherited Date method`, + ); + const args: IrExpr[] = []; + if (lowerer.overrideBelow(info, name)) { + lowerer.noteVirtualEdge(info, name); + return { + kind: "virtualCall", + className: info.def.name, + method: name, + args: [lowerer.upcastTo(receiver, info.def.name), ...args], + type: found.sig.ret, + loc, + }; + } + lowerer.noteEdge(`%${found.declarer.def.name}.${name}`); + return { + kind: "call", + callee: `%${found.declarer.def.name}.${name}`, + args: [lowerer.upcastTo(receiver, found.declarer.def.name), ...args], + type: found.sig.ret, + loc, + }; + } + let result = inherited; + // Deeper declarations are tested first. + const declaring: ClassInfo[] = []; + const visit = (c: ClassInfo): void => { + for (const sub of c.subclasses) { + if (sub.methods.has(name)) declaring.push(sub); + else visit(sub); + } + }; + visit(info); + for (const sub of declaring) { + const call = instanceMethodCall( + lowerer, + blame, + sub, + { kind: "downcast", value: receiver, type: { kind: "object", className: sub.def.name }, loc }, + name, + inherited, + loc, + ); + const type = result.type; + result = { + kind: "ternary", + cond: { kind: "instanceOf", value: receiver, className: sub.def.name, type: BOOL, loc }, + then: lowerer.coerceToExpected(call, type), + else_: result, + type, + loc, + }; + } + return result; +} + +/** ToNumber of `value`, the lowered value of `node`. */ +function numberValue(lowerer: Lowerer, node: ts.Expression, value: IrExpr, loc: SrcLoc): IrExpr { + if (value.type.kind === "f64") return value; + if (value.type.kind === "dyn") + return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [value], type: F64, loc }; + if (lowerer.dynConvertible(value.type)) + return { + kind: "libCall", + fn: "dyn.toNumberCoerce", + args: [{ kind: "dynFrom", value, type: DYN, loc }], + type: F64, + loc, + }; + return lowerer.coerceInto(node, value, F64); +} + +/** A Date.prototype method a class rooted at `root` does not declare, + * called on `receiver` (an instance of `info`, evaluated here once). The + * method reads or sets the hidden time value; toJSON calls the instance's + * toISOString, as Date.prototype.toJSON does. */ +export function lowerInheritedDateMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + info: ClassInfo, + receiver: IrExpr, +): IrExpr { + const root = dateRootOf(info)!; + const loc = locOf(call); + const local = lowerer.declareHiddenLocal("%dateReceiver", receiver.type); + const value = varRef(local.id, receiver.type, loc); + const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }]; + const args = call.arguments.nodes; + if (args.some(ts.isSpreadElement)) + lowerer.unsupported("SC1090", call, `spread arguments to 'Date.prototype.${name}'`); + const done = (result: IrExpr): IrExpr => ({ + kind: "seqExpr", + stmts, + result, + type: result.type, + loc, + }); + if (name === "setTime") { + // Every argument is evaluated before setTime converts the first one. + let ms: IrExpr = { kind: "numLit", value: Number.NaN, type: F64, loc }; + if (args[0]) { + const first = lowerer.lowerExpr(args[0]); + const saved = lowerer.declareHiddenLocal("%dateArgument", first.type); + stmts.push({ kind: "varDecl", localId: saved.id, init: first, loc: locOf(args[0]) }); + ms = numberValue(lowerer, args[0], varRef(saved.id, first.type, loc), loc); + } + for (const arg of args.slice(1)) + stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }); + const time = lowerer.declareHiddenLocal("%dateTime", DATE_T); + stmts.push( + { + kind: "varDecl", + localId: time.id, + init: { kind: "libCall", fn: "date.newMs", args: [ms], type: DATE_T, loc }, + loc, + }, + { + kind: "fieldSet", + obj: lowerer.upcastTo(value, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + value: varRef(time.id, DATE_T, loc), + loc, + }, + ); + return done({ + kind: "libCall", + fn: "date.getTime", + args: [varRef(time.id, DATE_T, loc)], + type: F64, + loc, + }); + } + for (const arg of args) { + if (name !== "toJSON") + lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); + stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }); + } + if (name === "toJSON") return done(dateClassToJson(lowerer, call, info, value, loc)); + const result = dateValueMethod(name, timeOf(lowerer, value, root, loc), loc); + if (!result) lowerer.noLowering(`Date.prototype.${name}`, call, DATE_METHOD_HINT); + return done(result); +} + +/** Date.prototype.toJSON on `value` (a variable read of an instance of + * `info`): null for an invalid time, else the instance's toISOString(). */ +function dateClassToJson( + lowerer: Lowerer, + blame: ts.Node, + info: ClassInfo, + value: IrExpr, + loc: SrcLoc, +): IrExpr { + const root = dateRootOf(info)!; + const time: IrExpr = { + kind: "libCall", + fn: "date.getTime", + args: [timeOf(lowerer, value, root, loc)], + type: F64, + loc, + }; + const type = dateJsonType(lowerer); + const iso = instanceMethodCall( + lowerer, + blame, + info, + value, + "toISOString", + { + kind: "libCall", + fn: "date.toISOStringValue", + args: [timeOf(lowerer, value, root, loc)], + type: STRING, + loc, + }, + loc, + ); + return { + kind: "ternary", + // An invalid date's time value is NaN, the one value unequal to itself. + cond: { kind: "bin", op: "!==", left: time, right: time, type: BOOL, loc }, + then: lowerer.coerceToExpected({ kind: "unitLit", unit: "null", type: NULL_T, loc }, type), + else_: lowerer.coerceToExpected(iso, type), + type, + loc, + }; +} + +/** `super.name(...)` in a class whose base chain declares no `name`, when + * the chain is rooted at a class declared `extends Date`. */ +export function lowerDateSuperMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, + cls: ClassInfo, + thisRef: IrExpr, +): IrExpr | null { + if (!dateRootOf(cls) || !isDatePrototypeMethod(name)) return null; + // Date.prototype.toJSON reads this.toISOString, which this class or a + // subclass may declare. + return lowerInheritedDateMethodCall(lowerer, call, name, cls, thisRef); +} + +const DATE_CLASS_TIME = "%date.classTime"; +const DATE_CLASS_METHOD = "%date.classMethod"; + +/** The classes of a program rooted at a class declared `extends Date`. */ +function dateClasses(lowerer: Lowerer): ClassInfo[] { + return [...lowerer.classes.values()].filter( + (info) => !info.def.runtime && !info.generic && dateRootOf(info) !== null, + ); +} + +const PARAMS = ["op", "value", "argument"]; + +/** A lifted `(op, value, argument)` function over checked-dynamic values. */ +function liftOperation(lowerer: Lowerer, name: string, body: IrStmt[], loc: SrcLoc): void { + lowerer.liftedFns.push({ + name, + params: PARAMS.map((p) => ({ localId: p, name: p, type: DYN })), + returnType: DYN, + locals: PARAMS.map((p) => ({ id: p, name: p, type: DYN, mutable: false })), + body, + loc, + }); +} + +function opIs(code: number, loc: SrcLoc): IrExpr { + return { + kind: "bin", + op: "===", + left: { kind: "dynCheck", value: varRef("op", DYN, loc), type: F64, loc }, + right: { kind: "numLit", value: code, type: F64, loc }, + type: BOOL, + loc, + }; +} + +/** Lifts `%date.classMethod(op, value, argument)` when the program declares + * a class that extends Date: ops 2 to 5 call `value`'s toISOString, + * toJSON(argument), toString and valueOf as checked-dynamic method calls, + * which the class dispatch resolves to each class's declarations and the + * runtime to the inherited Date methods. Called before the class dispatch + * runs over the lowered functions. */ +export function noteDateClassMethods(lowerer: Lowerer): void { + if (lowerer.liftedFnNamed(DATE_CLASS_METHOD)) return; + const classes = dateClasses(lowerer); + if (classes.length === 0) return; + const loc = classes[0]!.def.loc; + const value = varRef("value", DYN, loc); + const body: IrStmt[] = ["toISOString", "toJSON", "toString", "valueOf"].map( + (method, index): IrStmt => ({ + kind: "if", + cond: opIs(index + 2, loc), + then: [ + { + kind: "return", + value: { + kind: "dynInvoke", + recv: value, + method, + calleeName: method, + args: method === "toJSON" ? [varRef("argument", DYN, loc)] : [], + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }), + ); + body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); + liftOperation(lowerer, DATE_CLASS_METHOD, body, loc); +} + +/** %main's installation of the functions the runtime calls for a + * checked-dynamic value that holds an instance of a class declared `extends + * Date` (scr_date_class_operation_install). `%date.classTime(op, value, + * argument)` answers the time value for op 0 (undefined for any other + * value), sets it from the number `argument` for op 1 and answers the + * class's name for op 6; `%date.classMethod` (noteDateClassMethods) calls + * its methods. */ +export function dateClassOperationPrelude(lowerer: Lowerer, loc: SrcLoc): IrStmt[] { + const classes = dateClasses(lowerer); + if (classes.length === 0) return []; + noteDateClassMethods(lowerer); + const value = varRef("value", DYN, loc); + const argument = varRef("argument", DYN, loc); + const body: IrStmt[] = []; + for (const info of classes) { + const root = dateRootOf(info)!; + const type: IrType = { kind: "object", className: info.def.name }; + const instance: IrExpr = { kind: "dynCheck", value, type, loc }; + body.push({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then: [ + { + kind: "if", + cond: opIs(6, loc), + then: [ + { + kind: "return", + value: { + kind: "dynFrom", + value: { + kind: "strLit", + value: info.def.jsName ?? info.def.name, + type: STRING, + loc, + }, + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + { + kind: "if", + cond: opIs(1, loc), + then: [ + { + kind: "fieldSet", + obj: lowerer.upcastTo(instance, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + value: { + kind: "libCall", + fn: "date.newMs", + args: [{ kind: "dynCheck", value: argument, type: F64, loc }], + type: DATE_T, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }, + { + kind: "return", + value: { + kind: "dynFrom", + value: { + kind: "libCall", + fn: "date.getTime", + args: [timeOf(lowerer, instance, root, loc)], + type: F64, + loc, + }, + type: DYN, + loc, + }, + loc, + }, + ], + else_: null, + loc, + }); + } + body.push({ kind: "return", value: dynUndefinedExpr(loc), loc }); + liftOperation(lowerer, DATE_CLASS_TIME, body, loc); + const closure = (fnName: string): IrExpr => ({ + kind: "dynFrom", + value: { + kind: "closure", + fnName, + captures: [], + type: { kind: "func", params: [DYN, DYN, DYN], ret: DYN }, + loc, + }, + type: DYN, + loc, + }); + return [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "date.classOperation", + args: [closure(DATE_CLASS_TIME), closure(DATE_CLASS_METHOD)], + type: VOID, + loc, + }, + loc, + }, + ]; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts index 31be995e7..a598f4ee6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/dates.ts +++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts @@ -1,6 +1,8 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; import { locOf } from "../../program.js"; +import { tryLowerExpression } from "../expressions/try-lower-expression.js"; +import { programDeclaresDateClass } from "./date-classes.js"; import { BOOL, F64, @@ -9,6 +11,7 @@ import { type IrStmt, type IrType, NULL_T, + type SrcLoc, STRING, typeKey, } from "../../../ir/ir.js"; @@ -39,7 +42,7 @@ const DATE_TEXT_FNS: ReadonlyMap = new Map([ ["toTimeString", "date.toTimeString"], ]); -const DATE_METHOD_HINT = +export const DATE_METHOD_HINT = "getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), toString(), toDateString(), toTimeString(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale formatters have no lowering"; /** The Date slice: statics plus read-only Date values backed by one @@ -52,7 +55,52 @@ export function lowerDateCall( ): IrExpr | null { if (call.questionDotToken || access.questionDotToken) return null; const loc = locOf(call); - if (lowerer.stdlibGlobalMember(access, "Date") === "now") { + const member = lowerer.stdlibGlobalMember(access, "Date"); + if (member === "now" || member === "parse" || member === "UTC") + return lowerDateStaticCall(lowerer, call, member); + if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null; + if (!lowerer.isStdlibMember(access)) return null; + const name = access.name.text; + // A Date a JavaScript file constructs is a mutable object; setTime on + // it takes the dynamic method call. So does every Date method on a + // checked-dynamic receiver when the program declares a class that extends + // Date: the value can be an instance, whose class may declare the method. + if ( + (name === "setTime" || programDeclaresDateClass(lowerer)) && + tryLowerExpression(lowerer, access.expression)?.type.kind === "dyn" + ) + return null; + if (name === "toJSON") return lowerDateToJson(lowerer, call, access); + if (call.arguments.nodes.length !== 0) { + lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); + } + const raw = lowerer.lowerExpr(access.expression); + const receiver: IrExpr = + raw.type.kind === "dyn" + ? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc } + : raw; + if (receiver.type.kind !== "date") + lowerer.badType(access.expression, lowerer.typeOf(access.expression)); + const result = dateValueMethod(name, receiver, loc); + if (result) return result; + lowerer.noLowering( + `Date.prototype.${name}`, + call, + DATE_METHOD_HINT, + lowerer.checker.getSymbolAtLocation(access.name), + ); +} + +/** Date.now(), Date.parse(text) and Date.UTC(...), also called through a + * class declared `extends Date`, which inherits them. Null for other + * names. */ +export function lowerDateStaticCall( + lowerer: Lowerer, + call: ts.CallExpression, + name: string, +): IrExpr | null { + const loc = locOf(call); + if (name === "now") { if (call.arguments.nodes.length !== 0) { lowerer.noLowering(`Date.now with ${call.arguments.nodes.length} arguments`, call); } @@ -60,7 +108,7 @@ export function lowerDateCall( } // Date.parse uses the same date-string parser as new Date(dateString) // (V8's grammar in the runtime), so their NaN and TimeClip behavior agree. - if (lowerer.stdlibGlobalMember(access, "Date") === "parse") { + if (name === "parse") { if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) { lowerer.noLowering(`Date.parse with ${call.arguments.nodes.length} arguments`, call); } @@ -72,7 +120,7 @@ export function lowerDateCall( // TimeClip. Omitted trailing arguments complete with the spec's // defaults (month 0, date 1, time parts 0); tsc pins every present // argument to number, so the seven-f64 ABI is exact. - if (lowerer.stdlibGlobalMember(access, "Date") === "UTC") { + if (name === "UTC") { if ( call.arguments.nodes.length < 1 || call.arguments.nodes.length > 7 || @@ -92,49 +140,38 @@ export function lowerDateCall( } return { kind: "libCall", fn: "date.utc", args, type: F64, loc }; } - if (lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind !== "date") return null; - if (!lowerer.isStdlibMember(access)) return null; - const name = access.name.text; - if (name === "toJSON") return lowerDateToJson(lowerer, call, access); - if (call.arguments.nodes.length !== 0) { - lowerer.noLowering(`Date.prototype.${name} with arguments`, call, DATE_METHOD_HINT); - } - const raw = lowerer.lowerExpr(access.expression); - const receiver: IrExpr = - raw.type.kind === "dyn" - ? { kind: "libCall", fn: "date.checkedValue", args: [raw], type: { kind: "date" }, loc } - : raw; - if (receiver.type.kind !== "date") - lowerer.badType(access.expression, lowerer.typeOf(access.expression)); + return null; +} + +/** The Date methods without arguments that have a lowering, applied to the + * time value `value` (a `date`): getTime, valueOf, toISOString, + * toUTCString, the calendar getters and the text forms. Null for other + * names. */ +export function dateValueMethod(name: string, value: IrExpr, loc: SrcLoc): IrExpr | null { if (name === "getTime" || name === "valueOf") { return { kind: "libCall", fn: name === "getTime" ? "date.getTime" : "date.valueOf", - args: [receiver], + args: [value], type: F64, loc, }; } if (name === "toISOString") { - return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc }; + return { kind: "libCall", fn: "date.toISOStringValue", args: [value], type: STRING, loc }; } if (name === "toUTCString") { - return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc }; + return { kind: "libCall", fn: "date.toUTCString", args: [value], type: STRING, loc }; } const fn = own(DATE_GETTER_FNS, name); if (fn !== undefined) { - return { kind: "libCall", fn, args: [receiver], type: F64, loc }; + return { kind: "libCall", fn, args: [value], type: F64, loc }; } const textFn = DATE_TEXT_FNS.get(name); if (textFn !== undefined) { - return { kind: "libCall", fn: textFn, args: [receiver], type: STRING, loc }; + return { kind: "libCall", fn: textFn, args: [value], type: STRING, loc }; } - lowerer.noLowering( - `Date.prototype.${name}`, - call, - DATE_METHOD_HINT, - lowerer.checker.getSymbolAtLocation(access.name), - ); + return null; } /** `d.toJSON(key?)`: Node answers null for an invalid date and the diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts index 1b9a9c03a..375c3f1e7 100644 --- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts +++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -84,7 +85,7 @@ export function lowerDisposableStackUseCall( const key = `disposableStack.use:${className}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%disposableStack.use.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%disposableStack.use", key); lowerer.valueHelpers.set(key, helper); const context = newFnCtx(false, null, null, VOID); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts index d95442829..1371769d6 100644 --- a/packages/compiler/src/frontend/lowering/builtins/errors.ts +++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts @@ -1,6 +1,8 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; +import { tryLowerExpression } from "../expressions/try-lower-expression.js"; +import { dynUndefinedExpr } from "../../../ir/build.js"; import { BOOL, CAUGHT, @@ -8,6 +10,7 @@ import { DYN_CLASS_PROPERTIES, F64, type IrExpr, + type IrStmt, type IrType, STRING, } from "../../../ir/ir.js"; @@ -267,6 +270,114 @@ function isErrorClassType(lowerer: Lowerer, type: IrType): boolean { ); } +/** Error classes that construct when called without `new`. */ +const CALLABLE_ERROR_CLASSES: ReadonlySet = new Set([ + "%Error", + "%TypeError", + "%RangeError", + "%SyntaxError", + "%ReferenceError", + "%EvalError", + "%URIError", +]); + +/** `Error(message?, options?)` and `Error.call(thisArg, message?, options?)` + * (an ES5 constructor function's `Error.call(this)`), and the same for the + * other standard error classes: called as a function, the constructor + * returns a new error and ignores the receiver. `thisArg` is evaluated + * first. Null for other callees. */ +export function lowerErrorFunctionCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null { + if (call.questionDotToken) return null; + let callee = call.expression; + let args = call.arguments.nodes; + let thisArg: ts.Expression | undefined; + const viaCall = + ts.isPropertyAccessExpression(callee) && + !callee.questionDotToken && + callee.name.text === "call" && + ts.isIdentifier(callee.expression); + if (viaCall) { + callee = (callee as ts.PropertyAccessExpression).expression; + thisArg = args[0]; + args = args.slice(1); + } + if (!ts.isIdentifier(callee) || args.some(ts.isSpreadElement)) return null; + if (thisArg && ts.isSpreadElement(thisArg)) return null; + const info = lowerer.builtinErrorInfoOf(lowerer.checker.getSymbolAtLocation(callee)); + if (!info || !CALLABLE_ERROR_CLASSES.has(info.def.name)) return null; + const loc = locOf(call); + const stmts: IrStmt[] = []; + if (thisArg) { + const receiver = lowerer.lowerExpr(thisArg); + if (receiver.kind !== "varRef" && receiver.kind !== "unitLit") + stmts.push({ kind: "exprStmt", expr: receiver, loc: receiver.loc }); + } + const error: IrExpr = { + kind: "libCall", + fn: "error.newOptions", + args: lowerer.errorConstructorArgs(args, loc, call), + type: { kind: "object", className: info.def.name }, + loc, + }; + return stmts.length > 0 + ? { kind: "seqExpr", stmts, result: error, type: error.type, loc } + : error; +} + +/** `Error.captureStackTrace(target[, constructorOpt])` (also called as + * `Error.captureStackTrace?.(...)`, which always calls it). On a + * checked-dynamic target (`this` in a JavaScript constructor function) the + * runtime defines target's `stack`. An instance of a class that extends + * Error derives its stack from its name and message when the stack is + * read, so the call only evaluates its arguments. Both arguments are + * evaluated; the second only selects frames in Node. Null for other + * targets and callees. */ +export function lowerErrorCaptureStackTraceCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.questionDotToken) return null; + if (lowerer.stdlibGlobalMember(access, "Error") !== "captureStackTrace") return null; + const args = call.arguments.nodes; + if (args.length < 1 || args.length > 2 || args.some(ts.isSpreadElement)) return null; + const probed = tryLowerExpression(lowerer, args[0]!); + const loc = locOf(call); + if ( + probed?.type.kind === "object" && + (probed.type.className === "%Error" || lowerer.isSubclassOf(probed.type.className, "%Error")) + ) { + const stmts: IrStmt[] = []; + for (const arg of args) { + const value = lowerer.lowerExpr(arg); + if (value.kind !== "varRef") stmts.push({ kind: "exprStmt", expr: value, loc: value.loc }); + } + const result = dynUndefinedExpr(loc); + return stmts.length > 0 ? { kind: "seqExpr", stmts, result, type: DYN, loc } : result; + } + if (probed?.type.kind !== "dyn") return null; + const target = lowerer.lowerExpr(args[0]!); + const result: IrExpr = { + kind: "libCall", + fn: "dyn.errorCaptureStackTrace", + args: [target], + type: DYN, + loc, + }; + if (args.length === 1) return result; + const local = lowerer.declareHiddenLocal("%captureTarget", DYN); + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: local.id, init: target, loc }, + { kind: "exprStmt", expr: lowerer.lowerExprExpecting(args[1]!, DYN), loc }, + ], + result: { ...result, args: [{ kind: "varRef", localId: local.id, type: DYN, loc }] }, + type: DYN, + loc, + }; +} + /** `Error.isError(value)`. A catch binding tests its payload against the * Error class, an `unknown` value tests the checked-dynamic tree's error * encoding, and a union tests its runtime tag against the arms whose class diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts index 0e1c062c5..6b988c491 100644 --- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts +++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr, boolLit, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, ladderFenceExpr } from "../lowerer.js"; @@ -442,7 +443,7 @@ export function lowerStringOrBytesWrite( const key = `fs.${append ? "append" : "write"}:${dataType.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%fs.writeData.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%fs.writeData", key); lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; for (let tag = 0; tag < arms.length; tag++) { diff --git a/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts b/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts index 382d15eaf..88f6c2d95 100644 --- a/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts +++ b/packages/compiler/src/frontend/lowering/builtins/hash-stream.ts @@ -1,17 +1,27 @@ import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; +import type { SrcLoc } from "../../../ir/ir.js"; import { varRef } from "../../../ir/build.js"; -import { type IrExpr, type IrType } from "../../../ir/ir.js"; +import { type IrExpr, type IrStmt, type IrType } from "../../../ir/ir.js"; +import { withStreamReceiver } from "../dyn-streams.js"; import { EMITTER_API_MEMBERS } from "../lower-classes.js"; import { lowerEmitterMethodCall } from "../lower-event-emitter.js"; -import { STREAM_API_MEMBERS, STREAM_OPERATORS, lowerStreamMethodCall } from "../lower-stream.js"; +import { + STREAM_API_MEMBERS, + STREAM_OPERATORS, + lowerStreamMethodCall, + lowerStreamProperty, +} from "../lower-stream.js"; +import { isCipherType } from "./crypto.js"; -/* node:crypto's Hash and Hmac are Transforms: a value written, read, - * listened to, or piped as a stream reads and writes through its own - * native Transform (scr_stream.c), created on first use. update() and - * digest() keep the value's static lowering; the stream members lower - * through the stream and emitter spokes over that Transform. */ +/* node:crypto's Hash, Hmac, Cipheriv and Decipheriv are Transforms: a + * value written, read, listened to, or piped as a stream reads and writes + * through its own native Transform, which scr_stream.c creates on first + * use. update(), digest() and final() keep the value's own lowering; the + * stream members lower through the stream and emitter spokes over that + * Transform. A Hash or Hmac is known by its IR type; a cipher is a + * checked-dynamic handle and is known by its checker type. */ const TRANSFORM_CLASS = "%Transform"; const TRANSFORM: IrType = { kind: "object", className: TRANSFORM_CLASS }; @@ -20,6 +30,12 @@ export function isHashValueType(type: IrType | null | undefined): boolean { return type?.kind === "cryptoHash" || type?.kind === "cryptoHmac"; } +/** Whether `node` evaluates to a Hash, Hmac, Cipheriv or Decipheriv. */ +export function isValueStream(lowerer: Lowerer, node: ts.Expression): boolean { + const type = lowerer.typeOf(node); + return isHashValueType(lowerer.mapTypeOf(type)) || isCipherType(lowerer, type); +} + /** The Transform of a lowered Hash or Hmac value (+1). A pipe * destination or pipeline stage (`piped`) also keeps the value alive * until its stream flushes or is destroyed. */ @@ -33,9 +49,27 @@ export function hashStreamOf(value: IrExpr, piped = false): IrExpr { }; } -/** A stream member called on a Hash or Hmac (`hash.write(chunk)`, - * `hash.on("data", cb)`, `hash.setEncoding("hex")`). Members that answer - * the stream (`this`) answer the Hash itself. */ +/** The Transform of a lowered Hash, Hmac, Cipheriv or Decipheriv value + * (+1), with hashStreamOf's `piped` (a cipher's stream always holds its + * cipher). */ +export function valueStreamOf(value: IrExpr, piped = false): IrExpr { + if (isHashValueType(value.type)) return hashStreamOf(value, piped); + // A value already lowered as its stream (addAbortSignal answers the + // Transform it was given, which its generic signature types as the + // cipher). + if (value.type.kind === "object") return value; + return { + kind: "libCall", + fn: "stream.handleStream", + args: [value], + type: TRANSFORM, + loc: value.loc, + }; +} + +/** A stream member called on a Hash, Hmac, Cipheriv or Decipheriv + * (`hash.write(chunk)`, `cipher.on("data", cb)`, `hash.setEncoding("hex")`). + * Members that answer the stream (`this`) answer the value itself. */ export function lowerHashStreamCall( lowerer: Lowerer, call: ts.CallExpression, @@ -45,39 +79,96 @@ export function lowerHashStreamCall( const name = access.name.text; const streamMember = STREAM_API_MEMBERS.has(name) || STREAM_OPERATORS.has(name); if (!streamMember && !EMITTER_API_MEMBERS.has(name)) return null; - if (!isHashValueType(lowerer.mapTypeOf(lowerer.typeOf(access.expression)))) return null; + if (!isValueStream(lowerer, access.expression)) return null; const info = lowerer.classes.get(TRANSFORM_CLASS); if (!info) return null; - const loc = locOf(call); - const hash = lowerer.lowerExpr(access.expression); - const local = lowerer.declareHiddenLocal("%hash", hash.type); - // The spokes lower the receiver to the value's Transform. - lowerer.hashStreamReceivers.set(access.expression, { localId: local.id, type: hash.type }); + // pipe(dest) answers its destination, which may be a Transform too; + // the other members typed as the Transform answer `this` + const answersThis = name !== "pipe"; + return withValueStreamReceiver(lowerer, access.expression, locOf(call), answersThis, () => + streamMember + ? lowerStreamMethodCall(lowerer, call, access, info) + : lowerEmitterMethodCall(lowerer, call, access, info), + ); +} + +/** A stream property read on a Hash, Hmac, Cipheriv or Decipheriv + * (`cipher.destroyed`, `hash.readableEnded`). */ +export function lowerValueStreamProperty( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): IrExpr | null { + if (expr.questionDotToken || !isValueStream(lowerer, expr.expression)) return null; + const info = lowerer.classes.get(TRANSFORM_CLASS); + if (!info) return null; + return withValueStreamReceiver(lowerer, expr.expression, locOf(expr), true, () => + lowerStreamProperty(lowerer, expr, info), + ); +} + +/** Evaluates the value `receiver` names once into a hidden local and runs + * `lower` with the receiver lowering to that value's Transform. When + * `answersThis`, a result typed as the Transform (the member answered + * `this`) answers the value itself. */ +function withValueStreamReceiver( + lowerer: Lowerer, + receiver: ts.Expression, + loc: SrcLoc, + answersThis: boolean, + lower: () => IrExpr | null, +): IrExpr | null { + const value = lowerer.lowerExpr(receiver); + const local = lowerer.declareHiddenLocal("%streamValue", value.type); + lowerer.valueStreamReceivers.set(receiver, { localId: local.id, type: value.type }); let lowered: IrExpr | null; try { - lowered = streamMember - ? lowerStreamMethodCall(lowerer, call, access, info) - : lowerEmitterMethodCall(lowerer, call, access, info); + lowered = lower(); } finally { - lowerer.hashStreamReceivers.delete(access.expression); + lowerer.valueStreamReceivers.delete(receiver); } if (!lowered) return null; const answersStream = - lowered.type.kind === "object" && lowered.type.className === TRANSFORM_CLASS; + answersThis && lowered.type.kind === "object" && lowered.type.className === TRANSFORM_CLASS; const result: IrExpr = answersStream ? { kind: "seqExpr", stmts: [{ kind: "exprStmt", expr: lowered, loc }], - result: varRef(local.id, hash.type, loc), - type: hash.type, + result: varRef(local.id, value.type, loc), + type: value.type, loc, } : lowered; return { kind: "seqExpr", - stmts: [{ kind: "varDecl", localId: local.id, init: hash, loc }], + stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }], result, type: result.type, loc, }; } + +/** `for await (const chunk of value)` over a Hash, Hmac, Cipheriv or + * Decipheriv: the loop iterates the value's Transform. */ +export function lowerForAwaitValueStream( + lowerer: Lowerer, + stmt: ts.ForOfStatement, + lowerLoop: () => IrStmt | null, +): IrStmt | null { + if (!isValueStream(lowerer, stmt.expression)) return null; + const loc = locOf(stmt.expression); + const local = lowerer.declareHiddenLocal("%streamIterated", TRANSFORM); + const init = valueStreamOf(lowerer.lowerExpr(stmt.expression)); + const loop = withStreamReceiver( + lowerer, + stmt.expression, + varRef(local.id, TRANSFORM, loc), + TRANSFORM, + lowerLoop, + ); + if (loop === null) return null; + return { + kind: "block", + body: [{ kind: "varDecl", localId: local.id, init, loc }, loop], + loc: locOf(stmt), + }; +} diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts index 7eb8aa9b2..64f829865 100644 --- a/packages/compiler/src/frontend/lowering/builtins/json.ts +++ b/packages/compiler/src/frontend/lowering/builtins/json.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { isDateJsonSource } from "./dates.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -253,7 +254,7 @@ function lowerOptionalStringifyRoot( const key = `json.optionalString:${value.type.unionId}:${JSON.stringify(indent)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%json.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%json.optionalString", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", value.type, loc); const serialized: IrExpr = { diff --git a/packages/compiler/src/frontend/lowering/builtins/numbers.ts b/packages/compiler/src/frontend/lowering/builtins/numbers.ts index 18ca0f1f3..1ff35fbb5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/numbers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/numbers.ts @@ -72,17 +72,14 @@ export function lowerNumberStaticCall( return { kind: "libCall", fn, args: [arg], type: BOOL, loc }; } if (member === "parseFloat" || member === "parseInt") { - const want = member === "parseFloat" ? 1 : 2; - if (call.arguments.nodes.length !== want) { - lowerer.noLowering( - `Number.${member} with ${call.arguments.nodes.length} argument${call.arguments.nodes.length === 1 ? "" : "s"}`, - call, - member === "parseInt" ? "pass an explicit radix: Number.parseInt(s, 10)" : undefined, - ); + const count = call.arguments.nodes.length; + if (count < 1 || count > (member === "parseFloat" ? 1 : 2)) { + lowerer.noLowering(`Number.${member} with ${count} argument${count === 1 ? "" : "s"}`, call); } // Number.parseFloat/Number.parseInt ARE the global parsers: over a // string argument they lower to the same static parse (parseInt with - // its explicit radix); only the coercing argument shapes are refused. + // its radix, or radix 0 when it is missing); only the coercing argument + // shapes are refused. const probed = tryLowerExpression(lowerer, call.arguments.nodes[0]!); if (probed !== null && isStringLike(lowerer, probed.type)) { const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments.nodes, loc); @@ -101,7 +98,10 @@ export function lowerNumberStaticCall( }; if (member === "parseFloat") return { kind: "libCall", fn: "num.parseFloat", args: [text], type: F64, loc }; - const radix = lowerer.lowerExprExpecting(call.arguments.nodes[1]!, F64); + const radixNode = call.arguments.nodes[1]; + const radix: IrExpr = radixNode + ? lowerer.lowerExprExpecting(radixNode, F64) + : { kind: "numLit", value: 0, type: F64, loc }; return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc }; } const argText = diff --git a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts index c10465039..ee189a1f3 100644 --- a/packages/compiler/src/frontend/lowering/builtins/operating-system.ts +++ b/packages/compiler/src/frontend/lowering/builtins/operating-system.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { numLit, strLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -219,7 +220,7 @@ export function lowerOsCpusCall(lowerer: Lowerer, call: ts.CallExpression, loc: const key = `os.cpus:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%os.cpus.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%os.cpus", key); lowerer.valueHelpers.set(key, helper); const index = varRef("i.0", F64, loc); const field = (k: number): IrExpr => ({ @@ -346,7 +347,7 @@ export function lowerPathParseCall( const key = `${fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%path.parse.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%path.parse", key); lowerer.valueHelpers.set(key, helper); const parts = varRef("parts.0", arrayOf(STRING), loc); lowerer.liftedFns.push({ @@ -448,7 +449,7 @@ export function lowerPathFormatCall( const key = `${fn}:${typeKey(recordT)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%path.format.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%path.format", key); lowerer.valueHelpers.set(key, helper); const object = varRef("object.0", recordT, loc); const read = (name: string): IrExpr => { diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts index 72304e6a9..bae525c17 100644 --- a/packages/compiler/src/frontend/lowering/builtins/performance.ts +++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { locOf } from "../../program.js"; @@ -330,7 +331,7 @@ function lowerPerfTimelineCall( const key = `perf.${many ? "entries" : timeline.fn}:${typeKey(result)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%perf.${many ? "entries" : member}.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%perf.${many ? "entries" : member}`, key); lowerer.valueHelpers.set(key, helper); const argsRef = varRef("a.0", DYN, loc); const whichRef = varRef("w.0", F64, loc); @@ -461,7 +462,7 @@ export function perfEntryTag( const key = `perf.tag:${entryT.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%perf.tag.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%perf.tag", key); lowerer.valueHelpers.set(key, helper); const e = varRef("e.0", entryT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts index 25653f541..842f42b57 100644 --- a/packages/compiler/src/frontend/lowering/builtins/process.ts +++ b/packages/compiler/src/frontend/lowering/builtins/process.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { stdinKeypressListener } from "./readline.js"; import { envSnapshotHelper, lowerProcessLoadEnvFile } from "./environment.js"; import { dynUndefinedExpr, boolLit, numLit, strLit, varRef } from "../../../ir/build.js"; @@ -1561,7 +1562,7 @@ export function lowerProcessMethodCall( const key = `${prefix}usage.diff:${prevT.shapeId}:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%${prefix}usage.diff.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%${prefix}usage.diff`, key); lowerer.valueHelpers.set(key, helper); const pRef: IrExpr = { kind: "varRef", localId: "p.0", type: prevT, loc }; const fieldOf = (name: string): IrExpr => ({ @@ -1636,7 +1637,7 @@ export function lowerProcessMethodCall( const key = `process.memoryUsage:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%process.memoryUsage.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%process.memoryUsage", key); lowerer.valueHelpers.set(key, helper); lowerer.liftedFns.push({ name: helper, diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts index 668ef01f8..75e527aa5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promises.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { checkedPromiseAll, thenReader } from "../checked-promise-all.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -55,7 +56,7 @@ function lowerTuplePromiseAll( const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`; let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`); if (name === undefined) { - name = `%promise.allTuple.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%promise.allTuple", `promise.allTuple:${key}`); lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name); const ctx = newFnCtx(false, null, null, resultT.inner); ctx.isAsync = true; @@ -557,7 +558,7 @@ export function lowerPromiseStaticCall( const key = `promise.try:${typeKey(f.type)}:${args.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%promise.try.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%promise.try", key); lowerer.arrHofHelpers.set(key, helper); const fRef: IrExpr = { kind: "varRef", localId: "f.0", type: f.type, loc }; const argLocals = args.map((a, i) => ({ id: `a${i}.0`, name: `a${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts index 822a4fecc..b4dbbf513 100644 --- a/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts +++ b/packages/compiler/src/frontend/lowering/builtins/promisify-callback.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; import { varRef } from "../../../ir/build.js"; @@ -170,7 +171,7 @@ function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): st const key = `promisify:${typeKey(shape.fnT)}=>${typeKey(shape.resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const base = `%promisify.${lowerer.arrHofHelpers.size}`; + const base = keyedHelperName(lowerer, "%promisify", key); lowerer.arrHofHelpers.set(key, base); const { fnT, resultT, args, callbackT, callbackParamT, inner, error } = shape; const promiseT = resultT.ret; diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts index ede65d788..846b9a926 100644 --- a/packages/compiler/src/frontend/lowering/builtins/readline.ts +++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts @@ -353,7 +353,8 @@ function lowerPromptArg(lowerer: Lowerer, node: ts.Expression): { value: IrExpr; * - on/once/addListener for 'line' ((input: string) or no parameters), * 'error' ((err: Error) or no parameters), 'history' ((history: * string[]) or no parameters), and 'close'/'pause'/'resume'/'SIGINT'/ - * 'SIGTSTP'/'SIGCONT' (no parameters), as statements. + * 'SIGTSTP'/'SIGCONT' (no parameters); as an expression the call yields + * the interface, so registrations chain. * Everything else the lib declares fences member-qualified. Null for * non-Interface receivers. */ export function lowerReadlineMethodCall( @@ -505,14 +506,19 @@ export function lowerReadlineMethodCall( `${RL_EVENT_LIST} are the supported readline events`, ); } - if (!ts.isExpressionStatement(call.parent)) { - lowerer.unsupported( - "SC1090", - call, - "chaining readline listener registration (register each listener as its own statement)", - ); - } - const recv = receiver(); + // As an expression (`rl.on("line", f).on("close", g)`), the call + // registers the listener and yields the interface, as Node's on() does. + const recvLocal = ts.isExpressionStatement(call.parent) + ? undefined + : lowerer.declareHiddenLocal("%readlineListener", F64); + const saved: IrStmt[] = + recvLocal === undefined + ? [] + : [{ kind: "varDecl", localId: recvLocal.id, init: receiver(), loc }]; + const recv: IrExpr = + recvLocal === undefined + ? receiver() + : { kind: "varRef", localId: recvLocal.id, type: F64, loc }; const cb = lowerer.lowerExpr(args[1]!); const want = event === "line" @@ -547,13 +553,21 @@ export function lowerReadlineMethodCall( : `'${event}' listeners with parameters or a return value (use ())`, ); } - return { + const register: IrExpr = { kind: "libCall", fn: "rl.on", args: [recv, numLit(ev, loc), cb, boolLit(name === "once", loc)], type: VOID, loc, }; + if (recvLocal === undefined) return register; + return { + kind: "seqExpr", + stmts: [...saved, { kind: "exprStmt", expr: register, loc }], + result: { kind: "varRef", localId: recvLocal.id, type: F64, loc }, + type: F64, + loc, + }; } lowerer.noLowering( `readline.Interface.${name}`, diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts index 7229d5b84..8d47c8e3a 100644 --- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts +++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { isBuiltinClassInstance } from "./receiver-types.js"; import { strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import { staticTextDecoderEncoding } from "../text-decoder-encoding.js"; @@ -129,7 +130,7 @@ function stringDecoderHelper( const key = `strdec.${op}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%strdec.${op}.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%strdec.${op}`, key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts index 2d531663f..d7cbd63b5 100644 --- a/packages/compiler/src/frontend/lowering/builtins/timers.ts +++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts @@ -85,7 +85,7 @@ export function lowerTimersPromisesSetInterval( const genT = timersPromisesIntervalType(lowerer, expr); if (genT === null) lowerer.badType(expr, lowerer.typeOf(expr)); const yielded = lowerer.coerceInto(valueNode, value, genT.yieldT); - const fnName = `%fn${lowerer.lambdaCounter++}_tpInterval`; + const fnName = lowerer.closureName(loc, "_tpInterval"); const msParam: IrLocal = { id: "%tp.ms", name: "delay", type: F64, mutable: false }; const valueParam: IrLocal = { id: "%tp.value", name: "value", type: genT.yieldT, mutable: false }; const promiseVoid: IrType = { kind: "promise", inner: VOID }; diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts index dca8a7cc0..2a77fa85c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/url.ts +++ b/packages/compiler/src/frontend/lowering/builtins/url.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, countedFor, strLit, varRef, dynUndefinedExpr } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer, own } from "../lowerer.js"; @@ -292,7 +293,7 @@ function searchParamsFromTuples( const key = `sp.fromTuples:${row.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%sp.fromTuples.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.fromTuples", key); lowerer.valueHelpers.set(key, helper); const pairsT = init.type; const sp = (): IrExpr => varRef("sp.0", SEARCH_PARAMS_T, loc); @@ -372,7 +373,7 @@ function lowerOptionalStringSearchParams( const key = `sp.optionalString:${init.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%sp.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.optionalString", key); lowerer.valueHelpers.set(key, helper); const value = varRef("init.0", init.type, loc); lowerer.liftedFns.push({ @@ -678,7 +679,7 @@ function lowerSpForEachCall( const key = `${arity}:${typeKey(fnRet)}`; let helper = lowerer.spHofHelpers.get(key); if (!helper) { - helper = `%sp.forEach.${lowerer.spHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%sp.forEach", key); lowerer.spHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSpForEachFn(helper, arity, fnRet, loc)); } diff --git a/packages/compiler/src/frontend/lowering/builtins/v8.ts b/packages/compiler/src/frontend/lowering/builtins/v8.ts index 3582ce8ae..989a61a3f 100644 --- a/packages/compiler/src/frontend/lowering/builtins/v8.ts +++ b/packages/compiler/src/frontend/lowering/builtins/v8.ts @@ -5,6 +5,7 @@ * spirit of process.memoryUsage(): approximate values, not V8 heap * figures (a compiled binary embeds no V8 engine). Every other member * fences at its use with the by-design reason (builtinFenceHintOf). */ +import { keyedHelperName } from "../stable-names.js"; import { numLit } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import { type Lowerer } from "../lowerer.js"; @@ -55,7 +56,7 @@ export function lowerV8HeapStatisticsCall( const key = `v8.getHeapStatistics:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%v8.getHeapStatistics.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%v8.getHeapStatistics", key); lowerer.valueHelpers.set(key, helper); const sample = (index: number): IrExpr => ({ kind: "libCall", diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts index 93cba84c5..fe310294c 100644 --- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts +++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts @@ -5,34 +5,57 @@ import { BOOL, DYN, type IrExpr, type IrLibFn } from "../../../ir/ir.js"; import { lowerDynObjectLiteral } from "../expressions/object-literals.js"; import { stdlibGlobalNameOf } from "../surfaces.js"; import { WEBCRYPTO_DYN_TYPES } from "../../type-mapper.js"; +import { isWebCryptoReceiver } from "./crypto.js"; -/* WebCrypto's HMAC slice (scr_webcrypto.c): `crypto.subtle.importKey`, - * `sign`, `verify` and `digest` called on the crypto global (also through - * globalThis) lower to runtime entry points that take the argument vector - * as one dyn array and answer a promise, so Web IDL conversion, algorithm - * normalization and every error stay Node-shaped at run time. CryptoKey +/* WebCrypto's secret-key slice (scr_webcrypto.c, with encrypt and + * decrypt in scr_webcrypto_aes.c): `crypto.subtle`'s `importKey`, + * `generateKey`, `exportKey`, `encrypt`, `decrypt`, `deriveBits`, + * `deriveKey`, `sign`, `verify` and `digest` + * called on the crypto global (also through + * globalThis, node:crypto's webcrypto and node:crypto's subtle) lower to + * runtime entry points that take the argument vector as one dyn array and + * answer a promise, so Web IDL conversion, algorithm normalization and + * every error stay Node-shaped at run time. CryptoKey * values are native handles; their type maps to dyn and their members * are handle reads. Other SubtleCrypto methods are refused by name. */ const SUBTLE_LOWERED: ReadonlyMap = new Map([ ["importKey", "webcrypto.importKey"], + ["generateKey", "webcrypto.generateKey"], + ["exportKey", "webcrypto.exportKey"], + ["encrypt", "webcrypto.encrypt"], + ["decrypt", "webcrypto.decrypt"], + ["deriveBits", "webcrypto.deriveBits"], + ["deriveKey", "webcrypto.deriveKey"], ["sign", "webcrypto.sign"], ["verify", "webcrypto.verify"], ["digest", "webcrypto.digest"], ]); const SUBTLE_HINT = - "the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; + "the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle"; -/** Whether `node` is `crypto.subtle` on the WebCrypto global. */ +/** Whether `node` is a SubtleCrypto object: `subtle` on a WebCrypto + * Crypto object (the global, globalThis.crypto or node:crypto's + * `webcrypto`) or node:crypto's own `subtle` export, named or through + * the namespace. */ function isSubtleReceiver(lowerer: Lowerer, node: ts.Expression): boolean { while (ts.isParenthesizedExpression(node)) node = node.expression; - return ( - ts.isPropertyAccessExpression(node) && - !node.questionDotToken && - node.name.text === "subtle" && - stdlibGlobalNameOf(lowerer, node.expression) === "crypto" - ); + if (ts.isIdentifier(node)) { + const imported = lowerer.builtinImportOf(node); + return imported?.module === "crypto" && imported.member === "subtle"; + } + if ( + !ts.isPropertyAccessExpression(node) || + node.questionDotToken || + node.name.text !== "subtle" + ) { + return false; + } + if (stdlibGlobalNameOf(lowerer, node.expression) === "crypto") return true; + if (isWebCryptoReceiver(lowerer, node.expression)) return true; + const member = lowerer.builtinMemberOf(node); + return member?.module === "crypto" && member.member === "subtle"; } /** `crypto.subtle.(...args)`: the lowered methods become their @@ -51,17 +74,6 @@ export function lowerWebCryptoCall(lowerer: Lowerer, call: ts.CallExpression): I if (call.arguments.nodes.some(ts.isSpreadElement)) { lowerer.noLowering(`crypto.subtle.${member} with spread arguments`, call, SUBTLE_HINT); } - // JsonWebKey imports are not implemented: a literal 'jwk' format is - // refused here (the runtime refuses a computed one). - const format = call.arguments.nodes[0]; - if ( - member === "importKey" && - format !== undefined && - ts.isStringLiteralLike(format) && - format.text === "jwk" - ) { - lowerer.noLowering("crypto.subtle.importKey with the 'jwk' key format", format, SUBTLE_HINT); - } const loc = locOf(call); const args = call.arguments.nodes.map((arg) => ts.isObjectLiteralExpression(arg) diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts index 49f7c8ec0..060ffe9d2 100644 --- a/packages/compiler/src/frontend/lowering/call-arguments.ts +++ b/packages/compiler/src/frontend/lowering/call-arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -753,7 +754,7 @@ function holeReadingLift(lowerer: Lowerer, from: IrType, to: IrType, loc: SrcLoc const key = `arr.holes:${typeKey(elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.holes.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.holes", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push( buildArrayConversion( diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts index 34343ad27..dacb4d324 100644 --- a/packages/compiler/src/frontend/lowering/call-signatures.ts +++ b/packages/compiler/src/frontend/lowering/call-signatures.ts @@ -849,7 +849,10 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(lowerer, decl) + nameText, + ), typeParams: [], instances: new Map(), implicitParams: implicit, @@ -897,7 +900,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara // "%default" spelling ('%' cannot appear in a user identifier). name: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(decl) + (decl.name?.text ?? "%default"), + nsPathPrefix(lowerer, decl) + (decl.name?.text ?? "%default"), ), params, returnType, diff --git a/packages/compiler/src/frontend/lowering/class-assertions.ts b/packages/compiler/src/frontend/lowering/class-assertions.ts index 900c32426..52654fab8 100644 --- a/packages/compiler/src/frontend/lowering/class-assertions.ts +++ b/packages/compiler/src/frontend/lowering/class-assertions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -122,7 +123,7 @@ export function checkedClassAssertion( const key = `classassert:${typeKey(source.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%class.assert.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%class.assert", key); lowerer.coercions.retags.set(key, name); const input = varRef("value.0", source.type, loc); const body: IrStmt[] = []; @@ -228,7 +229,7 @@ export function checkedClassUnionAssertion( const key = `classunionassert:${typeKey(value.type)}:${typeKey(target)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%class.assert.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%class.assert", key); lowerer.coercions.retags.set(key, name); const input = varRef("value.0", value.type, loc); const locals = [{ id: "value.0", name: "value", type: value.type, mutable: false }]; diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index c90e1135b..fefe85d0c 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -2,8 +2,17 @@ import { primitiveConstructorOr } from "./lower-exprs.js"; import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js"; import { constructedValue } from "./constructor-returns.js"; import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js"; +import { + DATE_PROTOTYPE_METHOD_NAMES, + DATE_TIME_FIELD, + dateInheritedMember, + dateRootOf, + programDeclaresDateClass, +} from "./builtins/date-classes.js"; +import { dateValueMethod } from "./builtins/dates.js"; import { BOOL, + DATE_T, DYN, F64, DYN_CLASS_PROPERTIES as PROPERTY_BAG, @@ -296,6 +305,32 @@ export function classInstanceOf( ); const body: IrStmt[] = prototype ? [ + // In a program that declares a class extending Date, a Date can + // meet a class test (`date instanceof Moment`): its prototype + // chain holds no class prototype. + ...(programDeclaresDateClass(lowerer) + ? [ + { + kind: "if" as const, + cond: { + kind: "libCall" as const, + fn: "dyn.nativeDateHandleIs" as const, + args: [varRef("value", DYN, loc)], + type: BOOL, + loc, + }, + then: [ + { + kind: "return" as const, + value: { kind: "boolLit" as const, value: false, type: BOOL, loc }, + loc, + }, + ], + else_: null, + loc, + }, + ] + : []), { kind: "varDecl", localId: "candidate", init: varRef("value", DYN, loc), loc }, { kind: "while", @@ -2871,6 +2906,7 @@ export class ClassDynamicDispatch { !isClassOwnEnumerableFieldName(dispatch.name) || (!(info.builtinStream !== undefined && streamProperty(dispatch.name)) && !eventTargetInheritedMember(info, dispatch.name) && + !dateInheritedMember(lowerer, info, dispatch.name) && !findMethodOn(lowerer, info, dispatch.name) && !findGenericMethodOn(lowerer, info, dispatch.name) && dispatch.name !== "constructor" && @@ -2987,7 +3023,8 @@ export class ClassDynamicDispatch { ]; if ( ((info.builtinStream !== undefined && streamProperty(name)) || - eventTargetInheritedMember(info, name)) && + eventTargetInheritedMember(info, name) || + dateInheritedMember(lowerer, info, name)) && dispatch.probe ) return [ @@ -3200,6 +3237,9 @@ export class ClassDynamicDispatch { for (const name of [...STREAM_BOOL_PROPERTIES, ...STREAM_NUM_PROPERTIES]) names.add(name); const inherited = eventTargetApiMembers(info); if (inherited) for (const name of inherited) names.add(name); + if (dateRootOf(info)) + for (const name of DATE_PROTOTYPE_METHOD_NAMES) + if (dateInheritedMember(lowerer, info, name)) names.add(name); for (let owner: ClassInfo | null = info; owner; owner = owner.base) { const methodNames = [...owner.methods.keys()]; @@ -3863,6 +3903,70 @@ export class ClassDynamicDispatch { ]; return { kind: "if", cond: condition, then: body, else_: null, loc }; } + // Date.prototype[Symbol.toPrimitive] takes "default" as "string", and a + // class declared `extends Date` inherits Date's toString and valueOf, + // which answer primitives from the time value. + const dateRoot = dateRootOf(info); + if (dateRoot) { + const dateOrder: ("valueOf" | "toString")[] = + hint === "number" ? ["valueOf", "toString"] : ["toString", "valueOf"]; + const { receiver } = this.propertyReceiver(plan, loc); + const body: IrStmt[] = []; + for (const method of dateOrder) { + if (!conversionMethod(lowerer, info, method)) { + const time: IrExpr = { + kind: "fieldGet", + obj: upcastTo(lowerer, receiver, dateRoot.def.name), + className: dateRoot.def.name, + field: DATE_TIME_FIELD, + type: DATE_T, + loc, + }; + body.push({ + kind: "return", + value: lowerer.coerceToExpected(dateValueMethod(method, time, loc)!, DYN), + loc, + }); + break; + } + const result = `%toPrimitive.${method}.${helper.locals.length}`; + helper.locals.push({ id: result, name: method, type: DYN, mutable: false }); + body.push( + { + kind: "varDecl", + localId: result, + init: { + kind: "call", + callee: this.toPrimitiveCall(lowerer, plan, method, 0, loc), + args: [varRef("p.0", DYN, loc)], + type: DYN, + loc, + }, + loc, + }, + { + kind: "if", + cond: isPrimitive(varRef(result, DYN, loc), loc), + then: [{ kind: "return", value: varRef(result, DYN, loc), loc }], + else_: null, + loc, + }, + ); + } + if (body.at(-1)?.kind !== "return") + body.push({ + kind: "return", + value: nodeThrowExpr(1, "", "Cannot convert object to primitive value", DYN, loc), + loc, + }); + return { + kind: "if", + cond: this.propertyReceiver(plan, loc).condition, + then: body, + else_: null, + loc, + }; + } const order: ("valueOf" | "toString")[] = hint === "string" ? ["toString", "valueOf"] : ["valueOf", "toString"]; if (!order.some((method) => conversionMethod(lowerer, info, method))) return null; @@ -4731,9 +4835,11 @@ export class ClassDynamicDispatch { ): IrStmt[] { const { name, write } = dispatch; const loc = dispatch.fn.loc; - // The inherited EventTarget/Event members of a subclass instance: the - // runtime's capsule view answers the accessors and method values. - if (eventTargetInheritedMember(info, name)) { + // The inherited EventTarget/Event members of a subclass instance, and + // the inherited Date methods of an instance of a class declared + // `extends Date`: the runtime's capsule view answers the accessors and + // method values. + if (eventTargetInheritedMember(info, name) || dateInheritedMember(lowerer, info, name)) { const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc }; if (!write) return [ diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts index cc8700469..abbaf3a75 100644 --- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts +++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts @@ -1,3 +1,4 @@ +import { isErrorPrototypeValue } from "./class-prototypes.js"; import * as ts from "../ts7/adapter.js"; import { literalValues } from "../literal-values.js"; import { isJsSourceFile } from "../program.js"; @@ -377,6 +378,8 @@ export function collectClassMethodMutations( node.initializer.kind !== ts.SyntaxKind.MissingDeclaration ) markTarget(node.initializer, node); + if (ts.isPropertyAccessExpression(node) && isErrorPrototypeValue(lowerer, node)) + lowerer.errorPrototypeValues = true; if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) { const base = unwrap(node.expression); const prototype = diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 4cbd8e5cf..2a05cf977 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -457,6 +457,28 @@ export function isCompiledPrototypeMember( ); } +/** `Error.prototype` read as the checked-dynamic object, as a value or for + * a member other than `toString` and `constructor`, which keep the Error + * class's prototype data (`this.toString !== Error.prototype.toString` + * compares class method values). */ +export function isErrorPrototypeValue( + lowerer: Lowerer, + expr: ts.PropertyAccessExpression, +): boolean { + let node: ts.Node = expr; + while (ts.isParenthesizedExpression(node.parent)) node = node.parent; + const parent = node.parent; + return ( + expr.name.text === "prototype" && + lowerer.isStdlibGlobal(expr.expression, "Error") && + !( + ts.isPropertyAccessExpression(parent) && + parent.expression === node && + (parent.name.text === "toString" || parent.name.text === "constructor") + ) + ); +} + /** Named data and method slots have native descriptors. Bare reflection and * accessor replacement still require a complete view of the prototype. */ export function lowerClassPrototypeData( @@ -469,6 +491,11 @@ export function lowerClassPrototypeData( exactClassOfReceiver(lowerer, expr.expression) ?? (stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined); if (!info) return null; + // Error.prototype as a value: the checked-dynamic object an ES5 + // constructor function inherits from (`F.prototype = + // Object.create(Error.prototype)`). + if (isErrorPrototypeValue(lowerer, expr)) + return { kind: "libCall", fn: "dyn.errorPrototype", args: [], type: DYN, loc: locOf(expr) }; // EventEmitter.prototype and the stream prototypes: the untyped // constructor's prototype object (builtin-class-dyn.ts). const builtinKind = builtinClassKind(info.def.name); diff --git a/packages/compiler/src/frontend/lowering/class-to-primitive.ts b/packages/compiler/src/frontend/lowering/class-to-primitive.ts index f3a6b23fa..0b107ab4e 100644 --- a/packages/compiler/src/frontend/lowering/class-to-primitive.ts +++ b/packages/compiler/src/frontend/lowering/class-to-primitive.ts @@ -1,10 +1,21 @@ import { varRef } from "../../ir/build.js"; -import { BOOL, DYN, F64, STRING, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; +import { + BOOL, + DATE_T, + DYN, + F64, + STRING, + type IrExpr, + type IrStmt, + type IrType, +} from "../../ir/ir.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js"; import { isClassCallback } from "./class-callbacks.js"; import { isSafeToRepeat } from "./expressions/evaluation-safety.js"; +import { DATE_TIME_FIELD, dateRootOf } from "./builtins/date-classes.js"; +import { dateValueMethod } from "./builtins/dates.js"; /** The ToPrimitive hint of a conversion: `String(x)` and template literals * take "string", `+` "default", `Number(x)` "number". */ @@ -13,9 +24,12 @@ export type StringConversionHint = "string" | "default"; type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString"; -/** How an instance of one class converts: through one of its methods, or - * through Object.prototype.toString ("[object Object]"). */ -type Plan = { method: ConversionMethod; ret: IrType } | { method: null }; +/** How an instance of one class converts: through one of its methods, + * through the Date.prototype method `date` of a class declared `extends + * Date`, or through Object.prototype.toString ("[object Object]"). */ +type Plan = + | { method: ConversionMethod; ret: IrType } + | { method: null; date?: "toString" | "valueOf" }; const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]); @@ -138,6 +152,18 @@ function classConversion( } const convert = (cls: ClassInfo, object: IrExpr): IrExpr => { const plan = plans.get(cls)!; + if (plan.method === null && plan.date) { + const root = dateRootOf(cls)!; + const time: IrExpr = { + kind: "fieldGet", + obj: lowerer.upcastTo(object, root.def.name), + className: root.def.name, + field: DATE_TIME_FIELD, + type: DATE_T, + loc, + }; + return finish(dateValueMethod(plan.date, time, loc)!); + } if (plan.method === null) return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc }); const args: IrExpr[] = @@ -202,7 +228,10 @@ function depth(info: ClassInfo): number { } function samePlan(a: Plan, b: Plan): boolean { - return a.method === b.method; + return ( + a.method === b.method && + (a.method !== null || b.method !== null || a.date === (b as { date?: string }).date) + ); } /** A conversion method the class declares or inherits (a field of that @@ -227,11 +256,17 @@ function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: t // OrdinaryToPrimitive: "string" tries toString first, which every object // has (Object.prototype.toString answers a string); "default" and // "number" try valueOf first, whose Object.prototype version answers the - // object itself. - const order: ConversionMethod[] = hint === "string" ? ["toString"] : ["valueOf", "toString"]; + // object itself. Date.prototype[Symbol.toPrimitive] takes "default" as + // "string", and Date.prototype answers both methods with primitives. + const date = dateRootOf(info) !== null; + const order: ConversionMethod[] = + hint === "string" || (date && hint === "default") ? ["toString"] : ["valueOf", "toString"]; for (const method of order) { const found = declared(lowerer, info, method); - if (!found) continue; + if (!found) { + if (date && method !== "sym:toPrimitive") return { method: null, date: method }; + continue; + } if (!primitive(lowerer, found.ret)) refuse(lowerer, node, info, `a '${method}' method that can return an object`); return { method, ret: found.ret }; diff --git a/packages/compiler/src/frontend/lowering/class-unions.ts b/packages/compiler/src/frontend/lowering/class-unions.ts index f52026ba4..0021b2cf8 100644 --- a/packages/compiler/src/frontend/lowering/class-unions.ts +++ b/packages/compiler/src/frontend/lowering/class-unions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, typeKey, type IrExpr, type IrStmt, type IrType } from "../../ir/ir.js"; import { nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -132,7 +133,7 @@ export function narrowClassUnion( const key = `classUnion:${source.className}:${target.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%class.union.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%class.union", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", source, loc); // Rank by ancestry before sorting: returning zero for unrelated pairs diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts index ffdac19f0..fe3c14605 100644 --- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts +++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dynUndefinedExpr } from "../../../ir/build.js"; import { buildFunctionAdapter } from "./builders.js"; import { InternalCompilerError } from "../../../errors.js"; @@ -33,7 +34,7 @@ export function funcReturnWidthAdapter( const key = `fn:${typeKey(fromT.ret.elem)}:${typeKey(toT.ret.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%fn.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%fn.width", key); lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); // The returned closure's body: call the captured original, width-map. @@ -163,7 +164,7 @@ function restPackingAdapter( const key = `fnrestadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const fixed = fromT.params.length - 1; @@ -220,7 +221,7 @@ function funcIntoDynRestAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = [{ localId: "rest.0", name: "args", type: DYN }]; @@ -376,7 +377,7 @@ export function funcCoerceAdapter( const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%fn.adapt.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%fn.adapt", key); lowerer.coercions.retags.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((t, i) => ({ @@ -604,7 +605,7 @@ export function spawnResFnAdapter( const key = `fnspawn:${typeKey(fromT)}:${typeKey(toT)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%fnval.spawnres.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%fnval.spawnres", key); lowerer.valueHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); const params: IrParam[] = toT.params.map((p, i) => ({ diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts index 496a29607..5b5e283b3 100644 --- a/packages/compiler/src/frontend/lowering/coercions/structural.ts +++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { constructedValue } from "../constructor-returns.js"; import { dynUndefinedExpr, varRef } from "../../../ir/build.js"; import type { FieldLift } from "./structural-plans.js"; @@ -253,7 +254,7 @@ export function unionRecordWidthHelper( const key = `unionWidth:${fromId}:${target.shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%union.record.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%union.record", key); lowerer.valueHelpers.set(key, name); const type: IrType = { kind: "union", unionId: fromId }; const value = varRef("value.0", type, loc); @@ -312,7 +313,7 @@ export function recordWidthHelper( const key = `rec:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.width", key); // Interned BEFORE the body builds: a recursive nested-width field // (self-referential shapes) resolves to this helper itself. lowerer.valueHelpers.set(key, name); @@ -341,7 +342,7 @@ export function tupleArrayWidthHelper( const key = `tuparr:${fromId}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%tup.arr.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%tup.arr", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const t: IrExpr = { kind: "varRef", localId: "t.0", type: fromT, loc }; @@ -388,7 +389,7 @@ export function emptyArrayLiftHelper( const key = `emptyarr:${typeKey(fromT.elem)}:${typeKey(toT.elem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.empty.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.empty", key); lowerer.valueHelpers.set(key, name); const a: IrExpr = { kind: "varRef", localId: "a.0", type: fromT, loc }; lowerer.liftedFns.push({ @@ -452,7 +453,7 @@ export function arrayWidthHelper( const key = `arr:${typeKey(fromElem)}:${typeKey(toElem)}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%arr.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.width", key); lowerer.valueHelpers.set(key, name); const arrT: IrType = { kind: "array", elem: fromElem }; const outT: IrType = { kind: "array", elem: toElem }; @@ -492,7 +493,7 @@ export function objRecordWidthHelper( const key = `obj:${className}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%obj.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%obj.width", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "object", className }; lowerer.liftedFns.push(buildRecordProjection(lowerer, name, fromT, to, plan, loc)); @@ -517,7 +518,7 @@ export function recordClassWidthHelper( const key = `cls:${fromId}:${className}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%cls.width.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%cls.width", key); lowerer.valueHelpers.set(key, name); lowerer.noteEdge(`%${className}.constructor`); const fromT: IrType = { kind: "record", shapeId: fromId }; diff --git a/packages/compiler/src/frontend/lowering/coercions/unions.ts b/packages/compiler/src/frontend/lowering/coercions/unions.ts index 06c0af9c6..434e687a5 100644 --- a/packages/compiler/src/frontend/lowering/coercions/unions.ts +++ b/packages/compiler/src/frontend/lowering/coercions/unions.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { buildUnionNarrow, unionNarrowFailureMessage } from "../union-narrow.js"; import { isSafeToRepeat } from "../expressions/evaluation-safety.js"; import { nodeThrowExpr } from "../../../ir/build.js"; @@ -147,7 +148,7 @@ export function strandedUnitTrap( const key = `strandunit:${typeKey(expected)}:${expr.type.kind}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%unit.strand.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%unit.strand", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push({ name, @@ -227,7 +228,7 @@ export function strandedCoercionTrap( const key = `strand:${expected.unionId}:${typeKey(src)}`; let name = lowerer.coercions.retags.get(key); if (!name) { - name = `%union.strand.${lowerer.coercions.retags.size}`; + name = keyedHelperName(lowerer, "%union.strand", key); lowerer.coercions.retags.set(key, name); const toT: IrType = { kind: "union", unionId: expected.unionId }; lowerer.liftedFns.push({ @@ -275,7 +276,7 @@ export function recordUnionWrapHelper( const key = `recordWrap:${source.shapeId}:${toId}`; const existing = lowerer.coercions.retags.get(key); if (existing) return existing; - const name = `%record.wrap.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%record.wrap", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -307,7 +308,7 @@ export function recordUnionRouteHelper( (from, dst) => lowerer.widthLiftPlan(from, dst), ); if (!plan) return null; - const name = `%record.route.${lowerer.coercions.retags.size}`; + const name = keyedHelperName(lowerer, "%record.route", key); lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( buildRecordUnionWrap(name, source, to, plan, loc, (lift, value, dst) => @@ -352,7 +353,7 @@ export function unionRetagHelper( }); const key = `${fromId}:${toId}:${stranded.join(".")}`; const existing = lowerer.coercions.retags.get(key); - const name = existing ?? `%union.retag.${lowerer.coercions.retags.size}`; + const name = existing ?? keyedHelperName(lowerer, "%union.retag", key); if (!existing) { lowerer.coercions.retags.set(key, name); lowerer.liftedFns.push( @@ -410,7 +411,7 @@ export function narrowedArmHelper( lowerer.coercions.checkedNarrows.add(existing); return existing; } - const name = `%union.narrow.${lowerer.coercions.narrows.size}`; + const name = keyedHelperName(lowerer, "%union.narrow", key); lowerer.coercions.narrows.set(key, name); lowerer.coercions.checkedNarrows.add(name); const fn = buildUnionNarrow(name, from, target, loc, (type) => lowerer.fmt(type)); @@ -587,7 +588,7 @@ export function deferredReadHelper( const key = `deferred:${fromId}:${tag}`; const existing = lowerer.coercions.narrows.get(key); if (existing) return existing; - const name = `%deferred.read.${lowerer.coercions.narrows.size}`; + const name = keyedHelperName(lowerer, "%deferred.read", key); lowerer.coercions.narrows.set(key, name); const dflt: IrExpr = target.kind === "bool" diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts index 5d14d5492..8fe2a6bbb 100644 --- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts +++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { InternalCompilerError } from "../../errors.js"; import { typeKey } from "../type-mapper.js"; @@ -197,7 +198,7 @@ export function ingestCollection( const key = `ingest:${input.collection ? typeKey(input.collection) + ":" + input.selection : "cursor:" + typeKey(input.element)}:${typeKey(destination)}:${mapper ? typeKey(mapper.type) : "copy"}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%collection.ingest.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%collection.ingest", key); lowerer.arrHofHelpers.set(key, name); const context = newFnCtx(false, null, null, destination); lowerer.fnStack.push(context); diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts index eb99592bb..7ca69d9ba 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { transformStmtList } from "../../../ir/traverse.js"; import type { ScrDiagnostic } from "../../../diagnostics/diagnostic.js"; @@ -506,7 +507,7 @@ function arrayHofHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}:${typeKey(outElem)}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, elem, fnRet, arity, outElem, present, split: !!fastT }); const fn = buildArrayHofFn( @@ -619,7 +620,7 @@ function arrayUnionHofHelper( const key = `union:${method}:${receiverT.unionId}:${typeKey(callbackT)}:${typeKey(resultT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.union.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.union", key); lowerer.arrHofHelpers.set(key, name); const receiver = varRef("a.0", receiverT, loc); const callback = varRef("f.0", callbackT, loc); @@ -1204,7 +1205,7 @@ function findHelper( const key = `${method}:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}${site}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const valueT = callbackValueType(lowerer, elem, present); @@ -1304,7 +1305,7 @@ function findIndexHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); if (!last) lowerer.arrHofInfo.set(name, { @@ -1388,7 +1389,7 @@ function someEveryHelper( const key = `${method}:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method, @@ -1587,7 +1588,7 @@ function flatMapHelper( const key = `flatMap:${typeKey(elem)}:${typeKey(fnRet)}:${arity}${site}${fastT ? ":split" : ""}${present ? ":present" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.flatMap.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.flatMap", key); lowerer.arrHofHelpers.set(key, name); lowerer.arrHofInfo.set(name, { method: "flatMapArray", @@ -1829,7 +1830,7 @@ function reduceHelper( const key = `${method}:${typeKey(elem)}:${typeKey(accT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : "seed"}${site}${fastT ? ":split" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); if (method === "reduce") lowerer.arrHofInfo.set(name, { @@ -2121,7 +2122,7 @@ export function lowerArraySortCall( const key = `${method}:${typeKey(elem)}:${arity}${native ? ":native" : ""}${keyed ? `:keyed${order}${reads ? `:field${reads.length}` : ""}` : ""}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const undefinedTag = elem.kind === "union" ? lowerer.armTag(elem.unionId, UNDEFINED_T) : -1; lowerer.liftedFns.push( @@ -2167,7 +2168,7 @@ function sortKeyReader( const cacheKey = `sortKey:${typeKey(elem)}:${sortKeysId(reads.map((read) => ({ order: 1, read })))}`; let reader = lowerer.arrHofHelpers.get(cacheKey); if (!reader) { - reader = `%arr.sortKey.${lowerer.arrHofHelpers.size}`; + reader = keyedHelperName(lowerer, "%arr.sortKey", cacheKey); lowerer.arrHofHelpers.set(cacheKey, reader); lowerer.liftedFns.push(buildSortKeyFn(reader, elem, reads, loc)); } @@ -2308,7 +2309,7 @@ function dynFlatMapHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: S const key = `dynflatmap:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%dyn.flatmap.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%dyn.flatmap", key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFlatMapFn(name, elemT, arity, loc)); return name; @@ -2481,7 +2482,7 @@ function dynFilterHelper(lowerer: Lowerer, elemT: IrType, arity: number, loc: Sr const key = `dynfilter:${typeKey(elemT)}:${arity}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%dyn.filter.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%dyn.filter", key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push(buildDynFilterFn(name, elemT, arity, loc)); return name; @@ -2676,7 +2677,7 @@ export function lowerTuplePredicateCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(resultT)}:${arity}${callbackSite(fnArg)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const v = varRef("v.0", elem, loc); const out = varRef("out.0", resultT, loc); diff --git a/packages/compiler/src/frontend/lowering/containers/array-concat.ts b/packages/compiler/src/frontend/lowering/containers/array-concat.ts index 4f6e4ca43..b4e285ee9 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-concat.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-concat.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { @@ -28,7 +29,7 @@ export function arrayConcatHelper( const key = `concat:${typeKey(elem)}:${shape.map((type) => (type ? `a:${typeKey(type)}` : "e")).join(";")}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.concat.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.concat", key); const arrT = arrayOf(elem); const params = [ { localId: "a.0", name: "a", type: arrT }, diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts index 40e4d6436..1f10640c0 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { generatorDrain } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; @@ -256,7 +257,7 @@ function appendWidenedArrayValues( const key = `insertValues:${typeKey(sourceType.elem)}:${typeKey(elem)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.insertValues.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.insertValues", key); lowerer.arrHofHelpers.set(key, name); const outType = arrayOf(elem); const valueType = arrayValueType(lowerer, source.type.elem); @@ -1035,7 +1036,7 @@ export function lowerArrayFromCall( const key = `fromArray:${typeKey(arrT.elem)}:${typeKey(outElem)}:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.fromArray.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%arr.fromArray", key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildArrayFromArrayFn(lowerer, helper, arrT.elem, outElem, fnRet, arity, loc), @@ -1122,7 +1123,7 @@ export function lowerArrayFromCall( const key = `fromLen:${typeKey(fnRet)}:${arity}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%arr.fromLen.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%arr.fromLen", key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push(buildArrayFromLenFn(helper, fnRet, arity, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts index e9a130a2d..44182fbda 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { liftedFreshSpread } from "../coercions/fresh-spread.js"; import { lowerArrayIteratorValue } from "../iterator-adapters.js"; import { identityPreservingWidening } from "../coercions/identity.js"; @@ -1616,7 +1617,7 @@ function arraySearchHelper( const key = `${method}:${typeKey(elem)}:${typeKey(valueT)}:${typeKey(needleT)}:${directNeedle}:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%arr.${method}`, key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); const resultT = method === "includes" ? BOOL : F64; @@ -1972,7 +1973,7 @@ export function lowerTupleReadMethodCall( let helper = lowerer.arrHofHelpers.get(key); const resultType = method === "includes" ? BOOL : F64; if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const params = [ { localId: "a.0", name: "a", type: receiver.type }, @@ -2034,7 +2035,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.slice:${typeKey(receiver.type)}:${args.length}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.slice.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%tuple.slice", key); lowerer.arrHofHelpers.set(key, helper); const params = [receiver, ...args].map((arg, i) => ({ localId: i === 0 ? "a.0" : `arg.${i}`, @@ -2125,7 +2126,7 @@ export function lowerTupleReadMethodCall( const key = `tuple.${method}:${typeKey(receiver.type)}:${typeKey(fnArg.type)}:${typeKey(outElem)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%tuple.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%tuple.${method}`, key); lowerer.arrHofHelpers.set(key, helper); const out = varRef("out.0", outType, loc); const result = varRef("r.0", ret, loc); @@ -2253,7 +2254,7 @@ function lowerArrayAtCall( const key = `at:${typeKey(elem)}:${typeKey(resultT)}:${typeKey(index.type)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.at.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.at", key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); diff --git a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts index cf45a7575..180f40435 100644 --- a/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts +++ b/packages/compiler/src/frontend/lowering/containers/array-pipelines.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, countedFor, numLit, varRef } from "../../../ir/build.js"; import { BOOL, @@ -851,7 +852,7 @@ class PipelineFusion { if (!built) return null; let name = this.helpers.get(built.key); if (!name) { - name = `%arr.fused.${this.helpers.size}`; + name = keyedHelperName(this.lowerer, "%arr.fused", built.key); this.helpers.set(built.key, name); this.added.push({ ...built.fn, name }); } diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts index bf9821d31..8a4364754 100644 --- a/packages/compiler/src/frontend/lowering/containers/bytes.ts +++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js"; import { BUF_NUM_METHODS } from "./buffer-numeric-methods.js"; import * as ts from "../../ts7/adapter.js"; @@ -77,7 +78,7 @@ function lowerBytesSortCall( const cacheKey = `bytes.${method}:${bytesT.elem}:${key}`; let helper = lowerer.arrHofHelpers.get(cacheKey); if (!helper) { - helper = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%bytes.${method}`, cacheKey); lowerer.arrHofHelpers.set(cacheKey, helper); lowerer.liftedFns.push( buildBytesSortFn(helper, bytesT, arity, hasComparator, copyFirst, order, loc), @@ -270,7 +271,7 @@ function bytesCallbackHelper( const key = `bytes.${method}:${bytesT.elem}:${typeKey(resultT)}:${typeKey(fnRet)}:${arity}:${hasInit ? "init" : ""}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%bytes.${method}.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, `%bytes.${method}`, key); lowerer.arrHofHelpers.set(key, name); const reduce = method === "reduce" || method === "reduceRight"; const backward = method === "reduceRight" || method === "findLast" || method === "findLastIndex"; @@ -1077,11 +1078,20 @@ export function lowerBytesMethodCall( // Array-toString (comma join), a different operation: fenced. const declaredOnBuffer = (() => { const nameSym = lowerer.checker.getSymbolAtLocation(access.name); - return ( + if ( nameSym !== undefined && nameSym.declarations.some( (d) => ts.isInterfaceDeclaration(d.parent) && d.parent.name.text === "Buffer", ) + ) + return true; + // A receiver the lowering types as a Buffer where the checker says + // `any` (a JavaScript pipeline stage's chunk). + const receiverSym = lowerer.typeOf(access.expression).getSymbol(); + return ( + receiverSym !== undefined && + ts.symbolName(receiverSym) === "Buffer" && + receiverSym.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) ); })(); if (receiverIr.elem !== "u8" || !declaredOnBuffer) { @@ -1743,8 +1753,9 @@ function knownBufferProducer( /** Buffer.from over a checked-dynamic source and encoding, both already in * repeatable locals: Node's fromString takes utf8 for an absent, non-string - * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; - * other sources ignore the encoding. */ + * or empty encoding and throws ERR_UNKNOWN_ENCODING for an unknown name; an + * ArrayBuffer takes the second argument as its byte offset; other sources + * ignore it. */ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrExpr { const utf8: IrExpr = { kind: "strLit", value: "utf8", type: STRING, loc }; const isString = (x: IrExpr): IrExpr => ({ @@ -1786,10 +1797,12 @@ function dynBufferFromParts(source: IrExpr, encoding: IrExpr, loc: SrcLoc): IrEx type: BYTES_U8, loc, }, + // An ArrayBuffer takes the second argument as its byte offset + // (scr_buffer_from_dyn_offset_pooled); other values convert with utf8. else_: { kind: "libCall", - fn: "buffer.poolFromDynRef", - args: [source, utf8], + fn: "buffer.poolFromDynOffset", + args: [source, encoding], type: BYTES_U8, loc, }, diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts index 18754f088..9db04e95b 100644 --- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts +++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; import { isJsSourceFile, locOf } from "../../program.js"; @@ -5,7 +6,9 @@ import { varRef } from "../../../ir/build.js"; import { typeKey } from "../../type-mapper.js"; import { BOOL, + DYN, F64, + STRING, type IrExpr, type IrType, type SrcLoc, @@ -68,6 +71,20 @@ export function lowerArrayCallback( if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode); return { fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), arity: 1 }; } + // The ambient `String` (`values.map(String)`): String(value) converts its + // first argument as a direct call does, whatever the element type; the + // constructor's own value form takes a string. + if ( + ts.isIdentifier(argNode) && + argNode.text === "String" && + !isJsSourceFile(ts.getSourceFileOfNode(argNode)) && + lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined) + ) { + const valueT = lead[0]; + if (valueT === undefined || valueT.kind === "void") + lowerer.badType(argNode, lowerer.typeOf(argNode)); + return { fnArg: stringConversionCallback(lowerer, valueT, argNode), arity: 1 }; + } const full = [...lead, F64, arrT]; // A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`): // the contextual signature types the unannotated param `any` (the @@ -273,6 +290,49 @@ function runtimeOptionalPairVariant(lowerer: Lowerer, expected: IrType, mapped: return lowerer.isRuntimeOptionalRecordVariant(strip(expected), strip(mapped)); } +/** `String` as a one-parameter callback over `valueT`: the conversion a + * direct `String(value)` call lowers to (the runtime's String() for unknown + * values and unions holding a symbol, ToString otherwise), interned per + * value type. */ +function stringConversionCallback( + lowerer: Lowerer, + valueT: IrType, + node: ts.Expression, +): IrExpr & { type: IrType & { kind: "func" } } { + const loc = locOf(node); + const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: STRING }; + const key = `mapString:${typeKey(valueT)}`; + let name = lowerer.arrHofHelpers.get(key); + if (!name) { + name = `%arr.mapString.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, name); + const value = varRef("v.0", valueT, loc); + const runtimeString = + valueT.kind === "dyn" || + (valueT.kind === "union" && + lowerer.unions.get(valueT.unionId)?.arms.some((arm) => arm.kind === "symbol") === true && + lowerer.dynConvertible(valueT)); + const converted: IrExpr = runtimeString + ? { + kind: "libCall", + fn: "dyn.stringConstructor", + args: [lowerer.coerceToExpected(value, DYN)], + type: STRING, + loc, + } + : lowerer.ensureString(value, node); + lowerer.liftedFns.push({ + name, + params: [{ localId: "v.0", name: "v", type: valueT }], + returnType: STRING, + locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }], + body: [{ kind: "return", value: converted, loc }], + loc, + }); + } + return { kind: "closure", fnName: name, captures: [], type: fnT, loc }; +} + /** A native closure for the ambient Boolean callback. Reifying the * JavaScript constructor itself is unnecessary; Array#filter only observes * its one-argument ToBoolean result. */ @@ -285,7 +345,7 @@ export function truthyFilterCallback( const key = `filterBoolean:${typeKey(valueT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.filterBoolean", key); lowerer.arrHofHelpers.set(key, name); const value = varRef("v.0", valueT, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts index 0bf126830..dae68d518 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import * as ts from "../../ts7/adapter.js"; import type { Lowerer } from "../lowerer.js"; @@ -104,7 +105,7 @@ export function lowerGroupByStaticCall( const key = `map.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.groupBy.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.groupBy", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } @@ -141,7 +142,7 @@ export function lowerGroupByStaticCall( const key = `object.groupBy:${typeKey(items.type)}:${typeKey(f.type)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%object.groupBy.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%object.groupBy", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildGroupByFn(lowerer, helper, items.type, f.type, resultT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts index 370974c85..d3169ec5c 100644 --- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts +++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nodeThrowExpr, boolLit, countedFor, numLit, strLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -232,7 +233,7 @@ function lowerMapIterDrainCall( const key = `${method}:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.${method}.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%map.${method}`, key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapIterDrainFn(helper, mapT, method, elemT, tupleT, loc)); } @@ -348,7 +349,7 @@ export function mapEntriesArray(lowerer: Lowerer, receiver: IrExpr): IrExpr { const key = `entries:${typeKey(mapT.key)}:${typeKey(mapT.value)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.entries.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.entries", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push( buildMapIterDrainFn(helper, mapT, "entries", tupleT, tupleT, receiver.loc), @@ -519,7 +520,7 @@ export function lowerMapForEachCall( const key = `${typeKey(mapT.key)}:${typeKey(mapT.value)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.forEach.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.forEach", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, mapT, arity, fnRet, loc)); } @@ -693,7 +694,7 @@ export function lowerSetForEachCall( const key = `${typeKey(setT.elem)}:${arity}:${typeKey(fnRet)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = `%set.forEach.${lowerer.setHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%set.forEach", key); lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildCollectionForEachFn(helper, setT, arity, fnRet, loc)); } @@ -745,7 +746,7 @@ function lowerSetCombineCall( const key = `${name}:${typeKey(setT.elem)}:${typeKey(otherT)}`; let helper = lowerer.setHofHelpers.get(key); if (!helper) { - helper = `%set.${name}.${lowerer.setHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%set.${name}`, key); lowerer.setHofHelpers.set(key, helper); lowerer.liftedFns.push(buildSetCombineFn(helper, name, setT, otherT, loc)); } @@ -1357,7 +1358,7 @@ export function mapFromSeedValue( const key = `checked-seed:${typeKey(mapT)}`; let name = lowerer.mapHofHelpers.get(key); if (!name) { - name = `%map.checkedSeed.${lowerer.mapHofHelpers.size}`; + name = keyedHelperName(lowerer, "%map.checkedSeed", key); lowerer.mapHofHelpers.set(key, name); const source = varRef("source.0", DYN, loc); const entries = varRef("entries.0", DYN, loc); @@ -1457,7 +1458,7 @@ export function mapFromSeedValue( const key = `seed:${typeKey(mapT.key)}:${typeKey(mapT.value)}:${elem.shapeId}`; let helper = lowerer.mapHofHelpers.get(key); if (!helper) { - helper = `%map.seed.${lowerer.mapHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%map.seed", key); lowerer.mapHofHelpers.set(key, helper); lowerer.liftedFns.push(buildMapSeedFn(mapT, helper, seed.type, elem, kT, vT, loc)); } diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts index efc173ffc..b3faf649e 100644 --- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts +++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; import { countedFor, numLit, varRef } from "../../../ir/build.js"; import { objectEnumerationReceiver } from "../object-enumeration-receiver.js"; @@ -184,7 +185,7 @@ export function objectIterOverIndexShape( const key = `obj.${member}:ovf:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%obj.${member}`, key); const outRef = varRef("out.0", resultT, loc); const rRef = varRef("r.0", argIr, loc); @@ -608,7 +609,7 @@ function lowerTypedObjectFromEntriesCall( const key = `obj.fromEntries:${argIr.elem.shapeId}:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.fromEntries", key); const tupleT = argIr.elem as IrType & { kind: "record" }; const tRef = varRef("t.0", tupleT, loc); @@ -712,7 +713,7 @@ function lowerFromEntriesStringRows( const key = `obj.fromEntries:strrows:${resultT.shapeId}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.fromEntries.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.fromEntries", key); const tRef = varRef("t.0", rowT, loc); const rowLen: IrExpr = { @@ -931,7 +932,7 @@ export function lowerRecordOvfCaptureHelper( const key = `ovf:${fromId}:${toId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.capture.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.capture", key); lowerer.valueHelpers.set(key, name); const fromT: IrType = { kind: "record", shapeId: fromId }; const toT: IrType = { kind: "record", shapeId: toId }; @@ -1204,7 +1205,7 @@ export function lowerObjectAssignIndexShape( const key = `assign:${targetIr.shapeId}:${plan.fromId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%obj.assign.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%obj.assign", key); lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: targetIr.shapeId }; const fromT: IrType = { kind: "record", shapeId: plan.fromId }; @@ -1474,7 +1475,7 @@ export function lowerIndexMergeHelper( .join(","); const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%rec.merge.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%rec.merge", key); lowerer.valueHelpers.set(key, name); const toT: IrType = { kind: "record", shapeId: toId }; const ksT = arrayOf(STRING); @@ -1805,7 +1806,7 @@ export function lowerEnvToPairsHelper( const key = `env.pairs:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%env.pairs.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, "%env.pairs", key); lowerer.valueHelpers.set(key, name); const recT: IrType = { kind: "record", shapeId }; const arrT = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts index c23e6931c..ba5d0684d 100644 --- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts +++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { boolLit, numLit, varRef } from "../../../ir/build.js"; import { InternalCompilerError } from "../../../errors.js"; import * as ts from "../../ts7/adapter.js"; @@ -966,7 +967,7 @@ function lowerFusedChain( const key = `iter:${srcKey}:${stageKeys.join(",")}:${terminal}:${termArgs.map((a) => typeKey(a.type)).join(",")}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%iter.${terminal}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%iter.${terminal}`, key); lowerer.arrHofHelpers.set(key, helper); lowerer.liftedFns.push( buildIterChainFn( @@ -1038,7 +1039,7 @@ function lowerHelperObject( const key = `iterHelper:${stage.kind}:${srcKey}:${typeKey(srcElem)}:${typeKey(arg.type)}:${typeKey(outT)}`; let name = lowerer.arrHofHelpers.get(key); if (!name) { - name = `%iter.${stage.kind}.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, `%iter.${stage.kind}`, key); lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( buildHelperGenFn(lowerer, name, source, srcElem, stage, arg.type, outT, resultType, loc), @@ -1058,7 +1059,7 @@ function internBudgetChecker(lowerer: Lowerer, loc: SrcLoc, srcT: GenType | null const key = srcT === null ? "iter:budget" : `iter:budget:close:${typeKey(srcT)}`; let name = lowerer.arrHofHelpers.get(key); if (name !== undefined) return name; - name = srcT === null ? "%iter.budget" : `%iter.budget.close.${lowerer.arrHofHelpers.size}`; + name = srcT === null ? "%iter.budget" : keyedHelperName(lowerer, "%iter.budget.close", key); lowerer.arrHofHelpers.set(key, name); const v = (): IrExpr => ({ kind: "varRef", localId: "v.0", type: F64, loc }); @@ -1222,7 +1223,7 @@ class HostFrame implements ChainFrame { /** The locals and parameters of a lifted helper function under * construction. */ -class LiftedFrame implements ChainFrame { +export class LiftedFrame implements ChainFrame { readonly locals: IrLocal[] = []; readonly params: IrParam[] = []; private n = 0; @@ -1252,7 +1253,7 @@ class LiftedFrame implements ChainFrame { * step leave it unclosed, as in Node. The generator counts as unfinished * before the first step, so a `pre` that breaks at once (take(0)) closes * a source it never pulled. `genT` must be stepDrivable. */ -function driveGenerator( +export function driveGenerator( lowerer: Lowerer, frame: ChainFrame, genId: string, diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 9567ee0f6..1947f0d75 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { matchAllRowsReadByElements } from "./match-row-uses.js"; import { boolLit, @@ -159,7 +160,7 @@ export function lowerStringSplitCall( const key = `str.split:${receiver.type.kind === "union" ? typeKey(receiver.type) : receiver.type.kind}:${typeKey(separator.type)}:${typeKey(limit.type)}:${absent}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.split.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.split", key); const values = [receiver, separator, limit]; const params = values.map((value, index) => ({ localId: `arg.${index}`, @@ -395,7 +396,7 @@ function stringIndexHelperCall( const key = `str.index:${method}:${typeKey(receiver.type)}:${typeKey(index.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.index.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.index", key); lowerer.valueHelpers.set(key, helper); const rawReceiver = varRef("arg.0", receiver.type, loc); const rawIndex = varRef("arg.1", index.type, loc); @@ -673,7 +674,7 @@ export function lowerStringPaddingCall( const key = `str.pad:${method}:${values.map((value) => typeKey(value.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.pad.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.pad", key); const params = values.map((value, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -919,12 +920,12 @@ export function lowerRegexMethodCall( })(); if ((receiverKind === "string" || nullableStringRecv) && name === "match") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern match: the SC2020 fence + if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence if (call.arguments.nodes.length !== 1) return null; const receiver = nullableStringRecv ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text) : lowerReceiver(); - const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); + const re = lowerRegexPatternArgument(lowerer, arg0); // RegExpMatchArray | null maps to the string[] | null union by // itself; intern it directly when the checker's spelling doesn't — // or when it maps to something WIDER (an optional-chain call node @@ -982,10 +983,10 @@ export function lowerRegexMethodCall( // 0, exactly Node. Never throws. if (receiverKind === "string" && name === "search") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || lowerer.mapTypeOf(lowerer.typeOf(arg0))?.kind !== "regex") return null; // string-pattern form: the SC2020 fence + if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence if (call.arguments.nodes.length !== 1) return null; const receiver = lowerReceiver(); - const re = lowerer.lowerExprExpecting(arg0, { kind: "regex" }); + const re = lowerRegexPatternArgument(lowerer, arg0); return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc }; } if (receiverKind === "string" && (name === "replace" || name === "replaceAll")) { @@ -1309,7 +1310,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${typeKey(needle.type)}:${typeKey(position.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.positions.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.positions", key); const params = [receiver, needle, position].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1411,7 +1412,7 @@ export function lowerStringMethodCall( const key = `str.positions:${entry.method}:${args.map((arg) => typeKey(arg.type)).join(":")}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.positions.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%str.positions", key); const params = [receiver, ...args].map((arg, index) => ({ localId: `arg.${index}`, name: `arg${index}`, @@ -1920,3 +1921,31 @@ export function lowerDynPrimitiveMethodCall( ); return seq(stmts, result); } + +/** Whether the pattern of `match` or `search` is a RegExp, or a RegExp that + * may be undefined (`s.match(patterns[i])` under noUncheckedIndexedAccess). */ +function isRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean { + const t = lowerer.mapTypeOf(lowerer.typeOf(node)); + if (t?.kind === "regex") return true; + if (t?.kind !== "union") return false; + const arms = lowerer.unions.get(t.unionId)?.arms ?? []; + return ( + arms.length === 2 && + arms.some((arm) => arm.kind === "regex") && + arms.some((arm) => arm.kind === "undefinedT") + ); +} + +/** The pattern of `match` or `search` as a RegExp: an undefined pattern is + * `new RegExp(undefined)`, the empty pattern, as in Node. */ +function lowerRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { + const regex: IrType = { kind: "regex" }; + const empty: IrExpr = { + kind: "regexLit", + pattern: "(?:)", + flags: "", + type: regex, + loc: locOf(node), + }; + return lowerOptionalArgument(lowerer, node, regex, empty); +} diff --git a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts index 6856bc50b..780c77d0f 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-replacement.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-replacement.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { BOOL, @@ -162,7 +163,7 @@ function lowerStringReplacementValues( const key = `str.${method}:${typeKey(replacement.type)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%str.${method}.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%str.${method}`, key); lowerer.valueHelpers.set(key, helper); const subject = varRef("s.0", STRING, loc); const needle = varRef("search.0", STRING, loc); diff --git a/packages/compiler/src/frontend/lowering/cycle-initialization.ts b/packages/compiler/src/frontend/lowering/cycle-initialization.ts index 092588a6e..4b1e74fcf 100644 --- a/packages/compiler/src/frontend/lowering/cycle-initialization.ts +++ b/packages/compiler/src/frontend/lowering/cycle-initialization.ts @@ -463,16 +463,38 @@ export function checkCycleBindingAccesses(lowerer: Lowerer, mod: IrModule): void /** Writes to a binding declared without an initializer that run after its * declaration store into an initialized binding whose slot may still be * empty: the pointer guard must not refuse them. Writes that can run - * earlier carry the flag check rewriteFunction inserted. */ + * earlier carry the flag check rewriteFunction inserted. An assignment in + * expression position (`(f = v)`) becomes an initializing `assign` whose + * value a temp holds and yields. */ function markUninitializedWrites(lowerer: Lowerer, mod: IrModule): void { const ids = new Set(); for (const [id, flag] of lowerer.cycleInitFlags) if (flag.uninitialized) ids.add(id); if (ids.size === 0) return; const mark = (s: IrStmt): IrStmt => s.kind === "assign" && !s.initializes && ids.has(s.localId) ? { ...s, initializes: true } : s; - const rewriteExpr = (e: IrExpr): IrExpr => mapExprChildren(e, rewriteExpr, rewriteStmt); - const rewriteStmt = (s: IrStmt): IrStmt => mark(mapStmtChildren(s, rewriteExpr, rewriteStmt)); - for (const fn of mod.functions) fn.body = fn.body.map(rewriteStmt); + for (const fn of mod.functions) { + let temps = 0; + const markExpr = (e: IrExpr): IrExpr => { + if (e.kind !== "assignExpr" || !ids.has(e.localId)) return e; + const t = `%uninitWrite.${temps++}`; + fn.locals.push({ id: t, name: "%uninitWrite", type: e.value.type, mutable: false }); + const value: IrExpr = { kind: "varRef", localId: t, type: e.value.type, loc: e.loc }; + return { + kind: "seqExpr", + stmts: [ + { kind: "varDecl", localId: t, init: e.value, loc: e.loc }, + { kind: "assign", localId: e.localId, value, initializes: true, loc: e.loc }, + ], + result: value, + type: e.type, + loc: e.loc, + }; + }; + const rewriteExpr = (e: IrExpr): IrExpr => + markExpr(mapExprChildren(e, rewriteExpr, rewriteStmt)); + const rewriteStmt = (s: IrStmt): IrStmt => mark(mapStmtChildren(s, rewriteExpr, rewriteStmt)); + fn.body = fn.body.map(rewriteStmt); + } } function rewriteFunction( diff --git a/packages/compiler/src/frontend/lowering/dyn-streams.ts b/packages/compiler/src/frontend/lowering/dyn-streams.ts index 2ac09e8e5..2167eba23 100644 --- a/packages/compiler/src/frontend/lowering/dyn-streams.ts +++ b/packages/compiler/src/frontend/lowering/dyn-streams.ts @@ -76,7 +76,7 @@ function dynNativeStream( /** Runs `lower` with `node` lowering to `value` and its member calls taking * `type` as the receiver representation. */ -function withStreamReceiver( +export function withStreamReceiver( lowerer: Lowerer, node: ts.Expression, value: IrExpr, diff --git a/packages/compiler/src/frontend/lowering/dynamic-tier.ts b/packages/compiler/src/frontend/lowering/dynamic-tier.ts index 0293f9ab7..3e0cea20a 100644 --- a/packages/compiler/src/frontend/lowering/dynamic-tier.ts +++ b/packages/compiler/src/frontend/lowering/dynamic-tier.ts @@ -304,7 +304,18 @@ export function scanDynamicTier(lowerer: Lowerer, module: IrModule): DynamicTier }); } const rank = { loop: 0, "called-in-loop": 1, recursive: 2 } as const; - hot.sort((a, b) => rank[a.hot] - rank[b.hot] || b.loopOperations - a.loopOperations); + // Functions that rank alike list in source order: by the file's place in + // the module order, then by position. + const fileOrder = new Map(lowerer.moduleOrder.map((sf, index) => [sf.fileName, index])); + const fileRank = (file: string): number => fileOrder.get(file) ?? lowerer.moduleOrder.length; + hot.sort( + (a, b) => + rank[a.hot] - rank[b.hot] || + b.loopOperations - a.loopOperations || + fileRank(a.loc.file) - fileRank(b.loc.file) || + (a.loc.file < b.loc.file ? -1 : a.loc.file > b.loc.file ? 1 : 0) || + a.loc.start - b.loc.start, + ); return { hot, packages: [...packages] @@ -332,14 +343,15 @@ function boundName(lowerer: Lowerer, loc: SrcLoc): string | null { /** A function's name as the source spells it, without a module prefix * (`%%m144.Session.finish` → `Session.finish`, `m163.find` → `find`). A - * function expression (`%fn`) takes its own name, else the name it is - * bound to, else `anonymous function`. */ + * function expression (`%fn.`, with `_` when the expression is + * named) takes its own name, else the name it is bound to, else + * `anonymous function`. */ function displayName(lowerer: Lowerer, f: HotDynamicFunction): string { const plain = f.name .replace(/^%+/, "") .replace(/^m\d+\./, "") .replace(/^%+/, ""); - const lambda = /^fn\d+(?:_(.+))?$/.exec(plain); + const lambda = /^fn\.[0-9a-f]+(?:_(.+))?$/.exec(plain); if (lambda === null) return plain; return lambda[1] ?? boundName(lowerer, f.loc) ?? "anonymous function"; } diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts index 00eae1041..8f119504c 100644 --- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts @@ -184,6 +184,21 @@ export function lowerArrayLiteral( if (arms.length === 1) mapped = arms[0]!; } } + // A NON-EMPTY literal whose own type is an array, under a union with + // exactly one array arm and no tuple arm (`[1, { n: true }]` in a + // `Json[]` arm of a recursive `Json`): the checker types each element + // from the arm, so its object literals lower as the arm's elements, + // and the literal's own element type (`number | { n: boolean }`) + // cannot hold them. Build it as the arm; its value only flows into the + // union slot. + if (ctxUnion && expr.elements.nodes.length > 0 && mapped?.kind === "array") { + const def = lowerer.unions.get(ctxUnion.unionId); + const arrayArms = def?.arms.filter((a) => a.kind === "array") ?? []; + const tupleArms = + def?.arms.filter((a) => a.kind === "record" && !!lowerer.shapes.get(a.shapeId)?.tuple) ?? + []; + if (arrayArms.length === 1 && tupleArms.length === 0) mapped = arrayArms[0]!; + } } // A Date's toJSON() element keeps its null in a literal with no declared // destination: the lib types say string. diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts index fafbbf695..9702a6dd9 100644 --- a/packages/compiler/src/frontend/lowering/expressions/binary.ts +++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts @@ -1,4 +1,5 @@ /* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */ +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { cleanFuncAdaptable } from "../coercions/function-adapters.js"; import { type Lowerer, jsFuncNameOf } from "../lowerer.js"; @@ -801,7 +802,7 @@ function nullProtoToString(lowerer: Lowerer, e: IrExpr, t: IrType & { kind: "rec const key = `nullProtoToString:${t.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%rec.tostr.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.tostr", key); lowerer.valueHelpers.set(key, helper); const r = varRef("r.0", t, loc); lowerer.liftedFns.push({ @@ -902,10 +903,15 @@ export function lowerCompoundValueToTarget( // operator on both values, then the checked conversion into the // target's declared type (a string result for a number target throws the // catchable TypeError, as the expanded assignment does). + // A union operand (`fraction += "0"` on a binding the program assigns + // both a number and a string) takes the same runtime operator. if ( - (target.type.kind === "dyn" || rhs.type.kind === "dyn") && - (isJsSourceFile(ts.getSourceFileOfNode(expr)) || - (lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type))) + ((target.type.kind === "dyn" || rhs.type.kind === "dyn") && + (isJsSourceFile(ts.getSourceFileOfNode(expr)) || + (lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type)))) || + ((target.type.kind === "union" || rhs.type.kind === "union") && + lowerer.dynConvertible(read.type) && + lowerer.dynConvertible(rhs.type)) ) { const left = lowerer.coerceInto(expr.left, read, DYN); const right = lowerer.coerceInto(expr.right, rhs, DYN); @@ -1276,6 +1282,48 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr loc, }; } + // `a[k] = b[k] = v` in a JavaScript file: a member write in value + // position lowers as its statement form and yields the value. The + // receiver, the key and the value are evaluated in that order into + // temporaries first. A receiver held in a temporary must be a + // checked-dynamic value, which the temporary shares; any other receiver + // must be a variable or `this`. + if ( + op === ts.SyntaxKind.EqualsToken && + isJsSourceFile(ts.getSourceFileOfNode(expr)) && + (ts.isPropertyAccessExpression(expr.left) || ts.isElementAccessExpression(expr.left)) && + !expr.left.questionDotToken + ) { + const target = expr.left; + const receiverNode = target.expression; + const bareReceiver = + ts.isIdentifier(receiverNode) || receiverNode.kind === ts.SyntaxKind.ThisKeyword; + if (bareReceiver || tryLowerExpression(lowerer, receiverNode)?.type.kind === "dyn") { + const stmts: IrStmt[] = []; + const overrides: { node: ts.Expression; value: IrExpr }[] = []; + const hold = (node: ts.Expression, name: string): IrExpr => { + const value = lowerer.lowerExpr(node); + const local = lowerer.declareHiddenLocal(name, value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc }); + const held = varRef(local.id, value.type, value.loc); + overrides.push({ node, value: held }); + return held; + }; + if (!bareReceiver) hold(receiverNode, "%assignReceiver"); + if ( + ts.isElementAccessExpression(target) && + !ts.isIdentifier(target.argumentExpression) && + !ts.isStringLiteral(target.argumentExpression) && + !ts.isNumericLiteral(target.argumentExpression) + ) + hold(target.argumentExpression, "%assignKey"); + const result = hold(expr.right, "%assignValue"); + stmts.push( + lowerer.withExpressionOverrides(overrides, () => lowerer.lowerExprStatement(expr)), + ); + return { kind: "seqExpr", stmts, result, type: result.type, loc }; + } + } lowerer.unsupported( "SC1090", expr, diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 74f0be955..3ca1471ae 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -616,9 +616,25 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre // An indexed string property is absent for non-integer and missing // indexes. charAt has different coercion and out-of-range semantics. const recv = lowerer.lowerExpr(expr.expression); + const rawIndex = lowerer.lowerExpr(expr.argumentExpression); + // A checked-dynamic index (an untyped JavaScript parameter, a parsed + // value) reads the property its key names, as JavaScript does: a code + // unit for an integer index, the length for "length", else undefined. + // The checker's string type does not describe those results, so the + // value stays checked-dynamic and consumers convert it. + if (rawIndex.type.kind === "dyn" && lowerer.dynConvertible(recv.type)) { + const key = lowerRecordPropertyKey(lowerer, rawIndex, expr.argumentExpression); + return { + kind: "dynKeyGet", + value: lowerer.coerceInto(expr.expression, recv, DYN), + key, + type: DYN, + loc: locOf(expr), + }; + } const index = lowerOptionalNumber( lowerer, - lowerer.lowerExpr(expr.argumentExpression), + rawIndex, locOf(expr.argumentExpression), expr.argumentExpression, ); diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts index b5f99d682..1f74cea42 100644 --- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts +++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts @@ -11,6 +11,7 @@ import { DYN, F64, type IrExpr, + type IrLocal, type IrType, STRING, type SrcLoc, @@ -226,7 +227,10 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: if (use?.kind === "element") { const key = runtimeOptionalElementKey(use.access.argumentExpression); if (key === null) { - return varRef(local.id, local.type, loc); + return ( + runtimeOptionalKeyedRecord(lowerer, expr, local, loc) ?? + varRef(local.id, local.type, loc) + ); } return runtimeOptionalReceiverRead(lowerer, expr, local, key, loc); } @@ -549,6 +553,9 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: const callable = lowerer.lowerBuiltinCallableValue(bi, loc); if (callable) return callable; } + // node:crypto's webcrypto is the WebCrypto global's object. + if (bi.module === "crypto" && bi.member === "webcrypto") + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; // JavaScript sources: an otherwise unsupported builtin VALUE is // the same identity-token story as stdlib globals above (the // harness adds worker_threads.Worker to its identity Set). @@ -721,7 +728,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc: return { kind: "closure", fnName: name, captures: [], type, loc }; } if (lowerer.isStdlibGlobal(expr, "crypto")) - return { kind: "libCall", fn: "crypto.native", args: [], type: DYN, loc }; + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; const nativeAlias = stdlibGlobalNameOf(lowerer, expr); if (nativeAlias === "Buffer" && expr.text !== "Buffer") { return { kind: "strLit", value: "[builtin Buffer]", type: STRING, loc }; @@ -935,10 +942,43 @@ function moduleFileName(lowerer: Lowerer, sf: ts.SourceFile): string { : sf.fileName; } -/** Object.keys/Object.entries require an object receiver, but their first - * argument is not syntactically a member receiver. Recognize that one - * standard-library position so a runtime-optional loop element can be - * checked against the checker-narrowed record arm before builtin lowering. */ +/** A computed-key access (`value[key]` after `typeof value === "object"`) + * of a runtime-optional binding the checker narrowed to one record arm of a + * wider union. The binding re-tags to that record or undefined, so the keyed + * read or write evaluates its operands and then throws Node's TypeError for + * an undefined receiver. The other arms, which the checker excluded, fail + * the checked re-tag. Null when the binding stores only the record and + * undefined, or the narrowed record is not one of its arms. */ +function runtimeOptionalKeyedRecord( + lowerer: Lowerer, + expr: ts.Identifier, + local: IrLocal, + loc: SrcLoc, +): IrExpr | null { + if (local.type.kind !== "union") return null; + const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr)); + if (narrowed?.kind !== "record") return null; + const fromId = local.type.unionId; + const arms = lowerer.unions.get(fromId)?.arms; + const recordTag = lowerer.armTag(fromId, narrowed); + const undefTag = lowerer.armTag(fromId, UNDEFINED_T); + if (!arms || arms.length <= 2 || recordTag < 0 || undefTag < 0) return null; + const target = lowerer.withUndefinedArm(narrowed); + if (target.kind !== "union") return null; + const trappable = new Set(); + for (let tag = 0; tag < arms.length; tag++) { + if (tag !== recordTag && tag !== undefTag) trappable.add(tag); + } + const helper = lowerer.unionRetagHelper(fromId, target.unionId, loc, trappable); + if (!helper) return null; + return lowerer.checkedNarrowCall(helper, varRef(local.id, local.type, loc), target, loc); +} + +/** Object.keys, Object.values and Object.entries require an object + * receiver, but their first argument is not syntactically a member + * receiver. Recognize that one standard-library position so a + * runtime-optional loop element can be checked against the checker-narrowed + * record arm before builtin lowering. */ function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression): boolean { let arg = expr; while ( @@ -959,7 +999,9 @@ function runtimeOptionalObjectWalkerArg(lowerer: Lowerer, expr: ts.Expression): return false; const access = call.expression; return ( - (access.name.text === "keys" || access.name.text === "entries") && + (access.name.text === "keys" || + access.name.text === "values" || + access.name.text === "entries") && lowerer.isStdlibMember(access) ); } diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts index 565db0cd8..015be7914 100644 --- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts +++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "../stable-names.js"; import { dateJsonType, isDateJsonSource } from "../builtins/dates.js"; import { dynUndefinedExpr, numLit, strLit, varRef } from "../../../ir/build.js"; import { pathModuleValue } from "../lower-builtin-values.js"; @@ -406,7 +407,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError) || !pureMember) throw err; const fence = lowerer.deferToRuntimeFence(propDiagsBefore, prop, { kind: "closure", - name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, + name: () => lowerer.closureName(loc, "_dynfence"), returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -461,7 +462,7 @@ export function lowerDynObjectLiteral( if (!(err instanceof PoisonError)) throw err; const fence = lowerer.deferToRuntimeFence(convDiagsBefore, prop, { kind: "closure", - name: () => `%fn${lowerer.lambdaCounter++}_dynfence`, + name: () => lowerer.closureName(loc, "_dynfence"), returnType: VOID, type: { kind: "func", params: [], ret: VOID }, }); @@ -797,6 +798,7 @@ export function lowerObjectLiteral( ): IrExpr { // Unannotated JavaScript accumulator fields have no element contract. // Preserve their checked arrays instead of assigning never[] a numeric ABI. + // An empty object field is a map the program adds keys to. if ( !expected && isJsSourceFile(ts.getSourceFileOfNode(expr)) && @@ -805,6 +807,8 @@ export function lowerObjectLiteral( ts.isPropertyAssignment(prop) && ((ts.isArrayLiteralExpression(prop.initializer) && prop.initializer.elements.nodes.length === 0) || + (ts.isObjectLiteralExpression(prop.initializer) && + prop.initializer.properties.nodes.length === 0) || neverTaintedJsType(lowerer, prop.initializer, lowerer.typeOf(prop.initializer))), ) ) { @@ -2956,7 +2960,7 @@ function lowerDeclaredSpreadMerge( const key = `declmerge-source:${type.shapeId}:${source.shapeId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%rec.declmerge.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.declmerge", key); const sourceType = source.value.type; const destination = varRef("out.0", type, loc); const receiver = varRef("source.0", sourceType, loc); diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts index fbcecdc32..4b8c5692a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts +++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts @@ -240,7 +240,7 @@ export function lowerOptionalChain( const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver; if (receiver.type.kind === "dyn") { if (dotNode === expr && ts.isCallExpression(expr)) { - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const callee: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; const stmts: IrStmt[] = []; let guarded = receiver; @@ -298,7 +298,7 @@ export function lowerOptionalChain( ); } if (dotNode === expr && ts.isElementAccessExpression(expr)) { - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -331,7 +331,7 @@ export function lowerOptionalChain( dotNode === expr.expression ) { const m = dotNode.name.text; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const recvRef: IrExpr = { kind: "chainRecv", id, type: DYN, loc: locOf(recvNode) }; lowerer.chainRecvByNode.set(recvNode, recvRef); lowerer.chainHandled.add(dotNode); @@ -358,7 +358,7 @@ export function lowerOptionalChain( } return { kind: "optChain", id, receiver, body, type: DYN, loc }; } - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const reference: IrExpr = { kind: "chainRecv", id, type: DYN, loc }; lowerer.chainRecvByNode.set(recvNode, reference); lowerer.chainHandled.add(dotNode); @@ -420,7 +420,7 @@ export function lowerOptionalChain( rest.length === 1 ? rest[0]! : { kind: "union" as const, unionId: lowerer.unions.transform(def, rest) }; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); // A single present arm binds its payload. Multiple present arms bind // the original tagged value, then retag inside the guarded body. The // nullish arms never enter that body, and each record keeps its identity. diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts index f0b980da0..957dd1ddf 100644 --- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts @@ -1,11 +1,16 @@ /* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */ +import { keyedHelperName } from "../stable-names.js"; import { staticallySelected } from "../static-typeof.js"; import * as ts from "../../ts7/adapter.js"; import { lowerHistogramProperty } from "../builtins/performance.js"; import { staticFunctionNameRead } from "../static-function-names.js"; import { typedArgumentsArity } from "../typed-arguments.js"; import { isThisParameter } from "../call-signatures.js"; -import { localPrivateBrandClass } from "../private-brands.js"; +import { + checkedPrivateValue, + localPrivateBrandClass, + privateNameClass, +} from "../private-brands.js"; import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js"; import { ARRAY_METHODS, @@ -90,7 +95,7 @@ import { runtimeConstructorReceiver, } from "../builtins/runtime-constructor.js"; import { findGenericMethodOn, lowerStaticFieldRead } from "../lower-classes.js"; -import { lowerClassPrototypeData } from "../class-prototypes.js"; +import { isErrorPrototypeValue, lowerClassPrototypeData } from "../class-prototypes.js"; import { lowerEnumAccess } from "../lower-enums.js"; import { hasOptionalChainGuard, @@ -131,6 +136,7 @@ import { lowerProtocolPropertyRead } from "../class-protocol-dispatch.js"; import { lowerReadlineProperty } from "../builtins/readline.js"; import { lowerEventStaticProperty } from "../builtins/numbers.js"; import { lowerStreamProperty, lowerStreamStateProperty, streamSidesOf } from "../lower-stream.js"; +import { lowerValueStreamProperty } from "../builtins/hash-stream.js"; import { lowerEventObjectProperty } from "../lower-event-target.js"; import { iteratorResultValueRead } from "../lower-generators.js"; import { lowerGlobalValue } from "../lower-global-value.js"; @@ -162,6 +168,7 @@ import { lowerUnionFieldRead } from "./union-fields.js"; import { errorPropertyRead } from "../error-methods.js"; import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js"; import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "../builtins/compression.js"; +import { isWebCryptoReceiver } from "../builtins/crypto.js"; import { isFsStreamMember, lowerFsStreamProperty } from "../builtins/fs-streams.js"; import { keyedFieldRead } from "./keyed-field-targets.js"; import { promisePrototypeValue } from "../thenables.js"; @@ -281,6 +288,24 @@ export function lowerPropertyAccessExpr( expr: ts.PropertyAccessExpression, loc: SrcLoc, ): IrExpr { + // `value.#x` on a checked-dynamic value, or on a checked extraction of + // one: the field of the brand-checked receiver (checkedPrivateValue). + if (ts.isPrivateIdentifier(expr.name)) { + const owner = privateNameClass(lowerer, expr.name); + const field = owner?.fields.get(expr.name.text); + const probed = field ? tryLowerExpression(lowerer, expr.expression) : null; + if (owner && field && probed && checkedDynamicValue(probed)) { + const value = checkedDynamicValue(lowerer.lowerExpr(expr.expression))!; + return { + kind: "fieldGet", + obj: checkedPrivateValue(lowerer, owner, expr, value, loc), + className: owner.def.name, + field: expr.name.text, + type: field, + loc, + }; + } + } if (!expr.questionDotToken) { const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION); if (navigator) return navigator; @@ -842,8 +867,16 @@ export function lowerPropertyAccessExpr( if (runtimeOptionalClassField(lowerer, handled, expr)) return handled; return iteratorResultValueRead(lowerer, handled, expr) ?? lowerer.maybeNarrow(handled, expr); } + // `globalThis.crypto` is the WebCrypto object the bare `crypto` answers. + if ( + !expr.questionDotToken && + stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis" && + stdlibGlobalNameOf(lowerer, expr) === "crypto" + ) { + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; + } // `globalThis.` that no lowering above claimed - // (globalThis.crypto, globalThis.SubtleCrypto, globalThis.localStorage + // (globalThis.SubtleCrypto, globalThis.localStorage // — the harness's capability-conditional knownGlobals adds): the // PROPERTY spelling of the identifier chokepoint's JS rule — a stdlib // global taken as a VALUE in a JavaScript source is the same opaque @@ -1664,6 +1697,19 @@ export function lowerIntrinsicProperty( return { kind: "libCall", fn: "dyn.arrayPrototype", args: [], type: DYN, loc: locOf(expr) }; if (lowerer.stdlibGlobalMember(expr, "Object") === "prototype") return { kind: "libCall", fn: "dyn.objectPrototype", args: [], type: DYN, loc: locOf(expr) }; + // Error.prototype as a value (`F.prototype = Object.create(Error.prototype)`) + // and Error.captureStackTrace read as a function (`if + // (Error.captureStackTrace) ...`). + if (isErrorPrototypeValue(lowerer, expr)) + return { kind: "libCall", fn: "dyn.errorPrototype", args: [], type: DYN, loc: locOf(expr) }; + if (lowerer.stdlibGlobalMember(expr, "Error") === "captureStackTrace") + return { + kind: "libCall", + fn: "dyn.errorCaptureStackTraceFunction", + args: [], + type: DYN, + loc: locOf(expr), + }; // Function.prototype as a VALUE (a prototype-chain link: // `Object.setPrototypeOf(F.prototype, Function.prototype)`): one // anonymous function answering undefined, length 0 — a zero-capture @@ -2783,7 +2829,7 @@ export function lowerGroupsProjection( const key = `regexgroups:${shapeId}:${groups.map((g) => `${g.name}=${g.index}`).join(",")}:${recvKey}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%regex.groups.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%regex.groups", key); lowerer.valueHelpers.set(key, helper); const body: IrStmt[] = []; const locals: IrLocal[] = [{ id: "m.0", name: "m", type: recv.type, mutable: true }]; @@ -2959,6 +3005,9 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi if ( receiverIr?.kind !== "object" && propSym && + // The WebCrypto object's generic getRandomValues reads off its + // dynamic object. + !isWebCryptoReceiver(lowerer, expr.expression) && isGenericCallableMemberType(lowerer.checker.getTypeOfSymbol(propSym), lowerer.checker) && // CLASS members keep the class-path fences (a poisoned class's own // diagnostics, the bound-method fence above). @@ -3533,6 +3582,13 @@ export function classFieldTarget( * known class instance OR a record, and the member is a field or (class * receivers) a declared accessor property. Returns the pieces of a * fieldSet/recordSet/accessor-call (minus value/kind) or null. */ +/** The checked-dynamic value `expr` reads: itself, or the value a checked + * extraction validates. Null for other expressions. */ +function checkedDynamicValue(expr: IrExpr): IrExpr | null { + if (expr.type.kind === "dyn") return expr; + return expr.kind === "dynCheck" && expr.value.type.kind === "dyn" ? expr.value : null; +} + export function fieldTarget( lowerer: Lowerer, access: ts.PropertyAccessExpression, @@ -3569,6 +3625,30 @@ export function fieldTarget( return classFieldTarget(lowerer, access.expression, ownerType, access.name.text); } } + // `value.#x = v` on a checked-dynamic value: the field of the + // brand-checked receiver (checkedPrivateValue). + if (ts.isPrivateIdentifier(access.name)) { + const owner = privateNameClass(lowerer, access.name); + const probed = owner?.fields.has(access.name.text) + ? tryLowerExpression(lowerer, access.expression) + : null; + if (owner && probed && checkedDynamicValue(probed)) { + const receiver = checkedPrivateValue( + lowerer, + owner, + access, + checkedDynamicValue(lowerer.lowerExpr(access.expression))!, + locOf(access), + ); + return classFieldTarget( + lowerer, + access.expression, + { kind: "object", className: owner.def.name }, + access.name.text, + receiver, + ); + } + } const stored = ts.isIdentifier(access.expression) ? (lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type) : undefined; @@ -3994,6 +4074,8 @@ function lowerStreamObjectProperty( expr: ts.PropertyAccessExpression, ): IrExpr | null { if (expr.questionDotToken && !lowerer.chainHandled.has(expr)) return null; + const valueStream = lowerValueStreamProperty(lowerer, expr); + if (valueStream) return valueStream; const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.expression)); if (recvT?.kind !== "object") return null; const info = lowerer.classes.get(recvT.className); diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index 088b45cff..61558f008 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -1,4 +1,6 @@ /* instanceof and in, including private-name and runtime-key in. */ +import { dateRootOf } from "../builtins/date-classes.js"; +import { keyedHelperName } from "../stable-names.js"; import * as ts from "../../ts7/adapter.js"; import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js"; import { @@ -265,7 +267,12 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc stmts: [{ kind: "exprStmt", expr: value, loc }], result: { kind: "boolLit", - value: value.type.kind === (date ? "date" : "url"), + value: + value.type.kind === (date ? "date" : "url") || + // An instance of a class declared `extends Date`. + (date && + value.type.kind === "object" && + dateRootOf(lowerer.classes.get(value.type.className)) !== null), type: BOOL, loc, }, @@ -565,7 +572,33 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc loc, }; }; - const test = caughtInstanceOf(target); + let test = caughtInstanceOf(target); + // A thrown checked-dynamic object whose prototype chain reaches + // Error.prototype (an ES5 constructor function inheriting from it). + if (target.def.name === "%Error" && lowerer.errorPrototypeValues) { + const value = varRef(caughtLocal.id, CAUGHT, loc); + test = { + kind: "logical", + op: "||", + left: test, + right: { + kind: "logical", + op: "&&", + left: { kind: "caughtTest", value, test: "dynamic", type: BOOL, loc }, + right: { + kind: "libCall", + fn: "dyn.inheritsErrorPrototype", + args: [{ kind: "caughtToDyn", value, type: DYN, loc }], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } return duplex ? { kind: "logical", op: "||", left: test, right: caughtInstanceOf(duplex), type: BOOL, loc } : test; @@ -590,9 +623,35 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc ) { return classInstanceOf(lowerer, left, target, loc); } - if (left.type.kind === "dyn" && target.def.name === "%Error") { + if (left.type.kind === "dyn" && target.def.name === "%Error" && !lowerer.errorPrototypeValues) { return { kind: "dynTest", test: "error", value: left, type: BOOL, loc }; } + // An object whose prototype chain reaches Error.prototype (an ES5 + // constructor function inheriting from it) is an instance as well. + if (left.type.kind === "dyn" && target.def.name === "%Error") { + const local = lowerer.declareHiddenLocal("%instanceofValue", DYN); + const value = varRef(local.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: left, loc }], + result: { + kind: "logical", + op: "||", + left: { kind: "dynTest", test: "error", value, type: BOOL, loc }, + right: { + kind: "libCall", + fn: "dyn.inheritsErrorPrototype", + args: [value], + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } // `u instanceof TypeError` (and the other BUILTIN error classes) on a // dyn value: the from_error cache holds the checked-dynamic tree↔error identity edge, // so the runtime resolves the encoding back to its error and asks the @@ -647,7 +706,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const key = `instanceof.union:${unionId}:${target.def.name}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%instanceof.union.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%instanceof.union", key); lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: left.type, loc }; const body: IrStmt[] = []; @@ -1847,7 +1906,7 @@ function lowerRuntimeKeyIn( const hkey = `haskey:${recvT.shapeId}`; let helper = lowerer.valueHelpers.get(hkey); if (!helper) { - helper = `%rec.haskey.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%rec.haskey", hkey); lowerer.valueHelpers.set(hkey, helper); const recT: IrType = { kind: "record", shapeId: recvT.shapeId }; diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts index 13baca7d2..128093dad 100644 --- a/packages/compiler/src/frontend/lowering/expressions/updates.ts +++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts @@ -23,6 +23,7 @@ import { type IrExpr, type IrStmt, type IrType, + isUnitType, STRING, type SrcLoc, UNDEFINED_T, @@ -100,6 +101,27 @@ function writeFree(expr: IrExpr): boolean { } } +/** A union unary `+`/`-` converts with the runtime's ToNumber: one with an + * arm other than numbers and units (`string | undefined`). Number and unit + * unions keep their native lowering. */ +function coercedUnion(lowerer: Lowerer, type: IrType): boolean { + if (type.kind !== "union" || !lowerer.dynConvertible(type)) return false; + const arms = lowerer.unions.get(type.unionId)?.arms ?? []; + return arms.some((arm) => arm.kind !== "f64" && !isUnitType(arm)); +} + +/** ToNumber of a boolean: 1 for true, 0 for false. */ +function booleanToNumber(value: IrExpr, loc: SrcLoc): IrExpr { + return { + kind: "ternary", + cond: value, + then: { kind: "numLit", value: 1, type: F64, loc }, + else_: { kind: "numLit", value: 0, type: F64, loc }, + type: F64, + loc, + }; +} + export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpression): IrExpr { const loc = locOf(expr); switch (expr.operator) { @@ -111,9 +133,25 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio } // Match the checked numeric boundary used by JS binary arithmetic. const operand = - raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) - ? { kind: "dynCheck" as const, value: raw, type: F64, loc } - : lowerOptionalNumber(lowerer, raw, loc, expr.operand); + raw.type.kind === "bool" + ? booleanToNumber(raw, loc) + : coercedUnion(lowerer, raw.type) + ? ({ + kind: "libCall", + fn: "dyn.toNumberCoerce", + args: [{ kind: "dynFrom", value: raw, type: DYN, loc }], + type: F64, + loc, + } satisfies IrExpr) + : raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr)) + ? ({ + kind: "libCall", + fn: "dyn.toNumberCoerce", + args: [raw], + type: F64, + loc, + } satisfies IrExpr) + : lowerOptionalNumber(lowerer, raw, loc, expr.operand); if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]); if (operand.kind === "numLit") return { ...operand, value: -operand.value, loc }; return { kind: "unary", op: "-", operand, type: F64, loc }; @@ -123,6 +161,12 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio if (raw.type.kind === "dyn") { return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [raw], type: F64, loc }; } + // A union of primitives (`+match[1]` over `string | undefined`): ToNumber + // over the boxed value, as for a checked-dynamic operand. + if (coercedUnion(lowerer, raw.type)) { + const boxed: IrExpr = { kind: "dynFrom", value: raw, type: DYN, loc }; + return { kind: "libCall", fn: "dyn.toNumberCoerce", args: [boxed], type: F64, loc }; + } // Unary + is ToNumber; on an already-number operand it's identity, // and a STRING operand runs the runtime's ECMA-exact StringToNumber // (num.fromString — Number(aString)'s lowering, scr_string.c). @@ -130,6 +174,7 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio if (raw.type.kind === "string") { return { kind: "libCall", fn: "num.fromString", args: [raw], type: F64, loc }; } + if (raw.type.kind === "bool") return booleanToNumber(raw, loc); const classNumber = classToNumberConversion(lowerer, raw, expr.operand, "unary plus"); if (classNumber) return classNumber; const operand = lowerOptionalNumber(lowerer, raw, loc, expr.operand); @@ -321,9 +366,13 @@ export function lowerIncDec( !isJsSourceFile(ts.getSourceFileOfNode(expr)) && raw.type.kind === "dyn" && lowerer.mapTypeOf(lowerer.typeOf(access))?.kind === "f64"; + // A private name is the field of its class (fieldTarget). + const privateName = + ts.isPropertyAccessExpression(access) && ts.isPrivateIdentifier(access.name); if ( (isJsSourceFile(ts.getSourceFileOfNode(expr)) || checkedNumber) && - raw.type.kind === "dyn" + raw.type.kind === "dyn" && + !privateName ) { const receiver = save(raw, "%incrementReceiver"); const key = save( @@ -814,7 +863,8 @@ export function lowerFieldCompoundValue( probed?.type.kind === "classval" ? lowerer.classes.get(probed.type.className) : undefined; if (probed && staticClass && hasRuntimeStatics(staticClass)) probed = lowerer.coerceToExpected(probed, DYN); - if (probed?.type.kind === "dyn") { + // A private name is the field of its class (fieldTarget). + if (probed?.type.kind === "dyn" && !ts.isPrivateIdentifier(access.name)) { const receiver = save(probed, "%compoundReceiver"); const key: IrExpr = { kind: "strLit", diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index abcd5a584..1fff26585 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -1,4 +1,6 @@ import { staticNameBindings } from "./static-function-names.js"; +import { positionTag } from "./structural-positions.js"; +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { bodyReadsArguments } from "../arguments-usage.js"; import type { Lowerer } from "./lowerer.js"; @@ -129,6 +131,10 @@ export interface GenericInstance { tsBindings?: Map; /** Rendered type arguments ("") for diagnostics. */ typeArgsText: string; + /** A factory returning one of its local classes through a declared + * constructor type: that type (by id) and the class value it maps to + * while the body lowers (Lowerer.instanceResultClass). */ + resultClass?: { declared: number; value: IrType }; /** Implicit instances only: param symbol → the call site's (widened) * checker type, consulted by the Lowerer's typeOf while this instance's * body lowers (the implicit twin of `bindings`). */ @@ -229,7 +235,10 @@ export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDecla lowerer.genericFnsBySymbol.set(symbol, { decl, baseName: nameText, - qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText), + qualifiedName: lowerer.qualify( + ts.getSourceFileOfNode(decl), + nsPathPrefix(lowerer, decl) + nameText, + ), typeParams, instances: new Map(), }); @@ -468,7 +477,8 @@ function localClassResultInstance( params: ParamShape[], retTs: ts.Type, ): GenericInstance | null { - if (!localClassOfResult(lowerer, info, retTs)) return null; + const resultClass = localClassOfResult(lowerer, info, retTs); + if (!resultClass) return null; const tsBindings = new Map(); const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings); const bindingKey = info.typeParams @@ -483,12 +493,37 @@ function localClassResultInstance( tsBindings, }); if (inst.returnType !== pending) return inst; - const returnType = withInstanceScope(lowerer, info, inst, () => lowerer.mapTypeOf(retTs)); + // A result typed by a declared constructor type rather than the class + // (`Struct`) is the class value of the local class the body returns. + const declaredResult = info.decl.type + ? lowerer.checker.getTypeFromTypeNode(info.decl.type) + : undefined; + const viaConstructorType = retTs.getSymbol()?.valueDeclaration !== resultClass; + const returnType = withInstanceScope(lowerer, info, inst, () => { + if (!viaConstructorType) return lowerer.mapTypeOf(retTs); + // The local class's own constructor type: a class expression's type is + // its constructor type, named or not (`return class { ... } as Struct`). + const classSymbol = + !ts.isClassExpression(resultClass) && resultClass.name + ? lowerer.checker.getSymbolAtLocation(resultClass.name) + : undefined; + const classType = ts.isClassExpression(resultClass) + ? lowerer.checker.getTypeAtLocation(resultClass) + : classSymbol + ? lowerer.checker.getTypeOfSymbol(classSymbol) + : undefined; + return classType ? lowerer.mapTypeOf(classType) : null; + }); if (!returnType) { fenceGenericSignatureResult(lowerer, expr, retTs); lowerer.badType(expr, retTs); } inst.returnType = returnType; + if (viaConstructorType && returnType.kind === "classval") { + if (declaredResult) inst.resultClass = { declared: declaredResult.id, value: returnType }; + if (lowerer.typeParamBindings === null) lowerer.localClassValueTypes.set(retTs.id, returnType); + return inst; + } // Values of the class outside the factory (`const b = new B()`, `b.read()`) // carry the instantiated checker type; record which class it names. A // call inside another generic instance can have a type that depends on @@ -563,12 +598,102 @@ function localClassOfResult( type: ts.Type, ): ts.ClassLikeDeclaration | null { const symbol = type.getSymbol(); - if (!symbol || !(symbol.flags & ts.SymbolFlags.Class)) return null; - const decl = symbol.valueDeclaration; - if (!decl || !(ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) return null; - for (let parent = decl.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) - if (parent === info.decl) return decl; - return null; + const decl = symbol && symbol.flags & ts.SymbolFlags.Class ? symbol.valueDeclaration : undefined; + if (decl && (ts.isClassExpression(decl) || ts.isClassDeclaration(decl))) + return declaredInside(decl, info.decl) ? decl : null; + // A result typed by a constructor type (`Struct`, `new () => S`) whose + // every return gives back the one local class, through parentheses and + // assertions (`return Base as Struct`). + // A mixin (a class extending one of the function's parameters) keeps the + // mixin instantiation, which keys on the argument class. + if (lowerer.checker.getConstructSignatures(type).length === 0) return null; + const returned = returnedLocalClass(lowerer, info); + return returned && !extendsParameter(lowerer, returned, info) ? returned : null; +} + +function extendsParameter( + lowerer: Lowerer, + decl: ts.ClassLikeDeclaration, + info: GenericFnInfo, +): boolean { + const params = new Set(); + const fn = info.decl as { parameters?: { nodes: readonly ts.ParameterDeclaration[] } }; + for (const param of fn.parameters?.nodes ?? []) { + const symbol = ts.isIdentifier(param.name) + ? lowerer.checker.getSymbolAtLocation(param.name) + : undefined; + if (symbol !== undefined) params.add(symbol); + } + for (const clause of decl.heritageClauses?.nodes ?? []) + for (const base of clause.types.nodes) { + const symbol = ts.isIdentifier(base.expression) + ? lowerer.checker.getSymbolAtLocation(base.expression) + : undefined; + if (symbol !== undefined && params.has(symbol)) return true; + } + return false; +} + +function declaredInside(node: ts.Node, owner: ts.Node): boolean { + for (let parent = node.parent; parent && !ts.isSourceFile(parent); parent = parent.parent) + if (parent === owner) return true; + return false; +} + +/** The local class every return of `info`'s body gives back, or null when a + * return gives anything else (or nothing). Nested functions and classes keep + * their own returns. */ +function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLikeDeclaration | null { + const fn = info.decl as ts.Node & { body?: ts.Node }; + const body = fn.body; + if (!body) return null; + const classOf = (node: ts.Expression): ts.ClassLikeDeclaration | null => { + let expr = node; + while ( + ts.isParenthesizedExpression(expr) || + ts.isAsExpression(expr) || + ts.isSatisfiesExpression(expr) || + ts.isNonNullExpression(expr) + ) + expr = expr.expression; + if (ts.isClassExpression(expr)) return declaredInside(expr, info.decl) ? expr : null; + if (!ts.isIdentifier(expr)) return null; + const declaration = lowerer.checker.getSymbolAtLocation(expr)?.valueDeclaration; + if (!declaration || !declaredInside(declaration, info.decl)) return null; + if (ts.isClassDeclaration(declaration)) return declaration; + // A `const` bound to a class expression (`const C = class { ... }; return C`). + if ( + ts.isVariableDeclaration(declaration) && + ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const && + declaration.initializer + ) { + let init: ts.Expression = declaration.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isClassExpression(init)) return init; + } + return null; + }; + if (!ts.isBlock(body)) return classOf(body as ts.Expression); + let found: ts.ClassLikeDeclaration | null = null; + let valid = true; + const visit = (node: ts.Node): void => { + if ( + !valid || + ts.isFunctionLike(node) || + ts.isClassDeclaration(node) || + ts.isClassExpression(node) + ) + return; + if (ts.isReturnStatement(node)) { + const returned = node.expression ? classOf(node.expression) : null; + if (!returned || (found !== null && found !== returned)) valid = false; + else found = returned; + return; + } + ts.forEachChild(node, visit); + }; + ts.forEachChild(body, visit); + return valid ? found : null; } /** Runs `fn` under an instance's type bindings and local-class names, as @@ -583,14 +708,17 @@ function withInstanceScope( const prevTsBindings = lowerer.typeParamTsBindings; const previousPath = lowerer.genericInstantiationPath; const prevContext = lowerer.instantiationContext; + const prevResultClass = lowerer.instanceResultClass; lowerer.typeParamBindings = inst.bindings; lowerer.typeParamTsBindings = inst.tsBindings ?? null; lowerer.genericInstantiationPath = inst.path; lowerer.instantiationContext = `instantiating '${info.baseName}' with ${inst.typeArgsText}`; + lowerer.instanceResultClass = inst.resultClass ?? null; lowerer.localClassInstantiations.push({ owner: info.decl, name: inst.name }); try { return fn(); } finally { + lowerer.instanceResultClass = prevResultClass; lowerer.localClassInstantiations.pop(); lowerer.typeParamBindings = prevBindings; lowerer.typeParamTsBindings = prevTsBindings; @@ -651,7 +779,7 @@ function internGenericInstance( const typeArgsText = `<${rendered.length > 80 ? rendered.slice(0, 77) + "..." : rendered}>`; inst = { path, - name: `${info.qualifiedName}%${info.instances.size}`, + name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), ordinal: info.instances.size, params, returnType, @@ -2129,7 +2257,7 @@ function internImplicitInstance( const declared = dynamicBroadPromise ? null : implicitDeclaredReturn(lowerer, info); inst = { path, - name: `${info.qualifiedName}%${info.instances.size}`, + name: keyedHelperName(lowerer, `${info.qualifiedName}%`, key, ""), ordinal: info.instances.size, params: shapes, // The promise callers rely on before the body settles it: the @@ -2317,7 +2445,7 @@ export function implicitLocalFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(stmt ?? decl, decl) + `${name}%l${ts.tokenStartOfNode(decl)}`, + nsPathPrefix(lowerer, stmt ?? decl, decl) + `${name}%l${positionTag(lowerer, decl)}`, ), typeParams: [], instances: new Map(), @@ -2413,7 +2541,7 @@ export function objLitGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(fnNode), - `%ol${ts.tokenStartOfNode(fnNode)}.${name}`, + `%ol${positionTag(lowerer, fnNode)}.${name}`, ), typeParams, instances: new Map(), @@ -2559,7 +2687,7 @@ export function bindingGenericFnInfoOf( baseName: name, qualifiedName: lowerer.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(stmt ?? decl, decl) + + nsPathPrefix(lowerer, stmt ?? decl, decl) + (topLevel ? name : `${name}%l${ts.tokenStartOfNode(decl)}`), ), typeParams, diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts index fc69237db..e3f2b0889 100644 --- a/packages/compiler/src/frontend/lowering/intern-tables.ts +++ b/packages/compiler/src/frontend/lowering/intern-tables.ts @@ -23,6 +23,7 @@ * The entry handles that itable.find and itable.insert answer stay inside * these lowerings: f64 locals that only reach toBool and itable.value. * Any other use of the class or its members as values is refused. */ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { BOOL, F64, STRING, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; @@ -447,7 +448,11 @@ function helperName(lowerer: Lowerer, member: InternTableMember, signature: stri } function registerHelper(lowerer: Lowerer, member: InternTableMember, signature: string): string { - const name = `%${INTERN_TABLE_CLASS}.${member}.${lowerer.internTableHelpers.size}`; + const name = keyedHelperName( + lowerer, + `%${INTERN_TABLE_CLASS}.${member}`, + `${member}|${signature}`, + ); lowerer.internTableHelpers.set(`${member}|${signature}`, name); return name; } diff --git a/packages/compiler/src/frontend/lowering/iterator-adapters.ts b/packages/compiler/src/frontend/lowering/iterator-adapters.ts index 898c46f20..e50652182 100644 --- a/packages/compiler/src/frontend/lowering/iterator-adapters.ts +++ b/packages/compiler/src/frontend/lowering/iterator-adapters.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { BOOL, @@ -86,7 +87,7 @@ function dynIteratorAdapter( const key = `iterAdapt:${typeKey(expected)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%iter.adapt.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.adapt", key); lowerer.valueHelpers.set(key, name); const locals: IrLocal[] = []; const local = (id: string, type: IrType, mutable = false): IrExpr => { @@ -313,7 +314,7 @@ export function lowerCollectionIteratorValue( loc, }; if (!typeEquals(yielded.type, type.yieldT)) return null; - name = `%coll.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%coll.iter", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -411,7 +412,7 @@ export function lowerBytesIteratorValue( const key = `bytesIter:${method}:${typeKey(bytesT)}:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%bytes.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%bytes.iter", key); lowerer.valueHelpers.set(key, name); const list = varRef("list.0", bytesT, loc); const index = varRef("index.0", F64, loc); @@ -517,7 +518,7 @@ export function lowerStringIteratorValue( const key = `strIter:${typeKey(type)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%str.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%str.iter", key); lowerer.valueHelpers.set(key, name); const text = varRef("text.0", STRING, loc); const index = varRef("index.0", F64, loc); @@ -677,7 +678,7 @@ function generatorRetypeAdapter( type: resumeType(expected.nextT), loc, }; - name = `%gen.retype.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.retype", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -794,7 +795,7 @@ export function generatorDrain( const outRef = varRef("out.0", arrT, loc); const value = lowerer.coerceToExpected(genValue(genRef, loc), elem); if (!typeEquals(value.type, elem)) return null; - name = `%gen.drain.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -856,7 +857,7 @@ export function generatorDrain( if (!raw) return null; const value = lowerer.coerceToExpected(raw, elem); if (!typeEquals(value.type, elem)) return null; - name = `%gen.drain.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%gen.drain", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -978,7 +979,7 @@ export function lowerArrayIteratorValue( }; } if (!yielded || !typeEquals(yielded.type, type.yieldT)) return null; - name = `%arr.iter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%arr.iter", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts index b9bbf6216..e27dd4183 100644 --- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts +++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts @@ -21,6 +21,7 @@ * files only, and an empty answer leaves every mapping unchanged, so * TypeScript programs and JavaScript that never uses these patterns * compile exactly as before. */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile, locOf } from "../program.js"; import { BOOL, DYN, type IrExpr, type IrStmt } from "../../ir/ir.js"; @@ -215,10 +216,10 @@ export function lowerValueReturningConstructorNew( "spread arguments when constructing a class whose constructor returns a value", ); } - const key = `ctorValue:${ts.getSourceFileOfNode(decl).fileName}:${ts.tokenStartOfNode(decl)}`; + const key = `ctorValue:${lowerer.positions.of(decl)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = `%ctorValue.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%ctorValue", key); lowerer.valueHelpers.set(key, name); const savedClass = lowerer.currentClass; lowerer.currentClass = null; diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts index 927745253..d6c2da571 100644 --- a/packages/compiler/src/frontend/lowering/json-require.ts +++ b/packages/compiler/src/frontend/lowering/json-require.ts @@ -10,6 +10,7 @@ * JSON module is ordinary storage holding that object — destructuring * copies its properties at the require, exactly like Node. ES-module JSON * imports keep their own record (Node's ESM loader caches separately). */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -84,7 +85,7 @@ export function collectJsonRequires(lowerer: Lowerer, parts: FileParts[]): void ); throw new PoisonError(); } - const id = `%g.jsonRequire.${lowerer.jsonRequireModules.size}`; + const id = keyedHelperName(lowerer, "%g.jsonRequire", jsonSf.fileName); const global: IrGlobal = { id, name: "%json", type: mapped, mutable: true }; const loadedId = `${id}%loaded`; lowerer.globalsList.push(global, { diff --git a/packages/compiler/src/frontend/lowering/lifetime-regions.ts b/packages/compiler/src/frontend/lowering/lifetime-regions.ts index b212ae473..b684c0395 100644 --- a/packages/compiler/src/frontend/lowering/lifetime-regions.ts +++ b/packages/compiler/src/frontend/lowering/lifetime-regions.ts @@ -14,6 +14,7 @@ * Any other use of the class or its methods as values is refused. The * module is marked lifetimeRegions, which moves every ProgramLifetime * class into counted regions. */ +import { keyedHelperName } from "./stable-names.js"; import { F64, VOID, funcOf, typeKey } from "../../ir/ir.js"; import type { IrExpr, IrFunction, IrStmt, IrType, SrcLoc } from "../../ir/ir.js"; import { experimentalImportOf, lifetimeRegionMemberOf } from "../experimental-import.js"; @@ -152,7 +153,7 @@ function runHelper(lowerer: Lowerer, fnType: IrType & { kind: "func" }, loc: Src const key = typeKey(fnType); const known = lowerer.regionRunHelpers.get(key); if (known !== undefined) return known; - const name = `%${LIFETIME_REGION_CLASS}.run.${lowerer.regionRunHelpers.size}`; + const name = keyedHelperName(lowerer, `%${LIFETIME_REGION_CLASS}.run`, key); lowerer.regionRunHelpers.set(key, name); const regionT = lifetimeRegionType(lowerer); const ret = fnType.ret; diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts index 460f4d53a..bc51a9382 100644 --- a/packages/compiler/src/frontend/lowering/lower-assert.ts +++ b/packages/compiler/src/frontend/lowering/lower-assert.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { InternalCompilerError } from "../../errors.js"; /* The node:assert lowering (a spoke module like lower-dgram.ts): the * callable module form (`assert(x)` through a default import or a CJS @@ -667,7 +668,7 @@ function partialEqHelper(lowerer: Lowerer, ta: IrType, tb: IrType, loc: SrcLoc): const key = `assert.pdeq:${typeKey(ta)}|${typeKey(tb)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.pdeq.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, "%assert.pdeq", key); lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", ta, loc); const b = (): IrExpr => varRef("b.0", tb, loc); @@ -1280,7 +1281,7 @@ function deepCheckCall( const key = `assert.dcheck:${variant}:${negated}:${typeKey(a.type)}:${typeKey(b.type)}`; let name = lowerer.assertHelpers.get(key); if (!name) { - name = `%assert.dcheck.${lowerer.assertHelpers.size}`; + name = keyedHelperName(lowerer, "%assert.dcheck", key); lowerer.assertHelpers.set(key, name); const pa = varRef("a.0", a.type, loc); const pb = varRef("b.0", b.type, loc); @@ -1844,7 +1845,7 @@ function assertThrowsHelper( const key = `assert.${mode}:${recvIsPromise ? "p" : "f"}:${typeKey(recvType)}:${sig}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.${mode}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%assert.${mode}`, key); lowerer.assertHelpers.set(key, name); const caughtRef = (): IrExpr => varRef("e.0", CAUGHT, loc); @@ -2293,7 +2294,7 @@ function ifErrorHelper(lowerer: Lowerer, t: IrType & { kind: "union" }, loc: Src const key = `assert.iferr:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.iferr.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, "%assert.iferr", key); lowerer.assertHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`assert.ifError over unknown union ${t.unionId}`); @@ -2464,7 +2465,7 @@ function deepEqHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc, loose = false): const key = `assert.${prefix}:${typeKey(t)}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%assert.${prefix}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%assert.${prefix}`, key); lowerer.assertHelpers.set(key, name); const a = (): IrExpr => varRef("a.0", t, loc); diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 7102f37ab..bd5068d05 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js"; import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js"; import { lowerDynStreamMemberCall } from "./dyn-streams.js"; @@ -141,7 +142,7 @@ import { lowerCryptoHashMethodCall, lowerPromisifiedCryptoCall, } from "./builtins/crypto.js"; -import { lowerErrorIsErrorCall } from "./builtins/errors.js"; +import { lowerErrorCaptureStackTraceCall, lowerErrorIsErrorCall } from "./builtins/errors.js"; import { lowerPromisifiedDnsCall } from "./lower-dgram.js"; import { lowerDisposableStackUseCall } from "./builtins/disposable-stack.js"; import { @@ -172,6 +173,7 @@ import { stringWrapperToString, symbolFieldInfo, templateRawTextOf, + primitiveCtorClosure, } from "./lower-exprs.js"; import { isSafeToDiscard } from "./expressions/evaluation-safety.js"; import { tryLowerExpression } from "./expressions/try-lower-expression.js"; @@ -199,6 +201,11 @@ import { import { classCallbackCall, isClassCallback } from "./class-callbacks.js"; import { emitterRooted, lowerEmitterMethodCall } from "./lower-event-emitter.js"; import { eventTargetApiMethod, lowerEventTargetMethodCall } from "./lower-event-target.js"; +import { + dateRootOf, + isDatePrototypeMethod, + lowerInheritedDateMethodCall, +} from "./builtins/date-classes.js"; import { lowerConsoleInstanceCall, lowerConsoleMethodCall, @@ -1542,9 +1549,10 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.fenceStaticReadableStreamMember(expr.expression, "call") ?? lowerDisposableStackUseCall(lowerer, expr, expr.expression) ?? lowerer.lowerStdlibMethodCall(expr, expr.expression) ?? - // crypto.getRandomValues on the WebCrypto global (untyped in - // fallback builds, so before the dyn receiver path) or webcrypto. - lowerWebCryptoGetRandomValues(lowerer, expr, expr.expression) ?? + // crypto.getRandomValues / randomUUID on the WebCrypto global + // (untyped in fallback builds, so before the dyn receiver path) or + // webcrypto. + lowerWebCryptoMethodCall(lowerer, expr, expr.expression) ?? // Dyn receivers (JSON.parse-derived `unknown`/`any` values) — lowerDynReceiverMethodCall(lowerer, expr, expr.expression) ?? @@ -1554,6 +1562,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { lowerer.lowerFilterNarrowCall(expr, expr.expression) ?? lowerArrayIsArrayCall(lowerer, expr, expr.expression) ?? lowerErrorIsErrorCall(lowerer, expr, expr.expression) ?? + lowerErrorCaptureStackTraceCall(lowerer, expr, expr.expression) ?? lowerBigIntMethodCall(lowerer, expr, expr.expression) ?? lowerSymbolStaticCall(lowerer, expr, expr.expression) ?? lowerSymbolMethodCall(lowerer, expr, expr.expression) ?? @@ -2094,7 +2103,7 @@ function lowerOptionalNumberDefault( const key = `number.optionalDefault:${widened.unionId}:${dflt}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.optionalDefault.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.optionalDefault", key); lowerer.valueHelpers.set(key, helper); const input = varRef("value.0", widened, loc); lowerer.liftedFns.push({ @@ -2203,7 +2212,7 @@ function primitiveConversionHandler( const key = `then.${name}:${typeKey(inner)}`; let fnName = lowerer.arrHofHelpers.get(key); if (fnName === undefined) { - fnName = `%then.${name}.${lowerer.arrHofHelpers.size}`; + fnName = keyedHelperName(lowerer, `%then.${name}`, key); lowerer.arrHofHelpers.set(key, fnName); const value = varRef("v.0", inner, loc); let body: IrExpr; @@ -2372,7 +2381,7 @@ function lowerBigIntConstructorValue(lowerer: Lowerer, arg: IrExpr, loc: SrcLoc) const key = `bigint.scalar:${unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%bigint.scalar.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%bigint.scalar", key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const body: IrStmt[] = []; @@ -3234,7 +3243,7 @@ function lowerScalarUnionNumber( const key = `number.scalar:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%number.scalar.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%number.scalar", key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); const nan: IrExpr = { @@ -3279,7 +3288,7 @@ function lowerOptionalStringNumber( const key = `${label}.optionalString:${arg.type.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%${label}.optionalString.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, `%${label}.optionalString`, key); lowerer.valueHelpers.set(key, helper); const value = varRef("value.0", arg.type, loc); lowerer.liftedFns.push({ @@ -3727,6 +3736,18 @@ function lowerDynReceiverMethodCall( // IS Node's semantics for every possible dyn value. Shared names would // need a receiver-kind dispatch — they keep the fence. if (DYN_STRING_ONLY_METHODS.has(access.name.text) && !ownCallable) { + // An object-typed receiver the member is missing from (a JavaScript + // module object whose members are assigned in a function body, + // `common.repeat(" ", n)`) holds a string or an object with that + // member: dispatch on the receiver's runtime kind. + if ( + !checkerUntyped && + (lowerer.typeOf(access).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 && + !call.questionDotToken && + !access.questionDotToken && + !call.arguments.nodes.some(ts.isSpreadElement) + ) + return lowerStringOrMemberCall(lowerer, call, access, recv); const checked = (): IrExpr => dynStringReceiver(lowerer, recv, access); return ( lowerRegexMethodCall(lowerer, call, access, checked) ?? @@ -3935,6 +3956,98 @@ function lowerDynReceiverMethodCall( }; } +/** `value.name(...args)` on a dyn receiver that may be a string or an + * object: a string runs the String.prototype lowering, any other value + * calls its member. The member is read before the arguments are + * evaluated, and the arguments are evaluated once for either branch. */ +function lowerStringOrMemberCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + recv: IrExpr, +): IrExpr { + const loc = locOf(call); + const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN); + const receiver = varRef(receiverLocal.id, DYN, loc); + const isString: IrExpr = { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc }; + const calleeLocal = lowerer.declareHiddenLocal("%dynCallCallee", DYN); + const callee = varRef(calleeLocal.id, DYN, loc); + const stmts: IrStmt[] = [ + { kind: "varDecl", localId: receiverLocal.id, init: recv, loc }, + { + kind: "varDecl", + localId: calleeLocal.id, + init: { + kind: "ternary", + cond: isString, + then: dynUndefinedExpr(loc), + else_: { + kind: "dynKeyGet", + key: { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access) }, + value: receiver, + type: DYN, + loc: locOf(access), + }, + type: DYN, + loc, + }, + loc, + }, + ]; + const args = call.arguments.nodes.map((node) => { + const value = lowerer.lowerExpr(node); + const local = lowerer.declareHiddenLocal("%dynCallArg", value.type); + stmts.push({ kind: "varDecl", localId: local.id, init: value, loc: value.loc }); + return { node, value: varRef(local.id, value.type, value.loc) }; + }); + // String.prototype methods ignore arguments past the ones they read. + const arity = DYN_STRING_ONLY_ARITY[access.name.text] ?? call.arguments.nodes.length; + const stringReceiver = (): IrExpr => ({ kind: "dynCheck", value: receiver, type: STRING, loc }); + const lowerString = (): IrExpr | null => + (call.arguments.nodes.length <= arity + ? lowerRegexMethodCall(lowerer, call, access, stringReceiver) + : null) ?? + lowerStringMethodCall( + lowerer, + call, + access, + stringReceiver, + call.arguments.nodes.slice(0, arity), + ); + const stringCall = lowerer.withExpressionOverrides(args, lowerString); + const memberCall: IrExpr = { + kind: "dynCall", + callee, + receiver, + calleeName: ts.getTextOfNode(access), + args: args.map((arg) => lowerer.coerceToExpected(arg.value, DYN)), + type: DYN, + loc, + }; + return { + kind: "seqExpr", + stmts, + result: { + kind: "ternary", + cond: isString, + then: stringCall + ? lowerer.coerceToExpected(stringCall, DYN) + : nodeThrowExpr( + 0, + "SC1090", + `method calls like '${ts.getTextOfNode(access)}' on a string are not supported yet [SC1090 at ${ts.getSourceFileOfNode(call).fileName}:${ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(call), ts.tokenStartOfNode(call)).line + 1}]`, + DYN, + loc, + ), + else_: memberCall, + type: DYN, + loc, + }, + type: DYN, + loc, + }; +} + /** Prototype method names of the checked-dynamic tree-representable kinds (String, Array, * Object, Function, Number prototypes): a dyn receiver call on one of * these could be a REAL method on a real value, which a stored-member @@ -4341,7 +4454,25 @@ export function lowerDynDispatchMethodCall( } } } + // `String`, `Number` or `Boolean` as the callback of an array method + // (`values.map(String)` over an unknown array): the method calls it with + // each element, whatever its type, and cannot observe which function + // value it received, so it gets the constructor's form that converts any + // argument rather than the string-input form typed code stores. + const arrayCallbackReceiver = + ARRAY_CALLBACK_METHODS.has(method) && + lowerer.checker.isArrayType(lowerer.typeOf(access.expression)); const args = call.arguments.nodes.map((arg, i) => { + const primitive = + i === 0 && arrayCallbackReceiver && ts.isIdentifier(arg) + ? stdlibGlobalNameOf(lowerer, arg) + : null; + if (primitive === "String" || primitive === "Number" || primitive === "Boolean") + return lowerer.coerceInto( + arg, + primitiveCtorClosure(lowerer, primitive, locOf(arg), true), + DYN, + ); if (i === 0 && predicate) return lowerer.coerceInto(arg, predicate, DYN); const undefinedArg = lowerStaticallyUndefinedArgument(lowerer, arg); return undefinedArg @@ -4359,6 +4490,21 @@ export function lowerDynDispatchMethodCall( }; } +/** Array.prototype methods whose first argument is a callback they call + * with the elements. */ +const ARRAY_CALLBACK_METHODS = new Set([ + "map", + "filter", + "forEach", + "some", + "every", + "find", + "findIndex", + "findLast", + "findLastIndex", + "flatMap", +]); + /** Every String.prototype method name a typed or dyn string lowering * answers. */ function isStringMethodName(name: string): boolean { @@ -4372,6 +4518,29 @@ function isStringMethodName(name: string): boolean { ); } +/** How many arguments each DYN_STRING_ONLY_METHODS method reads. */ +const DYN_STRING_ONLY_ARITY: Readonly> = { + charCodeAt: 1, + charAt: 1, + startsWith: 2, + endsWith: 2, + substring: 2, + repeat: 1, + trim: 0, + trimStart: 0, + trimEnd: 0, + split: 2, + padStart: 2, + padEnd: 2, + toLowerCase: 0, + toUpperCase: 0, + replace: 2, + replaceAll: 2, + match: 1, + matchAll: 1, + search: 1, +}; + /** STR_METHODS ∪ the regex-form names, MINUS everything Array (or any * other dyn kind's prototype) also declares. */ const DYN_STRING_ONLY_METHODS = new Set([ @@ -4706,7 +4875,7 @@ function lowerDefaultToStringCall( const key = `objToStr:${typeKey(recv.type)}${missing ? `:${missing}` : ""}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%obj.tostr.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.tostr", key); lowerer.valueHelpers.set(key, helper); const o = varRef("o.0", recv.type, loc); lowerer.liftedFns.push({ @@ -5393,7 +5562,7 @@ function loweredTemplateStrings( const site = lowerer.globalsList.length; const globalId = `%g.templateStrings.${site}`; const readyId = `%g.templateStringsReady.${site}`; - getter = `%templateStrings.get.${lowerer.valueHelpers.size}`; + getter = keyedHelperName(lowerer, "%templateStrings.get", key); lowerer.valueHelpers.set(key, getter); lowerer.globalsList.push( { id: globalId, name: "%templateStrings", type, mutable: true }, @@ -5574,7 +5743,7 @@ export function lowerPromiseMethodCall( // IS the spec's identity/thrower/no-op substitution for all three // members. An argument expression, when present, is undefined/null // by construction — nothing to evaluate. - const fnName = `%fn${lowerer.lambdaCounter++}_${member}pass`; + const fnName = lowerer.closureName(loc, `_${member}pass`); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: promT }; const fnCtx = newFnCtx(true, null, funcType, inner); fnCtx.isAsync = true; @@ -5671,7 +5840,7 @@ export function lowerPromiseMethodCall( // Evaluate both callback expressions before the wrapper runs, in source order. const fulfilled = fulfilledAbsent ? null : boxHandler(fulfilledNode, true); const rejected = rejectedAbsent ? null : boxHandler(rejectedNode, false); - const fnName = `%fn${lowerer.lambdaCounter++}_then2`; + const fnName = lowerer.closureName(loc, "_then2"); const paramTypes: IrType[] = [ promT, ...(fulfilled ? [fulfilled.value.type] : []), @@ -5920,7 +6089,7 @@ export function lowerPromiseMethodCall( ); }; const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = `%fn${lowerer.lambdaCounter++}_then`; + const fnName = lowerer.closureName(loc, "_then"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6015,7 +6184,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = `%fn${lowerer.lambdaCounter++}_then`; + const fnName = lowerer.closureName(loc, "_then"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6118,7 +6287,7 @@ export function lowerPromiseMethodCall( "finally callbacks with parameters or a return value (use () => { ... })", ); } - const fnName = `%fn${lowerer.lambdaCounter++}_finally`; + const fnName = lowerer.closureName(loc, "_finally"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, cb.type], @@ -6221,7 +6390,7 @@ export function lowerPromiseMethodCall( } if (cb.type.kind === "dyn") { const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: DYN }; - const fnName = `%fn${lowerer.lambdaCounter++}_catchdyn`; + const fnName = lowerer.closureName(loc, "_catchdyn"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT, DYN], @@ -6351,7 +6520,7 @@ export function lowerPromiseMethodCall( ); } const R = resultT.inner; - const fnName = `%fn${lowerer.lambdaCounter++}_catch`; + const fnName = lowerer.closureName(loc, "_catch"); const funcType: IrType & { kind: "func" } = { kind: "func", params: [promT], ret: resultT }; const fnCtx = newFnCtx(true, null, funcType, R); fnCtx.isAsync = true; @@ -6588,7 +6757,7 @@ function filterNarrowHelper( const key = `filterNarrow:${test}:${typeKey(elem)}:${typeKey(outElem)}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const name = `%arr.filterNarrow.${lowerer.arrHofHelpers.size}`; + const name = keyedHelperName(lowerer, "%arr.filterNarrow", key); lowerer.arrHofHelpers.set(key, name); const arrT = arrayOf(elem); @@ -6834,7 +7003,7 @@ export function recordKeysArrayCall( const key = `obj.keys:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.keys.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%obj.keys", key); const ref: IrExpr = { kind: "varRef", localId: "r.0", type: argIr, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; lowerer.arrHofHelpers.set(key, helper); @@ -6904,7 +7073,7 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str const key = `obj.hasOwn:${shapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.hasOwn.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.hasOwn", key); lowerer.arrHofHelpers.set(key, helper); const shape = lowerer.shapes.get(shapeId)!; const recT: IrType = { kind: "record", shapeId }; @@ -6989,7 +7158,7 @@ function classAssignHelper( const key = `obj.assignClass:${className}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.assignClass", key); lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const oT: IrType = { kind: "object", className }; @@ -7057,7 +7226,7 @@ function recordAssignHelper( const key = `obj.assign:${targetShapeId}:${srcShapeId}`; const existing = lowerer.arrHofHelpers.get(key); if (existing) return existing; - const helper = `%obj.assign.${lowerer.arrHofHelpers.size}`; + const helper = keyedHelperName(lowerer, "%obj.assign", key); lowerer.arrHofHelpers.set(key, helper); const sShape = lowerer.shapes.get(srcShapeId)!; const tShape = lowerer.shapes.get(targetShapeId)!; @@ -8514,7 +8683,7 @@ function lowerObjectStaticCall( const key = `obj.${member}:${argIr.shapeId}:${typeKey(resultT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%obj.${member}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%obj.${member}`, key); const recT = argIr; const ref: IrExpr = { kind: "varRef", localId: "r.0", type: recT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: resultT, loc }; @@ -9150,7 +9319,7 @@ function lowerInlineArrowBind( const length = firstDefault < 0 ? arrow.parameters.nodes.length : firstDefault; const key = `arrow.bind:${typeKey(type)}:${length}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%arrow.bind.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%arrow.bind", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.freshClosureAdapters.add(name); @@ -9670,6 +9839,10 @@ export function lowerObjectMethodCall( if (info && !found && eventTargetApiMethod(info, access.name.text)) { return lowerEventTargetMethodCall(lowerer, call, access, info, lowerReceiver()); } + // The Date methods a class declared `extends Date` does not declare. + if (info && !found && dateRootOf(info) && isDatePrototypeMethod(access.name.text)) { + return lowerInheritedDateMethodCall(lowerer, call, access.name.text, info, lowerReceiver()); + } // GENERIC methods (own type parameters) never enter the methods table: // they monomorphize per call site and dispatch statically // (lowerClassGenericMethodCall has the exactness rules). @@ -10079,7 +10252,7 @@ function lowerUnionObjectDynFieldCall( const receiverRef = varRef(receiverLocal.id, receiverT, loc); let helper = lowerer.unionCallHelpers.get(key); if (!helper) { - helper = `%union.call.${lowerer.unionCallHelpers.size}`; + helper = keyedHelperName(lowerer, "%union.call", key); lowerer.unionCallHelpers.set(key, helper); const receiverRef = varRef("this.0", receiverT, loc); const branch = (plan: (typeof plans)[number]): IrStmt[] => { @@ -10172,7 +10345,7 @@ function unionObjectMethodHelper( const key = `${receiverT.unionId}:${method}:${shapes.map((shape) => `${shape.mode}:${typeKey(shape.type)}`).join(",")}:${typeKey(resultT)}`; const existing = lowerer.unionCallHelpers.get(key); if (existing) return existing; - const name = `%union.call.${lowerer.unionCallHelpers.size}`; + const name = keyedHelperName(lowerer, "%union.call", key); lowerer.unionCallHelpers.set(key, name); // A probe that catches a refusal while the body lowers must not leave // the name registered for a helper that was never emitted. @@ -10366,7 +10539,7 @@ export function lowerStaticNumberGlobal( const key = `parseInt.optional:${unionT.unionId}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%parseInt.optional.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%parseInt.optional", key); lowerer.valueHelpers.set(key, helper); const value: IrExpr = { kind: "varRef", localId: "value.0", type: unionT, loc }; const radixRef: IrExpr = { kind: "varRef", localId: "radix.0", type: F64, loc }; @@ -10458,6 +10631,17 @@ export function lowerStaticNumberGlobal( : lowerer.lowerExprExpecting(args[0]!, STRING); return { kind: "libCall", fn: "num.parseInt", args: [s, radix], type: F64, loc }; } + // parseFloat takes one argument; Node ignores the rest after evaluating + // them (`parseFloat(text, 10)`). Literal and identifier extras have + // nothing to evaluate. + if ( + name === "parseFloat" && + args.length > 1 && + args + .slice(1) + .every((arg) => ts.isNumericLiteral(arg) || ts.isStringLiteral(arg) || ts.isIdentifier(arg)) + ) + return lowerStaticNumberGlobal(lowerer, name, call, args.slice(0, 1), loc); // STATIC parseFloat over string arguments — parseInt's sibling // (num.parseFloat is ECMA 19.2.4's decimal-literal prefix parse in // scr_string.c). Other argument types retain their native coercion @@ -10492,20 +10676,40 @@ export function lowerStaticNumberGlobal( return null; } -/** `crypto.getRandomValues(view)` on the WebCrypto global or webcrypto. */ -function lowerWebCryptoGetRandomValues( +/** `crypto.getRandomValues(view)` and `crypto.randomUUID()` on the + * WebCrypto global or webcrypto. Other argument counts (JavaScript only) + * invoke the dynamic object's method, which ignores extra arguments and + * throws Node's missing-argument TypeError. */ +function lowerWebCryptoMethodCall( lowerer: Lowerer, call: ts.CallExpression, access: ts.PropertyAccessExpression, ): IrExpr | null { + const member = access.name.text; if ( - access.name.text !== "getRandomValues" || + (member !== "getRandomValues" && member !== "randomUUID") || call.questionDotToken || access.questionDotToken || !isWebCryptoReceiver(lowerer, access.expression) ) return null; - return lowerCryptoGetRandomValues(lowerer, call); + const args = call.arguments.nodes; + if (member === "getRandomValues" && args.length === 1) { + return lowerCryptoGetRandomValues(lowerer, call); + } + if (member === "randomUUID" && args.length === 0) { + return { kind: "libCall", fn: "crypto.randomUUID", args: [], type: STRING, loc: locOf(call) }; + } + if (args.some(ts.isSpreadElement)) return null; + return { + kind: "dynInvoke", + recv: lowerer.lowerExprExpecting(access.expression, DYN), + method: member, + calleeName: v8CalleeText(access), + args: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)), + type: DYN, + loc: locOf(call), + }; } /** A then handler's promise-armed result (`.then(() => cached ?? load())` diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 790efe988..2a772ae25 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -1,3 +1,12 @@ +import { + DATE_TIME_FIELD, + dateRootOf, + dateSuperCall, + isDatePrototypeMethod, + lowerDateSuperMethodCall, +} from "./builtins/date-classes.js"; +import { lowerDateStaticCall } from "./builtins/dates.js"; +import { keyedHelperName } from "./stable-names.js"; import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js"; import { adaptPromiseExecutor } from "./promise-executor-adapter.js"; import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js"; @@ -84,6 +93,7 @@ import { } from "./call-signatures.js"; import { omittedArgFor } from "./call-arguments.js"; import { + checkerTypeDescription, isGenericCallableMemberType, jsOpenObjectType, typeKey, @@ -564,6 +574,11 @@ export interface ClassInfo { * lower-event-target.ts over any class rooted here. Subclass structs * embed the runtime prefix. */ builtinEventTarget?: "target" | "event"; + /** A class declared `extends Date` (builtins/date-classes.ts): its first + * field is the hidden `%time` value, super() sets it with the Date + * constructor's rules, and the Date methods it does not declare lower + * over that value. Subclasses inherit the field. */ + extendsDate?: true; /** The synthetic LifetimeRegion class (@scriptc/experimental, * lifetime-regions.ts): its constructor is synthesized with the class, * and its methods lower at their call sites. */ @@ -1951,6 +1966,7 @@ export function collectClassShapeInner( : (computedBase?.classInfo ?? null); let genericBase: ts.ExpressionWithTypeArguments | undefined; let callableBase: ClassInfo["callableBase"]; + let extendsDate = false; let factoryBaseExpression: ts.Expression | undefined; const adoptCallableBase = (expression: ts.Expression): boolean => { if (familyMode || inst || mixin) return false; @@ -2151,6 +2167,11 @@ export function collectClassShapeInner( if (t && adoptCallableBase(t.expression)) continue; lowerer.unsupported("SC1090", clause, "extending computed expressions"); } + if (!familyMode && !inst && !mixin && lowerer.isStdlibGlobal(t.expression, "Date")) { + if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes"); + extendsDate = true; + continue; + } const symbol = lowerer.resolveValueSymbol(t.expression); // Extending a REBINDABLE decorated class (analysis already ran — // this collection is a class expression or a generic @@ -2404,7 +2425,8 @@ export function collectClassShapeInner( for (const member of decl.members.nodes) { if ( (functionLocalClass(decl) || checkedStatics) && - isJsSourceFile(ts.getSourceFileOfNode(decl)) && + (isJsSourceFile(ts.getSourceFileOfNode(decl)) || + (functionLocalClass(decl) && ts.isMethodDeclaration(member))) && (ts.isClassStaticBlockDeclaration(member) || ts .getModifiers(member) @@ -4350,7 +4372,8 @@ export function collectClassShapeInner( const ctorParams: ParamShape[] = ctor && !mixinForwarding ? lowerer.paramShapes(ctor.parameters.nodes) - : (base?.ctorParams ?? (callableBase ? [{ type: DYN, mode: "dynRest" }] : [])); + : (base?.ctorParams ?? + (callableBase || extendsDate ? [{ type: DYN, mode: "dynRest" }] : [])); if ( ctor && !mixinForwarding && @@ -4388,6 +4411,7 @@ export function collectClassShapeInner( // Redeclared INHERITED fields contribute no slot (their // initializers assign the prefix slot). fields: [ + ...(extendsDate ? [{ name: DATE_TIME_FIELD, type: DATE_T }] : []), ...(base?.def.fields ?? []), ...fieldOrder .filter((f) => f.redeclared !== true) @@ -4419,6 +4443,7 @@ export function collectClassShapeInner( base, ...(genericBase ? { genericBase } : {}), ...(callableBase ? { callableBase } : {}), + ...(extendsDate ? { extendsDate: true as const } : {}), ...(factoryBaseExpression ? { factoryBaseExpression } : {}), subclasses: [], throwingSetters, @@ -4663,7 +4688,25 @@ export function genericClassInstanceType( ); } const ordinal = gci.instances.size; - const name = `${gci.family.def.name}%${ordinal}`; + // The name does not take the checker type ids of the key: a literal + // argument is named by its text, another checker type by its + // description. Two keys that still describe alike number the later one. + const nameKey = + mapped.map(typeKey).join(",") + + (gci.literalSensitive.size > 0 + ? `@${gci.typeParams + .map((_, i) => + sensitive(i) + ? (literalText(i) ?? checkerTypeDescription(tsArgs[i]!, lowerer.typeCtx)) + : "*", + ) + .join(",")}` + : ""); + const prefix = `${gci.family.def.name}%`; + const taken = new Set([...gci.instances.values()].map((instance) => instance.name)); + let name = keyedHelperName(lowerer, prefix, nameKey, ""); + for (let n = 1; taken.has(name); n++) + name = keyedHelperName(lowerer, prefix, `${nameKey}#${n}`, ""); const entry: { name: string; info: ClassInfo | null; poisoned?: boolean } = { name, info: null }; gci.instances.set(key, entry); const bindings = new Map(); @@ -5647,6 +5690,15 @@ export function lowerStaticFieldRead( if (info.classDecorators?.valueGlobalId !== undefined) return null; const loc = locOf(expr); const found = findStaticOn(lowerer, info, expr.name.text); + // A Date static read as a value through a class declared `extends Date`: + // the statics lower only as calls. + if ( + !found && + dateRootOf(info) && + ["now", "parse", "UTC"].includes(expr.name.text) && + !(ts.isCallExpression(expr.parent) && expr.parent.expression === expr) + ) + lowerer.noLowering(`Date.${expr.name.text} read through a class that extends Date`, expr); if (hasRuntimeStatics(info) && !found?.field) return { kind: "dynKeyGet", @@ -5872,9 +5924,10 @@ export function lowerClassExpressionInfo( decoratorNodesOf(member).length || (!isJsSourceFile(ts.getSourceFileOfNode(expr)) && (ts.isClassStaticBlockDeclaration(member) || - ts - .getModifiers(member) - ?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))) || + (!ts.isMethodDeclaration(member) && + ts + .getModifiers(member) + ?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword)))) || (ts.isMethodDeclaration(member) && member.typeParameters?.nodes.length), )) ) { @@ -6140,13 +6193,55 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara loc, }); } - return info.localClass - ? initializeRuntimeStatics( - lowerer, - info, - value.kind === "classRef" ? { ...value, captures: info.localClass.captureSources } : value, - ) - : value; + if (!info.localClass) return value; + const classValue = initializeRuntimeStatics( + lowerer, + info, + value.kind === "classRef" ? { ...value, captures: info.localClass.captureSources } : value, + ); + // The base resolves statically here, but Node still evaluates the + // heritage expression each time the class definition runs, first. + const heritage = heritageWithEffects(expr); + if (heritage === undefined) return classValue; + return { + kind: "seqExpr", + stmts: [lowerer.lowerExprStatement(heritage)], + result: classValue, + type: classValue.type, + loc: classValue.loc, + }; +} + +/** The `extends` expression of `decl` when evaluating it can have effects + * (a call, construction, assignment, update, delete, await or yield + * outside nested functions). */ +function heritageWithEffects(decl: ts.ClassLikeDeclaration): ts.Expression | undefined { + const heritage = decl.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]?.expression; + if (heritage === undefined) return undefined; + const effects = (node: ts.Node): boolean => { + if (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node)) + return false; + if ( + ts.isCallExpression(node) || + ts.isNewExpression(node) || + ts.isTaggedTemplateExpression(node) || + ts.isDeleteExpression(node) || + ts.isAwaitExpression(node) || + ts.isYieldExpression(node) || + (ts.isPrefixUnaryExpression(node) && + (node.operator === ts.SyntaxKind.PlusPlusToken || + node.operator === ts.SyntaxKind.MinusMinusToken)) || + ts.isPostfixUnaryExpression(node) || + (ts.isBinaryExpression(node) && + node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && + node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) + ) + return true; + return ts.forEachChild(node, (child) => effects(child) || undefined) ?? false; + }; + return effects(heritage) ? heritage : undefined; } /** The EXACT class a receiver expression is statically known to BE (not @@ -6422,6 +6517,12 @@ function staticCallOn( const loc = locOf(call); const runtimeStatic = directRuntimeStaticCall(lowerer, call, access, info, throughValue); if (runtimeStatic) return runtimeStatic; + // Date's statics through a class declared `extends Date` + // (`Moment.UTC(...)`), which inherits them. + if (dateRootOf(info) && !findStaticOn(lowerer, info, access.name.text)) { + const date = lowerDateStaticCall(lowerer, call, access.name.text); + if (date) return date; + } if ( (hasRuntimeStatics(info) && !findStaticOn(lowerer, info, access.name.text)?.field) || (info.callableBase && @@ -7683,7 +7784,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { lowerer.ctx.argumentsLocal = local; } body.push(...declared.prologue); - if (!ctorBase && !info.callableBase) { + if (!ctorBase && !info.callableBase && !info.extendsDate) { // Node's base-class order: field initializers run at the start // of construction, the parameter-property assignments open the // constructor body (probed — a field initializer reading a @@ -7695,7 +7796,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction { body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal)); } } else { - if (ctorBase || info.callableBase) { + if (ctorBase || info.callableBase || info.extendsDate) { // Synthetic forwarding params (the inherited ABI signature). // Nothing references them by symbol — only the super call below, // which forwards the already-completed values UNCHANGED (defaults @@ -7910,7 +8011,7 @@ export function classIteratorDrainCall( const key = `${cit.className}:${typeKey(outElem)}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.drain", key); lowerer.iterDrainHelpers.set(key, name); const recvT: IrType = { kind: "object", className: cit.className }; const recvRef: IrExpr = { kind: "varRef", localId: "r.0", type: recvT, loc }; @@ -8024,7 +8125,7 @@ export function classIteratorRestDrainCall( const key = `it:${cit.iterT.className}`; let name = lowerer.iterDrainHelpers.get(key); if (!name) { - name = `%iter.drain.${lowerer.iterDrainHelpers.size}`; + name = keyedHelperName(lowerer, "%iter.drain", key); lowerer.iterDrainHelpers.set(key, name); const itRef: IrExpr = { kind: "varRef", localId: "it.0", type: cit.iterT, loc }; const outRef: IrExpr = { kind: "varRef", localId: "out.0", type: outT, loc }; @@ -8614,12 +8715,14 @@ export function lowerDerivedCtorBody( loc, }, ]; - if (info.callableBase) { + if (info.callableBase || info.extendsDate) { if (superCall.arguments.nodes.some(ts.isSpreadElement)) lowerer.unsupported( "SC1090", superCall, - "spreading explicit super arguments into an ordinary constructor", + info.extendsDate + ? "spreading super arguments into the Date constructor" + : "spreading explicit super arguments into an ordinary constructor", ); const pack: IrExpr = { kind: "dynArrLit", @@ -8804,6 +8907,7 @@ export function superCallStmt( type: { kind: "object", className: info.def.name }, loc, }; + if (info.extendsDate) return dateSuperCall(thisRef, info, args[0]!, loc); if (info.callableBase) return { kind: "exprStmt", @@ -8925,6 +9029,24 @@ export function lowerSuperMethodCall( access: ts.PropertyAccessExpression, ): IrExpr { const cls = lowerer.currentClass; + // `super.toISOString()` in a class declared `extends Date`. + if (cls?.extendsDate && !cls.base) { + const thisL = lowerer.resolveThis(); + if (!thisL) lowerer.unsupported("SC1080", access); + const thisRef: IrExpr = { + kind: "varRef", + localId: thisL.id, + type: thisL.type, + loc: locOf(call), + }; + const date = lowerDateSuperMethodCall(lowerer, call, access.name.text, cls, thisRef); + if (date) return date; + lowerer.unsupported( + "SC1090", + access, + `'super.${access.name.text}' (Date.prototype does not declare it)`, + ); + } if (!cls?.base) { // tsc rejects super outside derived-class bodies first; defensive. lowerer.unsupported("SC1090", access, "'super' outside a derived class"); @@ -8969,6 +9091,17 @@ export function lowerSuperMethodCall( // `super.dispatchEvent(e)`, `super.preventDefault()`: the inherited // runtime members over this method's own `this` (subclasses cannot // override them, so the static answer is the runtime's). + if (dateRootOf(cls.base) && isDatePrototypeMethod(access.name.text)) { + const thisL = lowerer.resolveThis(); + if (!thisL) lowerer.unsupported("SC1080", access); + const thisRef: IrExpr = { + kind: "varRef", + localId: thisL.id, + type: thisL.type, + loc: locOf(call), + }; + return lowerDateSuperMethodCall(lowerer, call, access.name.text, cls.base, thisRef)!; + } if (eventTargetApiMethod(cls.base, access.name.text)) { const thisL = lowerer.resolveThis(); if (!thisL) lowerer.unsupported("SC1080", access); @@ -10532,7 +10665,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr { const key = `x509.record:${isBytes ? "bytes" : "str"}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%x509.record.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%x509.record", key); lowerer.valueHelpers.set(key, helper); const dataT = data.type; const dRef: IrExpr = { kind: "varRef", localId: "d.0", type: dataT, loc }; diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts index 5c99ebc82..aa90fc7c7 100644 --- a/packages/compiler/src/frontend/lowering/lower-dgram.ts +++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit } from "../../ir/build.js"; /* The dgram/dns-surface lowering (node:dgram + node:dns — a spoke module * like lower-server.ts): module-function calls (dgram createSocket, dns @@ -357,7 +358,7 @@ function checkedDynPromise( const key = `dns.promise:${typeKey(declared)}`; let helper = lowerer.valueHelpers.get(key); if (!helper) { - helper = `%dns.promise.${lowerer.valueHelpers.size}`; + helper = keyedHelperName(lowerer, "%dns.promise", key); lowerer.valueHelpers.set(key, helper); const promiseT: IrType = { kind: "promise", inner: DYN }; lowerer.liftedFns.push({ @@ -891,7 +892,7 @@ export function lowerPromisifiedDnsCall( const key = `dnsPromise:${JSON.stringify(value)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%dnsPromise.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%dnsPromise", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts index f472672b2..f105af786 100644 --- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts +++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts @@ -42,6 +42,7 @@ * emit-overrides block at the bottom of this file (per-event * monomorphization of the override body, dispatch on the dynamic class, * super.emit as the prototype chain). */ +import { keyedHelperName } from "./stable-names.js"; import { checkKeypressListener } from "./builtins/readline.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; @@ -631,7 +632,7 @@ function lowerFlexListenerCall( const key = `emitter.${registering ? "onFlex" : "offDyn"}:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.${registering ? "onFlex" : "offFlex"}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%emitter.${registering ? "onFlex" : "offFlex"}`, key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -850,7 +851,7 @@ function lowerDynListenerCall( const existing = lowerer.arrHofHelpers.get(key); let helper = existing; if (!helper) { - helper = `%emitter.${registering ? "onDyn" : "offDyn"}.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, `%emitter.${registering ? "onDyn" : "offDyn"}`, key); lowerer.arrHofHelpers.set(key, helper); const check: IrStmt = { kind: "exprStmt", @@ -1000,7 +1001,7 @@ export function lowerDynamicEmitterEmit( const key = `emitter.dynEmit:${typeKey(recvT)}:${k}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.dynEmit.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.dynEmit", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -1092,7 +1093,7 @@ export function lowerDynamicEmitterOn( const key = `emitter.dynOn:${typeKey(recvT)}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.dynOn.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.dynOn", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2359,7 +2360,7 @@ function emitDispatchExpr( const key = `emitter.emitDispatch:${typeKey(recvT)}:${name}`; let helper = lowerer.arrHofHelpers.get(key); if (!helper) { - helper = `%emitter.emitDispatch.${lowerer.arrHofHelpers.size}`; + helper = keyedHelperName(lowerer, "%emitter.emitDispatch", key); lowerer.arrHofHelpers.set(key, helper); const params: IrParam[] = [ { localId: "r.0", name: "r", type: recvT }, @@ -2424,7 +2425,7 @@ function eventsOnceBoxer( const key = `eventsOnce:${typeKey(fnType)}`; let name = lowerer.valueHelpers.get(key); if (name === undefined) { - name = `%events.onceArgs.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%events.onceArgs", key); lowerer.valueHelpers.set(key, name); const params: IrParam[] = tuple.map((type, i) => ({ localId: `%once.a${i}`, diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index e7a1b79a8..b5acaf9e6 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; import { InternalCompilerError } from "../../errors.js"; @@ -2274,7 +2275,11 @@ export function cjsAccessorSnapshotOf( if (!lexerVisibleAccessors(ts.getSourceFileOfNode(getter)).includes(getter)) return null; const call = cjsAccessorCall(lowerer, getter, locOf(getter)); if (call === null) return null; - const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type }; + const at = locOf(getter); + const snapshot = { + id: keyedHelperName(lowerer, "%g.cjsSnapshot", `${at.file}:${at.start}:${at.end}`), + type: call.type, + }; lowerer.globalsList.push({ id: snapshot.id, name: "%cjsSnapshot", @@ -2299,7 +2304,7 @@ export function cjsAccessorSnapshotStores( if (getters.length === 0) return []; let readyId = lowerer.cjsSnapshotReady.get(dep); if (readyId === undefined) { - readyId = `%g.cjsSnapshotReady.${lowerer.cjsSnapshotReady.size}`; + readyId = keyedHelperName(lowerer, "%g.cjsSnapshotReady", dep.fileName); lowerer.globalsList.push({ id: readyId, name: "%cjsSnapshotReady", type: BOOL, mutable: true }); lowerer.cjsSnapshotReady.set(dep, readyId); } @@ -3052,8 +3057,10 @@ export function lowerFieldCompound( * returns the zero-capture closure over it. Typed sources use the mapped * `(value: string) => primitive` signature; a recognized JavaScript * concise-arrow selector (`() => String`, the picocolors fallback) takes - * dyn so its ordinary coercion signature survives inference residue. The - * dyn bodies run exact ToString/ToNumber/ToBoolean, including object hooks. + * dyn so its ordinary coercion signature survives inference residue, and a + * boxed constructor value takes dyn too (coerceToExpected). The dyn bodies + * run what the constructors do for any argument: String() (a symbol's + * description form included), ToNumber and ToBoolean, with object hooks. * Interning makes every reference under one ABI the SAME immortal closure, * so `opt.type === String` compares like JS function identity. */ export function primitiveCtorClosure( @@ -3076,7 +3083,7 @@ export function primitiveCtorClosure( ? name === "BigInt" ? { kind: "libCall", fn: "dyn.bigintConstructor", args: [v], type: BIGINT_T, loc } : name === "String" - ? { kind: "libCall", fn: "dyn.toStringCoerce", args: [v], type: STRING, loc } + ? { kind: "libCall", fn: "dyn.stringConstructor", args: [v], type: STRING, loc } : name === "Number" ? { kind: "libCall", fn: "dyn.numberConstructor", args: [v], type: F64, loc } : { kind: "dynTest", test: "truthy", value: v, type: BOOL, loc } diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts index cd8dc31d1..653c0d837 100644 --- a/packages/compiler/src/frontend/lowering/lower-functions.ts +++ b/packages/compiler/src/frontend/lowering/lower-functions.ts @@ -270,7 +270,7 @@ export function lowerLambda( const nameIdent = !ts.isArrowFunction(node) && node.name && ts.isIdentifier(node.name) ? node.name : null; const baseName = nameIdent ? nameIdent.text : ""; - const fnName = `%fn${lowerer.lambdaCounter++}${baseName ? `_${baseName}` : ""}`; + const fnName = lowerer.closureName(locOf(node), baseName ? `_${baseName}` : ""); // Named function expressions/declarations can self-reference by name; an // object-literal method's name is a PROPERTY, not a binding — no self. const selfSymbol = diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts index 4be5d0b7e..47f490c7b 100644 --- a/packages/compiler/src/frontend/lowering/lower-inspect.ts +++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, boolLit, @@ -45,6 +46,7 @@ import type { ClassInfo } from "./lower-classes.js"; import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js"; import { symbolFieldDisplayName } from "./symbol-fields.js"; import { eventTargetRootOf } from "./lower-event-target.js"; +import { dateRootOf } from "./builtins/date-classes.js"; import { lowerSystemErrorMap } from "./lower-system-errors.js"; import { isPerfEntryType, perfEntryJson } from "./builtins/performance.js"; import { recordMayKeepSource, recordSourceTest } from "./record-sources.js"; @@ -268,7 +270,7 @@ function errorInspectHelper( const key = `insp.userError:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = `%util.insp.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.insp", key); lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); const r = (): IrExpr => varRef("r.0", F64, loc); @@ -804,8 +806,9 @@ function inspectSupport( const info = lowerer.classes.get(t.className); if (!info) return `class '${t.className}' has no inspect lowering`; // EventTarget and Event subclasses render through Node's inspectors - // for those classes (the runtime's capsule view). - if (eventTargetRootOf(info) !== null) return null; + // for those classes (the runtime's capsule view), and so do classes + // declared `extends Date`. + if (eventTargetRootOf(info) !== null || dateRootOf(info) !== null) return null; if (isErrorClass(lowerer, t.className)) { if (userErrorClasses(info).length > 0 && errorHeaderOverridden(lowerer, info)) return "error subclasses whose name, message or stack is an accessor or a prototype property have no inspect lowering yet"; @@ -957,7 +960,10 @@ export function inspectExpr( loc, }; case "object": - if (eventTargetRootOf(lowerer.classes.get(t.className)) !== null) { + if ( + eventTargetRootOf(lowerer.classes.get(t.className)) !== null || + dateRootOf(lowerer.classes.get(t.className)) !== null + ) { return { kind: "libCall", fn: "insp.dyn", @@ -1145,7 +1151,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string { const key = `insp:${typeKey(t)}`; const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; - const name = `%util.insp.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.insp", key); lowerer.inspectHelpers.set(key, name); const v = (): IrExpr => varRef("v.0", t, loc); @@ -2074,7 +2080,7 @@ function formatUnionHelper( const existing = lowerer.inspectHelpers.get(key); if (existing) return existing; const loc = helperLoc(site); - const name = `%util.fmtv.${lowerer.inspectHelpers.size}`; + const name = keyedHelperName(lowerer, "%util.fmtv", key); lowerer.inspectHelpers.set(key, name); const def = lowerer.unions.get(t.unionId); if (!def) throw new InternalCompilerError(`format value over unknown union ${t.unionId}`); diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts index 766059dfb..9de027563 100644 --- a/packages/compiler/src/frontend/lowering/lower-mixins.ts +++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts @@ -34,6 +34,7 @@ * statics-bearing mixin classes outside a plain top-level statement * (their declaration-time code must run exactly where the call * evaluates). */ +import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { PoisonError } from "./lowerer.js"; @@ -452,7 +453,7 @@ function instantiateMixinCall( const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!); const instName = lowerer.qualify( ts.getSourceFileOfNode(call), - `%mx${ts.tokenStartOfNode(call)}.${shape.classNode.name?.text ?? ""}`, + `%mx${positionTag(lowerer, call)}.${shape.classNode.name?.text ?? ""}`, ); const ordinal = lowerer.mixinOrdinals.get(shape.classNode) ?? 0; lowerer.mixinOrdinals.set(shape.classNode, ordinal + 1); diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts index fcc68c787..8c50dee46 100644 --- a/packages/compiler/src/frontend/lowering/lower-mock.ts +++ b/packages/compiler/src/frontend/lowering/lower-mock.ts @@ -17,6 +17,7 @@ * writes the original back on restore(). Each mock registers a closure * that calls its restore() with its tracker (scr_test.c): the module's * `mock`, or the test's t.mock, which is reset when the test ends. */ +import { keyedHelperName } from "./stable-names.js"; import * as ts from "../ts7/adapter.js"; import { BOOL, @@ -136,7 +137,7 @@ function liftOnce( const key = `mock.${role}:${owner}`; const existing = lowerer.assertHelpers.get(key); if (existing) return existing; - const name = `%mock.${role}.${lowerer.assertHelpers.size}`; + const name = keyedHelperName(lowerer, `%mock.${role}`, key); lowerer.assertHelpers.set(key, name); build(name); return name; diff --git a/packages/compiler/src/frontend/lowering/lower-modules.test.ts b/packages/compiler/src/frontend/lowering/lower-modules.test.ts index 84cc96e7d..f1021e4da 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.test.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.test.ts @@ -34,7 +34,7 @@ test("retains dependencies from function storage, captures, generator channels a const completed = record("completed"); const body = record("body"); const global = record("global", record("nested")); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); record("unused"); const unionId = lowerer.unions.intern([body, UNDEFINED_T]); const unusedUnion = lowerer.unions.intern([F64, UNDEFINED_T]); @@ -162,7 +162,7 @@ test("closes nested structural types and index-signature values", () => { sent = record("sent"), indexed = record("indexed"); const containerId = lowerer.shapes.intern([], false, indexed); - const wanted = lowerer.shapes.shapes.map((shape) => shape.id); + const wanted = lowerer.shapes.shapes.map((shape) => shape.id).sort(); record("unused"); const nested: IrType = { kind: "func", diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts index d1f276d44..5b78c7278 100644 --- a/packages/compiler/src/frontend/lowering/lower-modules.ts +++ b/packages/compiler/src/frontend/lowering/lower-modules.ts @@ -1,4 +1,5 @@ import { THREADS_MODULE } from "../threads-import.js"; +import { positionTag } from "./structural-positions.js"; import { EXPERIMENTAL_MODULE } from "../experimental-import.js"; import { dynUndefinedExpr } from "../../ir/build.js"; import { everyStmtList, everyTypeChild } from "../../ir/traverse.js"; @@ -148,6 +149,7 @@ import { forkTargetPaths } from "../fork-target.js"; import { workerTargetPaths } from "../worker-target.js"; import { isNativeProxyInitializer } from "./expressions/native-proxy.js"; import { handleEventReaderPrelude } from "./lower-event-target.js"; +import { dateClassOperationPrelude } from "./builtins/date-classes.js"; import { cjsAccessorSnapshotStores, jsHoldsDynObject } from "./lower-exprs.js"; import { keptIdentityAssertion } from "./assertion-identity.js"; @@ -1074,8 +1076,14 @@ export function moduleArtifacts( else delete rest.abstractMethods; return rest; }), - records: lowerer.shapes.shapes.filter((r) => shapeIds.has(r.id)), - unions: lowerer.unions.unions.filter((u) => unionIds.has(u.id)), + // Ids come from what each shape and union is (keyedHelperName), so + // ordering by id gives every lowering order the same module. + records: lowerer.shapes.shapes + .filter((r) => shapeIds.has(r.id)) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), + unions: lowerer.unions.unions + .filter((u) => unionIds.has(u.id)) + .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)), }; } @@ -1677,7 +1685,7 @@ export function collectGlobals( } if (type.kind === "void") lowerer.badType(stmt.name, lowerer.typeOf(stmt.name)); const g: IrGlobal = { - id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${ts.tokenStartOfNode(stmt)}`, + id: `%g.${tag}${nsPathPrefix(lowerer, stmt)}%alias.${stmt.name.text}%${positionTag(lowerer, stmt)}`, name: stmt.name.text, type, mutable: false, @@ -1754,7 +1762,7 @@ export function collectGlobals( // (`%g.e.A%B.x`), so `namespace A { export const x }` and a // top-level `const x` never collide; non-exported members add the // declaring block's position (block-local bindings). - const nsPrefix = nsPathPrefix(stmt, list.declarations.nodes[0]); + const nsPrefix = nsPathPrefix(lowerer, stmt, list.declarations.nodes[0]); for (const decl of list.declarations.nodes) { // --provenance-sources: an elided pure-annotated dead const in a // fetched source module registers no global (the statement @@ -3376,6 +3384,7 @@ export function buildMain(lowerer: Lowerer): IrFunction { ...lowerer.mainPrelude, ...cjsModuleRegistryPrelude(lowerer, loc), ...handleEventReaderPrelude(lowerer, loc), + ...dateClassOperationPrelude(lowerer, loc), ]; const roots = [lowerer.entry, ...lowerer.forkTargets, ...lowerer.workerTargets]; const isAsync = roots.some((root) => lowerer.asyncInitFiles.has(root)); diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts index dca835d77..19123dae3 100644 --- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts +++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts @@ -36,6 +36,7 @@ * of the compiler already takes (a body running before the declaring * statement is not modeled — same as top-level const reads through early * calls). */ +import { positionTag, type PositionTagOwner } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import { packageShimFor } from "../package-shims.js"; import type { Lowerer } from "./lowerer.js"; @@ -164,7 +165,11 @@ function registerTypeOnlyBlocks(lowerer: Lowerer, decl: ts.ModuleDeclaration): v * distinct (resolution is by symbol; names only need uniqueness and * determinism). `modifierDecl` carries the declaration whose modifiers * decide exportedness (the first declarator of a variable statement). */ -export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { +export function nsPathPrefix( + owner: PositionTagOwner, + node: ts.Node, + modifierDecl?: ts.Node, +): string { const parent = node.parent; if (parent === undefined || !ts.isModuleBlock(parent)) return ""; const flagsHost = modifierDecl ?? node; @@ -179,10 +184,10 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string { const nested = p.parent !== undefined && ts.isModuleBlock(p.parent); const nonExported = nested && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Export) === 0; - segs.unshift(p.name.text + (nonExported ? `%${ts.tokenStartOfNode(p)}` : "")); + segs.unshift(p.name.text + (nonExported ? `%${positionTag(owner, p)}` : "")); } } - return `${segs.join("%")}${exported ? "" : `%${ts.tokenStartOfNode(parent)}`}.`; + return `${segs.join("%")}${exported ? "" : `%${positionTag(owner, parent)}`}.`; } /** The registered namespace-block kind a symbol's declaration sits in diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 058d7874a..94d49d8b3 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, nodeThrowExpr, @@ -183,7 +184,7 @@ export function voidizedCallback(lowerer: Lowerer, cb: IrExpr, loc: SrcLoc): IrE const toT = funcOf([...fromT.params], VOID); const key = `server.voidcb:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.voidcb.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.voidcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -279,7 +280,7 @@ function receiverReturningCall( ): IrExpr { const key = `server.recvret:${fn}:${opts?.prefix?.fn ?? ""}:${(opts?.mainArgIndices ?? []).join(".")}:${callArgs.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.recvret.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.recvret", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = callArgs.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); @@ -590,7 +591,7 @@ function h2HeadersCallbackAdapter( ); const key = `h2.hdrcb:${handleKind}:${withStatus}:${typeKey(fromT)}:${shapeId ?? ""}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%h2.hdrcb.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%h2.hdrcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; @@ -3583,7 +3584,7 @@ export function lowerCompatReqStreamOptionalCall( type: unionT, loc, }; - const id = `chain.${lowerer.chainCounter++}`; + const id = lowerer.closureName(loc, "", "chain"); const body = lowerH2StreamMethodCall(lowerer, call, call.expression, { kind: "chainRecv", id, @@ -4424,7 +4425,7 @@ function lowerHttpCreateServerForms( const hasMax = opts.maxHeaderSize !== null; const key = `server.opts:${opts.joinDup ? 1 : 0}:${opts.allowMissingHost ? 1 : 0}:${hasBuffer ? 1 : 0}:${hasMax ? 1 : 0}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%server.opts.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%server.opts", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const params = [ @@ -8009,7 +8010,7 @@ function lowerHttpResMethodCall( const all = [...callArgs, cb]; const key = `server.endcb:${fn}:${all.map((a) => typeKey(a.type)).join(",")}`; const existing = lowerer.arrHofHelpers.get(key); - const helper = existing ?? `%server.endcb.${lowerer.arrHofHelpers.size}`; + const helper = existing ?? keyedHelperName(lowerer, "%server.endcb", key); if (!existing) { lowerer.arrHofHelpers.set(key, helper); const params = all.map((a, i) => ({ localId: `p${i}.0`, name: `p${i}`, type: a.type })); diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts index 2ad3657c7..0beb24a0e 100644 --- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts +++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts @@ -2278,7 +2278,7 @@ function lowerStaticNamespaceImport( ): IrExpr { const loc = locOf(call); const resultT: IrType & { kind: "promise" } = { kind: "promise", inner: ns }; - const fnName = `%fn${lowerer.lambdaCounter++}_staticImport`; + const fnName = lowerer.closureName(loc, "_staticImport"); const fnType: IrType & { kind: "func" } = { kind: "func", params: [], ret: resultT }; const fnCtx = newFnCtx(true, null, fnType, ns); fnCtx.isAsync = true; diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts index e3480cf9c..2840b0b45 100644 --- a/packages/compiler/src/frontend/lowering/lower-stmts.ts +++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts @@ -3,6 +3,7 @@ import { declaredMemberOf, isPromisePrototypeRead } from "./expressions/property import { hostsAll } from "./expressions/hosted-narrowing.js"; import { requestGeneralStreamSurface } from "./general-streams.js"; import { lowerForAwaitDynStream } from "./dyn-streams.js"; +import { lowerForAwaitValueStream } from "./builtins/hash-stream.js"; import { lowerChildPropertyWrite } from "./handle-properties.js"; import { matchAllRowsReadByElements, @@ -3964,9 +3965,13 @@ function stdlibGlobalTokenDestructure( const canonical = ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member); token = `[builtin ${canonical}]`; alias = canonical; - // Builtin constructors read as their interned function values, the - // same values the bare and `globalThis.X` spellings read. - value = builtinGlobalValue(canonical, locOf(el), true); + // Builtin constructors read as their interned function values, and + // crypto as the WebCrypto object: the same values the bare and + // `globalThis.X` spellings read. + value = + canonical === "crypto" + ? { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc: locOf(el) } + : builtinGlobalValue(canonical, locOf(el), true); } else if (globalName === "console" && !CONSOLE_CALL_MEMBERS.has(propName)) { token = `[builtin console.${propName}]`; } else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) { @@ -6110,6 +6115,18 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar ) ) return true; + // A write that can store null or undefined into a binding the checker + // types as a primitive (`offset = match[3] || null`): JavaScript files + // are checked without strict null checks, so the declared type does not + // admit them. + const primitive = lowerer.mapTypeOf(lowerer.typeOf(decl.name)); + if ( + (primitive?.kind === "string" || primitive?.kind === "f64" || primitive?.kind === "bool") && + candidates.some( + (node) => node.operatorToken.kind === ts.SyntaxKind.EqualsToken && mayBeNullish(node.right), + ) + ) + return true; // A plain JavaScript write of a value the declared type cannot hold (the // checker does not report it): buffer-crc32 replaces its number[] table // with `new Int32Array(table)`. Only a different representation counts; @@ -6140,6 +6157,31 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar }); } +/** Whether `node` can evaluate to null or undefined by its form: the + * literals, `void`, and conditional or logical expressions with such an + * operand in result position. */ +function mayBeNullish(node: ts.Expression): boolean { + while (ts.isParenthesizedExpression(node)) node = node.expression; + if (node.kind === ts.SyntaxKind.NullKeyword || ts.isVoidExpression(node)) return true; + if (ts.isIdentifier(node)) return node.text === "undefined"; + if (ts.isConditionalExpression(node)) + return mayBeNullish(node.whenTrue) || mayBeNullish(node.whenFalse); + if (ts.isBinaryExpression(node)) { + const op = node.operatorToken.kind; + if ( + op === ts.SyntaxKind.BarBarToken || + op === ts.SyntaxKind.QuestionQuestionToken || + op === ts.SyntaxKind.AmpersandAmpersandToken + ) + return ( + mayBeNullish(node.right) || + (op === ts.SyntaxKind.AmpersandAmpersandToken && mayBeNullish(node.left)) + ); + if (op === ts.SyntaxKind.CommaToken) return mayBeNullish(node.right); + } + return false; +} + function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean { const guard = immediateAbsenceGuard(lowerer, decl); // `if (!x)`: the falsy test sees a missing value. @@ -7560,8 +7602,9 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm // (closures box), the runtime sets the member on OBJ receivers // and throws Node's TypeErrors on the rest. The receiver is // PROBED (tryLowerExpression) and claimed exactly when it lowers to - // dyn; typed receivers keep their own lowerings and fences. - if (!expr.left.questionDotToken) { + // dyn; typed receivers keep their own lowerings and fences. A + // private name is the field of its class (fieldTarget). + if (!expr.left.questionDotToken && !ts.isPrivateIdentifier(expr.left.name)) { const recv = tryLowerExpression(lowerer, expr.left.expression); if (recv && recv.type.kind === "dyn") { const loc = locOf(expr); @@ -9390,10 +9433,11 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt { ) { return lowerForAwaitStdin(lowerer, stmt); } - // A checked-dynamic NodeJS.ReadableStream iterates its native stream. - const dynStream = lowerForAwaitDynStream(lowerer, stmt, () => - lowerForAwaitBuiltin(lowerer, stmt), - ); + // A checked-dynamic NodeJS.ReadableStream iterates its native stream, + // and a Hash, Hmac, Cipheriv or Decipheriv iterates its Transform. + const dynStream = + lowerForAwaitDynStream(lowerer, stmt, () => lowerForAwaitBuiltin(lowerer, stmt)) ?? + lowerForAwaitValueStream(lowerer, stmt, () => lowerForAwaitBuiltin(lowerer, stmt)); if (dynStream) return dynStream; const builtin = lowerForAwaitBuiltin(lowerer, stmt); if (builtin) return builtin; diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts index b9add95a6..d6f89dc11 100644 --- a/packages/compiler/src/frontend/lowering/lower-stream.ts +++ b/packages/compiler/src/frontend/lowering/lower-stream.ts @@ -1,5 +1,6 @@ +import { keyedHelperName } from "./stable-names.js"; import { nodeThrowExpr, boolLit, numLit, strLit } from "../../ir/build.js"; -import { hashStreamOf, isHashValueType } from "./builtins/hash-stream.js"; +import { isValueStream, valueStreamOf } from "./builtins/hash-stream.js"; import { InternalCompilerError } from "../../errors.js"; /* The node:stream lowering (the spoke-module pattern, like * lower-event-emitter.ts): construction of the five runtime stream classes @@ -51,7 +52,10 @@ import { dynUndefinedExpr, varRef } from "../../ir/build.js"; import { genResultRecord } from "../type-mapper.js"; import { noteUntypedStreamFlow, requestGeneralStreamSurface } from "./general-streams.js"; import { dynNativeBranch } from "./dyn-streams.js"; +import { bindingNeverReassigned } from "./binding-analysis.js"; import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js"; +import { LiftedFrame, driveGenerator } from "./containers/iterator-helpers.js"; +import { lowerArrayIteratorValue, lowerStringIteratorValue } from "./iterator-adapters.js"; import { BOOL, DYN, @@ -73,6 +77,7 @@ import { funcOf, typeEquals, typeKey, + UNDEFINED_T, } from "../../ir/ir.js"; const BYTES = BYTES_U8; @@ -311,7 +316,7 @@ function lowerStreamCallback( ); } } - const fnName = `%fn${lowerer.lambdaCounter++}_${which}cb`; + const fnName = lowerer.closureName(loc, `_${which}cb`); const fnCtx = newFnCtx(true, null, funcType, funcType.ret); lowerer.fnStack.push(fnCtx); try { @@ -1144,7 +1149,7 @@ function streamMethodWrapper( paramTypes.push(got.type); } const ret = found.sig.ret; - const fnName = `%fn${lowerer.lambdaCounter++}_${which}vt`; + const fnName = lowerer.closureName(loc, `_${which}vt`); const funcType = funcOf([thisType, ...paramTypes], ret); lowerer.fnStack.push(newFnCtx(true, null, funcType, ret)); try { @@ -1690,7 +1695,7 @@ function fromGenHelpers( const key = `streamFromGen:${role}:${typeKey(gen)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromGen.${role}.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, `%stream.fromGen.${role}`, key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -1779,7 +1784,7 @@ function classIteratorObject( const key = `streamFromIter:${cit.iterT.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromIter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.fromIter", key); lowerer.valueHelpers.set(key, name); const nextName = `${name}.next`; const nextT = funcOf([], DYN); @@ -1940,7 +1945,7 @@ function classAsyncIteratorObject( const key = `streamFromAsyncIter:${cit.className}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.fromAsyncIter.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.fromAsyncIter", key); lowerer.valueHelpers.set(key, name); const it: IrLocal = { id: "it.0", name: "it", type: cit.iterT, mutable: false, boxed: true }; const src: IrLocal = { @@ -2083,10 +2088,10 @@ function lowerStreamArg( what: "finished" | "pipeline" | "addAbortSignal" | "compose", ): IrExpr { const lowered = lowerer.lowerExpr(node); - // A Hash or Hmac stage is its Transform; pipeline and compose stages - // keep the value alive until the stream settles. - const v = isHashValueType(lowered.type) - ? hashStreamOf(lowered, what === "pipeline" || what === "compose") + // A Hash, Hmac, Cipheriv or Decipheriv stage is its Transform; pipeline + // and compose stages keep the value alive until the stream settles. + const v = isValueStream(lowerer, node) + ? valueStreamOf(lowered, what === "pipeline" || what === "compose") : lowered; // A checked-dynamic pipeline stage (an untyped source or destination): // a boxed native stream unwraps at run time; other values (iterables, @@ -2428,10 +2433,10 @@ export function lowerStreamModuleCall( ? pipelineOptions(lowerer, lastArg) : null; const stageArgs = opts ? args.slice(0, -1) : args; - if (stageArgs.some(isPipelineFunctionStage)) { + if (hasPipelineFunctionStage(lowerer, stageArgs)) { if (opts) { lowerer.noLowering( - "pipeline with function stages and options", + "pipeline with function stages or iterable sources and options", lastArg, "pipeline(source, async function* (source) { ... }, destination) without options is supported", ); @@ -2502,7 +2507,10 @@ export function lowerStreamModuleCall( // (finished/pipeline throw synchronously; consumers do not). A // sync throw here would be observably wrong timing, so the // provable case takes the named fence instead. - const argV = lowerer.lowerExpr(args[0]!); + // A Hash, Hmac, Cipheriv or Decipheriv is read through its Transform. + const argV = isValueStream(lowerer, args[0]!) + ? valueStreamOf(lowerer.lowerExpr(args[0]!)) + : lowerer.lowerExpr(args[0]!); const argInfo = argV.type.kind === "object" ? lowerer.classes.get(argV.type.className) : undefined; if (argV.type.kind !== "dyn" && !streamSidesOf(lowerer, argInfo)) { @@ -2620,7 +2628,7 @@ export function lowerStreamModuleCall( "the supported form is pipeline(source, ...streams, callback)", ); } - if (args.slice(0, -1).some(isPipelineFunctionStage)) { + if (hasPipelineFunctionStage(lowerer, args.slice(0, -1))) { const staged = lowerPipelineStages(lowerer, args.slice(0, -1), loc); if (staged.dest !== 0) { // callback(err, value): the destination's value reaches the @@ -2679,19 +2687,215 @@ export function lowerStreamModuleCall( return null; } -/** A pipeline() argument written as a function: an async generator - * function stage. */ -function isPipelineFunctionStage(node: ts.Expression): boolean { - return ts.isFunctionExpression(node) || ts.isArrowFunction(node); +/** A pipeline() function stage: an inline function or arrow function, or + * an identifier bound to a function declaration or to a `const` + * initialized with one, which nothing reassigns. */ +type StageFunction = ts.FunctionExpression | ts.ArrowFunction | ts.FunctionDeclaration; + +/** The function a pipeline() argument writes or names, or null when the + * argument is not a function stage. */ +function pipelineStageFunction(lowerer: Lowerer, node: ts.Expression): StageFunction | null { + if (ts.isFunctionExpression(node) || ts.isArrowFunction(node)) return node; + if (!ts.isIdentifier(node)) return null; + // The stage lowers the declaration's body again with its source typed + // by the pipeline, so resolving it here must not report what collecting + // the declaration's own signature deferred (resolveValueSymbol would). + const resolve = (ident: ts.Identifier): ts.Symbol | undefined => { + const found = lowerer.checker.getSymbolAtLocation(ident); + return found && found.flags & ts.SymbolFlags.Alias + ? lowerer.checker.getAliasedSymbol(found) + : found; + }; + const symbol = resolve(node); + const declarations = symbol?.declarations ?? []; + const bodies = declarations.filter((d) => !ts.isFunctionDeclaration(d) || d.body !== undefined); + if (!symbol || bodies.length !== 1) return null; + const decl = bodies[0]!; + if (lowerer.isStdlibFile(ts.getSourceFileOfNode(decl))) return null; + if (ts.isFunctionDeclaration(decl)) { + return bindingNeverReassigned(lowerer, symbol, decl, resolve) ? decl : null; + } + if ( + ts.isVariableDeclaration(decl) && + decl.initializer && + ts.isVariableDeclarationList(decl.parent) && + (decl.parent.flags & ts.NodeFlags.Const) !== 0 + ) { + let init: ts.Expression = decl.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) return init; + } + return null; } -/** The lowered function stages of a pipeline: the libcall head [n, at, k, - * flags, mask, streams..., stages..., (next, read, return)?, box?, +function hasPipelineFunctionStage(lowerer: Lowerer, args: readonly ts.Expression[]): boolean { + return ( + args.some((a) => pipelineStageFunction(lowerer, a) !== null) || + (args.length > 0 && iterableSourceType(lowerer, args[0]!) !== null) + ); +} + +type SyncGenType = IrType & { kind: "generator" }; + +/** The element type of a pipeline source that is an iterable value (an + * array, a string or a synchronous generator object), or null. */ +function iterableSourceType(lowerer: Lowerer, node: ts.Expression): IrType | null { + if (pipelineStageFunction(lowerer, node) !== null) return null; + let t: IrType | null; + try { + t = lowerer.mapTypeOf(lowerer.typeOf(node)); + } catch { + return null; + } + if (t?.kind === "array") return t.elem; + if (t?.kind === "string") return STRING; + if (t?.kind === "generator" && t.async !== true && stepDrivable(t)) return t.yieldT; + return null; +} + +/** A pipeline source that is an iterable value or a synchronous generator + * function without parameters, as an async generator closure that steps + * the value's iterator (or the generator the function returns) and yields + * each element. Node's pipeline iterates such a source with `for await`, + * through an async-from-sync iterator, and an async generator that + * yields the same elements resumes its consumer on the same ticks. Null + * when the source has no such lowering. */ +function lowerIterableSource( + lowerer: Lowerer, + node: ts.Expression, + fn: StageFunction | null, + loc: SrcLoc, +): { closure: IrExpr; out: SyncGenType } | null { + let value: IrExpr; + if (fn !== null) { + if (fn.parameters.nodes.length > 0) { + lowerer.noLowering( + "a synchronous generator function pipeline source with parameters", + fn.parameters.nodes[0]!, + "declare the source without parameters, or make it an async generator function", + ); + } + value = lowerer.lowerLambda(fn); + if (value.type.kind !== "func" || value.type.ret.kind !== "generator") return null; + } else { + value = lowerer.lowerExpr(node); + } + const srcT = value.type; + const elemT: IrType | null = + srcT.kind === "func" && srcT.ret.kind === "generator" + ? srcT.ret.yieldT + : srcT.kind === "array" + ? srcT.elem + : srcT.kind === "string" + ? STRING + : srcT.kind === "generator" && srcT.async !== true + ? srcT.yieldT + : null; + if (elemT === null) return null; + const out: SyncGenType = { + kind: "generator", + async: true, + yieldT: elemT, + retT: VOID, + nextT: UNDEFINED_T, + }; + const resultType = genResultRecord(elemT, VOID, lowerer.shapes, lowerer.unions); + if (!resultType) return null; + const key = `pipelineSource:${typeKey(srcT)}`; + let maker = lowerer.valueHelpers.get(key); + if (!maker) { + maker = `%stream.pipelineSource.${lowerer.valueHelpers.size}`; + const genName = `${maker}.gen`; + // The generator: g = the source's iterator; for (v of g) yield v. + const frame = new LiftedFrame(loc); + const srcId = frame.local("src", srcT, false); + const src = varRef(srcId, srcT, loc); + const syncT: SyncGenType = { + kind: "generator", + yieldT: elemT, + retT: VOID, + nextT: UNDEFINED_T, + }; + let iterator: IrExpr | null; + if (srcT.kind === "func") { + iterator = { kind: "callValue", callee: src, args: [], type: srcT.ret, loc }; + } else if (srcT.kind === "array") { + iterator = lowerArrayIteratorValue(lowerer, src, "values", syncT, loc); + } else if (srcT.kind === "string") { + iterator = lowerStringIteratorValue(lowerer, src, syncT, loc); + } else { + iterator = src; + } + if (iterator === null || iterator.type.kind !== "generator" || !stepDrivable(iterator.type)) + return null; + const iterT = iterator.type; + const gId = frame.local("g", iterT, false); + const body: IrStmt[] = [ + { kind: "varDecl", localId: gId, init: iterator, loc }, + ...driveGenerator( + lowerer, + frame, + gId, + iterT, + elemT, + [], + (v) => [{ kind: "exprStmt", expr: { kind: "yieldExpr", value: v, type: VOID, loc }, loc }], + loc, + ), + { kind: "return", value: null, loc }, + ]; + const srcLocal = frame.locals.find((l) => l.id === srcId)!; + srcLocal.boxed = true; + lowerer.valueHelpers.set(key, maker); + lowerer.liftedFns.push({ + name: genName, + params: [], + returnType: VOID, + async: true, + captures: [{ localId: srcId, name: "src", type: srcT }], + locals: frame.locals, + body, + generator: { yieldT: elemT, nextT: UNDEFINED_T, resultType }, + loc, + }); + // The maker: (src) => the generator function closed over src. + const makerSrc: IrLocal = { id: "src.0", name: "src", type: srcT, mutable: false, boxed: true }; + lowerer.liftedFns.push({ + name: maker, + params: [{ localId: makerSrc.id, name: makerSrc.name, type: srcT }], + returnType: funcOf([], out), + locals: [makerSrc], + body: [ + { + kind: "return", + value: { + kind: "closure", + fnName: genName, + captures: [makerSrc.id], + type: funcOf([], out), + loc, + }, + loc, + }, + ], + loc, + }); + } + return { + closure: { kind: "call", callee: maker, args: [value], type: funcOf([], out), loc }, + out, + }; +} + +/** The lowered function stages of a pipeline: the libcall head [n, k, + * positions, flags, mask, streams..., stages..., helpers..., box?, * (signal, abort)?], the destination's type (the last stream, or the * PassThrough a function destination answers), the destination kind * (SCR_PIPE_DEST_*), a promise destination's promise type, and a wrapper * that binds the pipeline's AbortController around the call when a - * stage takes `{ signal }`. */ + * stage takes `{ signal }`. positions has bit j set when the pipeline's + * j-th stage is a function; helpers holds (next, read, return) for each + * generator pumped into a stream. */ interface PipelineStagesLowering { args: IrExpr[]; last: IrType; @@ -2700,56 +2904,95 @@ interface PipelineStagesLowering { wrap: (call: IrExpr) => IrExpr; } -/** pipeline(...) with one run of function stages (pipeline.js). The run - * may start the pipeline (a source function, called with `{ signal }`) - * and may end it (a destination function: an async generator pumped into - * a PassThrough, or an async function whose promise's value reaches the - * callback or the promise). Each stage's `source` parameter is typed as - * what the pipeline passes it: makeAsyncIterable answers the previous - * stream itself (a Readable is async iterable) or the previous stage's +/** pipeline(...) with function stages (pipeline.js). A function may + * start the pipeline (a source function, called with `{ signal }`), may + * end it (an async function whose promise's value reaches the callback or + * the promise), and runs of async generator functions may sit between + * streams; the last generator of a run that a stream follows is pumped + * into that stream. Each stage's `source` parameter is typed as what the + * pipeline passes it: makeAsyncIterable answers the previous stream + * itself (a Readable is async iterable) or the previous stage's * generator. */ function lowerPipelineStages( lowerer: Lowerer, stageArgs: readonly ts.Expression[], loc: SrcLoc, ): PipelineStagesLowering { - const first = stageArgs.findIndex(isPipelineFunctionStage); - let end = first; - while (end < stageArgs.length && isPipelineFunctionStage(stageArgs[end]!)) end++; - if (stageArgs.slice(end).some(isPipelineFunctionStage)) { + if (stageArgs.length > 52) { lowerer.noLowering( - "pipeline with function stages separated by streams", - stageArgs[end]!, - "one run of function stages is supported", + "pipeline with function stages and more than 52 stages", + stageArgs[52]!, + "split the pipeline", ); } - const source = first === 0; - const destFn = end === stageArgs.length; - const before: IrExpr[] = []; - const after: IrExpr[] = []; - try { - for (const a of stageArgs.slice(0, first)) before.push(lowerStreamArg(lowerer, a, "pipeline")); - } catch (e) { - if (e instanceof StreamArgTypeThrow) - lowerer.noLowering("pipeline over this value", stageArgs[0]!); - throw e; - } - let srcT: IrType | null = source ? null : before[before.length - 1]!.type; + const streams: IrExpr[] = []; const stages: IrExpr[] = []; + const helperArgs: IrExpr[] = []; + let positions = 0; let mask = 0; let dest: 0 | 2 = 0; let destPromise: (IrType & { kind: "promise" }) | null = null; - for (let i = first; i < end; i++) { - const fn = stageArgs[i] as ts.FunctionExpression | ts.ArrowFunction; - const isLast = destFn && i === end - 1; + // What the next function stage receives: null before the first stage, + // else the previous stream's type or the previous stage's generator. + let srcT: IrType | null = null; + // The streams and stages in argument order: the head lists all streams + // before all stages, so when a stage precedes a stream the arguments + // are bound to locals first, keeping JavaScript's evaluation order. + const argOrder: { list: IrExpr[]; index: number }[] = []; + for (let j = 0; j < stageArgs.length; j++) { + const arg = stageArgs[j]!; + const fn = pipelineStageFunction(lowerer, arg); + const syncSourceFn = + j === 0 && + fn !== null && + !ts.isArrowFunction(fn) && + fn.asteriskToken !== undefined && + !(fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); + if (j === 0 && (syncSourceFn || (fn === null && iterableSourceType(lowerer, arg) !== null))) { + const source = lowerIterableSource(lowerer, arg, syncSourceFn ? fn : null, loc); + if (source === null) lowerer.noLowering("pipeline over this source", arg); + positions += 1; + argOrder.push({ list: stages, index: stages.length }); + stages.push(source!.closure); + srcT = source!.out; + continue; + } + if (fn === null) { + if (srcT?.kind === "generator") { + // the generator before this stream, pumped into it + if (!fromGenChunk(lowerer, srcT.yieldT)) { + lowerer.noLowering( + `a pipeline stage yielding '${lowerer.fmt(srcT.yieldT)}'`, + stageArgs[j - 1]!, + "stage values written to a stream are strings or Buffers", + ); + } + const helpers = fromGenHelpers(lowerer, srcT, loc); + if (!helpers) + lowerer.noLowering("a pipeline stage with this generator type", stageArgs[j - 1]!); + helperArgs.push(helpers![0]!, helpers![1]!, helpers![2]!); + } + let stream: IrExpr; + try { + stream = lowerStreamArg(lowerer, arg, "pipeline"); + } catch (e) { + if (e instanceof StreamArgTypeThrow) lowerer.noLowering("pipeline over this value", arg); + throw e; + } + argOrder.push({ list: streams, index: streams.length }); + streams.push(stream); + srcT = stream.type; + continue; + } + const isLast = j === stageArgs.length - 1; const isAsync = (fn.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.AsyncKeyword); - const isGen = ts.isFunctionExpression(fn) && fn.asteriskToken !== undefined; + const isGen = !ts.isArrowFunction(fn) && fn.asteriskToken !== undefined; if (isLast && isGen) { // pipeline() pumps it into an object-mode PassThrough, whose // buffering decides when the pipeline finishes lowerer.noLowering( "a pipeline destination that is an async generator function", - fn, + arg, "end the pipeline with a stream or an async function (source) { for await (const chunk of source) ... }", ); } @@ -2760,7 +3003,7 @@ function lowerPipelineStages( : srcT === null ? "a pipeline source function that is not an async generator function" : "pipeline stages that are not async generator functions", - fn, + arg, "write the stage as async function* (source) { for await (const chunk of source) ... }", ); } @@ -2782,8 +3025,10 @@ function lowerPipelineStages( "type it as { signal: AbortSignal }", ); } - mask |= 1 << (i - first); + mask |= 1 << stages.length; } + positions += 2 ** j; + argOrder.push({ list: stages, index: stages.length }); stages.push(lowered.closure); if (lowered.out.kind === "promise") { dest = 2; @@ -2793,29 +3038,7 @@ function lowerPipelineStages( srcT = lowered.out; } } - try { - for (const a of stageArgs.slice(end)) after.push(lowerStreamArg(lowerer, a, "pipeline")); - } catch (e) { - if (e instanceof StreamArgTypeThrow) - lowerer.noLowering("pipeline over this value", stageArgs[end]!); - throw e; - } - const helperArgs: IrExpr[] = []; - if (dest !== 2) { - // the generator pumped into the next stream or the PassThrough - const lastGen = srcT as IrType & { kind: "generator" }; - if (!fromGenChunk(lowerer, lastGen.yieldT)) { - lowerer.noLowering( - `a pipeline stage yielding '${lowerer.fmt(lastGen.yieldT)}'`, - stageArgs[end - 1]!, - "stage values written to a stream are strings or Buffers", - ); - } - const helpers = fromGenHelpers(lowerer, lastGen, loc); - if (!helpers) - lowerer.noLowering("a pipeline stage with this generator type", stageArgs[end - 1]!); - helperArgs.push(helpers![0]!, helpers![1]!, helpers![2]!); - } else { + if (dest === 2) { const valueT = destPromise!.inner; if ( valueT.kind !== "void" && @@ -2827,29 +3050,40 @@ function lowerPipelineStages( ) { lowerer.noLowering( `a pipeline destination resolving to '${lowerer.fmt(valueT)}'`, - stageArgs[end - 1]!, + stageArgs[stageArgs.length - 1]!, ); } helperArgs.push(pipelineBoxHelper(lowerer, destPromise!, loc)); } - const streams = [...before, ...after]; - // SCR_PIPE_* flags: bit 0 a source function, bits 1-2 the destination, - // bit 3 the helpers, bit 4 the box, bit 5 the signal and abort. - const flags = (source ? 1 : 0) | (dest << 1) | (dest !== 2 ? 8 : 16) | (mask !== 0 ? 32 : 0); + const argDecls: IrStmt[] = []; + const firstStage = argOrder.findIndex((a) => a.list === stages); + if (firstStage >= 0 && argOrder.slice(firstStage).some((a) => a.list === streams)) { + for (const a of argOrder) { + const init = a.list[a.index]!; + const local = lowerer.declareHiddenLocal("%pipelineArg", init.type); + argDecls.push({ kind: "varDecl", localId: local.id, init, loc: init.loc }); + a.list[a.index] = varRef(local.id, init.type, init.loc); + } + } + const bindArgs = (call: IrExpr): IrExpr => + argDecls.length === 0 + ? call + : { kind: "seqExpr", stmts: argDecls, result: call, type: call.type, loc }; + // SCR_PIPE_* flags: bits 1-2 the destination, bit 5 the signal and abort. + const flags = (dest << 1) | (mask !== 0 ? 32 : 0); const head: IrExpr[] = [ numLit(streams.length, loc), - numLit(before.length, loc), numLit(stages.length, loc), + numLit(positions, loc), numLit(flags, loc), numLit(mask, loc), ...streams, ...stages, ...helperArgs, ]; - const last: IrType = destFn - ? { kind: "object", className: "%PassThrough" } - : streams[streams.length - 1]!.type; - if (mask === 0) return { args: head, last, dest, destPromise, wrap: (call) => call }; + const last: IrType = + dest === 2 ? { kind: "object", className: "%PassThrough" } : streams[streams.length - 1]!.type; + if (mask === 0) return { args: head, last, dest, destPromise, wrap: bindArgs }; // const ac = new AbortController(): its signal goes to the stages, and // finishImpl's ac.abort() runs through the abort closure. const ac = lowerer.declareHiddenLocal("%pipelineAbort", DYN); @@ -2866,20 +3100,21 @@ function lowerPipelineStages( last, dest, destPromise, - wrap: (call) => ({ - kind: "seqExpr", - stmts: [ - { - kind: "varDecl", - localId: ac.id, - init: { kind: "libCall", fn: "fetch.abortControllerNew", args: [], type: DYN, loc }, - loc, - }, - ], - result: call, - type: call.type, - loc, - }), + wrap: (call) => + bindArgs({ + kind: "seqExpr", + stmts: [ + { + kind: "varDecl", + localId: ac.id, + init: { kind: "libCall", fn: "fetch.abortControllerNew", args: [], type: DYN, loc }, + loc, + }, + ], + result: call, + type: call.type, + loc, + }), }; } @@ -2894,7 +3129,7 @@ function pipelineBoxHelper( const key = `pipelineBox:${typeKey(promiseT)}`; let name = lowerer.valueHelpers.get(key); if (!name) { - name = `%stream.pipelineBox.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.pipelineBox", key); lowerer.valueHelpers.set(key, name); lowerer.liftedFns.push({ name, @@ -2921,7 +3156,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr let name = lowerer.valueHelpers.get(key); const abortT = funcOf([], VOID); if (!name) { - name = `%stream.pipelineAbort.${lowerer.valueHelpers.size}`; + name = keyedHelperName(lowerer, "%stream.pipelineAbort", key); lowerer.valueHelpers.set(key, name); const acLocal: IrLocal = { id: "ac.0", name: "ac", type: DYN, mutable: false, boxed: true }; const acRef = varRef(acLocal.id, DYN, loc); @@ -2979,7 +3214,7 @@ function pipelineAbortHelper(lowerer: Lowerer, ac: IrExpr, loc: SrcLoc): IrExpr * generator, or a promise for a destination async function. */ function lowerStageFunction( lowerer: Lowerer, - fn: ts.FunctionExpression | ts.ArrowFunction, + fn: StageFunction, srcT: IrType | null, ): { closure: IrExpr; out: IrType & ({ kind: "generator" } | { kind: "promise" }) } { const param = srcT === null ? undefined : fn.parameters.nodes[0]; @@ -2994,6 +3229,8 @@ function lowerStageFunction( "name the source parameter", ); } + // The loop variables of the `for await` loops over the source. + const chunkSymbols: ts.Symbol[] = []; if (symbol && fn.body) { const visit = (node: ts.Node): void => { if ( @@ -3016,14 +3253,36 @@ function lowerStageFunction( "iterate the source with for await (const chunk of source)", ); } + const binding = ts.isVariableDeclarationList(parent.initializer) + ? parent.initializer.declarations.nodes[0]?.name + : undefined; + const chunk = + binding && ts.isIdentifier(binding) + ? lowerer.checker.getSymbolAtLocation(binding) + : undefined; + if (chunk) chunkSymbols.push(chunk); } ts.forEachChild(node, visit); }; visit(fn.body); } if (symbol) lowerer.stageSourceTypes.set(symbol, srcT!); + // In JavaScript the checker types the chunks of a source that is the + // previous stage's generator `any`; they read as the strings or Buffers + // the generator yields, so their members take the string and Buffer + // lowerings. + const previousImplicit = lowerer.implicitParamTypes; + const chunkType = + srcT?.kind === "generator" && isJsSourceFile(ts.getSourceFileOfNode(fn)) + ? stageChunkCheckerType(lowerer, srcT.yieldT, fn) + : null; + if (chunkType && chunkSymbols.length > 0) { + const implicit = new Map(previousImplicit ?? []); + for (const chunk of chunkSymbols) implicit.set(chunk, chunkType); + lowerer.implicitParamTypes = implicit; + } try { - const closure = lowerer.lowerExpr(fn); + const closure = lowerer.lowerLambda(fn); const out = closure.type.kind === "func" ? closure.type.ret : null; if (!(out?.kind === "generator" && out.async) && out?.kind !== "promise") { lowerer.noLowering("a pipeline stage that does not return an async generator", fn); @@ -3031,9 +3290,19 @@ function lowerStageFunction( return { closure, out: out as IrType & ({ kind: "generator" } | { kind: "promise" }) }; } finally { if (symbol) lowerer.stageSourceTypes.delete(symbol); + lowerer.implicitParamTypes = previousImplicit; } } +/** The checker type of a stage's string or Buffer chunks, or null for + * other chunk types. */ +function stageChunkCheckerType(lowerer: Lowerer, chunk: IrType, at: ts.Node): ts.Type | null { + if (chunk.kind === "string") return lowerer.checker.getStringType(); + if (chunk.kind !== "bytes") return null; + const buffer = lowerer.checker.resolveName("Buffer", ts.SymbolFlags.Type, at, false); + return buffer ? lowerer.checker.getDeclaredTypeOfSymbol(buffer) : null; +} + /** `stream.compose(...streams)` over stream instances: a Duplex writing * to the first stream and reading from the last (scr_stream_compose). The * classes fix which positions can be readable and writable; a stream in @@ -3805,9 +4074,10 @@ export function lowerStreamMethodCall( "the supported forms are (destination) and (destination, { end })", ); } - // A Hash or Hmac destination pipes into its Transform, and pipe() - // answers the Hash itself (the receiver evaluates first, as in JS). - if (isHashValueType(lowerer.mapTypeOf(lowerer.typeOf(args[0]!)))) { + // A Hash, Hmac, Cipheriv or Decipheriv destination pipes into its + // Transform, and pipe() answers the value itself (the receiver + // evaluates first, as in JS). + if (isValueStream(lowerer, args[0]!)) { const receiverValue = lowerer.lowerExpr(access.expression); const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type); const hashValue = lowerer.lowerExpr(args[0]!); @@ -3825,7 +4095,10 @@ export function lowerStreamMethodCall( fn: "readable.pipe", args: [ { kind: "varRef", localId: source.id, type: receiverValue.type, loc }, - hashStreamOf({ kind: "varRef", localId: hash.id, type: hashValue.type, loc }, true), + valueStreamOf( + { kind: "varRef", localId: hash.id, type: hashValue.type, loc }, + true, + ), end, ], type: { kind: "object", className: "%Transform" }, diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts index 7d4bba34b..03c9c5c1f 100644 --- a/packages/compiler/src/frontend/lowering/lower-timers.ts +++ b/packages/compiler/src/frontend/lowering/lower-timers.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js"; import * as ts from "../ts7/adapter.js"; import { isJsSourceFile } from "../program.js"; @@ -55,7 +56,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.dropret:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.dropret.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.dropret", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -117,7 +118,7 @@ export function adaptZeroArgTimerCallback( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.droparg:${typeKey(fromT)}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.droparg.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.droparg", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); lowerer.liftedFns.push( @@ -203,7 +204,7 @@ function makeTimerArgsThunk( const toT: IrType = { kind: "func", params: [], ret: VOID }; const key = `timer.argsthunk:${n}`; const existing = lowerer.arrHofHelpers.get(key); - const name = existing ?? `%timer.argsthunk.${lowerer.arrHofHelpers.size}`; + const name = existing ?? keyedHelperName(lowerer, "%timer.argsthunk", key); if (!existing) { lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; diff --git a/packages/compiler/src/frontend/lowering/lowerer.test.ts b/packages/compiler/src/frontend/lowering/lowerer.test.ts index bacdc7941..7fbb9661f 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.test.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.test.ts @@ -10,6 +10,7 @@ import { } from "../../ir/ir.js"; import type { Node, Program, SourceFile, Symbol, Type } from "../ts7/adapter.js"; import { Lowerer } from "./lowerer.js"; +import { UnionRegistry } from "../type-mapper.js"; import { classMethodValue } from "./class-method-values.js"; const loc = { file: "island.ts", start: 0, end: 1 }; @@ -32,7 +33,7 @@ test("reuses complete copy routes without repeating discriminator planning", () expect(plan).not.toHaveBeenCalled(); expect(lowerer.liftedFns.filter((fn) => fn.name === helper)).toHaveLength(1); const recursive = {} as Type; - lowerer.shapes.recursiveRef(recursive); + lowerer.shapes.recursiveRef(recursive, () => "Recursive"); lowerer.shapes.finalizeRecursive(recursive, [{ name: "kind", type: STRING }]); expect(lowerer.unionRetagHelper(from, to, loc)).toBe(helper); expect(plan).toHaveBeenCalled(); @@ -54,7 +55,7 @@ test("replans after a recursive union gains its final arms", () => { const lowerer = context(); const recursive = {} as Type; const from = lowerer.unions.intern([UNDEFINED_T, NULL_T]); - const to = lowerer.unions.recursiveRef(recursive); + const to = lowerer.unions.recursiveRef(recursive, () => "Recursive"); const trapped = lowerer.unionRetagHelper(from, to, loc); expect(trapped).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [UNDEFINED_T]); @@ -75,7 +76,7 @@ test("rechecks width conversions after recursive record definitions settle", () const lowerer = context(); const recursive = {} as Type; const source = record(lowerer.shapes.intern([{ name: "value", type: F64 }])); - const target = record(lowerer.shapes.recursiveRef(recursive)); + const target = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); const from = lowerer.unions.intern([source, UNDEFINED_T]); const to = lowerer.unions.intern([target, NULL_T]); expect(lowerer.unionRetagHelper(from, to, loc)).not.toBeNull(); @@ -129,7 +130,7 @@ test("literal ownership is reused across values and rechecked when recursive rec expect(inspect).not.toHaveBeenCalled(); const recursive = {} as Type; - const pendingShape = record(lowerer.shapes.recursiveRef(recursive)); + const pendingShape = record(lowerer.shapes.recursiveRef(recursive, () => "Recursive")); const pending = lowerer.unions.intern([pendingShape], { field: "kind", cases: [{ tag: 0, values: ["a"] }], @@ -143,7 +144,7 @@ test("literal ownership is reused across values and rechecked when recursive rec test("literal ownership sees a recursive union's finalized discriminator", () => { const lowerer = context(); const recursive = {} as Type; - const union = lowerer.unions.recursiveRef(recursive); + const union = lowerer.unions.recursiveRef(recursive, () => "Recursive"); const shape = record(lowerer.shapes.intern([{ name: "kind", type: STRING }])); expect(lowerer.literalUnionArm(union, ["ready"])).toBeNull(); lowerer.unions.finalizeRecursive(recursive, [shape], { @@ -182,7 +183,10 @@ test("removing undefined reuses the discriminator transformation without hiding ); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), + }; expect(lowerer.stripUndefinedArm(pending)).toEqual(pending); lowerer.unions.finalizeRecursive(recursive, [STRING, UNDEFINED_T]); expect(lowerer.stripUndefinedArm(pending)).toEqual(STRING); @@ -215,7 +219,10 @@ test("adding undefined reuses semantic union transformations and sees recursive expect(lowerer.unions.get(added.unionId)?.discriminant).toEqual(discriminant); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), + }; const before = lowerer.withUndefinedArmOf(pending); lowerer.unions.finalizeRecursive(recursive, [a, b], discriminant); expect(lowerer.withUndefinedArmOf(pending)).toEqual(added); @@ -227,13 +234,18 @@ test("adding undefined reuses semantic union transformations and sees recursive test("optional union widening keeps refused and missing contracts current", () => { const lowerer = context(); const recursive = {} as Type; - const pending: IrType = { kind: "union", unionId: lowerer.unions.recursiveRef(recursive) }; + const pending: IrType = { + kind: "union", + unionId: lowerer.unions.recursiveRef(recursive, () => "Recursive"), + }; expect(lowerer.withUndefinedArmOf(pending)).not.toBeNull(); lowerer.unions.finalizeRecursive(recursive, [{ kind: "date" }, STRING]); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); expect(lowerer.withUndefinedArmOf(pending)).toBeNull(); - const missing: IrType = { kind: "union", unionId: `u${lowerer.unions.unions.length}` }; + // Union ids come from their arms, so a fresh registry names the union + // this one has not interned yet. + const missing: IrType = { kind: "union", unionId: new UnionRegistry().intern([F64, STRING]) }; expect(lowerer.withUndefinedArmOf(missing)).toBeNull(); expect(lowerer.unions.intern([F64, STRING])).toBe(missing.unionId); expect(lowerer.withUndefinedArmOf(missing)?.kind).toBe("union"); diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index ee8ba7ecb..f3ab173f8 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -1,4 +1,6 @@ import { relieveHeapPressure } from "../../heap-pressure.js"; +import { keyedHelperName } from "./stable-names.js"; +import { positionTag, StructuralPositions } from "./structural-positions.js"; import { onlyForwarded, stripTypeWrappers } from "./typed-arguments.js"; import { needsGeneralNodeStreams, @@ -60,7 +62,7 @@ import { resolveRequireRuntime } from "../runtime-resolve.js"; import { lowerKeyObjectCall } from "./builtins/key-object.js"; import { isEcdhType, lowerEcdhCall } from "./builtins/ecdh.js"; import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js"; -import { hashStreamOf } from "./builtins/hash-stream.js"; +import { valueStreamOf } from "./builtins/hash-stream.js"; import { lowerNetAddressCall, lowerNetAddressNew } from "./builtins/net-address.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, varRef } from "../../ir/build.js"; import { iteratorSlotAdapter } from "./iterator-adapters.js"; @@ -497,7 +499,8 @@ import { lowerTracingChannelProperty, } from "./builtins/async-context.js"; import { lowerDisposableStackNew } from "./builtins/disposable-stack.js"; -import { lowerErrorCodeProperty } from "./builtins/errors.js"; +import { lowerErrorCodeProperty, lowerErrorFunctionCall } from "./builtins/errors.js"; +import { noteDateClassMethods } from "./builtins/date-classes.js"; import { lowerProcessProperty, lowerProcessMethodCall, @@ -1004,6 +1007,11 @@ function lowerShuffle(): ((wave: string[]) => void) | null { * the bodies reachable emit did NOT mark (plus deferred collection * diagnostics nothing flushed), reported separately: whole-program analysis without * letting unreached code fail builds. */ +/** The closure root of an emit-override specialization body. */ +function emitSpecRoot(spec: EmitSpecRequest): string { + return `%emit.${spec.info.def.name}.${spec.event}.${spec.ordinal}`; +} + export function lowerToIr( program: ts.Program, entry: ts.SourceFile, @@ -2171,6 +2179,12 @@ export class Lowerer { /** Instance types (by checker type id) of the local classes generic * factory calls return, and the per-instance class each names. */ readonly localClassInstanceTypes = new Map(); + /** A generic factory's instantiated result type → the class value of the + * local class it returns (TypeMapperCtx.localClassValue). */ + readonly localClassValueTypes = new Map(); + /** While a generic factory instance lowers: its declared result type → the + * class value of the local class it returns. */ + instanceResultClass: { declared: number; value: IrType } | null = null; /** The ts-level twin of typeParamBindings, non-null while a CALL-keyed * instance body lowers: type-parameter symbol → the bound CHECKER type, * consulted where the mapped IrType has already widened away information @@ -2801,6 +2815,8 @@ export class Lowerer { /** Generated value helpers, keyed by operation and type. Callers own * their key prefixes; this includes more than structural conversions. */ readonly valueHelpers = new Map(); + /** Each name keyedHelperName made, with a second hash of its key. */ + readonly keyedNameKeys = new Map(); /** structuralNarrowing's assignable classes, keyed by static and * narrowed class name. */ readonly structuralClassPeers = new Map(); @@ -2937,13 +2953,16 @@ export class Lowerer { ctx: FnCtx; } >(); - chainCounter = 0; /** Keyed by program-wide qualified class name (what IR object types carry). */ readonly classes = new Map(); readonly classBySymbol = new Map(); /** Named method slots observed before class collection need a callable * ABI and shared prototype descriptors instead of call-site specialization. */ readonly prototypeMethodAccesses = new Map(); + /** Whether the program reads `Error.prototype`, the checked-dynamic + * object an ES5 constructor function can inherit from: only then can a + * value that is not an Error instance be `instanceof Error`. */ + errorPrototypeValues = false; /** Well-known symbol methods (`sym:iterator`) the program may replace * through a symbol-keyed write: an instance or prototype property, or a * reflection call. */ @@ -3022,10 +3041,10 @@ export class Lowerer { /** Inline Promise.then parameters whose contextual type is a builtin * module namespace anonymous object. The settled native token is the * ABI truth even when the checker type has no stable module symbol. */ - /** Receivers of a stream call on a Hash or Hmac (lowerHashStreamCall): - * for the duration of that call, the receiver lowers to the Transform of - * the value held in the named local. */ - readonly hashStreamReceivers = new Map(); + /** Receivers of a stream call on a Hash, Hmac, Cipheriv or Decipheriv + * (lowerHashStreamCall): for the duration of that call, the receiver + * lowers to the Transform of the value held in the named local. */ + readonly valueStreamReceivers = new Map(); /** fs call arguments evaluated ahead of a URL path's conversion * (builtins/fs-paths.ts): each lowers to the hidden local that holds its * value, which is null until the path's lowering has saved it. */ @@ -3167,7 +3186,53 @@ export class Lowerer { * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; errorPropertyDispatchRevision: string | null = null; - lambdaCounter = 0; + /** Structural source positions (structural-positions.ts) for names that + * stay the same when unrelated code moves. */ + /** Default library files are named by their base name: two checkers + * install them in different directories. */ + readonly positions = new StructuralPositions((sf) => + this.program.isSourceFileDefaultLibrary(sf) + ? `lib:${sf.fileName.slice(sf.fileName.lastIndexOf("/") + 1)}` + : sf.fileName, + ); + + /** The outermost node whose token span is exactly `loc`, or null. */ + nodeAtLoc(loc: SrcLoc): ts.Node | null { + const sf = this.program.getSourceFile(loc.file); + return sf === undefined ? null : (ts.nodeAtSpan(sf, loc.start, loc.end) ?? null); + } + + /** The lowering unit whose name scopes the closures it lifts. */ + private closureRoot = "%pre"; + private readonly closureNameCounts = new Map(); + + /** Runs `fn` with the closures it lifts named under `root`. */ + withClosureRoot(root: string, fn: () => T): T { + const previous = this.closureRoot; + this.closureRoot = root; + try { + return fn(); + } finally { + this.closureRoot = previous; + } + } + + /** The name of a closure (or wrapper) lifted for the source at `loc`: + * `.` of the current lowering unit, the location and + * `suffix`, then `suffix`. A unit that lifts several functions for the same + * location and suffix numbers them in the order it lifts them, so a + * closure's name depends only on its unit, not on which units lowered + * before it. */ + closureName(loc: SrcLoc, suffix = "", prefix = "%fn"): string { + const node = this.nodeAtLoc(loc); + const where = node === null ? `${loc.file}:${loc.start}:${loc.end}` : this.positions.of(node); + const key = `${prefix}|${this.closureRoot}|${where}|${suffix}`; + const first = keyedHelperName(this, prefix, key); + const seen = this.closureNameCounts.get(first) ?? 0; + this.closureNameCounts.set(first, seen + 1); + return (seen === 0 ? first : keyedHelperName(this, prefix, `${key}|${seen}`)) + suffix; + } + readonly preparedClassFactories = new Map(); readonly preparingClassFactories = new Set(); @@ -3520,6 +3585,7 @@ export class Lowerer { this.checker = program.getTypeChecker(); this.typeCtx = { checker: this.checker, + positionOf: (node) => this.positions.of(node), shapes: this.shapes, unions: this.unions, classNamer: this.classNamer, @@ -3529,6 +3595,10 @@ export class Lowerer { genericClassInstance: (decl, ref, mapArgument) => this.genericClassInstanceType(decl, ref, mapArgument), runtimeClassValue: (type) => runtimeClassValueOfType(this, type), + localClassValue: (type) => + this.instanceResultClass?.declared === type.id + ? this.instanceResultClass.value + : (this.localClassValueTypes.get(type.id) ?? null), mixinClassInstance: (decl) => this.mixinTypeContext && this.mixinTypeContext.classNode === decl ? { kind: "object", className: this.mixinTypeContext.className } @@ -3808,11 +3878,11 @@ export class Lowerer { })() ? this.qualify( ts.getSourceFileOfNode(decl), - `%cx${ts.tokenStartOfNode(decl)}.${decl.name?.text ?? ""}`, + `%cx${positionTag(this, decl)}.${decl.name?.text ?? ""}`, ) : this.qualify( ts.getSourceFileOfNode(decl), - nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"), + nsPathPrefix(this, decl) + (decl.name ? decl.name.text : "%anon"), ); for (let index = this.localClassInstantiations.length - 1; index >= 0; index--) { const entry = this.localClassInstantiations[index]!; @@ -7371,7 +7441,10 @@ export class Lowerer { } for (const decl of fp.classDecls) { const info = this.classes.get(this.classNamer(decl)); - if (info) functions.push(...this.lowerClassMembers(info)); + if (info) + functions.push( + ...this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), + ); else if (this.countsSkips()) this.countSkippedBody(decl); } } @@ -7384,7 +7457,10 @@ export class Lowerer { // are reachable by definition, already counted by reachable emit. if (!this.remainder) { for (const fp of parts) { - functions.push(this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!)); + const initName = this.initNameOf.get(fp.sf)!; + functions.push( + this.withClosureRoot(initName, () => this.lowerFileInit(fp.sf, fp.topStmts, initName)), + ); } functions.push(this.buildMain()); } @@ -7426,7 +7502,9 @@ export class Lowerer { // default): the diagnostic is recorded — the instance skips // like a signature-blocked function (lowerFunction's rule). try { - functions.push(this.lowerGenericInstance(info, inst)); + functions.push( + this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), + ); } catch (e) { if (!(e instanceof PoisonError)) throw e; } @@ -7436,7 +7514,10 @@ export class Lowerer { // instances of its own; the joint fixpoint covers both). while (es < this.emitSpecQueue.length) { try { - const fn = lowerEmitOverrideSpec(this, this.emitSpecQueue[es++]!); + const spec = this.emitSpecQueue[es++]!; + const fn = this.withClosureRoot(emitSpecRoot(spec), () => + lowerEmitOverrideSpec(this, spec), + ); if (fn) functions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -7990,7 +8071,9 @@ export class Lowerer { this.genericClassDemandPriority.set(info, ref); instanceFunctions.push( ...this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(ref, () => this.lowerClassMembers(info)), + this.shapes.withDeclaredOrderPriority(ref, () => + this.withClosureRoot(info.def.name, () => this.lowerClassMembers(info)), + ), ), ); relieveHeapPressure(); @@ -8017,7 +8100,7 @@ export class Lowerer { instanceFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(ref, () => - this.lowerGenericInstance(info, inst), + this.withClosureRoot(inst.name, () => this.lowerGenericInstance(info, inst)), ), ), ); @@ -8050,9 +8133,12 @@ export class Lowerer { ); while (specLowered < waveEnd) { try { - const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => - lowerEmitOverrideSpec(this, this.emitSpecQueue[specLowered++]!), - ); + const fn = this.shapes.withDeclaredOrderPriority([4, instanceMetadataOrder++], () => { + const spec = this.emitSpecQueue[specLowered++]!; + return this.withClosureRoot(emitSpecRoot(spec), () => + lowerEmitOverrideSpec(this, spec), + ); + }); if (fn) instanceFunctions.push(fn); } catch (e) { if (!(e instanceof PoisonError)) throw e; @@ -8080,7 +8166,9 @@ export class Lowerer { initFunctions.push( this.withGenericDemandOwner(owner, () => this.shapes.withDeclaredOrderPriority(priority, () => - this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), + this.withClosureRoot(this.initNameOf.get(fp.sf)!, () => + this.lowerFileInit(fp.sf, fp.topStmts, this.initNameOf.get(fp.sf)!), + ), ), ), ); @@ -8106,7 +8194,9 @@ export class Lowerer { const priority = metadataPriority.get(name) ?? [3, expressionMetadataOrder++]; const owner = demandOwner(priority); const fn = this.withGenericDemandOwner(owner, () => - this.shapes.withDeclaredOrderPriority(priority, unit.lower), + this.shapes.withDeclaredOrderPriority(priority, () => + this.withClosureRoot(name, unit.lower), + ), ); if (fn) loweredUnits.set(name, fn); relieveHeapPressure(); @@ -8123,6 +8213,7 @@ export class Lowerer { // turn queue more instances. Continue to the joint fixpoint; the initial // queue above is the only portion whose discovery order differed from // historical emit order. + noteDateClassMethods(this); const classDispatch = new ClassDynamicDispatch(extraRoots !== undefined); // Units in declaration order, so the passes below meet functions, and the // classes they collect, in an order that does not depend on lowering order. @@ -12417,8 +12508,9 @@ export class Lowerer { /* ── expressions ──────────────────────────────────────────────────── */ lowerExpr(expr: ts.Expression): IrExpr { - const hash = this.hashStreamReceivers.get(expr); - if (hash) return hashStreamOf(varRef(hash.localId, hash.type, locOf(expr))); + const streamValue = this.valueStreamReceivers.get(expr); + if (streamValue) + return valueStreamOf(varRef(streamValue.localId, streamValue.type, locOf(expr))); return this.stagedArgument(expr) ?? lowerExpr(this, expr); } @@ -12758,6 +12850,25 @@ export class Lowerer { ); } + /** withExpressionOverride for several nodes at once. */ + withExpressionOverrides( + overrides: readonly { node: ts.Expression; value: IrExpr }[], + lower: () => T, + ): T { + const previous = overrides.map((override) => this.chainRecvByNode.get(override.node)); + for (const override of overrides) this.chainRecvByNode.set(override.node, override.value); + try { + return lower(); + } finally { + for (let i = overrides.length - 1; i >= 0; i--) { + const node = overrides[i]!.node; + const value = previous[i]; + if (value) this.chainRecvByNode.set(node, value); + else this.chainRecvByNode.delete(node); + } + } + } + withExpressionOverride(node: ts.Expression, value: IrExpr, lower: () => T): T { const previous = this.chainRecvByNode.get(node); this.chainRecvByNode.set(node, value); @@ -12777,6 +12888,7 @@ export class Lowerer { if (webAssembly !== null) return webAssembly; refuseWebAssemblyUse(this, expr.expression); return ( + lowerErrorFunctionCall(this, expr) ?? lowerLifetimeRegionCall(this, expr) ?? lowerInternTableCall(this, expr) ?? lowerThreadSlotsCall(this, expr) ?? @@ -13410,6 +13522,10 @@ export class Lowerer { const callable = this.lowerBuiltinCallableValue(bi, loc); if (callable) return callable; } + // node:crypto's webcrypto is the WebCrypto global's object. + if (bi.module === "crypto" && bi.member === "webcrypto") { + return { kind: "libCall", fn: "webcrypto.object", args: [], type: DYN, loc }; + } // node:stream/web's exports are the web stream globals themselves: in // JavaScript sources the same identity token as the global spelling // (`require("stream/web").ReadableStream === globalThis.ReadableStream`). @@ -13489,7 +13605,7 @@ export class Lowerer { const key = `${fn.valueFn ?? fn.fn}:${typeKey(funcType)}:${adapterShape}`; let fnName = this.builtinCallableValueFns.get(key); if (!fnName) { - fnName = `%builtin.value.${fn.valueFn ?? fn.fn}.${this.builtinCallableValueFns.size}`; + fnName = keyedHelperName(this, `%builtin.value.${fn.valueFn ?? fn.fn}`, key); this.builtinCallableValueFns.set(key, fnName); this.builtinCallableValueNames.set(fnName, display.slice(display.lastIndexOf(".") + 1)); const params = funcType.params.map((type, index) => ({ diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts index 0f932ba93..62f2be9f1 100644 --- a/packages/compiler/src/frontend/lowering/native-ffi.ts +++ b/packages/compiler/src/frontend/lowering/native-ffi.ts @@ -562,7 +562,7 @@ export function lowerFfiMemoryModule(lowerer: Lowerer, loc: SrcLoc): IrExpr { const entries = lowerer.ffiImports .filter((entry) => entry.library !== undefined) .map((entry) => { - const name = `%ffi.module.${lowerer.lambdaCounter++}`; + const name = lowerer.closureName(loc, "", "%ffi.module"); const sourceTypes = ffiSourceParamTypes(entry.params); const locals: IrLocal[] = sourceTypes.map((_, i) => ({ id: `%arg${i}`, diff --git a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts index 855da3bda..388ac3904 100644 --- a/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts +++ b/packages/compiler/src/frontend/lowering/object-enumeration-receiver.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { BOOL, STRING, @@ -27,7 +28,7 @@ export function objectEnumerationReceiver( const key = `obj.receiver:${source.unionId}:${expected.shapeId}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = `%obj.receiver.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%obj.receiver", key); lowerer.arrHofHelpers.set(key, name); const value = varRef("value.0", source, loc); lowerer.liftedFns.push({ diff --git a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts index 3b33fb0cd..acf7b3bf9 100644 --- a/packages/compiler/src/frontend/lowering/pairs-snapshot.ts +++ b/packages/compiler/src/frontend/lowering/pairs-snapshot.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { arrayOf, BOOL, @@ -49,7 +50,7 @@ export function pairsSnapshotHelper( const key = `${options.keyPrefix}.snapshot:${shapeId}`; const existing = lowerer.valueHelpers.get(key); if (existing) return existing; - const name = `%${options.keyPrefix}.snapshot.${lowerer.valueHelpers.size}`; + const name = keyedHelperName(lowerer, `%${options.keyPrefix}.snapshot`, key); lowerer.valueHelpers.set(key, name); const recordType: IrType = { kind: "record", shapeId }; const pairsType = arrayOf(STRING); diff --git a/packages/compiler/src/frontend/lowering/private-brands.ts b/packages/compiler/src/frontend/lowering/private-brands.ts index 8507cc81a..c7881bce1 100644 --- a/packages/compiler/src/frontend/lowering/private-brands.ts +++ b/packages/compiler/src/frontend/lowering/private-brands.ts @@ -160,6 +160,60 @@ function brandCall( return { kind: "call", callee: helper, args: [receiver, evaluation], type: resultType, loc }; } +/** The class whose body declares the private name `priv` (the nearest + * enclosing one, as JavaScript scoping resolves it), when it is a + * declaration-level class, or null. */ +export function privateNameClass(lowerer: Lowerer, priv: ts.PrivateIdentifier): ClassInfo | null { + for (let node: ts.Node | undefined = priv.parent; node; node = node.parent) { + if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) continue; + const declares = node.members.nodes.some((member) => { + const name = (member as { name?: ts.PropertyName }).name; + return name !== undefined && ts.isPrivateIdentifier(name) && name.text === priv.text; + }); + if (!declares) continue; + const symbol = node.name ? lowerer.checker.getSymbolAtLocation(node.name) : undefined; + const info = + lowerer.currentClass?.decl === node + ? lowerer.currentClass + : symbol + ? lowerer.classBySymbol.get(symbol) + : undefined; + if (!info || info.localClass || info.generic || info.genericInstance) return null; + return info; + } + return null; +} + +/** `value.#x` on a checked-dynamic `value` in a declaration-level class + * `info`: the receiver as an instance of `info` when the value is one (an + * instance of the class or a subclass), else Node's TypeError for a missing + * private member (also for undefined and null). */ +export function checkedPrivateValue( + lowerer: Lowerer, + info: ClassInfo, + access: ts.PropertyAccessExpression, + receiver: IrExpr, + loc: SrcLoc, +): IrExpr { + const use: BrandUse = isPrivateWriteTarget(access) ? "write" : "read"; + const className = info.def.name; + const resultType: IrType = { kind: "object", className }; + const helper = `${PREFIX}${use}:${className}:${access.name.text}`; + if (!lowerer.valueHelpers.has(helper)) { + lowerer.liftedFns.push({ + name: helper, + params: [{ localId: "value.0", name: "value", type: DYN }], + returnType: resultType, + locals: [{ id: "value.0", name: "value", type: DYN, mutable: false }], + // finalizePrivateBrands fills the body. + body: [], + loc: helperLoc(loc), + }); + lowerer.valueHelpers.set(helper, helper); + } + return { kind: "call", callee: helper, args: [receiver], type: resultType, loc }; +} + /** A plain assignment, a destructuring target, or a for-in/of target writes * the field. Compound assignments and updates read it first, and Node * reports the failed read. */ @@ -203,7 +257,9 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]) const name = spec.slice(spec.lastIndexOf(":") + 1); const loc = fn.loc; const value = varRef("value.0", DYN, loc); - const evaluation = varRef("class.0", { kind: "classval", className }, loc); + // A declaration-level class has one brand: the helper takes no class. + const evaluation = + fn.params.length > 1 ? varRef("class.0", { kind: "classval", className }, loc) : null; const ownerType: IrType = { kind: "object", className }; const body: IrStmt[] = []; let matches = 0; @@ -218,21 +274,23 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]) const local = `receiver.${matches++}`; fn.locals.push({ id: local, name: "receiver", type: ownerType, mutable: false }); const receiver = varRef(local, ownerType, loc); - const sameEvaluation: IrExpr = { - kind: "bin", - op: "===", - left: { - kind: "fieldGet", - obj: receiver, - className, - field: `%classEnvironment:${className}`, - type: evaluation.type, - loc, - }, - right: evaluation, - type: BOOL, - loc, - }; + const sameEvaluation: IrExpr = evaluation + ? { + kind: "bin", + op: "===", + left: { + kind: "fieldGet", + obj: receiver, + className, + field: `%classEnvironment:${className}`, + type: evaluation.type, + loc, + }, + right: evaluation, + type: BOOL, + loc, + } + : { kind: "boolLit", value: true, type: BOOL, loc }; const match = (typed: IrExpr): IrStmt[] => [ { kind: "varDecl", localId: local, init: typed, loc }, { diff --git a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts index 09d228b05..84aa8dac0 100644 --- a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts +++ b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { varRef } from "../../ir/build.js"; import { type IrExpr, @@ -40,7 +41,7 @@ export function adaptPromiseExecutor( const key = `promise.executor:${typeKey(execT)}:${typeKey(inner)}`; let name = lowerer.arrHofHelpers.get(key); if (name === undefined) { - name = `%promise.executor.${lowerer.arrHofHelpers.size}`; + name = keyedHelperName(lowerer, "%promise.executor", key); lowerer.arrHofHelpers.set(key, name); const impl = `${name}.impl`; const captured: IrLocal = { id: "f.0", name: "f", type: execT, mutable: false, boxed: true }; diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts index d979423b0..c8fc9c730 100644 --- a/packages/compiler/src/frontend/lowering/require-initialization.ts +++ b/packages/compiler/src/frontend/lowering/require-initialization.ts @@ -9,6 +9,7 @@ * and every reference that can run earlier checks it first — the same * module.tdzCheck the import-cycle bindings use. A reference whose lowering * leaves no expression to check keeps the SC1013 fence. */ +import { positionTag } from "./structural-positions.js"; import * as ts from "../ts7/adapter.js"; import type { Lowerer } from "./lowerer.js"; import { @@ -119,7 +120,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void { const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, ""); const bySymbol = new Map(); for (const { decl } of declared) { - const flagId = `%g.${tag}%required.${ts.tokenStartOfNode(decl)}`; + const flagId = `%g.${tag}%required.${positionTag(lowerer, decl)}`; lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true }); const flag: RequireInitFlag = { flagId, names: new Map() }; for (const id of boundNames(decl.name)) { diff --git a/packages/compiler/src/frontend/lowering/stable-names.ts b/packages/compiler/src/frontend/lowering/stable-names.ts new file mode 100644 index 000000000..8f7d0967e --- /dev/null +++ b/packages/compiler/src/frontend/lowering/stable-names.ts @@ -0,0 +1,48 @@ +import { InternalCompilerError } from "../../errors.js"; + +/** The owner of the names keyedHelperName hands out. */ +export interface KeyedNameOwner { + /** Each keyed name with a second hash of the key it was made from, which + * tells a repeated key from a different key with the same name. */ + readonly keyedNameKeys: Map; +} + +/** 48 bits of two FNV-1a hashes of `key`, as 12 hex digits. */ +function keyHash(key: string): string { + let a = 0x811c9dc5; + let b = 0x01000193 ^ 0x5bd1e995; + for (let i = 0; i < key.length; i++) { + const c = key.charCodeAt(i); + a = Math.imul(a ^ c, 0x01000193); + b = Math.imul(b ^ c, 0x01000193) ^ (b >>> 13); + } + const high = (b >>> 0).toString(16).padStart(8, "0").slice(4); + return high + (a >>> 0).toString(16).padStart(8, "0"); +} + +/** A 32-bit hash of `key` independent of keyHash's bits. */ +function checkHash(key: string): number { + let h = 0x9e3779b9; + for (let i = 0; i < key.length; i++) + h = Math.imul(h ^ key.charCodeAt(i), 0x85ebca6b) ^ (h >>> 15); + return h >>> 0; +} + +/** The name of a helper interned by `key`: ``. It depends only on the key, not on how many helpers were interned + * before, so a helper has the same name whichever unit first needs it. Two + * keys that hash to the same name throw an InternalCompilerError. */ +export function keyedHelperName( + owner: KeyedNameOwner, + prefix: string, + key: string, + separator = ".", +): string { + const name = `${prefix}${separator}${keyHash(key)}`; + const check = checkHash(key); + const previous = owner.keyedNameKeys.get(name); + if (previous === undefined) owner.keyedNameKeys.set(name, check); + else if (previous !== check) + throw new InternalCompilerError(`helper name ${name} is shared by two helper keys`); + return name; +} diff --git a/packages/compiler/src/frontend/lowering/strict-equality.ts b/packages/compiler/src/frontend/lowering/strict-equality.ts index 7b9b6902b..8a87c8111 100644 --- a/packages/compiler/src/frontend/lowering/strict-equality.ts +++ b/packages/compiler/src/frontend/lowering/strict-equality.ts @@ -1,3 +1,4 @@ +import { keyedHelperName } from "./stable-names.js"; import { boolLit, varRef } from "../../ir/build.js"; import { BOOL, @@ -135,7 +136,7 @@ export function lowerUnionEquality( loc, ); if (!result) return null; - name = `%value.eq.${lowerer.equalityHelpers.size}`; + name = keyedHelperName(lowerer, "%value.eq", key); lowerer.equalityHelpers.set(key, name); lowerer.liftedFns.push({ name, diff --git a/packages/compiler/src/frontend/lowering/structural-positions.ts b/packages/compiler/src/frontend/lowering/structural-positions.ts new file mode 100644 index 000000000..43b6d4a79 --- /dev/null +++ b/packages/compiler/src/frontend/lowering/structural-positions.ts @@ -0,0 +1,111 @@ +import * as ts from "../ts7/adapter.js"; +import { keyedHelperName, type KeyedNameOwner } from "./stable-names.js"; + +/** The declaration kinds whose names anchor the positions inside them. */ +const ANCHOR_KINDS: ReadonlySet = new Set([ + ts.SyntaxKind.FunctionDeclaration, + ts.SyntaxKind.ClassDeclaration, + ts.SyntaxKind.ClassExpression, + ts.SyntaxKind.VariableDeclaration, + ts.SyntaxKind.MethodDeclaration, + ts.SyntaxKind.GetAccessor, + ts.SyntaxKind.SetAccessor, + ts.SyntaxKind.PropertyDeclaration, + ts.SyntaxKind.PropertyAssignment, + ts.SyntaxKind.ModuleDeclaration, + ts.SyntaxKind.EnumDeclaration, + ts.SyntaxKind.Constructor, + ts.SyntaxKind.InterfaceDeclaration, + ts.SyntaxKind.TypeAliasDeclaration, +]); + +/** The name of `node` when it is a named declaration that anchors the + * positions of the nodes inside it, or null. */ +function anchorName(node: ts.Node): string | null { + return ts.declarationNameAt(node, node.index, ANCHOR_KINDS); +} + +/** The nearest enclosing anchor of `node` (not `node` itself), or its + * source file. */ +function anchorOf(node: ts.Node): ts.Node { + for (let parent = node.parent; parent !== undefined; parent = parent.parent) { + if (ts.isSourceFile(parent) || anchorName(parent) !== null) return parent; + } + return ts.getSourceFileOfNode(node); +} + +/** Source positions that stay the same when code outside the enclosing + * declaration changes. A node's structural position is its file, the chain + * of enclosing named declarations (each with its kind, its name and its + * ordinal among the declarations of the same kind and name in its own + * anchor), and its ordinal among the nodes of its kind whose nearest anchor + * is the same declaration. Editing another declaration, or code nested in another + * named declaration of the same anchor, leaves the position unchanged. */ +export class StructuralPositions { + /** Per anchor and key: the indices of the nodes with that key whose + * nearest anchor it is, in preorder (anchoredNodes). */ + private readonly nodesByKey = new Map>(); + private readonly paths = new Map(); + + /** `fileKey` names a source file in positions (its file name when + * absent). */ + constructor(private readonly fileKey?: (sf: ts.SourceFile) => string) {} + + /** The ordinal of `node` among the nodes of `anchor` with its key. */ + private ordinalIn(anchor: ts.Node, node: ts.Node, key: string): number { + let byKey = this.nodesByKey.get(anchor); + if (byKey === undefined) this.nodesByKey.set(anchor, (byKey = new Map())); + let indices = byKey.get(key); + if (indices === undefined) { + indices = ts.anchoredNodes(anchor, key, ANCHOR_KINDS); + byKey.set(key, indices); + } + // Indices increase in preorder. + let low = 0; + let high = indices.length - 1; + while (low <= high) { + const middle = (low + high) >> 1; + const at = indices[middle]!; + if (at === node.index) return middle; + if (at < node.index) low = middle + 1; + else high = middle - 1; + } + return -1; + } + + /** The anchor chain of `anchor` from the file down. */ + private pathOf(anchor: ts.Node): string { + const cached = this.paths.get(anchor); + if (cached !== undefined) return cached; + let path: string; + if (ts.isSourceFile(anchor)) path = this.fileKey?.(anchor) ?? anchor.fileName; + else { + const parent = anchorOf(anchor); + // The kind keeps declarations of one name apart: a getter and its + // setter, or a class and the namespace merged with it. + const key = `${anchor.kind}:${anchorName(anchor)}`; + path = `${this.pathOf(parent)}/${key}#${this.ordinalIn(parent, anchor, key)}`; + } + this.paths.set(anchor, path); + return path; + } + + /** The structural position of `node`. */ + of(node: ts.Node): string { + const anchor = anchorOf(node); + const name = anchorName(node); + const key = name === null ? `${node.kind}` : `${node.kind}:${name}`; + return `${this.pathOf(anchor)}|${key}#${this.ordinalIn(anchor, node, key)}`; + } +} + +/** The owner of structural positions and the names made from them. */ +export interface PositionTagOwner extends KeyedNameOwner { + readonly positions: StructuralPositions; +} + +/** A short tag for `node`'s structural position, for names that must stay + * unique per source node (class expressions, namespace blocks). */ +export function positionTag(owner: PositionTagOwner, node: ts.Node): string { + return keyedHelperName(owner, "", owner.positions.of(node), ""); +} diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index c77938337..bb9a6c062 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -2210,15 +2210,26 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ id: "stdlib.webcrypto.subtle", kind: "stdlib", name: "globalThis.crypto.subtle", - fns: ["webcrypto.importKey", "webcrypto.sign", "webcrypto.verify", "webcrypto.digest"], - note: "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", + fns: [ + "webcrypto.importKey", + "webcrypto.generateKey", + "webcrypto.exportKey", + "webcrypto.encrypt", + "webcrypto.decrypt", + "webcrypto.deriveBits", + "webcrypto.deriveKey", + "webcrypto.sign", + "webcrypto.verify", + "webcrypto.digest", + ], + note: "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused", }, { id: "stdlib.webcrypto.CryptoKey", kind: "stdlib", name: "CryptoKey", fns: ["webcrypto.isCryptoKey"], - note: "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey", + note: "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey", }, { id: "stdlib.atomics", @@ -2262,6 +2273,8 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [ "date.newLocal", "date.nativeNewFields", "date.nativeNewValue", + "date.argumentsTime", + "date.classOperation", ], note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice", }, @@ -3088,9 +3101,6 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record< setFips: "a compiled binary has no FIPS provider to enable, and Node itself throws on setFips(true) " + "in a non-FIPS build — getFips() answers 0 here", - webcrypto: - "the WebCrypto object has no lowering — the lowered crypto surface is randomUUID, " + - "randomBytes, and the createHash chain", }, zlib: { brotliCompressSync: BROTLI_HINT, diff --git a/packages/compiler/src/frontend/ts7/ast.ts b/packages/compiler/src/frontend/ts7/ast.ts index 369213d89..0bddd1a80 100644 --- a/packages/compiler/src/frontend/ts7/ast.ts +++ b/packages/compiler/src/frontend/ts7/ast.ts @@ -47,8 +47,9 @@ import { } from "./ast-guards.generated.js"; import type { Ts7DiagnosticData as Diagnostic } from "./session-schema.generated.js"; import { ModifierFlags, SyntaxKind } from "./enums.js"; -import { AstKind } from "./ast-schema.generated.js"; +import { AstKind, KIND_NODE_LIST } from "./ast-schema.generated.js"; import { PREORDER_LIST } from "./ast-wire.js"; +import { tokenStartOfNode } from "./node-functions.js"; /* ---- everything 7 kept under the same name ---- */ @@ -196,6 +197,93 @@ function walkLinkedPreorder( } } +/** The semantic children of a wire node: list items stand in for their + * list, and attached JSDoc trees are left out (the walkPreorder view). */ +function semanticChildIndices(root: Node, index: number, out: number[]): void { + const wire = root.file.wire; + for (let child = wire.firstChild(index); child !== 0; child = wire.next(child)) { + const kind = wire.kind(child); + if (kind === KIND_NODE_LIST) { + for (let item = wire.firstChild(child); item !== 0; item = wire.next(item)) out.push(item); + } else if (kind !== AstKind.JSDoc) out.push(child); + } +} + +/** The outermost node of `sourceFile` whose token span is exactly + * [start, end), or undefined. Only nodes ending at `end` are + * materialized. */ +export function nodeAtSpan(sourceFile: SourceFile, start: number, end: number): Node | undefined { + const wire = sourceFile.file.wire; + const children: number[] = []; + let index = sourceFile.index; + for (;;) { + children.length = 0; + semanticChildIndices(sourceFile, index, children); + let next = 0; + for (const child of children) { + if (wire.pos(child) > start || wire.end(child) < end) continue; + if (wire.end(child) === end) { + const node = sourceFile.file.node(child); + if (tokenStartOfNode(node) === start) return node; + } + next = child; + break; + } + if (next === 0) return undefined; + index = next; + } +} + +/** The name of the declaration at wire `index` in `root`'s file when its + * kind is in `kinds`: its identifier, private identifier or string literal + * name, "constructor" for a constructor, or null. Reads the wire without + * materializing nodes. */ +export function declarationNameAt( + root: Node, + index: number, + kinds: ReadonlySet, +): string | null { + const wire = root.file.wire; + const kind = wire.kind(index); + if (!kinds.has(kind)) return null; + if (kind === SyntaxKind.Constructor) return "constructor"; + const name = wire.namedChild(index, "name"); + if (name === 0) return null; + const nameKind = wire.kind(name); + if ( + nameKind !== SyntaxKind.Identifier && + nameKind !== SyntaxKind.PrivateIdentifier && + nameKind !== SyntaxKind.StringLiteral + ) + return null; + return wire.text(name) ?? null; +} + +/** The indices, in preorder, of the nodes below `root` that have `key` and + * are not inside a nested named declaration (declarationNameAt with + * `kinds`). A named declaration is keyed `:` and its subtree is + * not visited; other nodes are keyed by their kind. No node is + * materialized. */ +export function anchoredNodes(root: Node, key: string, kinds: ReadonlySet): number[] { + const wire = root.file.wire; + const out: number[] = []; + const children: number[] = []; + const stack: number[] = []; + semanticChildIndices(root, root.index, children); + for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); + while (stack.length > 0) { + const index = stack.pop()!; + const kind = wire.kind(index); + const name = declarationNameAt(root, index, kinds); + if ((name === null ? String(kind) : `${kind}:${name}`) === key) out.push(index); + if (name !== null) continue; + children.length = 0; + semanticChildIndices(root, index, children); + for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]!); + } + return out; +} + export function getLineAndCharacterOfPosition( sourceFile: SourceFile, position: number, diff --git a/packages/compiler/src/frontend/type-mapper.test.ts b/packages/compiler/src/frontend/type-mapper.test.ts index cf78554b4..163ba7b9e 100644 --- a/packages/compiler/src/frontend/type-mapper.test.ts +++ b/packages/compiler/src/frontend/type-mapper.test.ts @@ -41,7 +41,7 @@ describe("union arm lookup", () => { test("sees finalized recursive arms after a missing lookup", () => { const unions = new UnionRegistry(); const recursive = {} as Type; - const union = unions.recursiveRef(recursive); + const union = unions.recursiveRef(recursive, () => "Recursive"); expect(unions.armTag(union, STRING)).toBe(-1); unions.finalizeRecursive(recursive, [ F64, diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index a233f0186..9abd5a0b0 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -1,4 +1,5 @@ import { InternalCompilerError } from "../errors.js"; +import { keyedHelperName, type KeyedNameOwner } from "./lowering/stable-names.js"; import { mockCallRecordType, mockContextType } from "./mock-shapes.js"; import * as ts from "./ts7/adapter.js"; import { bodyReadsArguments } from "./arguments-usage.js"; @@ -146,6 +147,16 @@ export const WEBCRYPTO_DYN_TYPES: ReadonlySet = new Set([ "KeyAlgorithm", "HmacImportParams", "HmacKeyAlgorithm", + "HmacKeyGenParams", + "AesKeyGenParams", + "AesKeyAlgorithm", + "JsonWebKey", + "AesGcmParams", + "AesCbcParams", + "AesCtrParams", + "Pbkdf2Params", + "HkdfParams", + "AesDerivedKeyParams", ]); export const FETCH_AMBIENT_TYPES = [ @@ -252,6 +263,8 @@ function comparePriority(left: DeclaredOrderPriority, right: DeclaredOrderPriori export class ShapeRegistry { /** Existing layouts change only when a recursive placeholder closes. */ revision = 0; + /** Each shape id with a second hash of its key (keyedHelperName). */ + readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); /** Historical emit-order rank of the declaration-order metadata each @@ -270,7 +283,7 @@ export class ShapeRegistry { * writer after all priority candidates have closed. */ private readonly derivedDeclaredOrderFinalizers: (() => void)[] = []; private currentDeclaredOrderPriority: DeclaredOrderPriority = [0, 0]; - /** All interned shapes in first-seen (`r0`, `r1`, ...) order. */ + /** All interned shapes in first-seen order. */ readonly shapes: IrRecordShape[] = []; /** ts.Types currently being mapped — a BACK-REFERENCE to one of these is * the recursive knot (`interface TreeNode { children: TreeNode[] }`): @@ -390,11 +403,12 @@ export class ShapeRegistry { /** The shape id a back-reference to an in-progress type resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty fields) the outer frame finalizes. */ - recursiveRef(t: ts.Type): string { + * entry (empty fields) the outer frame finalizes. `describe` names a new + * placeholder (checkerTypeDescription). */ + recursiveRef(t: ts.Type, describe: () => string): string { let id = this.recIds.get(t); if (id === undefined) { - id = `r${this.shapes.length}`; + id = recursivePlaceholderName(this, "r", describe(), (name) => this.byId.has(name)); const shape: IrRecordShape = { id, fields: [] }; this.byId.set(id, shape); this.shapes.push(shape); @@ -463,7 +477,7 @@ export class ShapeRegistry { const key = this.keyOf(fields, tuple, indexValue); let id = this.byKey.get(key); if (id === undefined) { - id = `r${this.shapes.length}`; + id = keyedHelperName(this, "r", key, ""); const shape: IrRecordShape = { id, fields, @@ -541,6 +555,58 @@ export class ShapeRegistry { } } +/** The id of a recursive placeholder described by `description`. Two + * different types can still share a description (checkerTypeDescription stops + * after a few levels of type arguments); the later one takes the first free + * name with a numbered suffix. */ +function recursivePlaceholderName( + owner: KeyedNameOwner, + prefix: string, + description: string, + taken: (name: string) => boolean, +): string { + let name = keyedHelperName(owner, prefix, `recursive:${description}`, ""); + for (let n = 1; taken(name); n++) + name = keyedHelperName(owner, prefix, `recursive:${description}#${n}`, ""); + return name; +} + +/** How many levels of type arguments checkerTypeDescription describes. */ +const RECURSIVE_NAME_DEPTH = 3; + +/** A checker type's description for names that must not depend on checker + * type ids (a recursive placeholder, a generic class instance keyed by a + * literal): its printed form, the structural position of its first + * declaration, and the same for its type arguments (two instantiations + * print the same when their type arguments are type parameters or classes + * of the same name from different declarations). */ +export function checkerTypeDescription( + t: ts.Type, + ctx: Pick, + depth = 0, +): string { + const symbol = t.getAliasSymbol() ?? t.getSymbol(); + const declaration = symbol?.declarations[0]; + const where = declaration + ? (ctx.positionOf?.(declaration) ?? + `${ts.getSourceFileOfNode(declaration).fileName}:${declaration.pos}`) + : ""; + let description = `${ctx.checker.typeToString(t)}@${where}`; + if (depth < RECURSIVE_NAME_DEPTH) { + const aliasArguments = + t.getAliasSymbol() !== undefined ? (t.getAliasTypeArguments() ?? []) : []; + const typeArguments = + aliasArguments.length > 0 + ? aliasArguments + : t.isTypeReference() + ? ctx.checker.getTypeArguments(t) + : []; + if (typeArguments.length > 0) + description += `<${typeArguments.map((arg) => checkerTypeDescription(arg, ctx, depth + 1)).join(",")}>`; + } + return description; +} + /** The frontend's union interner — mirrors ShapeRegistry. A union's * canonical identity includes its typeKey-sorted arms and literal * discriminator contract. An arm's index in that list IS its runtime tag. @@ -548,10 +614,12 @@ export class ShapeRegistry { export class UnionRegistry { /** Existing arm/discriminator contracts change when a placeholder closes. */ revision = 0; + /** Each union id with a second hash of its key (keyedHelperName). */ + readonly keyedNameKeys = new Map(); private readonly byKey = new Map(); private readonly byId = new Map(); private readonly armTags = new Map>(); - /** All interned unions in first-seen (`u0`, `u1`, ...) order. */ + /** All interned unions in first-seen order. */ readonly unions: IrUnionDef[] = []; /** ts.Types currently being mapped — a back-reference to one is the * recursive knot passing through a union (`type Tree = Leaf | Branch` @@ -567,11 +635,12 @@ export class UnionRegistry { /** The union id a back-reference to an in-progress union resolves to: * reuses the type's persistent recursive id or mints a PLACEHOLDER - * entry (empty arms) the outer frame finalizes. */ - recursiveRef(t: ts.Type): string { + * entry (empty arms) the outer frame finalizes. `describe` names a new + * placeholder (checkerTypeDescription). */ + recursiveRef(t: ts.Type, describe: () => string): string { let id = this.recIds.get(t); if (id === undefined) { - id = `u${this.unions.length}`; + id = recursivePlaceholderName(this, "u", describe(), (name) => this.byId.has(name)); const def: IrUnionDef = { id, arms: [] }; this.byId.set(id, def); this.unions.push(def); @@ -621,7 +690,7 @@ export class UnionRegistry { const key = JSON.stringify([arms.map(typeKey), discriminant]); let id = this.byKey.get(key); if (id === undefined) { - id = `u${this.unions.length}`; + id = keyedHelperName(this, "u", key, ""); const def: IrUnionDef = { id, arms, ...(discriminant ? { discriminant } : {}) }; this.byKey.set(key, id); this.byId.set(id, def); @@ -1114,7 +1183,17 @@ export interface TypeMapperCtx { * only the literal's objects, so it maps to the empty record (see the * empty object type rule in the object mapping). */ emptyLiteralArm?: boolean; + /** The class value a generic factory's result type names when the factory + * returns one of its own local classes through a constructor type + * (`function struct(v: S): Struct { class Base {}; return Base as + * Struct; }`), by the checker type: the declared result inside the + * instance being lowered, the instantiated result at its calls. */ + localClassValue?: (type: ts.Type) => IrType | null; checker: ts.TypeChecker; + /** The structural position of a declaration (structural-positions.ts), + * which names recursive placeholders independently of where unrelated + * code sits. The Lowerer supplies it. */ + positionOf?: (node: ts.Node) => string; /** JavaScript declarations whose instance or object-literal types only * the checked-dynamic representation holds (js-dynamic-objects.ts). * Absent when the program has none: every mapping is unchanged. */ @@ -1653,6 +1732,11 @@ export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null { return STRING; if (flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) return BOOL; } + const localValue = ctx.localClassValue?.(type); + if (localValue) { + contextResolutions++; + return localValue; + } const topLevel = mapTypeDepth === 0; const typeId = (type as { id?: number }).id; const memoizable = typeId !== undefined && (ctx.canMemoizeType?.() ?? true); @@ -4651,7 +4735,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { const knownRecursive = unions.recursiveUnionFor(widened); if (knownRecursive !== undefined) return { kind: "union", unionId: knownRecursive }; if (unions.inProgress.has(widened)) { - return { kind: "union", unionId: unions.recursiveRef(widened) }; + return { + kind: "union", + unionId: unions.recursiveRef(widened, () => checkerTypeDescription(widened, ctx)), + }; } unions.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; @@ -6995,7 +7082,10 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null { // frame completes (finalizeRecursive below). Backends represent records // as heap references, so the self-reference is an ordinary pointer. if (shapes.inProgress.has(widened)) { - return { kind: "record", shapeId: shapes.recursiveRef(widened) }; + return { + kind: "record", + shapeId: shapes.recursiveRef(widened, () => checkerTypeDescription(widened, ctx)), + }; } shapes.inProgress.add(widened); const sensitivityAtEntry = contextResolutions; diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index ca8c52118..dd82d06c2 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -80,6 +80,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "procStream.write", "child.stdioStream", "stream.unboxDyn", + "stream.handleStream", "stream.pipeDyn", "stream.unpipeDyn", "weakMap.new", @@ -97,6 +98,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "atomics.op", "atomics.wait", "atomics.notify", + "atomics.opDyn", + "atomics.waitDyn", + "atomics.notifyDyn", "arrayBuffer.byteLengthGetter", "arrayBuffer.viewU8C", "arrayBuffer.viewI8", @@ -534,6 +538,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "date.nativeNew", "date.nativeNewFields", "date.nativeNewValue", + "date.argumentsTime", "date.checkedValue", "util.parseArgs", "util.parseEnv", @@ -601,6 +606,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.objCreateWithProperties", "dyn.getPrototype", "dyn.setPrototype", + // Error.captureStackTrace: Node's TypeError for a primitive target, and + // the error string conversion of name and message + "dyn.errorCaptureStackTrace", // a proxy in the prototype chain can throw "dyn.builtinInstanceOf", "dyn.functionInstanceOf", @@ -786,6 +794,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "buffer.poolFromArray", "buffer.poolFromDyn", "buffer.poolFromDynRef", + "buffer.poolFromDynOffset", // The checked-dynamic compare/equals validators: Node's argument // ladders throw ERR_INVALID_ARG_TYPE / ERR_OUT_OF_RANGE catchably. "buffer.compareChk", diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index fa7876579..8f5928d12 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -272,6 +272,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.nativeMapIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeUrlIs": { argTypes: [DYN], result: BOOL }, "dyn.nativeDateIs": { argTypes: [DYN], result: BOOL }, + "dyn.nativeDateHandleIs": { argTypes: [DYN], result: BOOL }, /** A builtin constructor (`Object`, `Date`, `RegExp`, `Map`, `Set`, * `Promise`, `WeakMap`, `WeakSet`) read as a JavaScript value: one * interned function per name, the same one dynamic values answer for @@ -291,6 +292,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** `new Date(value)` in JavaScript: a string argument parses with V8's * full date parser; every other value constructs as date.nativeNew. */ "date.nativeNewValue": { argTypes: [DYN], result: DYN }, + "date.argumentsTime": { argTypes: [DYN], result: DATE_T }, + "date.classOperation": { argTypes: [DYN, DYN], result: VOID }, "date.checkedValue": { argTypes: [DYN], result: DATE_T }, "dyn.nativeRegexIs": { argTypes: [DYN], result: BOOL }, /** toString() on a checked-dynamic receiver: runtime kind dispatch @@ -1252,6 +1255,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "atomics.wait": { argTypes: [bytesOf("i32"), F64, F64, F64], result: STRING }, "atomics.notify": { argTypes: [bytesOf("i32"), F64, F64], result: F64 }, "atomics.op": { argTypes: [null, F64, F64, F64, F64], result: F64 }, + "atomics.opDyn": { argTypes: [DYN, F64, F64, F64, F64], result: F64 }, + "atomics.waitDyn": { argTypes: [DYN, F64, F64, F64], result: STRING }, + "atomics.notifyDyn": { argTypes: [DYN, F64, F64], result: F64 }, "worker.new": { argTypes: [F64, STRING, DYN], result: DYN }, "worker.isMainThread": { argTypes: [], result: BOOL }, "worker.threadId": { argTypes: [], result: F64 }, @@ -2712,6 +2718,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ argTypes: [DYN, STRING], result: { kind: "object", className: "%Duplex" }, }, + /** The Transform a Cipheriv or Decipheriv reads and writes as a stream + * (+1, created on first use and owned by the cipher). */ + "stream.handleStream": { + argTypes: [DYN], + result: { kind: "object", className: "%Transform" }, + }, /** Checked-dynamic stream slots (dyn-streams.ts): the native stream a * checked-dynamic value boxes (+1) — a runtime stream with the readable * (1) or writable (2) side — or null for any other value. Never throws; @@ -2846,10 +2858,17 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * one dyn array and answers a promise that argument and validation * failures reject. CryptoKey values are native handles. */ "webcrypto.importKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.generateKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.exportKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.encrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.decrypt": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.deriveBits": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, + "webcrypto.deriveKey": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.sign": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.verify": { argTypes: [DYN], result: { kind: "promise", inner: BOOL } }, "webcrypto.digest": { argTypes: [DYN], result: { kind: "promise", inner: DYN } }, "webcrypto.isCryptoKey": { argTypes: [DYN], result: BOOL }, + "webcrypto.object": { argTypes: [], result: DYN }, "crypto.randomUUIDv7": { argTypes: [DYN], result: STRING }, "crypto.pbkdf2": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: BYTES_U8 }, "crypto.randomBytesCb": { argTypes: [F64, null], result: VOID }, @@ -2918,6 +2937,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "buffer.poolFromDyn": { argTypes: [DYN, STRING], result: BYTES_U8 }, /** poolFromDyn that also copies an array crossing by reference. */ "buffer.poolFromDynRef": { argTypes: [DYN, STRING], result: BYTES_U8 }, + "buffer.poolFromDynOffset": { argTypes: [DYN, DYN], result: BYTES_U8 }, "buffer.poolSize": { argTypes: [], result: F64 }, "buffer.setPoolSize": { argTypes: [F64], result: VOID }, /** Buffer.byteLength(string, enc) — enc a NORMALIZED literal like @@ -3646,6 +3666,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.arrayPrototype": { argTypes: [], result: DYN }, "dyn.arrayConstructor": { argTypes: [], result: DYN }, "dyn.objectPrototype": { argTypes: [], result: DYN }, + "dyn.errorPrototype": { argTypes: [], result: DYN }, + "dyn.inheritsErrorPrototype": { argTypes: [DYN], result: BOOL }, + "dyn.errorCaptureStackTrace": { argTypes: [DYN], result: DYN }, + "dyn.errorCaptureStackTraceFunction": { argTypes: [], result: DYN }, "dyn.functionApply": { argTypes: [], result: DYN }, "dyn.builtinMethod": { argTypes: [STRING, STRING], result: DYN }, "dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN }, @@ -3936,14 +3960,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "stream.finishedOpts": { argTypes: [null, F64, F64, BOOL, null], result: VOID }, "stream.finishedOptsSignal": { argTypes: [null, F64, F64, BOOL, DYN, null], result: VOID }, "sp.pipeline": { argTypes: [F64], result: { kind: "promise", inner: VOID } }, - /** pipeline with a run of function stages: [n, at, k, flags, mask, - * s1..sn, stage1..stagek, (next, read, return)?, box?, (signal, - * abort)?] (+ the callback for the stream.* forms): the user's stage - * functions, the frontend's helpers over the pumped generator or a - * promise destination's box, and the pipeline's AbortSignal with its - * abort closure; flags names which groups are present - * (scr_stream_pipeline_stages). A promise destination's promise type is - * sp.pipelineStages' result. */ + /** pipeline with function stages: [n, k, positions, flags, mask, + * s1..sn, stage1..stagek, helpers..., box?, (signal, abort)?] (+ the + * callback for the stream.* forms): the user's stage functions at the + * positions the bitmask names, the frontend's (next, read, return) + * helpers for each pumped generator (pipelinePumpedRuns) and a promise + * destination's box, and the pipeline's AbortSignal with its abort + * closure; flags bits 1-2 give the destination kind and bit 5 the + * signal group (scr_stream_pipeline_stages). A promise destination's + * promise type is sp.pipelineStages' result. */ "sp.pipelineStages": { argTypes: [F64, F64, F64, F64, F64], result: { kind: "promise", inner: VOID }, @@ -4734,3 +4759,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ /** A builtin name exists exactly when its runtime signature exists. */ export type IrLibFn = keyof typeof LIB_FN_SIGS; + +/** The function-stage runs of a pipeline of `total` stages that a stream + * follows (positions bit j: stage j is a function): each pumps its last + * generator into that stream through one (next, read, return) triple. */ +export function pipelinePumpedRuns(total: number, positions: number): number { + let runs = 0; + for (let j = 0; j + 1 < total; j++) { + if (Math.floor(positions / 2 ** j) % 2 === 1 && Math.floor(positions / 2 ** (j + 1)) % 2 === 0) + runs++; + } + return runs; +} diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 6ff148c55..21223862a 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -349,6 +349,7 @@ const DYN_READ_ONLY = new Set([ "date.nativeNew", "date.nativeNewFields", "date.nativeNewValue", + "date.argumentsTime", // The dynamic operators run the ToPrimitive and coercion helpers of // dyn.compare, dyn.numberConstructor and dyn.toStringCoerce, then build a // fresh number, string or bigint. dyn.increment steps dyn.toNumeric's result. diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts index 4142b6efe..9531d7845 100644 --- a/packages/compiler/src/ir/runtime-features.ts +++ b/packages/compiler/src/ir/runtime-features.ts @@ -75,6 +75,8 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([ "child.propsInstall", "child.propsResolve", "crypto.native", + // The crypto global's object extends crypto.native's. + "webcrypto.object", "dyn.promiseAll", "dyn.promiseAllSettled", "dyn.promiseAny", @@ -237,6 +239,7 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([ "stream.prop", "stream.stdioWritable", "stream.unboxDyn", + "stream.handleStream", "stream.unpipeDyn", "stream.workerStdio", ]); @@ -647,8 +650,9 @@ const RUNTIME_FEATURE_RULES = { }), /** scr_cipher.c over the mbedTLS archive; DEP0182 reports through the * process-warning unit, and unknown output encodings render through the - * inspect unit. */ - cipher: rule({ fns: ["crypto.cipherNew"] }), + * inspect unit. WebCrypto's encrypt and decrypt (scr_webcrypto_aes.c) + * link the same archive. */ + cipher: rule({ fns: ["crypto.cipherNew", "webcrypto.encrypt", "webcrypto.decrypt"] }), copying: rule({ expr: (node, _tables) => (node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) || @@ -675,12 +679,15 @@ const RUNTIME_FEATURE_RULES = { ), // The legacy url functions, crypto.createCipheriv and // ecdh.setPublicKey emit their deprecation warnings through the - // dyn-async unit; scr_wasm.c boxes its handles and promises through it. + // dyn-async unit (WebCrypto's encrypt and decrypt link scr_cipher.c + // too); scr_wasm.c boxes its handles and promises through it. fns: [ ...DYN_ASYNC_LIB_FNS, ...READABLE_DYN_ASYNC_LIB_FNS, ...LEGACY_URL_LIB_FNS, "crypto.cipherNew", + "webcrypto.encrypt", + "webcrypto.decrypt", "crypto.ecdhNew", ], prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."], @@ -787,6 +794,8 @@ const RUNTIME_FEATURE_RULES = { "global.native", "util.styleText", "crypto.cipherNew", + "webcrypto.encrypt", + "webcrypto.decrypt", "crypto.ecdhNew", "crypto.ecdhConvertKey", ...BUILTIN_CLASS_LIB_FNS, diff --git a/packages/compiler/src/ir/tiers.ts b/packages/compiler/src/ir/tiers.ts index ce2fdf0f4..13c464149 100644 --- a/packages/compiler/src/ir/tiers.ts +++ b/packages/compiler/src/ir/tiers.ts @@ -72,6 +72,7 @@ const DYN_KIND_TESTS: ReadonlySet = new Set([ "dyn.nativeMapIs", "dyn.nativeUrlIs", "dyn.nativeDateIs", + "dyn.nativeDateHandleIs", "dyn.nativeRegexIs", "dyn.dataViewIs", "dyn.typeof", diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 88ccbbc33..9de1b5b62 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -60,7 +60,7 @@ import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from import { unionWideningTags } from "./analysis.js"; import { alwaysReturns } from "./control-flow.js"; import { dynamicOperationOf, functionTier } from "./tiers.js"; -import { LIB_FN_SIGS } from "./builtin-signatures.js"; +import { LIB_FN_SIGS, pipelinePumpedRuns } from "./builtin-signatures.js"; /** The promisified crypto calls' value kinds besides checked-dynamic. */ const CRYPTO_PROMISE_VALUES: Readonly> = { @@ -5614,23 +5614,34 @@ function validateFunction( e.fn === "stream.pipelineStagesDyn" || e.fn === "sp.pipelineStages" ) { - // (n, at, k, flags, mask, s1..sn, stage1..stagek, (next, read, - // return)?, box?, (signal, abort)?[, cb]): flags bit 0 a source - // function, bits 1-2 the destination, bits 3/4/5 the groups. - const [n, at, k, flags] = e.args + // (n, k, positions, flags, mask, s1..sn, stage1..stagek, + // helpers..., box?, (signal, abort)?[, cb]): positions bit j marks + // stage j a function, flags bits 1-2 the destination, bit 5 the + // signal group; helpers are three per function run a stream + // follows. + const [n, k, positions, flags] = e.args .slice(0, 4) .map((a) => (a?.kind === "numLit" ? a.value : -1)); - const source = (flags! & 1) !== 0; + const total = n! + k!; const destKind = (flags! >> 1) & 3; - const groups = (flags! & 8 ? 3 : 0) + (flags! & 16 ? 1 : 0) + (flags! & 32 ? 2 : 0); + let functions = 0; + for (let j = 0; j < total && j < 53; j++) { + if (Math.floor(positions! / 2 ** j) % 2 === 1) functions++; + } + const lastIsFunction = Math.floor(positions! / 2 ** (total - 1)) % 2 === 1; + const helperCount = 3 * pipelinePumpedRuns(total, positions!); + const groups = helperCount + (destKind === 2 ? 1 : 0) + (flags! & 32 ? 2 : 0); const cbArgs = e.fn === "sp.pipelineStages" ? 0 : 1; if ( flags! < 0 || n! < 0 || k! < 1 || - (source ? at !== 0 : at! < 1) || - (destKind === 0 ? at! + 0 >= n! : at! !== n!) || - n! + k! < 2 || + positions! < 1 || + total > 52 || + positions! >= 2 ** total || + functions !== k || + (destKind === 2) !== lastIsFunction || + total < 2 || e.args.length !== 5 + n! + k! + groups + cbArgs ) { err(`libCall ${e.fn} count/arity mismatch`, e.loc); @@ -5647,7 +5658,7 @@ function validateFunction( err(`libCall ${e.fn} stage function ${i} must be a func`, e.loc); } at2 += k!; - const helpers = (flags! & 8 ? 3 : 0) + (flags! & 16 ? 1 : 0); + const helpers = helperCount + (destKind === 2 ? 1 : 0); for (let i = at2; i < at2 + helpers; i++) { if (e.args[i]?.type.kind !== "func") err(`libCall ${e.fn} helper ${i} must be a func`, e.loc); diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index c05e994a8..3e8b9df73 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -762,14 +762,6 @@ "code": "SC2020", "note": "asymmetric-key operations need a public-key stack (bignum, RSA/EC/EdDSA math) and public and private KeyObjects — neither exists in the static runtime, so no faithful lowering can be small; the lowered crypto surface is hashing, randomness, secret KeyObjects, and the introspection statics" }, - { - "id": "node-builtin.crypto.webcrypto", - "kind": "node-builtin", - "name": "crypto.webcrypto", - "status": "unsupported", - "code": "SC2020", - "note": "the WebCrypto object has no lowering — the lowered crypto surface is randomUUID, randomBytes, and the createHash chain" - }, { "id": "node-builtin.dgram", "kind": "node-builtin", @@ -5719,14 +5711,14 @@ "kind": "stdlib", "name": "CryptoKey", "status": "static", - "note": "secret HMAC keys with type, extractable, algorithm and usages, and instanceof CryptoKey" + "note": "secret HMAC, AES, PBKDF2 and HKDF keys with type, extractable, algorithm and usages, and instanceof CryptoKey" }, { "id": "stdlib.webcrypto.subtle", "kind": "stdlib", "name": "globalThis.crypto.subtle", "status": "static", - "note": "importKey('raw') for HMAC keys, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" + "note": "importKey, generateKey and exportKey (raw and jwk) for HMAC and AES keys, importKey for PBKDF2 and HKDF keys, AES-GCM, AES-CBC and AES-CTR encrypt and decrypt, PBKDF2 and HKDF deriveBits and deriveKey, HMAC sign and verify, and SHA-1/SHA-2 digest; other methods, algorithms and key formats are refused" }, { "id": "syntax.async-generators", diff --git a/packages/compiler/test/array-pipeline-fusion.test.ts b/packages/compiler/test/array-pipeline-fusion.test.ts index f5d743265..55cd0025e 100644 --- a/packages/compiler/test/array-pipeline-fusion.test.ts +++ b/packages/compiler/test/array-pipeline-fusion.test.ts @@ -26,7 +26,7 @@ function helperCalls(fn: IrFunction): string[] { everyStmtList(fn.body, { expr: (e) => { if (e.kind === "call" && e.callee.startsWith("%arr.")) - calls.push(e.callee.replace(/\.\d+$/, "")); + calls.push(e.callee.replace(/\.[0-9a-f]+$/, "")); return true; }, stmt: () => true, diff --git a/packages/compiler/test/bytes-element-emission.test.ts b/packages/compiler/test/bytes-element-emission.test.ts index 505b51963..c0a78a898 100644 --- a/packages/compiler/test/bytes-element-emission.test.ts +++ b/packages/compiler/test/bytes-element-emission.test.ts @@ -406,3 +406,45 @@ test("large record clones stay outlined while small clones remain inlineable", ( expect(large).toContain("define internal ptr @sc_rclone_large(ptr %src) #2"); expect(large).toContain("attributes #2 = { noinline sanitize_address }"); }); + +test("Uint8ClampedArray stores clamp and round without a runtime call", () => { + const type = bytesOf("u8c"); + const mod: IrModule = { + irVersion: 17, + sourceFile: loc.file, + entry: "__main", + functions: [ + { name: "__main", params: [], returnType: VOID, locals: [], body: [], loc }, + { + name: "store", + params: [ + { localId: "px", name: "px", type }, + { localId: "v", name: "v", type: F64 }, + ], + returnType: VOID, + locals: [ + { id: "px", name: "px", type, mutable: false }, + { id: "v", name: "v", type: F64, mutable: false }, + ], + body: [ + { + kind: "bytesSet", + arr: { kind: "varRef", localId: "px", type, loc }, + index: { kind: "numLit", value: 0, type: F64, loc }, + value: { kind: "varRef", localId: "v", type: F64, loc }, + loc, + }, + ], + loc, + }, + ], + }; + expect(validateModule(mod)).toEqual([]); + const body = /^define internal [^\n]*@sc_bf_store[^]*?^}/m.exec(emitLlvmModule(mod))![0]; + expect(body).not.toContain("scr_bytes_to_u8_clamp"); + expect(body).toMatch(/fcmp ogt double %\S+, 0\.0/); + expect(body).toMatch(/fcmp oge double %\S+, 255\.0/); + expect(body).toMatch(/fadd double %\S+, 0x4330000000000000/); + expect(body).toMatch(/fsub double %\S+, 0x4330000000000000/); + expect(body).toMatch(/store i8 [^\n]*, align 1/); +}); diff --git a/packages/compiler/test/format-float-arguments.test.ts b/packages/compiler/test/format-float-arguments.test.ts index e59e023ca..6ce9c4bc0 100644 --- a/packages/compiler/test/format-float-arguments.test.ts +++ b/packages/compiler/test/format-float-arguments.test.ts @@ -27,7 +27,7 @@ test("util.format %f lowers a boolean argument once", async () => { const helpers = mod.functions .map((f) => f.name) .filter((name) => name.startsWith("%arr.")) - .map((name) => name.replace(/\.\d+$/, "")) + .map((name) => name.replace(/\.[0-9a-f]+$/, "")) .sort(); expect(helpers).toEqual(["%arr.every", "%arr.some"]); } finally { diff --git a/packages/compiler/test/in-runtime-key-emission.test.ts b/packages/compiler/test/in-runtime-key-emission.test.ts index aebbb2ea4..589685a7a 100644 --- a/packages/compiler/test/in-runtime-key-emission.test.ts +++ b/packages/compiler/test/in-runtime-key-emission.test.ts @@ -55,7 +55,7 @@ const d: Record = {}; d["a"] = 1; console.log(has(d, "a"), has(d, "b")); `); - const helper = body(llvm, /sc_b?f__x25_rec_haskey_\d+/); + const helper = body(llvm, /sc_b?f__x25_rec_haskey_[0-9a-f]+/); expect(helper).toContain("@scr_map_has_str("); // Not a scan over a fresh key snapshot per probe. expect(helper).not.toContain("@scr_map_keys_js_order("); diff --git a/packages/compiler/test/native-codegen-integration.test.ts b/packages/compiler/test/native-codegen-integration.test.ts index d8f158ebc..c05e7f994 100644 --- a/packages/compiler/test/native-codegen-integration.test.ts +++ b/packages/compiler/test/native-codegen-integration.test.ts @@ -378,7 +378,7 @@ done: expect(await symbols(helperObject, ["-u"])).toEqual(await symbols(clangObject, ["-u"])); expect(await symbols(helperObject, ["-gU"])).toEqual(await symbols(clangObject, ["-gU"])); expect(await symbols(helperObject, ["-gU"])).toEqual(["0000000000000000 T _main"]); - expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v24"); + expect(await symbols(helperObject, ["-u"])).toContain("_scr_runtime_abi_v25"); const clangExe = join(dir, "clang-program"); const linkDriver = join(dir, "clang-driver.c"); @@ -712,7 +712,7 @@ done: .split("\n"); expect(undefinedSymbols).toContain("_sf_scale"); expect(undefinedSymbols).toContain("_sf_callback_mix"); - expect(undefinedSymbols).toContain("_scr_runtime_abi_v24"); + expect(undefinedSymbols).toContain("_scr_runtime_abi_v25"); }); test("helper and clang object paths retain the same outbound FFI C ABI", async () => { diff --git a/packages/compiler/test/stable-names.test.ts b/packages/compiler/test/stable-names.test.ts new file mode 100644 index 000000000..720472556 --- /dev/null +++ b/packages/compiler/test/stable-names.test.ts @@ -0,0 +1,112 @@ +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, type IrModule } from "../src/index.js"; + +/** Lowers each source in turn as the same file, so names that include the + * file's path compare equal. */ +async function lower(...sources: string[]): Promise { + const dir = await mkdtemp(join(tmpdir(), "scriptc-stable-names-")); + try { + const entry = join(dir, "main.ts"); + const outPath = join(dir, "main.ir.json"); + const modules: IrModule[] = []; + for (const source of sources) { + await writeFile(entry, source); + 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")); + modules.push(deserializeModule(await readFile(outPath, "utf8"))); + } + return modules; + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +function names(mod: IrModule): string[] { + return [ + ...mod.records.map((record) => record.id), + ...mod.unions.map((union) => union.id), + ...mod.functions.map((fn) => fn.name), + ...mod.classes.map((cls) => cls.name), + ].sort(); +} + +const recursive = ` +type Tree = { value: number; children: Tree[]; parent: Tree | null }; +function total(tree: Tree): number { + let sum = tree.value; + for (const child of tree.children) sum += total(child); + return sum; +} +const root: Tree = { value: 1, children: [], parent: null }; +root.children.push({ value: 2, children: [], parent: root }); +console.log(total(root)); +`; + +test("recursive record and union ids do not depend on the code above the type", async () => { + const [before, after] = await lower( + recursive, + `// A comment above the type.\n\ntype Unrelated = { name: string };\n\n${recursive}`, + ); + expect(before.records.some((record) => record.fields.some((f) => f.name === "children"))).toBe( + true, + ); + expect(names(after)).toEqual(names(before)); +}); + +test("recursive instantiations that print alike get distinct placeholders", async () => { + const [mod] = await lower(` +namespace A { + export class Item { + a = 1; + } +} +namespace B { + export class Item { + b = "b"; + } +} +type Chain = { item: T; next: Chain | null }; +const first: Chain = { item: new A.Item(), next: null }; +const second: Chain = { item: new B.Item(), next: null }; +first.next = { item: new A.Item(), next: null }; +second.next = { item: new B.Item(), next: null }; +console.log(first.next.item.a, second.next.item.b); +`); + const chains = mod.records.filter((record) => record.fields.some((f) => f.name === "next")); + expect(new Set(chains.map((record) => record.id)).size).toBe(chains.length); + expect(chains.length).toBeGreaterThanOrEqual(2); +}); + +test("a getter and a setter of one name keep their local classes apart", async () => { + const [mod] = await lower(` +class Box { + private v = 0; + get value(): number { + class Local { + n(): number { + return 1; + } + } + return new Local().n() + this.v; + } + set value(x: number) { + class Local { + n(): number { + return 2; + } + } + this.v = x * new Local().n(); + } +} +const box = new Box(); +box.value = 5; +console.log(box.value); +`); + const locals = mod.classes.filter((cls) => cls.name.includes("Local")); + expect(locals).toHaveLength(2); + expect(locals[0]!.name).not.toBe(locals[1]!.name); +}); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json new file mode 100644 index 000000000..0bf7b0871 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6826-map-primitive-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6826-map-primitive-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json new file mode 100644 index 000000000..6e11aa685 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6827-recursive-union-nested-literals.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6827-recursive-union-nested-literals.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json new file mode 100644 index 000000000..65c965b9e --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6828-recursive-union-object-walkers.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6828-recursive-union-object-walkers.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json new file mode 100644 index 000000000..5beb42eaf --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6862-readline-chained-listeners.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6862-readline-chained-listeners.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json new file mode 100644 index 000000000..225f2c462 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6865-number-parseint-default-radix.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6865-number-parseint-default-radix.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json new file mode 100644 index 000000000..f000b3a36 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6866-number-parseint-untyped.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6866-number-parseint-untyped.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json new file mode 100644 index 000000000..1d1105bab --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6867-string-match-optional-regexp.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6867-string-match-optional-regexp.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json new file mode 100644 index 000000000..baa6e1935 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json new file mode 100644 index 000000000..f91624eb9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6869-atomics-untyped-arrays.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6869-atomics-untyped-arrays.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json new file mode 100644 index 000000000..5fc7da1eb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6912-extend-factory-local-class.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6912-extend-factory-local-class.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json new file mode 100644 index 000000000..f5a6b8742 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6994-uint8-clamped-store-rounding.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6994-uint8-clamped-store-rounding.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json new file mode 100644 index 000000000..8078248f5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6995-worker-typed-array-loop-versions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6995-worker-typed-array-loop-versions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json new file mode 100644 index 000000000..b5f88af99 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6996-typed-array-index-temps.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6996-typed-array-index-temps.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json new file mode 100644 index 000000000..1a1a6951c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7040-es5-error-constructor.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7040-es5-error-constructor.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json new file mode 100644 index 000000000..f40c04116 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7041-error-called-without-new.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7041-error-called-without-new.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json new file mode 100644 index 000000000..c867cd88c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7042-string-method-names-on-objects.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7042-string-method-names-on-objects.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json new file mode 100644 index 000000000..8b369c633 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7043-date-set-time.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7043-date-set-time.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json new file mode 100644 index 000000000..ee3592ba6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7044-nested-empty-object-maps.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7044-nested-empty-object-maps.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json new file mode 100644 index 000000000..530aca0ad --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7045-instanceof-constructor-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7045-instanceof-constructor-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json new file mode 100644 index 000000000..0f37ad2a2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7046-parse-float-extra-arguments.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7046-parse-float-extra-arguments.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json new file mode 100644 index 000000000..7cd38cfb9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7047-dynamic-string-index.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7047-dynamic-string-index.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json new file mode 100644 index 000000000..10c99f165 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7048-unary-number-coercions.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7048-unary-number-coercions.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json new file mode 100644 index 000000000..5dc066127 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7049-member-assignment-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7049-member-assignment-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json new file mode 100644 index 000000000..fc73bb479 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7050-date-subclass.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7050-date-subclass.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json new file mode 100644 index 000000000..f37cd9763 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7051-date-subclass-typed.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7051-date-subclass-typed.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json new file mode 100644 index 000000000..7077aa5bb --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7052-date-subclass-dynamic.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7052-date-subclass-dynamic.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json new file mode 100644 index 000000000..d7d8ba2e1 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7053-private-fields-untyped-receivers.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7053-private-fields-untyped-receivers.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json new file mode 100644 index 000000000..25dcd3680 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7054-let-bindings-with-null-writes.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7054-let-bindings-with-null-writes.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7055-cipher-streams.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7055-cipher-streams.ts.json new file mode 100644 index 000000000..989c7f452 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7055-cipher-streams.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7055-cipher-streams.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7056-cipher-stream-errors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7056-cipher-stream-errors.ts.json new file mode 100644 index 000000000..16bd2586c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7056-cipher-stream-errors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7056-cipher-stream-errors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7057-cipher-streams.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7057-cipher-streams.mjs.json new file mode 100644 index 000000000..1304933ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7057-cipher-streams.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7057-cipher-streams.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7058-pipeline-function-stages.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7058-pipeline-function-stages.mjs.json new file mode 100644 index 000000000..3b9fd5a0f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7058-pipeline-function-stages.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7058-pipeline-function-stages.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7059-transform-backpressure.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7059-transform-backpressure.ts.json new file mode 100644 index 000000000..311882a39 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7059-transform-backpressure.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7059-transform-backpressure.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7109-uninitialized-let-assignment-expressions.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7109-uninitialized-let-assignment-expressions.ts.json new file mode 100644 index 000000000..47973fee5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7109-uninitialized-let-assignment-expressions.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7109-uninitialized-let-assignment-expressions.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json new file mode 100644 index 000000000..c70fdfa63 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7120-webcrypto-random-uuid.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7120-webcrypto-random-uuid.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json new file mode 100644 index 000000000..b152699ba --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7121-webcrypto-crypto-object.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7121-webcrypto-crypto-object.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json new file mode 100644 index 000000000..02451a516 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7122-webcrypto-node-crypto.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7122-webcrypto-node-crypto.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json new file mode 100644 index 000000000..821fb378d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7123-webcrypto-node-crypto.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7123-webcrypto-node-crypto.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json new file mode 100644 index 000000000..8fc0dab2a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7124-webcrypto-secret-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7124-webcrypto-secret-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json new file mode 100644 index 000000000..942de8c10 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7125-webcrypto-key-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7125-webcrypto-key-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json new file mode 100644 index 000000000..f113a3ca9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7126-webcrypto-aes.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7126-webcrypto-aes.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json new file mode 100644 index 000000000..3ec3d26f2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7127-webcrypto-aes-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7127-webcrypto-aes-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json new file mode 100644 index 000000000..b613fbce6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7128-webcrypto-derive.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7128-webcrypto-derive.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json new file mode 100644 index 000000000..11fc8c721 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7129-webcrypto-derive-errors.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7129-webcrypto-derive-errors.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7183-local-class-heritage-effects.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7183-local-class-heritage-effects.ts.json new file mode 100644 index 000000000..8f0ebb3c2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7183-local-class-heritage-effects.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7183-local-class-heritage-effects.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json new file mode 100644 index 000000000..19f14ed02 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/webcrypto-keys.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/webcrypto-keys.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/type-mapper-unions.test.ts b/packages/compiler/test/type-mapper-unions.test.ts index c714df4d7..684960002 100644 --- a/packages/compiler/test/type-mapper-unions.test.ts +++ b/packages/compiler/test/type-mapper-unions.test.ts @@ -139,7 +139,8 @@ describe("checker and synthesized optional union identity", () => { nullable.arms.filter((arm) => arm.kind !== "nullT" && arm.kind !== "undefinedT"), ), ).toBe(required.unionId); - expect(nullable.discriminant!.cases.map((entry) => entry.values)).toEqual([ + // Cases follow the arms, which are ordered by their type keys. + expect(nullable.discriminant!.cases.map((entry) => entry.values).sort()).toEqual([ ["a", "alias"], ["b"], ]); diff --git a/packages/runtime-darwin-arm64/scripts/build.mjs b/packages/runtime-darwin-arm64/scripts/build.mjs index 85c5ffb00..6cc32d7fa 100644 --- a/packages/runtime-darwin-arm64/scripts/build.mjs +++ b/packages/runtime-darwin-arm64/scripts/build.mjs @@ -172,7 +172,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_webcrypto_aes.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" @@ -337,7 +338,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: RUNTIME_PACK_MATRIX.target, - runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-pack-common/runtime-pack-matrix.mjs b/packages/runtime-pack-common/runtime-pack-matrix.mjs index 0f40218a1..3529df137 100644 --- a/packages/runtime-pack-common/runtime-pack-matrix.mjs +++ b/packages/runtime-pack-common/runtime-pack-matrix.mjs @@ -72,6 +72,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_js_object.c", "dynTier"], ["scr_event_target.c", any("eventTarget", "nativeFetch")], ["scr_async_dyn.c", any("dynAsync", "dynInvoke", "dc", "fileHandle", "nativeFetch")], + // The crypto global's object extends scr_async_dyn.c's crypto object. + ["scr_webcrypto_object.c", all("webcrypto", "dynAsync")], // The performance timeline emits its buffer warning through scr_async_dyn.c. ["scr_perf.c", "dynAsync"], // perf_hooks event-loop metrics, only for programs that read them. @@ -151,6 +153,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [ ["scr_ecdh.c", "ecdh"], ["scr_wasm.c", "wasm"], ["scr_webcrypto.c", "webcrypto"], + // WebCrypto's AES ciphers run over the cipher feature's mbedTLS archive. + ["scr_webcrypto_aes.c", all("cipher", "webcrypto")], ["scr_fetch.c", "nativeFetch"], ]; diff --git a/packages/runtime-pack-common/scripts/build.mjs b/packages/runtime-pack-common/scripts/build.mjs index b242b78aa..a2c987b97 100644 --- a/packages/runtime-pack-common/scripts/build.mjs +++ b/packages/runtime-pack-common/scripts/build.mjs @@ -223,7 +223,8 @@ async function build() { ...(unit.source === "scr_regex.c" ? ["-I", quickjs] : []), ...(unit.source === "scr_tls.c" || unit.source === "scr_cipher.c" || - unit.source === "scr_ecdh.c" + unit.source === "scr_ecdh.c" || + unit.source === "scr_webcrypto_aes.c" ? ["-I", join(mbedtls, "include")] : []), ...(unit.source === "scr_zlib.c" || unit.source === "scr_fetch.c" @@ -434,7 +435,7 @@ async function build() { package: packageManifest.name, version: packageManifest.version, target: matrix.target, - runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, compiler: { command: compiler, identity: compilerVersion, diff --git a/packages/runtime-wasm32-wasi/scripts/build.mjs b/packages/runtime-wasm32-wasi/scripts/build.mjs index 2dd983973..0a8699b23 100644 --- a/packages/runtime-wasm32-wasi/scripts/build.mjs +++ b/packages/runtime-wasm32-wasi/scripts/build.mjs @@ -22,6 +22,7 @@ process.env.SCRIPTC_RUNTIME_PACK_CONFIG = JSON.stringify({ "scr_tls.c", "scr_cipher.c", "scr_ecdh.c", + "scr_webcrypto_aes.c", "scr_wasm.c", "scr_fetch.c", ], diff --git a/packages/runtime/src/scr_bytes.h b/packages/runtime/src/scr_bytes.h index 9c81caea5..9b382dda5 100644 --- a/packages/runtime/src/scr_bytes.h +++ b/packages/runtime/src/scr_bytes.h @@ -397,6 +397,7 @@ ScrBytes *scr_buffer_from_bytes(const ScrBytes *src); /* a copy of any typed arr ScrBytes *scr_buffer_from_arr(const ScrArr *arr); /* number[] */ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding); ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *encoding); +ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second); ScrBytes *scr_buffer_concat(const ScrArr *list); ScrBytes *scr_buffer_concat_len(const ScrArr *list, double total); /* The untyped-list forms: Node's ERR_INVALID_ARG_TYPE checks on the list diff --git a/packages/runtime/src/scr_bytes_io.c b/packages/runtime/src/scr_bytes_io.c index 5532d96da..137208938 100644 --- a/packages/runtime/src/scr_bytes_io.c +++ b/packages/runtime/src/scr_bytes_io.c @@ -1299,8 +1299,48 @@ ScrBytes *scr_buffer_concat_len_dyn(const ScrDyn *list, double total) { return scr_buffer_concat_dyn_impl(list, total, true); } +/* Buffer.from(arrayBuffer, byteOffset) over a checked ArrayBuffer or + * SharedArrayBuffer, as Node's fromArrayBuffer: a Buffer from the offset to + * the end of the buffer that shares its memory. The offset converts with + * ToNumber, where a missing offset (NULL) or undefined and NaN are 0; an + * offset past the end throws ERR_BUFFER_OUT_OF_BOUNDS, and one at or below + * -1 V8's start-offset RangeError. NULL with the exception pending. */ +static ScrBytes *scr_buffer_from_storage(const ScrDyn *storage, const ScrDyn *offset_value) { + double offset = 0; + if (offset_value && offset_value->kind != SCR_DYN_UNDEF) { + if (!scr_dyn_number_coerce_js(offset_value, &offset)) return NULL; + if (isnan(offset)) offset = 0; + } + ScrBytes *bytes = storage->v.handle.ptr; + ScrBytes *owner = bytes->backing ? bytes->backing : bytes; + if ((double)scr_bytes_byte_len(owner) - offset < 0) { + static const char msg[] = "\"offset\" is outside of buffer bounds"; + scr_throw_error_msg_code(SCR_ERR_RANGE, msg, sizeof msg - 1, "ERR_BUFFER_OUT_OF_BOUNDS"); + return NULL; + } + double start = trunc(offset); + if (start < 0) { + /* The offset as JavaScript prints it (-1.5, -1e+21, -Infinity). */ + ScrStr *parts[3] = { + scr_str_new("Start offset ", 13), + scr_f64_to_scrstr(offset), + scr_str_new(" is outside the bounds of the buffer", 36), + }; + ScrStr *message = scr_str_concat_parts(parts, 3); + for (size_t i = 0; i < 3; i++) scr_str_release(parts[i]); + scr_throw_error(SCR_ERR_RANGE, message); + return NULL; + } + ScrBytes *out = scr_bytes_buffer_view(bytes, SCR_BYTES_U8, start, false, 0); + if (out) out->is_buffer = true; + return out; +} + ScrBytes *scr_buffer_from_dyn(const ScrDyn *value, const ScrStr *encoding) { if (value->kind == SCR_DYN_STR) return scr_bytes_from_str(value->v.str, encoding); + /* The second argument here is an encoding name, which as a byte offset + * converts to NaN: offset 0. */ + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); if (value->kind == SCR_DYN_BYTES) return scr_bytes_convert(SCR_BYTES_U8, value->v.bytes); if (value->kind == SCR_DYN_ARR) { return scr_buffer_from_array_like(value, (double)value->v.arr.len); @@ -1344,6 +1384,9 @@ ScrBytes *scr_buffer_from_dyn_pooled(const ScrDyn *value, const ScrStr *encoding if (value->kind == SCR_DYN_STR) return scr_buffer_from_str(value->v.str, encoding); if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) return scr_buffer_from_bytes(value->v.bytes); + /* A view of an ArrayBuffer shares its memory, so it never comes from the + * pool. The second argument here is an encoding name: offset 0. */ + if (scr_buffer_storage_is(value)) return scr_buffer_from_storage(value, NULL); ScrBytes *out = scr_buffer_from_dyn(value, encoding); if (!out) return NULL; out->is_buffer = true; @@ -1368,6 +1411,20 @@ ScrBytes *scr_buffer_from_dyn_ref_pooled(const ScrDyn *value, const ScrStr *enco return scr_buffer_from_dyn_pooled(value, encoding); } +/* Buffer.from(value, second) over checked values whose second argument is + * not a literal encoding name: an ArrayBuffer or SharedArrayBuffer takes it + * as the byte offset (scr_buffer_from_storage); other values convert as + * scr_buffer_from_dyn_ref_pooled with utf8. */ +ScrBytes *scr_buffer_from_dyn_offset_pooled(const ScrDyn *value, const ScrDyn *second) { + if (!scr_buffer_storage_is(value)) { + ScrStr *utf8 = scr_str_new("utf8", 4); + ScrBytes *out = scr_buffer_from_dyn_ref_pooled(value, utf8); + scr_str_release(utf8); + return out; + } + return scr_buffer_from_storage(value, second); +} + /* new Buffer(number, encoding) — the deprecated ctor's string arm with a * non-string first argument: Node's exact ERR_INVALID_ARG_TYPE (the * throwing path never fires DEP0005, so compiled silence matches). */ diff --git a/packages/runtime/src/scr_cipher.c b/packages/runtime/src/scr_cipher.c index df62a79a3..c8a10a731 100644 --- a/packages/runtime/src/scr_cipher.c +++ b/packages/runtime/src/scr_cipher.c @@ -9,7 +9,9 @@ * the runtime StringDecoder, which carries partial characters and base64 * groups between calls as Node's does. chacha20-poly1305 follows OpenSSL's * construction, which also accepts AAD after data. Authentication tags are - * compared in constant time. All arguments are borrowed. */ + * compared in constant time. A cipher used as a stream reads and writes + * through its own native Transform, which the stream unit builds on first + * use, as Node's LazyTransform is. All arguments are borrowed. */ #include "scr_runtime.h" #include @@ -103,6 +105,12 @@ typedef struct ScCipher { bool aad_open; /* AAD was added since the last data or padding */ ScrStr *decoder; /* the StringDecoder encoding once string output starts */ double pending; /* the decoder's carried bytes */ + /* The Transform of a cipher used as a stream (+1) and its release + * (scr_stream.c). Its steps hold the cipher, so the cipher is a + * collector-headered object behind a traced handle, and the pair is + * reclaimed as a cycle. */ + void *stream; + void (*stream_release)(void *); } ScCipher; static void *sc_retain(void *h) { @@ -110,16 +118,38 @@ static void *sc_retain(void *h) { return h; } -static void sc_release(void *h) { +static void sc_trace(void *h, ScrTraceVisit visit, void *ctx) { + ScCipher *c = h; + if (c->stream) visit(c->stream, ctx); +} + +/* The collector frees the stream itself; the rest is released here. */ +static void sc_gcfree(void *h) { ScCipher *c = h; - if (--c->rc) return; mbedtls_aes_free(&c->aes); mbedtls_gcm_free(&c->gcm); mbedtls_chacha20_free(&c->chacha); mbedtls_poly1305_free(&c->poly); scr_str_release(c->decoder); mbedtls_platform_zeroize(c, sizeof *c); - free(c); + scr_obj_free_note(); + scr_cyc_free(c); +} + +static void sc_destroy(void *h) { + ScCipher *c = h; + void *stream = c->stream; + c->stream = NULL; + if (stream) c->stream_release(stream); + sc_gcfree(c); +} + +static void sc_release(void *h) { + ScCipher *c = h; + if (--c->rc == 0) { + scr_cyc_dead(c); + scr_rc_destroy(c, sc_destroy); + } else scr_cyc_released(c); } /* ── errors ───────────────────────────────────────────────────────────── */ @@ -532,9 +562,10 @@ static ScrDyn *sc_cipher_new(bool decrypt, ScrDyn *cipher, ScrDyn *key, ScrDyn * sc_throw(SCR_ERR_RANGE, "ERR_CRYPTO_INVALID_KEYLEN", "Invalid key length"); goto done; } - ScCipher *c = calloc(1, sizeof *c); - if (!c) scr_trap("scriptc: out of memory\n"); + static ScrCycType cyc_type = {&sc_trace, &sc_gcfree, SCR_CYC_KIND_NONE, 0}; + ScCipher *c = scr_cyc_alloc(sizeof *c, &cyc_type); c->rc = 1; + scr_obj_alloc_note(); c->mode = spec->mode; c->decrypt = decrypt; c->active = true; @@ -883,7 +914,8 @@ static bool sc_aead(const ScCipher *c) { return c->mode == SC_GCM || c->mode == SC_CHACHA; } -static ScrDyn *sc_final(ScCipher *c, ScrDyn *output_encoding) { +/* final()'s output bytes (+1 Buffer), or NULL with the error thrown. */ +static ScrBytes *sc_final_bytes(ScCipher *c) { if (!c->active) { sc_throw(SCR_ERR_ERROR, "ERR_CRYPTO_INVALID_STATE", "Invalid state"); return NULL; @@ -953,7 +985,12 @@ static ScrDyn *sc_final(ScCipher *c, ScrDyn *output_encoding) { sc_throw(SCR_ERR_ERROR, NULL, "Unsupported state or unable to authenticate data"); return NULL; } - return sc_output(c, out, output_encoding, true); + return out; +} + +static ScrDyn *sc_final(ScCipher *c, ScrDyn *output_encoding) { + ScrBytes *out = sc_final_bytes(c); + return out ? sc_output(c, out, output_encoding, true) : NULL; } static ScrDyn *sc_set_aad(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn *options) { @@ -1055,6 +1092,49 @@ static ScrDyn *sc_set_auth_tag(ScCipher *c, ScrDyn *self, ScrDyn *buffer, ScrDyn return scr_dyn_retain(self); } +/* ── Cipheriv and Decipheriv as streams ─────────────────────────────── + * The stream unit builds a cipher's Transform from these operations + * (ScrDynHandleTransform): each chunk goes through the handle's update(), + * and the end pushes final() or reports its error, as Node's cipher.js + * _transform and _flush do. */ + +static void *sc_stream_get(void *h) { + return ((ScCipher *)h)->stream; +} + +static void sc_stream_set(void *h, void *stream, void (*release)(void *)) { + ScCipher *c = h; + c->stream = stream; + c->stream_release = release; +} + +/* The handle's update(): it throws out of the write, as in Node, once + * the cipher has finished. */ +static ScrBytes *sc_stream_update(void *h, ScrBytes *chunk) { + ScCipher *c = h; + if (!c->active) { + sc_throw(SCR_ERR_ERROR, NULL, "Trying to add data in unsupported state"); + return NULL; + } + SCR_BYTES_SNAPSHOT(chunk); + return sc_transform(c, chunk->data, chunk->len * scr_bytes_elem_size(chunk->elem)); +} + +static ScrBytes *sc_stream_final(void *h) { + return sc_final_bytes(h); +} + +static const ScrDynHandleTransform sc_transform_ops = { + sc_stream_get, sc_stream_set, sc_stream_update, sc_stream_final, sc_trace, +}; + +static ScrDynHandleOps sc_ops_cipher; +static ScrDynHandleOps sc_ops_decipher; + +static const ScrDynHandleOps *sc_ops_of(const ScCipher *c) { + return c->decrypt ? &sc_ops_decipher : &sc_ops_cipher; +} + static bool sc_name_is(const char *method, const char *name) { return strcmp(method, name) == 0; } @@ -1076,8 +1156,12 @@ static bool sc_stream_member(const char *name) { static void sc_stream_fence(void) { static const char message[] = - "Cipheriv and Decipheriv stream members have no lowering; call update() and final()"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + "This Cipheriv and Decipheriv stream member has no lowering when called through an untyped value"; + static const char streamless[] = + "Cipheriv and Decipheriv stream members called through an untyped value have no lowering in a " + "program that uses no streams"; + if (scr_handle_stream_hooks) scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + else scr_throw_error_msg_code(SCR_ERR_ERROR, streamless, sizeof streamless - 1, "SC2020"); } static ScrDyn *sc_dispatch(ScCipher *c, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, @@ -1091,6 +1175,12 @@ static ScrDyn *sc_dispatch(ScCipher *c, ScrDyn *self, const char *method, ScrDyn if (c->decrypt && sc_name_is(method, "setAuthTag")) return sc_set_auth_tag(c, self, sc_arg(args, argc, 0), sc_arg(args, argc, 1)); if (sc_stream_member(method)) { + /* The members a native stream answers to checked-dynamic calls run + * on the cipher's Transform; the others have no lowering. */ + ScrDyn *result = NULL; + if (scr_handle_stream_hooks && + scr_handle_stream_hooks->call(sc_ops_of(c), c, self, method, args, argc, &result)) + return result; sc_stream_fence(); return NULL; } @@ -1117,11 +1207,19 @@ done: } static ScrDyn *sc_get(void *h, const char *key, size_t key_len) { - (void)h; (void)key; (void)key_len; + /* The stream properties read the cipher's Transform. */ + if (scr_handle_stream_hooks) { + ScrDyn *value = scr_handle_stream_hooks->get(sc_ops_of(h), h, key, key_len); + if (value || scr_exc_pending()) return value; + } static const char message[] = - "Cipheriv and Decipheriv property reads have no lowering; call update(), final(), setAAD(), " - "setAutoPadding(), getAuthTag() or setAuthTag()"; - scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + "Cipheriv and Decipheriv property reads have no lowering other than the stream properties; call " + "update(), final(), setAAD(), setAutoPadding(), getAuthTag() or setAuthTag()"; + static const char streamless[] = + "Cipheriv and Decipheriv property reads through an untyped value have no lowering in a program " + "that uses no streams"; + if (scr_handle_stream_hooks) scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + else scr_throw_error_msg_code(SCR_ERR_ERROR, streamless, sizeof streamless - 1, "SC2020"); return NULL; } @@ -1132,5 +1230,7 @@ static bool sc_set(void *h, const char *key, size_t key_len, const ScrDyn *value return true; } -static ScrDynHandleOps sc_ops_cipher = {"Cipheriv", sc_retain, sc_release, sc_invoke, sc_get, sc_set, NULL, NULL, NULL}; -static ScrDynHandleOps sc_ops_decipher = {"Decipheriv", sc_retain, sc_release, sc_invoke, sc_get, sc_set, NULL, NULL, NULL}; +static ScrDynHandleOps sc_ops_cipher = {"Cipheriv", sc_retain, sc_release, sc_invoke, sc_get, sc_set, NULL, NULL, NULL, + .transform = &sc_transform_ops}; +static ScrDynHandleOps sc_ops_decipher = {"Decipheriv", sc_retain, sc_release, sc_invoke, sc_get, sc_set, NULL, NULL, NULL, + .transform = &sc_transform_ops}; diff --git a/packages/runtime/src/scr_console.c b/packages/runtime/src/scr_console.c index c29126a4a..c8fa40b38 100644 --- a/packages/runtime/src/scr_console.c +++ b/packages/runtime/src/scr_console.c @@ -148,7 +148,7 @@ static void scr_enable_utf8_console_output(void) { } #endif -void scr_runtime_abi_v24(void) {} +void scr_runtime_abi_v25(void) {} void scr_init(void) { #ifdef SCR_WORKERS diff --git a/packages/runtime/src/scr_core.h b/packages/runtime/src/scr_core.h index 94936a8aa..5e5278588 100644 --- a/packages/runtime/src/scr_core.h +++ b/packages/runtime/src/scr_core.h @@ -81,7 +81,7 @@ void scr_init(void); /* Program objects emitted by the bundled LLVM helper reference this symbol. * Its versioned spelling makes a mismatched manual runtime link fail before * the program can start. */ -void scr_runtime_abi_v24(void); +void scr_runtime_abi_v25(void); void scr_runtime_workers_v8(void); /* ── the trap funnel (scr_console.c; scr_library.c under -DSCR_LIB) ────── diff --git a/packages/runtime/src/scr_crypto.h b/packages/runtime/src/scr_crypto.h index d12858abf..ddc306598 100644 --- a/packages/runtime/src/scr_crypto.h +++ b/packages/runtime/src/scr_crypto.h @@ -20,6 +20,9 @@ ScrStr *scr_crypto_hash_digest_bytes(ScrStr *alg, ScrBytes *data, ScrStr *enc); typedef struct ScrCryptoHash ScrCryptoHash; ScrCryptoHash *scr_crypto_hash_new(ScrStr *alg); ScrDyn *scr_crypto_native(void); +/* The crypto global as a value: scr_crypto_native's object with the Crypto + * methods (scr_webcrypto_object.c, linked with the webcrypto feature). */ +ScrDyn *scr_webcrypto_object(void); ScrCryptoHash *scr_crypto_hmac_new_str(ScrStr *alg, ScrStr *key); ScrCryptoHash *scr_crypto_hmac_new_bytes(ScrStr *alg, ScrBytes *key); ScrCryptoHash *scr_crypto_hash_retain(ScrCryptoHash *h); @@ -202,19 +205,48 @@ size_t scr_crypto_digest_raw(const char *alg, const unsigned char *data, size_t size_t scr_crypto_hmac_raw(const char *alg, const unsigned char *key, size_t keylen, const unsigned char *data, size_t len, unsigned char out[64]); /* ── WebCrypto (scr_webcrypto.c) ───────────────────────────────────── - * crypto.subtle.importKey (raw HMAC keys), sign and verify (HMAC) and - * digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the + * crypto.subtle.importKey (HMAC, AES, PBKDF2 and HKDF keys, raw and jwk), + * generateKey and exportKey (HMAC and AES keys), sign and verify (HMAC) + * and digest (SHA-1/SHA-2) over node:crypto's digest core. Each takes the * call's argument vector as one dyn array (borrowed) and answers a +1 * promise that every argument or validation failure rejects with Node's * TypeError or DOMException; algorithms and formats outside the slice - * reject with a coded SC2020 Error. importKey settles at once with a - * CryptoKey handle; sign and digest fulfill with an ArrayBuffer and - * verify with a boolean on a later loop turn. scr_webcrypto_is_crypto_key - * answers `value instanceof CryptoKey`. */ + * reject with a coded SC2020 Error. importKey and exportKey settle at + * once with a CryptoKey handle, an ArrayBuffer or a JWK object; + * generateKey fulfills with a CryptoKey, sign and digest with an + * ArrayBuffer and verify with a boolean on a later loop turn. + * scr_webcrypto_is_crypto_key answers `value instanceof CryptoKey`. */ ScrPromise *scr_webcrypto_import_key(ScrDyn *args); +ScrPromise *scr_webcrypto_generate_key(ScrDyn *args); +ScrPromise *scr_webcrypto_export_key(ScrDyn *args); ScrPromise *scr_webcrypto_sign(ScrDyn *args); ScrPromise *scr_webcrypto_verify(ScrDyn *args); ScrPromise *scr_webcrypto_digest(ScrDyn *args); +/* encrypt and decrypt (scr_webcrypto_aes.c, linked with the cipher + * feature's mbedTLS archive) run scr_webcrypto_cipher with the AES cipher + * as `aes`, which answers the +1 output bytes or NULL when the operation + * fails. encrypt and decrypt fulfill with an ArrayBuffer, or reject with + * a job failure, on a later loop turn. */ +typedef struct { + const unsigned char *iv; /* the IV, or the CTR counter block */ + size_t iv_len; + const unsigned char *aad; + size_t aad_len; + size_t tag_len; /* bytes */ + unsigned counter_bits; +} ScrWebCryptoAesParams; +typedef ScrBytes *(*ScrWebCryptoAesFn)(const char *name, bool decrypt, const unsigned char *key, + size_t key_len, const ScrWebCryptoAesParams *params, + const unsigned char *in, size_t len); +ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes); +ScrPromise *scr_webcrypto_encrypt(ScrDyn *args); +ScrPromise *scr_webcrypto_decrypt(ScrDyn *args); +/* deriveBits and deriveKey (PBKDF2 and HKDF) over node:crypto's KDF core: + * deriveBits fulfills with an ArrayBuffer (at once for a zero length) and + * deriveKey with a CryptoKey of the derived key type, on a later loop + * turn. */ +ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args); +ScrPromise *scr_webcrypto_derive_key(ScrDyn *args); bool scr_webcrypto_is_crypto_key(const ScrDyn *value); /* The composed `new crypto.X509Certificate(data).fingerprint` read (the diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 02c4d828f..a427fc9a1 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -662,6 +662,40 @@ bool scr_dyn_native_url_is(const ScrDyn *value); ScrDyn *scr_dyn_native_search_params(ScrSearchParams *value); bool scr_dyn_native_search_params_is(const ScrDyn *value); bool scr_dyn_native_date_is(const ScrDyn *value); +/* Instances of program classes declared `extends Date` (scr_lib.c). */ +void scr_date_class_operation_install(ScrDyn *time, ScrDyn *methods); +/* Set by scr_date_class_operation_install; NULL in a program without such + * a class, where every wrapper below answers false or NULL. */ +typedef struct { + bool (*time)(const ScrDyn *value, double *time); + ScrDyn *(*to_json)(const ScrDyn *value, const ScrDyn *key); + ScrDyn *(*to_string)(const ScrDyn *value); + ScrDyn *(*value_of)(const ScrDyn *value); + ScrStr *(*name)(const ScrDyn *value); + ScrDyn *(*method)(const ScrDyn *value, const char *key, size_t key_len); +} ScrDateClassHooks; +extern const ScrDateClassHooks *scr_date_class_hooks; +static inline bool scr_dyn_date_class_time(const ScrDyn *value, double *time) { + return scr_date_class_hooks && scr_date_class_hooks->time(value, time); +} +static inline ScrDyn *scr_dyn_date_class_to_json(const ScrDyn *value, const ScrDyn *key) { + return scr_date_class_hooks->to_json(value, key); +} +static inline ScrDyn *scr_dyn_date_class_to_string(const ScrDyn *value) { + return scr_date_class_hooks->to_string(value); +} +static inline ScrDyn *scr_dyn_date_class_value_of(const ScrDyn *value) { + return scr_date_class_hooks->value_of(value); +} +static inline ScrStr *scr_dyn_date_class_name(const ScrDyn *value) { + return scr_date_class_hooks ? scr_date_class_hooks->name(value) : NULL; +} +static inline ScrDyn *scr_dyn_date_class_method(const ScrDyn *value, const char *key, size_t key_len) { + return scr_date_class_hooks ? scr_date_class_hooks->method(value, key, key_len) : NULL; +} +bool scr_dyn_date_instance_is(const ScrDyn *value); +ScrDyn *scr_dyn_date_class_member(const ScrDyn *d, const char *key, size_t key_len); +double scr_dyn_date_arguments_time(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments); ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments); @@ -1048,6 +1082,10 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc); * prototype chain has no toString (Node's answer). */ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what); ScrDyn *scr_dyn_object_prototype(void); +ScrDyn *scr_dyn_error_prototype(void); +bool scr_dyn_inherits_error_prototype(const ScrDyn *value); +ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target); +ScrDyn *scr_dyn_error_capture_stack_trace_function(void); ScrDyn *scr_dyn_function_apply(void); ScrDyn *scr_dyn_reflect_apply(ScrDyn *target, ScrDyn *receiver, ScrDyn *arguments_list); ScrDyn *scr_dyn_apply_array_like(const ScrDyn *target, const ScrDyn *receiver, const ScrDyn *arguments, const char *what); @@ -1388,8 +1426,37 @@ typedef struct ScrDynHandleOps { /* Cross-thread structured clone (postMessage, workerData); NULL = * DataCloneError. See ScrDynHandleTransfer. */ const struct ScrDynHandleTransfer *transfer; + /* A handle that is also a Node Transform (Cipheriv, Decipheriv): the + * stream unit builds the handle's Transform from these operations on its + * first stream use. NULL = the class is not a stream. */ + const struct ScrDynHandleTransform *transform; } ScrDynHandleOps; +/* The operations a Transform handle gives the stream unit (scr_stream.c): + * the slot where the handle keeps its stream (+1) with the stream's + * release, update() and final() over bytes (+1 Buffer, or NULL with the + * exception pending), and the handle's collector trace, since the + * stream's steps hold the handle. */ +typedef struct ScrDynHandleTransform { + void *(*stream)(void *h); + void (*set_stream)(void *h, void *stream, void (*release)(void *)); + struct ScrBytes *(*update)(void *h, struct ScrBytes *chunk); + struct ScrBytes *(*final)(void *h); + ScrTraceFn trace; +} ScrDynHandleTransform; + +/* Checked-dynamic member calls and property reads on a Transform handle, + * installed by the stream unit (scr_stream_install); NULL in a program + * without it. call answers true with *result set (+1, or NULL with the + * exception pending) for the members a native stream answers, and get + * answers a stream property (+1), or NULL for other keys. */ +typedef struct ScrHandleStreamHooks { + bool (*call)(const ScrDynHandleOps *ops, void *h, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc, ScrDyn **result); + ScrDyn *(*get)(const ScrDynHandleOps *ops, void *h, const char *key, size_t key_len); +} ScrHandleStreamHooks; +extern SCR_TL const ScrHandleStreamHooks *scr_handle_stream_hooks; + /* How a handle crosses threads, supplied by the handle's owning unit so the * message encoder (scr_message.c) needs no reference to gated units and no * per-tag registry. share runs on the sending thread: it answers a payload diff --git a/packages/runtime/src/scr_dyn_invoke.c b/packages/runtime/src/scr_dyn_invoke.c index 2a010f213..ec49dd383 100644 --- a/packages/runtime/src/scr_dyn_invoke.c +++ b/packages/runtime/src/scr_dyn_invoke.c @@ -845,6 +845,16 @@ static ScrDyn *scr_dyn_invoke_impl( if (scr_event_ref_hooks && scr_event_ref_hooks->invoke(recv, method, args, argc, what, &fast)) return fast; + /* An inherited Date method of an instance of a class declared `extends + * Date`. */ + ScrDyn *date_method = scr_dyn_date_class_member(recv, method, strlen(method)); + if (date_method) { + scr_dyn_this_push_dyn(recv); + ScrDyn *result = scr_dyn_call(date_method, args, argc, what); + scr_dyn_this_pop(); + scr_dyn_release(date_method); + return result; + } ScrDyn *materialized = scr_dyn_typed_ref_materialize(recv); if (scr_exc_pending()) { scr_dyn_release(materialized); diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c index 223517e92..a4d124ac3 100644 --- a/packages/runtime/src/scr_inspect.c +++ b/packages/runtime/src/scr_inspect.c @@ -1652,7 +1652,40 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) { * view of its fields does not carry. */ const ScrStr *name = scr_dyn_typed_ref_class_name(d); ScrStr *out; - if (name && materialized->kind == SCR_DYN_OBJ && + double time; + ScrStr *date_class = materialized->kind == SCR_DYN_OBJ && scr_dyn_date_class_time(d, &time) + ? scr_dyn_date_class_name(d) : NULL; + if (date_class) { + /* An instance of a class declared `extends Date`: the class name and + * the time as Date.prototype.toISOString formats it, then its own + * enumerable properties when it has any. */ + ScrStr *iso = scr_insp_date(time); + InspBuf head = {0}; + ib_bytes(&head, date_class->data, date_class->len); + scr_str_release(date_class); + ib_char(&head, ' '); + ib_bytes(&head, iso->data, iso->len); + scr_str_release(iso); + ScrStr *prefix = ib_take(&head); + bool fields = false; + for (size_t i = 0; i < materialized->v.obj.len && !fields; i++) + fields = materialized->v.obj.entries[i].enumerable; + if (!fields || recurse > depth) { + scr_dyn_release(materialized); + return prefix; + } + char *cls = malloc(prefix->len + 1); + if (!cls) scr_trap("scriptc: out of memory\n"); + memcpy(cls, prefix->data, prefix->len); + cls[prefix->len] = '\0'; + scr_str_release(prefix); + size_t nseen = g_nseen; + g_pending_seen = instance; + out = scr_insp_obj(materialized, recurse, depth, cls); + g_pending_seen = NULL; + if (g_nseen > nseen) g_nseen = nseen; + free(cls); + } else if (name && materialized->kind == SCR_DYN_OBJ && scr_dyn_obj_get(materialized, "%error", 6) == NULL) { char *cls = malloc(name->len + 1); if (!cls) scr_trap("scriptc: out of memory\n"); diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index b8f16630b..4e21fa397 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -37,6 +37,21 @@ static SCR_TL ScrDyn *scr_builtin_object_prototype; +const ScrDateClassHooks *scr_date_class_hooks = NULL; + +/* The checked-dynamic Error.prototype (scr_dyn_error_prototype) is not the + * prototype of scriptc's Error instances, so a change to it would not reach + * them: every mutation throws. */ +static SCR_TL ScrDyn *scr_builtin_error_prototype; +static bool scr_dyn_error_prototype_fence(const ScrDyn *value) { + if (!value || value != scr_builtin_error_prototype) return false; + static const char message[] = "Error.prototype mutation is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return true; +} + +SCR_TL const ScrHandleStreamHooks *scr_handle_stream_hooks = NULL; + /* Constructor capsules expose identity and name/length, but do not model * per-class static property tables yet. Never report a fabricated empty view. */ static bool scr_dyn_class_reflection_fence(const ScrDyn *value) { @@ -1816,6 +1831,149 @@ ScrDyn *scr_dyn_array_prototype_base(void) { return scr_dyn_retain(scr_builtin_array_prototype); } +/* Error.prototype as a checked-dynamic object, for JavaScript constructor + * functions that inherit from it (`F.prototype = + * Object.create(Error.prototype)`): non-enumerable `constructor`, `name` + * ("Error"), `message` ("") and `toString`, on Object.prototype. The Error + * constructor has no checked-dynamic value, so `constructor` is a function + * named Error that throws when called. */ +static const ScrDyn *scr_dyn_proto_next(const ScrDyn *object); +static void scr_builtin_error_prototype_cleanup(void) { + ScrDyn *prototype = scr_builtin_error_prototype; + scr_builtin_error_prototype = NULL; + scr_dyn_release(prototype); +} +static ScrDyn *scr_error_prototype_to_string(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (self->kind == SCR_DYN_OBJ || self->kind == SCR_DYN_ARR || self->kind == SCR_DYN_FUNC) { + ScrStr *text = scr_dyn_error_string(self); + if (text) { + result = scr_dyn_new_str(text); + scr_str_release(text); + } + } else { + ScrJsonBuf message; + scr_jb_init(&message); + scr_jb_puts(&message, "Method Error.prototype.toString called on incompatible receiver "); + ScrStr *shown = self->kind == SCR_DYN_SYMBOL ? self->v.symbol.render(self->v.symbol.value) + : scr_dyn_string_coerce_js(self); + if (shown) { + scr_jb_put_str(&message, shown); + scr_str_release(shown); + } + scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&message)); + } + scr_dyn_release(self); + return result; +} +static ScrDyn *scr_error_prototype_constructor(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; (void)args; (void)argc; + static const char message[] = "calling Error.prototype.constructor is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} +static void scr_error_prototype_hide(ScrDyn *object, const char *key, size_t key_len) { + ScrDynEntry *entry = scr_dyn_find_entry(object, key, key_len); + entry->enumerable = false; +} +ScrDyn *scr_dyn_error_prototype(void) { + if (!scr_builtin_error_prototype) { + ScrDyn *prototype = scr_dyn_alloc(SCR_DYN_OBJ); + scr_dyn_obj_set(prototype, "constructor", 11, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_prototype_constructor, 1, "native:Error", "Error")); + scr_error_prototype_hide(prototype, "constructor", 11); + ScrStr *name = scr_str_new("Error", 5); + scr_dyn_obj_set(prototype, "name", 4, scr_dyn_new_str(name)); + scr_str_release(name); + scr_error_prototype_hide(prototype, "name", 4); + ScrStr *empty = scr_str_new("", 0); + scr_dyn_obj_set(prototype, "message", 7, scr_dyn_new_str(empty)); + scr_str_release(empty); + scr_error_prototype_hide(prototype, "message", 7); + scr_dyn_obj_set(prototype, "toString", 8, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_error_prototype_to_string, 0, "native:Error", "toString")); + scr_error_prototype_hide(prototype, "toString", 8); + scr_builtin_error_prototype = prototype; + scr_atexit(scr_builtin_error_prototype_cleanup); + } + return scr_dyn_retain(scr_builtin_error_prototype); +} + +/* Error.captureStackTrace(target): defines a non-enumerable own `stack` + * on target. Node's value is the Error.prototype.toString text of target + * (read without calling an overriding toString) followed by stack frames; + * here, as for scriptc's own Error instances, it is the text alone. + * Answers undefined, or NULL with the exception pending. */ +ScrDyn *scr_dyn_error_capture_stack_trace(ScrDyn *target) { + /* An instance of a class that extends Error derives its stack from its + * name and message when the stack is read. */ + if (target->kind == SCR_DYN_TYPED_REF) { + ScrDyn *view = scr_dyn_typed_ref_materialize(target); + bool error = view && view->kind == SCR_DYN_OBJ && scr_dyn_obj_get(view, "%error", 6) != NULL; + scr_dyn_release(view); + if (scr_exc_pending()) return NULL; + if (error) return scr_dyn_retain(scr_dyn_undefined()); + } + if (target->kind != SCR_DYN_OBJ) { + if (target->kind == SCR_DYN_ARR || target->kind == SCR_DYN_FUNC || target->kind == SCR_DYN_TYPED_REF || + target->kind == SCR_DYN_HANDLE || target->kind == SCR_DYN_PROXY) { + static const char message[] = "Error.captureStackTrace on this object is not supported yet"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + } else { + static const char message[] = "invalid_argument"; + scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + } + return NULL; + } + ScrDynEntry *existing = scr_dyn_find_entry(target, "stack", 5); + if (existing ? existing->accessor || !existing->configurable : target->non_extensible) { + static const char message[] = "Cannot redefine property: stack"; + static const char extensible[] = "Cannot define property stack, object is not extensible"; + if (existing) scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); + else scr_throw_error_msg(SCR_ERR_TYPE, extensible, sizeof extensible - 1); + return NULL; + } + ScrStr *text = scr_dyn_error_string(target); + if (!text) return NULL; + scr_dyn_obj_set_field(target, "stack", 5, scr_dyn_new_str(text)); + scr_str_release(text); + ScrDynEntry *entry = scr_dyn_find_entry(target, "stack", 5); + entry->enumerable = false; + entry->writable = entry->configurable = true; + return scr_dyn_retain(scr_dyn_undefined()); +} +static ScrDyn *scr_error_capture_stack_trace_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + return scr_dyn_error_capture_stack_trace(argc > 0 ? args[0] : scr_dyn_undefined()); +} +/* Error.captureStackTrace read as a value: one function per thread. */ +static SCR_TL ScrDyn *scr_error_capture_stack_trace_fn; +static void scr_error_capture_stack_trace_fn_cleanup(void) { + ScrDyn *function = scr_error_capture_stack_trace_fn; + scr_error_capture_stack_trace_fn = NULL; + scr_dyn_release(function); +} +ScrDyn *scr_dyn_error_capture_stack_trace_function(void) { + if (!scr_error_capture_stack_trace_fn) { + scr_error_capture_stack_trace_fn = scr_dyn_new_func(scr_closure_new(NULL, 0), + scr_error_capture_stack_trace_call, 2, "native:Error", "captureStackTrace"); + scr_atexit(scr_error_capture_stack_trace_fn_cleanup); + } + return scr_dyn_retain(scr_error_capture_stack_trace_fn); +} + +/* `value instanceof Error` for a value that is not an Error instance: true + * when Error.prototype is on its prototype chain. */ +bool scr_dyn_inherits_error_prototype(const ScrDyn *value) { + if (!scr_builtin_error_prototype || !value || value->kind != SCR_DYN_OBJ) return false; + for (const ScrDyn *current = scr_dyn_proto_next(value); current; current = scr_dyn_proto_next(current)) { + if (current == scr_builtin_error_prototype) return true; + } + return false; +} + ScrDyn *scr_dyn_new_arr(void) { return scr_dyn_alloc(SCR_DYN_ARR); } ScrDyn *scr_dyn_new_obj(void) { return scr_dyn_alloc(SCR_DYN_OBJ); } ScrDyn *scr_dyn_new_obj_with_identity( @@ -1847,7 +2005,6 @@ ScrDyn *scr_dyn_new_obj_null_proto(void) { } static const ScrDyn *scr_dyn_chain_link(const ScrDyn *link); -static const ScrDyn *scr_dyn_proto_next(const ScrDyn *object); /* A typed capsule can be a prototype when its checked view is an object. */ static bool scr_dyn_typed_ref_prototype(const ScrDyn *value) { @@ -1942,7 +2099,7 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { object->kind == SCR_DYN_BIGINT || object->kind == SCR_DYN_SYMBOL) { return scr_dyn_prototype_valid(prototype) ? scr_dyn_retain(object) : NULL; } - if (scr_dyn_class_reflection_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL; + if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object) || scr_dyn_class_reflection_fence(prototype)) return NULL; if (object->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(object); if (scr_exc_pending()) { scr_dyn_release(view); return NULL; } @@ -2058,7 +2215,7 @@ static ScrDyn *scr_dyn_fn_existing_table(const ScrDyn *function) { } ScrDyn *scr_dyn_prevent_extensions(ScrDyn *object) { - if (scr_dyn_class_reflection_fence(object)) return NULL; + if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.preventExtensions"); return NULL; } if (scr_dyn_class_integrity_fence(object, "Object.preventExtensions")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(object); @@ -2089,7 +2246,7 @@ bool scr_dyn_is_extensible(ScrDyn *object) { } ScrDyn *scr_dyn_seal(ScrDyn *object) { - if (scr_dyn_class_reflection_fence(object)) return NULL; + if (scr_dyn_class_reflection_fence(object) || scr_dyn_error_prototype_fence(object)) return NULL; if (object->kind == SCR_DYN_PROXY) { scr_dyn_proxy_unsupported("Object.seal"); return NULL; } if (scr_dyn_class_integrity_fence(object, "Object.seal")) return NULL; if (object->symbol_properties) scr_dyn_release(scr_dyn_seal(object->symbol_properties)); @@ -3366,6 +3523,7 @@ const ScrStr *scr_dyn_typed_ref_class_name(const ScrDyn *d) { return ops ? ops->class_name : NULL; } + static bool scr_dyn_name_in_strs(const ScrStr *key, ScrStr *const *names, size_t count) { for (size_t i = 0; i < count; i++) { if (names[i] == key || scr_str_eq(names[i], (ScrStr *)key)) return true; @@ -3457,6 +3615,18 @@ ScrDyn *scr_dyn_index_get(const ScrDyn *d, double index) { } ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) { + /* An instance of a class declared `extends Date` converts through its + * toString, declared or inherited. With an exception pending the view is + * the empty string. */ + double time; + if (scr_dyn_date_class_time(d, &time)) { + ScrDyn *text = scr_dyn_date_class_to_string(d); + if (text) return text; + ScrStr *empty = scr_str_new("", 0); + ScrDyn *out = scr_dyn_new_str(empty); + scr_str_release(empty); + return out; + } ScrDyn *view = scr_dyn_typed_ref_materialize(d); const char *tag = scr_event_ref_hooks ? scr_event_ref_hooks->string_tag(d) : NULL; if (!tag || view->kind != SCR_DYN_OBJ || scr_dyn_property_owner(view, "toString", 8)) return view; @@ -3467,12 +3637,28 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) { return out; } +/* An inherited Date method of an instance of a class declared `extends + * Date`, when the instance's own members do not define `key`: +1, or + * NULL. */ +ScrDyn *scr_dyn_date_class_member(const ScrDyn *d, const char *key, size_t key_len) { + ScrDyn *method = scr_dyn_date_class_method(d, key, key_len); + if (!method) return NULL; + ScrDyn *view = scr_dyn_typed_ref_materialize(d); + bool declared = view && !scr_exc_pending() && scr_dyn_property_owner(view, key, key_len); + scr_dyn_release(view); + if (!declared) return method; + scr_dyn_release(method); + return NULL; +} + ScrDyn *scr_dyn_typed_ref_key_get(const ScrDyn *d, const ScrStr *key) { if (d->v.typed_ref.record) return scr_typed_ref_record_get(d, key); if (scr_event_ref_hooks) { ScrDyn *inherited = scr_event_ref_hooks->get(d, key); if (inherited) return inherited; } + ScrDyn *date_method = scr_dyn_date_class_member(d, key->data, key->len); + if (date_method) return date_method; if (!scr_dyn_typed_ref_array_ops(d)) return NULL; if (scr_typed_ref_is_length(key)) return scr_dyn_new_num((double)((ScrArr *)d->v.typed_ref.ptr)->len); size_t index; @@ -3708,6 +3894,32 @@ ScrDyn *scr_array_buffer_constructor(void) { return scr_dyn_retain(scr_array_buffer_ctor); } +/* `value instanceof C` for C a global constructor held as a value + * (`{ instanceOf: Date }`, scr_dyn_global_constructor): 1 or 0 by the + * value's native kind, or -1 when callee is not one of them, or for Object + * and for class instances, which these kinds cannot answer (except Date, + * whose subclasses' instances are known). A typed record or array is none + * of these kinds. */ +static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrDyn *callee) { + if (!callee->v.fn.sig || !callee->v.fn.name || strcmp(callee->v.fn.sig, "native:constructor") != 0 || + strcmp(callee->v.fn.name, "Object") == 0) + return -1; + /* A native Date, or an instance of a class declared `extends Date`. */ + if (strcmp(callee->v.fn.name, "Date") == 0) return scr_dyn_date_instance_is(value); + if (value->kind == SCR_DYN_TYPED_REF) + return strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value) ? 0 : -1; + static const struct { const char *name; int tag; } kinds[] = { + {"Date", SCR_DYNH_DATE}, {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, + {"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET}, + }; + if (strcmp(callee->v.fn.name, "Promise") == 0) return value->kind == SCR_DYN_PROMISE; + for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) { + if (strcmp(callee->v.fn.name, kinds[i].name) == 0) + return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == kinds[i].tag; + } + return -1; +} + bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { if (!callee || callee->kind != SCR_DYN_FUNC) { static const char message[] = "Right-hand side of 'instanceof' is not callable"; @@ -3720,6 +3932,38 @@ bool scr_bytes_instanceof(const ScrDyn *value, const ScrDyn *callee) { if (scr_bytes_ctors[i] && callee->v.fn.clo == scr_bytes_ctors[i]->v.fn.clo) return scr_dyn_typed_array_is(value, (int)i); } + int global = scr_dyn_global_constructor_instanceof(value, callee); + if (global >= 0) return global; + /* An ordinary constructor function (an untyped JavaScript function whose + * prototype is an object): OrdinaryHasInstance walks the value's + * prototype chain for callee.prototype. */ + { + ScrStr *name = scr_str_new("prototype", 9); + ScrDyn *key = scr_dyn_new_str(name); + scr_str_release(name); + ScrDyn *prototype = scr_dyn_reflect_get((ScrDyn *)callee, key, (ScrDyn *)callee); + scr_dyn_release(key); + if (!prototype) return false; /* the read threw: pending */ + if (prototype->kind == SCR_DYN_OBJ) { + bool found = false; + if (value->kind == SCR_DYN_OBJ || value->kind == SCR_DYN_ARR || value->kind == SCR_DYN_FUNC) { + ScrDyn *current = scr_dyn_get_prototype((ScrDyn *)value); + while (current && current->kind != SCR_DYN_NULL) { + if (current == prototype) { + found = true; + break; + } + ScrDyn *next = scr_dyn_get_prototype(current); + scr_dyn_release(current); + current = next; + } + if (current) scr_dyn_release(current); + } + scr_dyn_release(prototype); + return found; + } + scr_dyn_release(prototype); + } static const char message[] = "instanceof against this native constructor is not supported yet"; scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC1090"); return false; @@ -4309,6 +4553,7 @@ ScrDyn *scr_dyn_new_handle(void *h, ScrDynHandleTag tag) { d->v.handle.tag = tag; d->v.handle.traced = tag == SCR_DYNH_WEAK_MAP || tag == SCR_DYNH_WEAK_SET || tag == SCR_DYNH_WORKER || tag == SCR_DYNH_MESSAGE_PORT || tag == SCR_DYNH_DISPOSABLE_STACK || + tag == SCR_DYNH_CIPHER || tag == SCR_DYNH_DECIPHER || ((tag == SCR_DYNH_SET || tag == SCR_DYNH_MAP) && ((ScrMap *)h)->traced != 0); return d; @@ -6191,6 +6436,7 @@ void scr_dyn_symbol_key_placeholder(ScrDyn *value, ScrDyn *key) { } void scr_dyn_symbol_key_set(ScrDyn *value, ScrDyn *key, ScrDyn *stored) { + if (scr_dyn_error_prototype_fence(value)) return; ScrDyn *receiver = scr_dyn_symbol_receiver(value); if (!receiver) { if (!scr_exc_pending()) { @@ -6341,6 +6587,7 @@ static bool scr_dyn_fn_property_fence(const ScrStr *key) { } ScrDyn *scr_dyn_define_property(ScrDyn *target, ScrDyn *key, ScrDyn *descriptor) { + if (scr_dyn_error_prototype_fence(target)) return NULL; if (target->kind == SCR_DYN_TYPED_REF) { ScrDyn *view = scr_dyn_typed_ref_materialize(target); ScrDyn *defined = scr_exc_pending() ? NULL : scr_dyn_define_property(view, key, descriptor); @@ -7726,6 +7973,9 @@ ScrStr *scr_dyn_to_string(const ScrDyn *d, const ScrStr *enc) { } } +static ScrStr *scr_dyn_to_string_method_args(const ScrDyn *d, const ScrStr *enc, const ScrStr *what, + ScrDyn *const *args, size_t argc); + /* The METHOD-CALL spelling `d.toString(enc?)` — scr_dyn_to_string with * the one receiver whose prototype LACKS the method carved out: a * null-prototype dictionary (Object.create(null)) has no toString at @@ -7755,7 +8005,10 @@ ScrStr *scr_dyn_to_string_argument(const ScrDyn *d, const ScrDyn *argument, cons scr_throw_error(SCR_ERR_TYPE, scr_jb_finish(&b)); return scr_str_new("", 0); } - return scr_dyn_to_string_method(d, NULL, what); + /* A toString the program defines receives the argument + * (`error.toString(compact)`); undefined passes none. */ + ScrDyn *args[1] = {(ScrDyn *)argument}; + return scr_dyn_to_string_method_args(d, NULL, what, args, argument->kind == SCR_DYN_UNDEF ? 0 : 1); } /* The METHOD-CALL spelling `d.valueOf()`: a primitive answers itself (the @@ -7995,7 +8248,12 @@ bool scr_dyn_number_coerce_js(const ScrDyn *d, double *out) { return false; } if (d->kind == SCR_DYN_TYPED_REF) { - ScrDyn *materialized = scr_dyn_typed_ref_materialize(d); + /* An instance of a class declared `extends Date` converts through its + * valueOf, declared or inherited. */ + double time; + ScrDyn *materialized = scr_dyn_date_class_time(d, &time) ? scr_dyn_date_class_value_of(d) + : scr_dyn_typed_ref_materialize(d); + if (!materialized) return false; bool ok = scr_dyn_number_coerce_js(materialized, out); scr_dyn_release(materialized); return ok; @@ -8098,9 +8356,18 @@ double scr_dyn_number_coerce(const ScrDyn *d) { static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d); /* ToPrimitive with the number hint, which differs from the default hint - * only for a Date: its time value. */ + * only for a Date: its time value, or the valueOf of an instance of a class + * declared `extends Date`. */ static ScrDyn *scr_dyn_number_primitive(const ScrDyn *d) { - return scr_dyn_is_date(d) ? scr_dyn_date_value_of(d) : scr_dyn_add_primitive(d); + if (scr_dyn_is_date(d)) return scr_dyn_date_value_of(d); + double time; + if (d->kind == SCR_DYN_TYPED_REF && scr_dyn_date_class_time(d, &time)) { + ScrDyn *value = scr_dyn_date_class_value_of(d); + ScrDyn *out = value ? scr_dyn_add_primitive(value) : NULL; + scr_dyn_release(value); + return out; + } + return scr_dyn_add_primitive(d); } double scr_dyn_number_constructor(const ScrDyn *d) { @@ -8161,6 +8428,15 @@ static ScrDyn *scr_dyn_add_primitive(const ScrDyn *d) { return NULL; } if (d->kind == SCR_DYN_TYPED_REF) { + /* A Date's default hint is string: an instance of a class declared + * `extends Date` adds through its toString. */ + double time; + if (scr_dyn_date_class_time(d, &time)) { + ScrDyn *text = scr_dyn_date_class_to_string(d); + ScrDyn *out = text ? scr_dyn_add_primitive(text) : NULL; + scr_dyn_release(text); + return out; + } ScrDyn *view = scr_dyn_typed_ref_materialize(d); ScrDyn *out = scr_exc_pending() ? NULL : scr_dyn_add_primitive(view); scr_dyn_release(view); @@ -8580,7 +8856,7 @@ void scr_dyn_arr_named_put(ScrDyn *arr, const char *key, size_t key_len, ScrDyn } void scr_dyn_key_set(ScrDyn *recv, ScrStr *key, ScrDyn *value) { - if (scr_dyn_class_reflection_fence(recv)) return; + if (scr_dyn_class_reflection_fence(recv) || scr_dyn_error_prototype_fence(recv)) return; if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_set(recv, key, value); return; } if (recv->kind == SCR_DYN_TYPED_REF) { if (scr_typed_ref_array_set(recv, key, value)) return; @@ -10408,7 +10684,7 @@ static void scr_json_delete_member(ScrDyn *object, const ScrStr *key) { * arrays, typed references and handles keep their explicit boundary; * deleting from a materialized snapshot would lose the mutation. */ void scr_dyn_key_delete(ScrDyn *recv, const ScrStr *key, bool strict) { - if (scr_dyn_class_reflection_fence(recv)) return; + if (scr_dyn_class_reflection_fence(recv) || scr_dyn_error_prototype_fence(recv)) return; if (recv->kind == SCR_DYN_PROXY) { scr_dyn_proxy_delete(recv, key); return; } if (recv->kind == SCR_DYN_TYPED_REF && recv->v.typed_ref.commit && (!strncmp(recv->v.typed_ref.type_key, "record:", 7) || @@ -10634,6 +10910,16 @@ static bool scr_json_live_container(const ScrDyn *value, bool inspect) { * method, called with the key. Returns the value to serialize (+1), or NULL * with an exception pending. */ static ScrDyn *scr_json_to_json(ScrDyn *value, const ScrStr *key) { + /* An instance of a class declared `extends Date` converts through its + * toJSON, declared or inherited. */ + double time; + if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_date_class_time(value, &time)) { + ScrDyn *name = scr_dyn_new_str((ScrStr *)key); + ScrDyn *converted = scr_dyn_date_class_to_json(value, name); + scr_dyn_release(name); + scr_dyn_release(value); + return converted; + } /* Inspect a typed reference without replacing its live identity. Later * members must read the source again after a callback mutates it. A live * array, or a live record without a `toJSON` field, needs no conversion. */ @@ -12826,7 +13112,7 @@ static void scr_dyn_assign_from(ScrDyn *target, const ScrDyn *src, bool define) * nothing; index-keyed sources (arrays/strings/bytes) copy their index * keys like Node; the remaining kinds have no own enumerable keys. */ ScrDyn *scr_dyn_assign(ScrDyn *target, const ScrDyn *src) { - if (scr_dyn_class_reflection_fence(target) || scr_dyn_class_reflection_fence(src)) return NULL; + if (scr_dyn_class_reflection_fence(target) || scr_dyn_error_prototype_fence(target) || scr_dyn_class_reflection_fence(src)) return NULL; if (target->kind == SCR_DYN_UNDEF || target->kind == SCR_DYN_NULL) { const char *m = "Cannot convert undefined or null to object"; scr_throw_error_msg(SCR_ERR_TYPE, m, strlen(m)); @@ -14620,7 +14906,10 @@ ScrMap *scr_dyn_native_collection_check(const ScrDyn *value, int map, const char return scr_map_retain(value->v.handle.ptr); } -ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) { +/* scr_dyn_to_string_method, passing `args` to a toString the receiver's + * chain defines. */ +static ScrStr *scr_dyn_to_string_method_args(const ScrDyn *d, const ScrStr *enc, const ScrStr *what, + ScrDyn *const *args, size_t argc) { /* Object.create receivers must run their inherited formatter before the * default object tag; Effect shard keys use it to produce distinct hashes. */ const bool object_method = d->kind == SCR_DYN_OBJ && scr_dyn_property_owner(d, "toString", 8); @@ -14629,7 +14918,7 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt if (scr_exc_pending()) { scr_dyn_release(member); return NULL; } if (member && (object_method || member->kind != SCR_DYN_UNDEF)) { scr_dyn_this_push_dyn(d); - ScrDyn *value = scr_dyn_call(member, NULL, 0, what->data); + ScrDyn *value = scr_dyn_call(member, args, argc, what->data); scr_dyn_this_pop(); scr_dyn_release(member); if (!value) return NULL; @@ -14651,8 +14940,12 @@ ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrSt return scr_dyn_to_string(d, enc); } +ScrStr *scr_dyn_to_string_method(const ScrDyn *d, const ScrStr *enc, const ScrStr *what) { + return scr_dyn_to_string_method_args(d, enc, what, NULL, 0); +} + ScrDyn *scr_dyn_freeze(ScrDyn *value) { - if (scr_dyn_class_reflection_fence(value)) return NULL; + if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; ScrDyn *table = scr_dyn_fn_integrity_table(value); if (!table) return scr_dyn_freeze_data(value); @@ -14663,7 +14956,7 @@ ScrDyn *scr_dyn_freeze(ScrDyn *value) { } ScrDyn *scr_dyn_freeze_data(ScrDyn *value) { - if (scr_dyn_class_reflection_fence(value)) return NULL; + if (scr_dyn_class_reflection_fence(value) || scr_dyn_error_prototype_fence(value)) return NULL; if (scr_dyn_class_integrity_fence(value, "Object.freeze")) return NULL; if (value->symbol_properties) scr_dyn_release(scr_dyn_freeze_data(value->symbol_properties)); if (value->kind == SCR_DYN_ARR) { diff --git a/packages/runtime/src/scr_lib.c b/packages/runtime/src/scr_lib.c index ee4c34592..9bebdec5e 100644 --- a/packages/runtime/src/scr_lib.c +++ b/packages/runtime/src/scr_lib.c @@ -13008,9 +13008,116 @@ bool scr_dyn_native_date_is(const ScrDyn *value) { return value && value->kind == SCR_DYN_HANDLE && value->v.handle.tag == SCR_DYNH_DATE; } +/* Instances of program classes declared `extends Date` keep their time + * value in a compiled field. In checked-dynamic code such an instance is a + * typed reference, and the runtime reaches its time value and methods + * through the functions %main installs (builtins/date-classes.ts), called + * as (op, value, argument): for op 0 the time function answers the time + * value, or undefined for any other value, and for op 1 sets it; for ops 2 + * to 5 the method function calls the instance's toISOString, + * toJSON(argument), toString and valueOf. */ +enum { + SCR_DATE_CLASS_TIME, + SCR_DATE_CLASS_SET_TIME, + SCR_DATE_CLASS_TO_ISO_STRING, + SCR_DATE_CLASS_TO_JSON, + SCR_DATE_CLASS_TO_STRING, + SCR_DATE_CLASS_VALUE_OF, + SCR_DATE_CLASS_NAME, +}; +static SCR_TL ScrDyn *scr_date_class_operation; +static SCR_TL ScrDyn *scr_date_class_methods; + +static void scr_date_class_operation_cleanup(void) { + ScrDyn *operation = scr_date_class_operation; + ScrDyn *methods = scr_date_class_methods; + scr_date_class_operation = scr_date_class_methods = NULL; + scr_dyn_release(operation); + scr_dyn_release(methods); +} + +static bool scr_date_class_time_of(const ScrDyn *value, double *time); +static ScrDyn *scr_date_class_to_json(const ScrDyn *value, const ScrDyn *key); +static ScrDyn *scr_date_class_to_string(const ScrDyn *value); +static ScrDyn *scr_date_class_value_of(const ScrDyn *value); +static ScrStr *scr_date_class_name(const ScrDyn *value); +static ScrDyn *scr_date_class_method(const ScrDyn *value, const char *key, size_t key_len); + +/* The installation also publishes these functions to the rest of the + * runtime (scr_date_class_hooks), so only a program that declares a class + * extending Date links them. */ +static const ScrDateClassHooks scr_date_class_hook_table = { + scr_date_class_time_of, scr_date_class_to_json, scr_date_class_to_string, + scr_date_class_value_of, scr_date_class_name, scr_date_class_method, +}; + +void scr_date_class_operation_install(ScrDyn *time, ScrDyn *methods) { + scr_date_class_hooks = &scr_date_class_hook_table; + if (!scr_date_class_operation) scr_atexit(scr_date_class_operation_cleanup); + scr_dyn_release(scr_date_class_operation); + scr_dyn_release(scr_date_class_methods); + scr_date_class_operation = scr_dyn_retain(time); + scr_date_class_methods = scr_dyn_retain(methods); +} + +/* The installed operation `op` on `value`: +1, or NULL with an exception + * pending. */ +static ScrDyn *scr_date_class_call(int op, const ScrDyn *value, const ScrDyn *argument) { + ScrDyn *code = scr_dyn_new_num(op); + ScrDyn *args[] = { code, (ScrDyn *)value, (ScrDyn *)(argument ? argument : scr_dyn_undefined()) }; + bool time = op < SCR_DATE_CLASS_TO_ISO_STRING || op == SCR_DATE_CLASS_NAME; + ScrDyn *result = scr_dyn_call(time ? scr_date_class_operation : scr_date_class_methods, args, 3, "Date"); + scr_dyn_release(code); + return result; +} + +/* True, with the time value in *time, when `value` holds an instance of a + * class declared `extends Date`. */ +static bool scr_date_class_time_of(const ScrDyn *value, double *time) { + if (!scr_date_class_operation || !value || value->kind != SCR_DYN_TYPED_REF) return false; + ScrDyn *result = scr_date_class_call(SCR_DATE_CLASS_TIME, value, NULL); + bool found = result && result->kind == SCR_DYN_NUM; + if (found) *time = result->v.num; + scr_dyn_release(result); + return found; +} + +/* `value instanceof Date`: a Date, or an instance of a class declared + * `extends Date`. */ +bool scr_dyn_date_instance_is(const ScrDyn *value) { + double time; + return scr_dyn_native_date_is(value) || scr_dyn_date_class_time(value, &time); +} + +/* toJSON(key), toString() or valueOf() of an instance of a class declared + * `extends Date` (scr_dyn_date_class_time answered true): the class's + * declaration or the Date method. +1, or NULL with an exception pending. */ +static ScrDyn *scr_date_class_to_json(const ScrDyn *value, const ScrDyn *key) { + return scr_date_class_call(SCR_DATE_CLASS_TO_JSON, value, key); +} +static ScrDyn *scr_date_class_to_string(const ScrDyn *value) { + return scr_date_class_call(SCR_DATE_CLASS_TO_STRING, value, NULL); +} +static ScrDyn *scr_date_class_value_of(const ScrDyn *value) { + return scr_date_class_call(SCR_DATE_CLASS_VALUE_OF, value, NULL); +} + +/* The name of the class of an instance of a class declared `extends + * Date`: +1, or NULL for any other value. */ +static ScrStr *scr_date_class_name(const ScrDyn *value) { + if (!scr_date_class_operation || !value || value->kind != SCR_DYN_TYPED_REF) return NULL; + ScrDyn *result = scr_date_class_call(SCR_DATE_CLASS_NAME, value, NULL); + ScrStr *name = result && result->kind == SCR_DYN_STR ? scr_str_retain(result->v.str) : NULL; + scr_dyn_release(result); + return name; +} + double scr_dyn_native_date_value(const ScrDyn *value) { - if (!scr_dyn_native_date_is(value)) { scr_dyn_check_fail(NULL, "Date", value); return NAN; } - return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; + if (scr_dyn_native_date_is(value)) return ((ScrNativeDate *)value->v.handle.ptr)->milliseconds; + double time; + if (scr_dyn_date_class_time(value, &time)) return time; + scr_dyn_check_fail(NULL, "Date", value); + return NAN; } /* Method names without another use in this file, in named arrays (see @@ -13020,13 +13127,14 @@ static const char scr_date_to_time_string_name[] = "toTimeString"; static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { - (void)self; (void)args; (void)argc; (void)what; + (void)self; (void)what; double ms = ((ScrNativeDate *)ptr)->milliseconds; if (!strcmp(method, "getTime") || !strcmp(method, "valueOf")) return scr_dyn_new_num(ms); if (!strcmp(method, "toString") || !strcmp(method, scr_date_to_date_string_name) || - !strcmp(method, scr_date_to_time_string_name)) { + !strcmp(method, scr_date_to_time_string_name) || !strcmp(method, "toUTCString")) { ScrStr *text = !strcmp(method, "toString") ? scr_date_to_string(ms) : !strcmp(method, scr_date_to_date_string_name) ? scr_date_to_date_string(ms) + : !strcmp(method, "toUTCString") ? scr_date_to_utc_string(ms) : scr_date_to_time_string(ms); ScrDyn *result = scr_dyn_new_str(text); scr_str_release(text); @@ -13040,6 +13148,12 @@ static ScrDyn *scr_native_date_invoke(void *ptr, ScrDyn *self, const char *metho scr_str_release(text); return result; } + if (!strcmp(method, "setTime")) { + double time = NAN; + if (argc > 0 && !scr_dyn_number_coerce_js(args[0], &time)) return NULL; + ((ScrNativeDate *)ptr)->milliseconds = scr_date_new_ms(time); + return scr_dyn_new_num(((ScrNativeDate *)ptr)->milliseconds); + } #define DATE_GET(name, fn) if (!strcmp(method, name)) return scr_dyn_new_num(fn(ms)) DATE_GET("getFullYear", scr_date_get_full_year_local); DATE_GET("getUTCFullYear", scr_date_get_full_year_utc); @@ -13070,13 +13184,25 @@ ScrDyn *scr_dyn_native_date_new(const ScrDyn *arguments) { else { ScrDyn *value = arguments->v.arr.items[0]; if (scr_dyn_native_date_is(value)) ms = scr_dyn_native_date_value(value); - else if (value->kind == SCR_DYN_STR) ms = scr_date_parse_get_time(value->v.str); + else if (scr_dyn_date_class_time(value, &ms)) { + } else if (value->kind == SCR_DYN_STR) ms = scr_date_parse_get_time(value->v.str); else if (!scr_dyn_number_coerce_js(value, &ms)) return NULL; ms = scr_date_new_ms(ms); } return scr_dyn_native_date_from_ms(ms); } +/* The time value `new Date(...arguments)` constructs (super(...) in a + * class declared `extends Date`), or NaN with an exception pending. */ +double scr_dyn_date_arguments_time(const ScrDyn *arguments) { + ScrDyn *date = arguments->v.arr.len > 1 ? scr_dyn_native_date_new_fields(arguments) + : scr_dyn_native_date_new_value(arguments); + if (!date) return NAN; + double time = ((ScrNativeDate *)date->v.handle.ptr)->milliseconds; + scr_dyn_release(date); + return time; +} + /* new Date(value) in JavaScript: a string parses with the full parser; * every other value takes scr_dyn_native_date_new. */ ScrDyn *scr_dyn_native_date_new_value(const ScrDyn *arguments) { @@ -13103,11 +13229,11 @@ ScrDyn *scr_dyn_native_date_new_fields(const ScrDyn *arguments) { /* The Date.prototype methods scr_native_date_invoke answers. */ static const char *const scr_native_date_methods[] = { "getTime", "valueOf", "toISOString", "toJSON", "toString", scr_date_to_date_string_name, - scr_date_to_time_string_name, + scr_date_to_time_string_name, "toUTCString", "getFullYear", "getUTCFullYear", "getMonth", "getUTCMonth", "getDate", "getUTCDate", "getDay", "getUTCDay", "getHours", "getUTCHours", "getMinutes", "getUTCMinutes", "getSeconds", "getUTCSeconds", "getMilliseconds", - "getUTCMilliseconds", "getTimezoneOffset", + "getUTCMilliseconds", "getTimezoneOffset", "setTime", }; #define SCR_NATIVE_DATE_METHODS (sizeof scr_native_date_methods / sizeof scr_native_date_methods[0]) static SCR_TL ScrDyn *scr_native_date_method_values[SCR_NATIVE_DATE_METHODS]; @@ -13122,13 +13248,34 @@ static void scr_native_date_methods_cleanup(void) { } /* A Date method read as a value and called later: the closure's function - * slot holds the method's name, and the receiver must be a Date. */ + * slot holds the method's name, and the receiver must be a Date. On an + * instance of a class declared `extends Date` the method runs over its + * time value; toJSON calls the instance's toISOString. */ static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { const char *method = closure->fn; ScrDyn *self = scr_dyn_this_get(); ScrDyn *result = NULL; + double time; if (scr_dyn_native_date_is(self)) { result = scr_native_date_invoke(self->v.handle.ptr, self, method, args, argc, method); + } else if (scr_dyn_date_class_time(self, &time)) { + if (!strcmp(method, "toJSON")) { + result = isfinite(time) ? scr_date_class_call(SCR_DATE_CLASS_TO_ISO_STRING, self, NULL) + : scr_dyn_new_null(); + } else { + ScrNativeDate date = { 1, time }; + result = scr_native_date_invoke(&date, self, method, args, argc, method); + if (result && date.milliseconds != time && !(isnan(date.milliseconds) && isnan(time))) { + ScrDyn *stored = scr_dyn_new_num(date.milliseconds); + ScrDyn *set = scr_date_class_call(SCR_DATE_CLASS_SET_TIME, self, stored); + scr_dyn_release(stored); + if (!set) { + scr_dyn_release(result); + result = NULL; + } + scr_dyn_release(set); + } + } } else { static const char message[] = "this is not a Date object."; scr_throw_error_msg(SCR_ERR_TYPE, message, sizeof message - 1); @@ -13137,6 +13284,21 @@ static ScrDyn *scr_native_date_method_call(ScrClosure *closure, ScrDyn *const *a return result; } +static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len); + +/* The Date method `key` as a function value when `value` holds an instance + * of a class declared `extends Date`, or NULL. The class's own members are + * read first, by the caller. */ +static ScrDyn *scr_date_class_method(const ScrDyn *value, const char *key, size_t key_len) { + if (!scr_date_class_operation) return NULL; + bool named = false; + for (size_t i = 0; i < SCR_NATIVE_DATE_METHODS && !named; i++) + named = strlen(scr_native_date_methods[i]) == key_len && !memcmp(scr_native_date_methods[i], key, key_len); + double time; + if (!named || !scr_dyn_date_class_time(value, &time)) return NULL; + return scr_native_date_get(NULL, key, key_len); +} + /* Property reads of a Date: its methods as function values, one per name * and thread, and NULL (undefined) for other names. */ static ScrDyn *scr_native_date_get(void *ptr, const char *key, size_t key_len) { diff --git a/packages/runtime/src/scr_lib.h b/packages/runtime/src/scr_lib.h index 411d1e684..95f67044a 100644 --- a/packages/runtime/src/scr_lib.h +++ b/packages/runtime/src/scr_lib.h @@ -366,6 +366,9 @@ typedef struct ScrBytes ScrBytes; ScrStr *scr_atomics_wait(ScrBytes *, double, double, double); double scr_atomics_notify(ScrBytes *, double, double); double scr_atomics_op(ScrBytes *, double, double, double, double); +double scr_atomics_op_dyn(const ScrDyn *, double, double, double, double); +ScrStr *scr_atomics_wait_dyn(const ScrDyn *, double, double, double); +double scr_atomics_notify_dyn(const ScrDyn *, double, double); /* isatty(3) over an fd (0/1/2): a real boolean — false where Node's * non-TTY streams expose undefined (SEMANTICS.md). stdin_destroy is a diff --git a/packages/runtime/src/scr_shared.c b/packages/runtime/src/scr_shared.c index 2dac5a4d1..6505a71ab 100644 --- a/packages/runtime/src/scr_shared.c +++ b/packages/runtime/src/scr_shared.c @@ -7,7 +7,9 @@ #include "scr_numeric.h" #include #include +#include #include +#include #include #if defined(SCR_WORKERS) || (defined(SCR_LIB) && defined(SCR_THREAD_INSTANCES)) #ifdef _WIN32 @@ -360,3 +362,69 @@ double scr_atomics_notify(ScrBytes *bytes, double index, double count) { scr_wait_unlock(bucket); return notified; } + +/* V8's text for a value in the TypeErrors below: a typed array as + * [object Int8Array] and so on, an array as [object Array], an ArrayBuffer + * or other native object as #, an object as #, and + * other values as String() gives them (undefined and null by name). */ +static ScrStr *scr_atomics_value_text(const ScrDyn *value) { + char text[64]; + int n; + if (value->kind == SCR_DYN_BYTES) { + if (value->v.bytes->is_data_view) n = snprintf(text, sizeof text, "#"); + else n = snprintf(text, sizeof text, "[object %s]", scr_bytes_elem_name(value->v.bytes->elem)); + } else if (value->kind == SCR_DYN_ARR) { + n = snprintf(text, sizeof text, "[object Array]"); + } else if (value->kind == SCR_DYN_HANDLE) { + n = snprintf(text, sizeof text, "#<%s>", scr_dyn_handle_ops_of(value)->cls); + } else if (value->kind == SCR_DYN_OBJ) { + const char *name = scr_dyn_class_name(value); + n = snprintf(text, sizeof text, "#<%s>", name ? name : "Object"); + } else if (value->kind == SCR_DYN_TYPED_REF && scr_dyn_typed_ref_class_name(value)) { + const ScrStr *name = scr_dyn_typed_ref_class_name(value); + n = snprintf(text, sizeof text, "#<%.*s>", (int)name->len, (const char *)name->data); + } else if (value->kind == SCR_DYN_UNDEF) { + n = snprintf(text, sizeof text, "undefined"); + } else if (value->kind == SCR_DYN_NULL) { + n = snprintf(text, sizeof text, "null"); + } else { + return scr_dyn_to_string(value, NULL); + } + return scr_str_new(text, n < (int)sizeof text ? (size_t)n : sizeof text - 1); +} + +/* The integer typed array an untyped Atomics argument holds, or NULL with + * Node's TypeError pending. `waitable` requires an Int32Array, as wait and + * notify do. */ +static ScrBytes *scr_atomics_dyn_array(const ScrDyn *value, bool waitable) { + if (value->kind == SCR_DYN_BYTES && !value->v.bytes->is_data_view) { + ScrBytesElem elem = value->v.bytes->elem; + bool integer = elem == SCR_BYTES_I8 || elem == SCR_BYTES_U8 || elem == SCR_BYTES_I16 || + elem == SCR_BYTES_U16 || elem == SCR_BYTES_I32 || elem == SCR_BYTES_U32; + if (waitable ? elem == SCR_BYTES_I32 : integer) return value->v.bytes; + } + ScrStr *shown = scr_atomics_value_text(value); + if (!shown) return NULL; + const char *tail = waitable ? " is not an int32 or BigInt64 typed array." : " is not an integer typed array."; + ScrStr *parts[2] = {shown, scr_str_new(tail, strlen(tail))}; + ScrStr *message = scr_str_concat_parts(parts, 2); + scr_str_release(parts[0]); + scr_str_release(parts[1]); + scr_throw_error(SCR_ERR_TYPE, message); + return NULL; +} + +double scr_atomics_op_dyn(const ScrDyn *array, double index, double value, double replacement, double operation) { + ScrBytes *bytes = scr_atomics_dyn_array(array, false); + return bytes ? scr_atomics_op(bytes, index, value, replacement, operation) : 0; +} + +ScrStr *scr_atomics_wait_dyn(const ScrDyn *array, double index, double expected, double timeout) { + ScrBytes *bytes = scr_atomics_dyn_array(array, true); + return bytes ? scr_atomics_wait(bytes, index, expected, timeout) : NULL; +} + +double scr_atomics_notify_dyn(const ScrDyn *array, double index, double count) { + ScrBytes *bytes = scr_atomics_dyn_array(array, true); + return bytes ? scr_atomics_notify(bytes, index, count) : 0; +} diff --git a/packages/runtime/src/scr_stream.c b/packages/runtime/src/scr_stream.c index f7330b615..271937f2e 100644 --- a/packages/runtime/src/scr_stream.c +++ b/packages/runtime/src/scr_stream.c @@ -175,6 +175,11 @@ struct ScrStreamState { ScrStreamPlainInv flush_inv; bool flush_called; bool flush_pending; /* _flush called, its callback not yet run */ + /* Transform._write's backpressure: the readable length when the + * transform started, and whether its completed write waits for the + * readable side to read (Node's kCallback). */ + size_t xf_length; + bool xf_held; /* A _final that completes at once (zlib's ZlibBase): 'prefinish' starts * the flush and 'finish' does not wait for it. */ bool flush_on_prefinish; @@ -412,6 +417,7 @@ typedef enum { SCR_ST_ERROR_BUFFER, /* errorBuffer: buffered write and end() callbacks */ SCR_ST_CONSTRUCT, /* constructNT: run the construct hook */ SCR_ST_CONSTRUCTED, /* onConstruct: resume reads/writes/destroy */ + SCR_ST_XF_DONE, /* a transform's write callback after push(null) */ } ScrStreamTickOp; typedef struct ScrStreamTick { @@ -688,8 +694,13 @@ static void scr_stream_call_read(ScrStream *s) { } if (st->r.read_cb == NULL) { /* transform reads are push-driven; the attempt still clears the - * initial sync flag (Node's Transform._read ran) */ + * initial sync flag (Node's Transform._read ran) and runs a held + * write callback */ st->r.in_read_sync = false; + if (st->xf_held) { + st->xf_held = false; + scr_stream_write_done(s, NULL); + } return; } st->r.reading = true; @@ -1706,17 +1717,20 @@ static void scr_stream_wq_push(ScrStreamState *st, ScrBytes *chunk /*moves*/, Sc st->w.n++; } +static void scr_stream_transform_callback(ScrStream *s); + static void scr_stream_do_write(ScrStream *s, ScrBytes *chunk /*moves*/, ScrClosure *cb /*moves*/) { ScrStreamState *st = s->st; st->w.writing = true; st->w.wsync = true; st->w.inflight_len = chunk->len; st->w.inflight_cb = cb; + if (st->is_transform || st->passthrough) st->xf_length = st->r.length; if (st->passthrough && st->transform_cb == NULL) { /* identity: push through, complete synchronously */ scr_stream_add_chunk(s, chunk, false); scr_bytes_release(chunk); - if (!scr_exc_pending()) scr_stream_write_done(s, NULL); + if (!scr_exc_pending()) scr_stream_transform_callback(s); st->w.wsync = false; return; } @@ -3294,13 +3308,13 @@ ScrDyn *scr_stream_from_done(void) { } /* ── pipeline function stages (pipeline.js: makeAsyncIterable, pumpToNode) ── - * A run of async generator functions between two node streams. Each - * stage is called while the pipeline is set up, in pipeline order: - * makeAsyncIterable hands the first one the previous stream itself (a - * Readable is async iterable) and each later one the previous stage's - * generator. The last generator is pumped into the stream after the run - * with pumpToNode. The stages and helpers use the closure ABI (the - * closure first; the callee owns its parameters). */ + * Runs of async generator functions between node streams. Each stage is + * called while the pipeline is set up, in pipeline order: + * makeAsyncIterable hands a stage that follows a stream the stream itself + * (a Readable is async iterable) and any other stage the previous stage's + * generator. The last generator of a run that a stream follows is pumped + * into that stream with pumpToNode. The stages and helpers use the closure + * ABI (the closure first; the callee owns its parameters). */ typedef ScrPromise *(*ScrPipeNextFn)(ScrClosure *, ScrGen *); typedef ScrDyn *(*ScrPipeReadFn)(ScrClosure *, void *); @@ -3309,20 +3323,25 @@ typedef ScrDyn *(*ScrPipeBoxFn)(ScrClosure *, ScrPromise *); enum { SCR_PIPE_DEST_STREAM, SCR_PIPE_DEST_PROMISE = 2 }; typedef struct { - size_t at; /* streams[0..at-1] come before the stages */ size_t count; /* the function stages */ + uint64_t positions; /* bit j: the pipeline's j-th stage is a function */ /* stage(source[, { signal }]) answers an async generator, or a promise * for a promise destination; the source is absent for a source - * function, else the previous stream or async generator */ + * function (position 0), else the previous stream or async generator */ ScrClosure **stages; - bool source; /* stages[0] is the source (at == 0) */ int dest; /* the last stage: SCR_PIPE_DEST_* */ unsigned opts_mask; /* bit i: stages[i] takes the { signal } argument */ ScrDyn *signal; /* the pipeline's AbortSignal when opts_mask != 0 */ - ScrClosure *next, *read, *ret; /* the pumped generator's helpers, or NULL */ + /* (next, read, return) for each generator pumped into a stream, in + * pipeline order, or NULL */ + ScrClosure **helpers; ScrClosure *box; /* SCR_PIPE_DEST_PROMISE: boxes its promise as a dyn promise */ } ScrPipeStages; +static bool scr_pipe_is_stage(const ScrPipeStages *fs, size_t j) { + return fs && j < 64 && ((fs->positions >> j) & 1u); +} + /* stages[i](source[, { signal }]): the callee owns its parameters. */ static void *scr_pipe_call_stage(const ScrPipeStages *fs, size_t i, void *src /*moves*/) { ScrClosure *st = fs->stages[i]; @@ -3331,7 +3350,7 @@ static void *scr_pipe_call_stage(const ScrPipeStages *fs, size_t i, void *src /* opts = scr_dyn_new_obj(); scr_dyn_obj_set(opts, "signal", 6, scr_dyn_retain(fs->signal)); } - if (i == 0 && fs->source) { + if (i == 0 && (fs->positions & 1u)) { return opts ? ((void *(*)(ScrClosure *, ScrDyn *))st->fn)(st, opts) : ((void *(*)(ScrClosure *))st->fn)(st); } @@ -3580,17 +3599,17 @@ static void scr_pump_after_wait(ScrPump *pm, int stage, bool rejected) { } /* pumpToNode(gen, w, finish, { end }): starts at once, as the async - * function does. The helpers are borrowed. */ -static void scr_pump_start(ScrStreamPipeCtx *ctx, ScrGen *gen /*moves*/, const ScrPipeStages *fs, + * function does. The helpers (next, read, return) are borrowed. */ +static void scr_pump_start(ScrStreamPipeCtx *ctx, ScrGen *gen /*moves*/, ScrClosure *const *helpers, ScrStream *w, bool end) { ScrPump *pm = calloc(1, sizeof *pm); if (!pm) scr_stream_oom(); pm->rc = 1; pm->ctx = scr_stream_pipectx_retain_v(ctx); pm->gen = gen; - pm->next = scr_closure_retain(fs->next); - pm->read = scr_closure_retain(fs->read); - pm->ret = scr_closure_retain(fs->ret); + pm->next = scr_closure_retain(helpers[0]); + pm->read = scr_closure_retain(helpers[1]); + pm->ret = scr_closure_retain(helpers[2]); pm->w = scr_stream_retain(w); pm->end = end; pm->drain_l = scr_pump_closure((void *)&scr_pump_drain_l, pm); @@ -3721,27 +3740,27 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu ctx->dispose = add(signal, listener); scr_dyn_release(listener); } - /* pipelineImpl's loop over the stages: the n streams, with fs->count - * function stages before streams[fs->at] */ + /* pipelineImpl's loop over the stages: the n streams and fs->count + * function stages, at the positions fs->positions names */ size_t stages = n + (fs ? fs->count : 0); - size_t fn_first = fs ? fs->at : stages, fn_end = fs ? fs->at + fs->count : stages; ScrStream *ret = NULL; ScrGen *ret_gen = NULL; /* +1: ret when the previous stage was a function */ size_t i = 0; /* the next stream */ + size_t k = 0; /* the next function stage */ + size_t h = 0; /* the next pumped generator's helpers */ for (size_t j = 0; j < stages; j++) { bool reading = j + 1 < stages; bool end = reading || end_opt; bool last = j + 1 == stages; - if (j >= fn_first && j < fn_end) { + if (scr_pipe_is_stage(fs, j)) { /* ret = stage({ signal }) for the source, else * stage(makeAsyncIterable(ret), { signal }) */ - size_t k = j - fn_first; bool dest = last && fs->dest != SCR_PIPE_DEST_STREAM; - void *src = k == 0 && fs->source ? NULL - : ret_gen ? (void *)ret_gen - : (void *)scr_stream_retain(ret); + void *src = j == 0 ? NULL + : ret_gen ? (void *)ret_gen + : (void *)scr_stream_retain(ret); ret_gen = NULL; - void *out = scr_pipe_call_stage(fs, k, src); + void *out = scr_pipe_call_stage(fs, k++, src); if (scr_exc_pending()) { if (out && dest) scr_promise_release(out); else if (out) scr_gen_release(out); @@ -3776,7 +3795,7 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu } ScrStream *s = streams[i]; bool writing = j > 0; - if (reading && !(j + 1 >= fn_first && j + 1 < fn_end) && + if (reading && !scr_pipe_is_stage(fs, j + 1) && (streams[i + 1]->st->close_emitted || streams[i + 1]->st->destroyed)) { scr_pipe_throw("ERR_STREAM_UNABLE_TO_PIPE", "Cannot pipe to a closed or destroyed stream", SCR_ERR_ERROR); @@ -3803,7 +3822,7 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu ctx->finish_count++; ScrGen *g = ret_gen; ret_gen = NULL; - scr_pump_start(ctx, g, fs, s, end); + scr_pump_start(ctx, g, fs->helpers + 3 * h++, s, end); } else if (j > 0) { if (!ret->st->has_r) { static const char msg[] = @@ -3841,12 +3860,12 @@ static ScrStream *scr_stream_pipeline_ex(size_t n, ScrStream **streams, ScrClosu return scr_stream_retain(ret); } -static ScrPipeStages scr_pipe_stages_of(double at, double count, ScrClosure **stages, double flags, - double mask, ScrClosure *next, ScrClosure *read, - ScrClosure *ret, ScrClosure *box, ScrDyn *signal) { +static ScrPipeStages scr_pipe_stages_of(double count, ScrClosure **stages, double positions, + double flags, double mask, ScrClosure **helpers, + ScrClosure *box, ScrDyn *signal) { int bits = (int)flags; - ScrPipeStages fs = {(size_t)at, (size_t)count, stages, (bits & 1) != 0, (bits >> 1) & 3, - (unsigned)mask, signal, next, read, ret, box}; + ScrPipeStages fs = {(size_t)count, (uint64_t)positions, stages, (bits >> 1) & 3, (unsigned)mask, + signal, helpers, box}; return fs; } @@ -3864,25 +3883,25 @@ static void scr_pipe_dyn_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, if (r != NULL) scr_dyn_release(r); } -/* pipeline(...) with a run of function stages (see ScrPipeStages): flags - * bit 0 marks a source function and bits 1-2 the destination kind; mask - * names the stages taking { signal }. The helpers, box, signal and abort - * are borrowed (each NULL when unused); cb moves. */ -ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, ScrDyn *signal, ScrClosure *abort, - ScrClosure *cb, ScrStreamErrInv inv) { - ScrPipeStages fs = scr_pipe_stages_of(at, count, stages, flags, mask, next, read, ret, box, signal); +/* pipeline(...) with function stages (see ScrPipeStages): positions + * marks the function stages among the n + count stages, flags bits 1-2 + * give the destination kind, and mask names the stages taking + * { signal }. The helpers, box, signal and abort are borrowed (each NULL + * when unused); cb moves. */ +ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort, ScrClosure *cb, + ScrStreamErrInv inv) { + ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); return scr_stream_pipeline_ex((size_t)n, streams, cb, inv, true, NULL, NULL, &fs, abort, NULL); } -ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, ScrDyn *signal, ScrClosure *abort, - ScrDyn *cb) { - ScrPipeStages fs = scr_pipe_stages_of(at, count, stages, flags, mask, next, read, ret, box, signal); +ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort, ScrDyn *cb) { + ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); ScrClosure *c = scr_stream_dynopt_clo(cb, (void *)&scr_stream_finished_dyn_inv); /* a function destination's value reaches the callback */ ScrPipeValInv vi = fs.dest != SCR_PIPE_DEST_STREAM ? &scr_pipe_dyn_value_inv : NULL; @@ -4263,12 +4282,12 @@ static void scr_sp_value_inv(ScrClosure *cb, ScrError *err, ScrDyn *value, ScrPr scr_promise_release(p); } -ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, ScrDyn *signal, ScrClosure *abort) { +ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + ScrDyn *signal, ScrClosure *abort) { ScrPromise *p = scr_promise_new(); - ScrPipeStages fs = scr_pipe_stages_of(at, count, stages, flags, mask, next, read, ret, box, signal); + ScrPipeStages fs = scr_pipe_stages_of(count, stages, positions, flags, mask, helpers, box, signal); ScrPipeValInv vi = fs.dest != SCR_PIPE_DEST_STREAM ? &scr_sp_value_inv : NULL; ScrStream *dst = scr_stream_pipeline_ex((size_t)n, streams, scr_sp_watcher(p), &scr_sp_settle_inv, true, NULL, NULL, &fs, abort, vi); @@ -5800,6 +5819,19 @@ void scr_stream_destroy_done(ScrStream *s, ScrError *err /*moves*/) { } } +/* Transform._write's callback after the transform pushed its output: + * on a tick once the readable side has ended, at once when the writable + * side has ended, the transform pushed nothing, or the readable buffer is + * below its high-water mark, and otherwise when the readable side next + * reads (Transform._read). */ +static void scr_stream_transform_callback(ScrStream *s) { + ScrStreamState *st = s->st; + if (st->r.ended) scr_st_tick(s, SCR_ST_XF_DONE, NULL, NULL); + else if (st->w.ending || st->xf_length == st->r.length || st->r.length < st->r.hwm) + scr_stream_write_done(s, NULL); + else st->xf_held = true; +} + void scr_stream_transform_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes *data /*moves*/, ScrStr *data_str /*moves*/) { if (!err) { @@ -5820,7 +5852,8 @@ void scr_stream_transform_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes * if (err) scr_error_release(err); return; } - scr_stream_write_done(s, err); + if (err) scr_stream_write_done(s, err); + else scr_stream_transform_callback(s); } void scr_stream_flush_done(ScrStream *s, ScrError *err /*moves*/, ScrBytes *data /*moves*/, @@ -6171,6 +6204,9 @@ static void scr_stream_run_tick(ScrStreamTick *t) { } break; } + case SCR_ST_XF_DONE: + if (st->w.writing) scr_stream_write_done(s, NULL); + break; case SCR_ST_AFTER_WRITE: { st->w.after_scheduled = false; ScrClosure *cb = t->cb; @@ -6369,8 +6405,11 @@ static void scr_stream_dispatch(void) { } } +static const ScrHandleStreamHooks scr_handle_stream_hooks_impl; + void scr_stream_install(void) { scr_emitter_on_hook = &scr_stream_on_listener; + scr_handle_stream_hooks = &scr_handle_stream_hooks_impl; scr_loop_set_stream(&scr_stream_ticks_pending, &scr_stream_dispatch); } @@ -6409,23 +6448,138 @@ void scr_stream_worker_stdio(void) { #endif } -static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { - if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && - !memcmp(value->v.typed_ref.type_key, "object:", 7)) { - ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = scr_stream_vt_of(stream); - if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream; - value->v.typed_ref.release(stream); +/* ── handles that are Transforms (Cipheriv, Decipheriv) ────────────────── + * A Transform handle (ScrDynHandleOps.transform) used as a stream gets one + * native Transform, built here on first use and kept in the handle's + * slot. Its steps share the handle (held, and traced: the handle and its + * stream form a cycle the collector reclaims) and the handle's static + * ops: each chunk goes through the handle's update(), and the flush pushes + * final() or reports its error. */ + +static void *scr_handle_ops_retain(void *p) { return p; } +static void scr_handle_ops_release(void *p) { (void)p; } + +static const ScrDynHandleOps *scr_handle_step_handle_ops(ScrClosure *cb) { + return scr_box_get_ref(cb->caps[1]); +} + +/* The step's handle, borrowed: scr_box_get_ref answers +1, and the box + * keeps the handle alive. */ +static void *scr_handle_step_handle(ScrClosure *cb) { + void *h = scr_box_get_ref(cb->caps[0]); + scr_handle_step_handle_ops(cb)->release(h); + return h; +} + +static const ScrDynHandleTransform *scr_handle_step_ops(ScrClosure *cb) { + return scr_handle_step_handle_ops(cb)->transform; +} + +/* The pending exception as a stream error (+1). */ +static ScrError *scr_handle_step_caught(void) { + ScrCaught *caught = scr_exc_take(); + ScrDyn *value = scr_caught_to_dyn(caught); + scr_caught_release(caught); + ScrError *err = value ? scr_errdyn_err_of(value) : NULL; + if (value) scr_dyn_release(value); + return err ? err : scr_stream_mkerr("ERR_INTERNAL_ASSERTION", "a Transform handle threw a value that is not an Error"); +} + +static void scr_handle_step_transform(ScrClosure *cb, ScrStream *s, ScrBytes *chunk) { + /* A throwing update() leaves the exception pending: it throws out of + * the write, as Node's _transform does. */ + ScrBytes *out = scr_handle_step_ops(cb)->update(scr_handle_step_handle(cb), chunk); + if (out) scr_stream_transform_done(s, NULL, out, NULL); +} + +static void scr_handle_step_flush(ScrClosure *cb, ScrStream *s) { + ScrBytes *out = scr_handle_step_ops(cb)->final(scr_handle_step_handle(cb)); + scr_stream_flush_done(s, out ? NULL : scr_handle_step_caught(), out, NULL); +} + +static void scr_handle_step_destroy(ScrClosure *cb, ScrStream *s, ScrError *err) { + (void)cb; + scr_stream_destroy_done(s, err ? scr_error_retain(err) : NULL); +} + +static ScrClosure *scr_handle_step(const ScrDynHandleOps *ops, void *h) { + ScrClosure *step = scr_closure_new(NULL, 2); + step->caps[0] = scr_box_new_obj(ops->retain, ops->release, ops->transform->trace); + scr_box_set_ref(step->caps[0], ops->retain(h)); + step->caps[1] = scr_box_new_obj(scr_handle_ops_retain, scr_handle_ops_release, NULL); + scr_box_set_ref(step->caps[1], (void *)ops); + return step; +} + +/* The Transform of handle h (+1), built on first use. */ +static ScrStream *scr_handle_transform(const ScrDynHandleOps *ops, void *h) { + const ScrDynHandleTransform *t = ops->transform; + ScrStream *s = t->stream(h); + if (!s) { + s = scr_stream_new_transform(-1, -1, true, true, true, true, true, + scr_handle_step(ops, h), scr_handle_step_transform, + scr_handle_step(ops, h), scr_handle_step_flush, + scr_handle_step(ops, h), scr_handle_step_destroy); + s->cls = ops->cls; + t->set_stream(h, s, scr_stream_release_v); } + return scr_stream_retain(s); +} + +/* The native stream of a checked-dynamic Transform handle (+1), or NULL + * when the value is not one. */ +ScrStream *scr_stream_of_handle(const ScrDyn *value) { + if (value->kind != SCR_DYN_HANDLE) return NULL; + const ScrDynHandleOps *ops = scr_dyn_handle_ops_of(value); + return ops->transform ? scr_handle_transform(ops, value->v.handle.ptr) : NULL; +} + +/* A boxed native stream (+1), or NULL for any other value. */ +static ScrStream *scr_stream_boxed(const ScrDyn *value) { + if (value->kind != SCR_DYN_TYPED_REF || value->v.typed_ref.type_key_len <= 7 || + memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) + return NULL; + ScrStream *stream = scr_dyn_typed_ref_unbox(value); + const ScrVt *vt = scr_stream_vt_of(stream); + if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || + (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) + return stream; + value->v.typed_ref.release(stream); + return NULL; +} + +/* stream.handleStream: the stream of a value typed as a stream handle + * class (a Cipheriv or Decipheriv), +1. A value of that type can also be + * the stream itself, boxed (the stream addAbortSignal answers). */ +ScrStream *scr_stream_handle(const ScrDyn *value) { + ScrStream *stream = scr_stream_of_handle(value); + if (!stream) stream = scr_stream_boxed(value); + if (stream) return stream; + static const char message[] = "Stream use of a value that is not a native stream has no lowering"; + scr_throw_error_msg_code(SCR_ERR_ERROR, message, sizeof message - 1, "SC2020"); + return NULL; +} + +static ScrStream *scr_stream_view_receiver(const ScrDyn *value) { + ScrStream *stream = scr_stream_of_handle(value); + if (!stream) stream = scr_stream_boxed(value); + if (stream) return stream; scr_dyn_arg_type_fail("destination", "an instance of Stream", value); return NULL; } -static ScrDyn *scr_stream_view_call(ScrClosure *cb, ScrDyn *const *args, size_t argc) { - ScrStream *stream = scr_box_get_ref(cb->caps[0]); - ScrStr *name = scr_box_get_ref(cb->caps[1]); - const char *method = name->data; +/* The members a native stream answers to checked-dynamic calls. */ +static const char *const scr_stream_dyn_methods[] = { + "on", "once", "addListener", "prependListener", "prependOnceListener", + "off", "removeListener", "pipe", "unpipe", "read", "pause", "resume", "isPaused", + "setEncoding", "write", "end", "destroy", "cork", "uncork", +}; + +/* A checked-dynamic call of one of a native stream's members (borrowed + * stream): the result, +1, with `this` results answering `self`, or the + * current `this` when self is NULL. */ +static ScrDyn *scr_stream_dyn_call(ScrStream *stream, ScrDyn *self, const char *method, + ScrDyn *const *args, size_t argc) { ScrDyn *result = NULL; ScrDyn *first = argc ? args[0] : scr_dyn_undefined(); if (!strcmp(method, "on") || !strcmp(method, "once") || !strcmp(method, "addListener") || @@ -6501,21 +6655,72 @@ static ScrDyn *scr_stream_view_call(ScrClosure *cb, ScrDyn *const *args, size_t scr_error_release(error); } else if (!strcmp(method, "cork")) scr_stream_cork(stream); else if (!strcmp(method, "uncork")) scr_stream_uncork(stream); - if (!result && !scr_exc_pending()) result = scr_dyn_this_get(); + if (!result && !scr_exc_pending()) result = self ? scr_dyn_retain(self) : scr_dyn_this_get(); + return result; +} + +static ScrDyn *scr_stream_view_call(ScrClosure *cb, ScrDyn *const *args, size_t argc) { + ScrStream *stream = scr_box_get_ref(cb->caps[0]); + ScrStr *name = scr_box_get_ref(cb->caps[1]); + ScrDyn *result = scr_stream_dyn_call(stream, NULL, name->data, args, argc); scr_str_release(name); scr_stream_release(stream); return result; } +/* ScrHandleStreamHooks.call: a member a native stream answers to + * checked-dynamic calls, on the handle's Transform. Members that answer + * the stream answer `self`. */ +static bool scr_handle_stream_call(const ScrDynHandleOps *ops, void *h, ScrDyn *self, + const char *method, ScrDyn *const *args, size_t argc, + ScrDyn **result) { + for (size_t i = 0; i < sizeof scr_stream_dyn_methods / sizeof *scr_stream_dyn_methods; i++) { + if (strcmp(method, scr_stream_dyn_methods[i]) != 0) continue; + ScrStream *stream = scr_handle_transform(ops, h); + *result = scr_stream_dyn_call(stream, self, method, args, argc); + scr_stream_release(stream); + return true; + } + return false; +} + +/* The stream properties a checked-dynamic read answers. */ +static const char *const scr_stream_dyn_properties[] = { + "readable", "readableEnded", "readableLength", "readableHighWaterMark", + "writable", "writableEnded", "writableFinished", "writableLength", "writableHighWaterMark", + "writableNeedDrain", "destroyed", "closed", +}; + +static ScrDyn *scr_stream_dyn_property(ScrStream *stream, const char *name) { + double value = scr_stream_prop(stream, name); + bool number = strstr(name, "Length") || strstr(name, "HighWaterMark"); + return number ? scr_dyn_new_num(value) : scr_dyn_new_bool(value != 0); +} + +/* ScrHandleStreamHooks.get: a stream property of the handle's Transform. */ +static ScrDyn *scr_handle_stream_get(const ScrDynHandleOps *ops, void *h, const char *key, + size_t key_len) { + for (size_t i = 0; i < sizeof scr_stream_dyn_properties / sizeof *scr_stream_dyn_properties; i++) { + const char *name = scr_stream_dyn_properties[i]; + if (strlen(name) != key_len || memcmp(name, key, key_len) != 0) continue; + ScrStream *stream = scr_handle_transform(ops, h); + ScrDyn *value = scr_stream_dyn_property(stream, name); + scr_stream_release(stream); + return value; + } + return NULL; +} + +static const ScrHandleStreamHooks scr_handle_stream_hooks_impl = { + scr_handle_stream_call, + scr_handle_stream_get, +}; + ScrDyn *scr_stream_dyn_view(ScrStream *stream) { ScrDyn *view = scr_dyn_new_obj(); ScrDyn *prototype = scr_dyn_new_obj(); - static const char *const methods[] = { - "on", "once", "addListener", "prependListener", "prependOnceListener", - "off", "removeListener", "pipe", "unpipe", "read", "pause", "resume", "isPaused", - "setEncoding", "write", "end", "destroy", "cork", "uncork", - }; - for (size_t i = 0; i < sizeof methods / sizeof *methods; i++) { + const char *const *methods = scr_stream_dyn_methods; + for (size_t i = 0; i < sizeof scr_stream_dyn_methods / sizeof *scr_stream_dyn_methods; i++) { ScrClosure *cb = scr_closure_new((void *)scr_stream_view_call, 2); cb->caps[0] = scr_box_new_obj(scr_stream_retain_v, scr_stream_release_v, scr_stream_trace); cb->caps[1] = scr_box_new(SCR_BOX_STR); @@ -6524,16 +6729,9 @@ ScrDyn *scr_stream_dyn_view(ScrStream *stream) { scr_dyn_obj_set(prototype, methods[i], strlen(methods[i]), scr_dyn_new_func(cb, scr_stream_view_call, 0, "native stream method", methods[i])); } - static const char *const properties[] = { - "readable", "readableEnded", "readableLength", "readableHighWaterMark", - "writable", "writableEnded", "writableFinished", "writableLength", "writableHighWaterMark", - "writableNeedDrain", "destroyed", "closed", - }; - for (size_t i = 0; i < sizeof properties / sizeof *properties; i++) { - double value = scr_stream_prop(stream, properties[i]); - bool number = strstr(properties[i], "Length") || strstr(properties[i], "HighWaterMark"); - scr_dyn_obj_set(view, properties[i], strlen(properties[i]), - number ? scr_dyn_new_num(value) : scr_dyn_new_bool(value != 0)); + for (size_t i = 0; i < sizeof scr_stream_dyn_properties / sizeof *scr_stream_dyn_properties; i++) { + const char *name = scr_stream_dyn_properties[i]; + scr_dyn_obj_set(view, name, strlen(name), scr_stream_dyn_property(stream, name)); } if (stream->st->native && stream->st->native_ops && stream->st->native_ops->view) { ScrClosure *cb = scr_closure_new((void *)scr_stream_view_call, 2); @@ -6712,17 +6910,13 @@ ScrStream *scr_stream_pipe_general(ScrStream *src, ScrStream *dst, bool end) { /* A pipeline() stage that arrives as a checked-dynamic value (the * stream.unboxDyn libCall): a boxed native stream unwraps by identity - * (+1); any other value throws the compiled program's not-supported - * message (Node also takes iterables and functions there). */ + * (+1), a stream handle (a Cipheriv or Decipheriv) answers its stream, and + * any other value throws the compiled program's not-supported message + * (Node also takes iterables and functions there). */ ScrStream *scr_stream_unbox_dyn(const ScrDyn *value, const ScrStr *message) { - if (value->kind == SCR_DYN_TYPED_REF && value->v.typed_ref.type_key_len > 7 && - !memcmp(value->v.typed_ref.type_key, "object:", 7)) { - ScrStream *stream = scr_dyn_typed_ref_unbox(value); - const ScrVt *vt = scr_stream_vt_of(stream); - if (vt && ((vt->pre >= scr_readable_vt.pre && vt->pre <= scr_readable_vt.post) || - (vt->pre >= scr_writable_vt.pre && vt->pre <= scr_writable_vt.post))) return stream; - value->v.typed_ref.release(stream); - } + ScrStream *stream = scr_stream_of_handle(value); + if (!stream) stream = scr_stream_boxed(value); + if (stream) return stream; scr_throw_error_msg_code(SCR_ERR_ERROR, message->data, message->len, "SC2020"); return NULL; } diff --git a/packages/runtime/src/scr_stream.h b/packages/runtime/src/scr_stream.h index 99f58331c..1e5b789e7 100644 --- a/packages/runtime/src/scr_stream.h +++ b/packages/runtime/src/scr_stream.h @@ -143,6 +143,13 @@ ScrStream *scr_stream_pipe_general(ScrStream *src, ScrStream *dst, bool end); /* A boxed native stream out of a checked-dynamic pipeline stage (+1), or * NULL with `message` thrown as the not-supported Error. */ ScrStream *scr_stream_unbox_dyn(const struct ScrDyn *value, const ScrStr *message); +/* Handles that are Transforms (ScrDynHandleOps.transform: Cipheriv and + * Decipheriv). scr_stream_of_handle: the handle's Transform (+1), built on + * first use, or NULL when the value is not such a handle. + * stream.handleStream: the same, throwing the coded SC2020 error for any + * other value. */ +ScrStream *scr_stream_of_handle(const struct ScrDyn *value); +ScrStream *scr_stream_handle(const struct ScrDyn *value); /* Checked-dynamic stream slots (scr_stream_dyn.c): the native stream a * value boxes with the requested side (1 readable, 2 writable; +1, or * NULL), and readable.pipe()/unpipe() for a destination that is not a @@ -197,35 +204,34 @@ ScrStream *scr_stream_pipeline(double n, ScrStream **streams, ScrClosure *cb, Sc * mustCall wrappers; borrowed). */ ScrClosure *scr_stream_finished_dyn(ScrStream *s, struct ScrDyn *cb); ScrStream *scr_stream_pipeline_dyn(double n, ScrStream **streams, struct ScrDyn *cb); -/* pipeline(...[, cb]) with one run of function stages (pipeline.js): the - * run starts at streams[at] (count functions). flags bit 0: the first - * stage is a source function, called with only its options; bits 1-2: - * the last stage is a function destination (2: an async function whose - * promise, boxed through box, carries the value to the callback), and - * pipeline() answers a PassThrough. Other - * stages get the previous stream itself or the previous stage's async - * generator. mask bit i: stage i takes { signal } (the pipeline's own - * AbortSignal, aborted through abort when the pipeline finishes). The - * last generator before a stream or a PassThrough is pumped - * (pumpToNode) through its helpers: next/ret answer the resume's - * promise, read maps its IteratorResult to the value or - * scr_stream_from_done. Streams and closures borrowed (helpers, box, - * signal and abort NULL when unused); cb moves. */ -ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, struct ScrDyn *signal, ScrClosure *abort, - ScrClosure *cb, ScrStreamErrInv inv); -ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, struct ScrDyn *signal, - ScrClosure *abort, struct ScrDyn *cb); -struct ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double at, double count, - ScrClosure **stages, double flags, double mask, - ScrClosure *next, ScrClosure *read, ScrClosure *ret, - ScrClosure *box, struct ScrDyn *signal, - ScrClosure *abort); +/* pipeline(...[, cb]) with function stages (pipeline.js): positions bit j + * marks the j-th of the n + count stages a function. A function at + * position 0 is a source function, called with only its options. flags + * bits 1-2: the last stage is a function destination (2: an async + * function whose promise, boxed through box, carries the value to the + * callback), and pipeline() answers a PassThrough. Other stages get the + * previous stream itself or the previous stage's async generator. mask + * bit i: function i takes { signal } (the pipeline's own AbortSignal, + * aborted through abort when the pipeline finishes). The last generator + * of each run that a stream follows is pumped (pumpToNode) through its + * three helpers in `helpers`: next/ret answer the resume's promise, read + * maps its IteratorResult to the value or scr_stream_from_done. Streams + * and closures borrowed (helpers, box, signal and abort NULL when + * unused); cb moves. */ +ScrStream *scr_stream_pipeline_stages(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + struct ScrDyn *signal, ScrClosure *abort, ScrClosure *cb, + ScrStreamErrInv inv); +ScrStream *scr_stream_pipeline_stages_dyn(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + struct ScrDyn *signal, ScrClosure *abort, + struct ScrDyn *cb); +struct ScrPromise *scr_sp_pipeline_stages(double n, ScrStream **streams, double count, + ScrClosure **stages, double positions, double flags, + double mask, ScrClosure **helpers, ScrClosure *box, + struct ScrDyn *signal, ScrClosure *abort); /* stream.compose(s1..sn): a pipeline over the streams and a Duplex that * writes to the first and reads from the last (compose.js). One stream * answers itself (Duplex.from of a duplex). +1, or NULL with diff --git a/packages/runtime/src/scr_stream_dyn.c b/packages/runtime/src/scr_stream_dyn.c index 11dcf6ee2..5cef9d536 100644 --- a/packages/runtime/src/scr_stream_dyn.c +++ b/packages/runtime/src/scr_stream_dyn.c @@ -17,8 +17,11 @@ static bool scr_stream_dyn_within(const ScrVt *vt, const ScrVt *cls) { /* The native stream a checked-dynamic value boxes (+1), when it has the * requested side (1 readable, 2 writable; Duplex, Transform and PassThrough - * have both), or NULL for any other value. Never throws. */ + * have both), or the stream of a stream handle, or NULL for any other + * value. Never throws. */ ScrStream *scr_stream_dyn_native(const ScrDyn *value, double side) { + /* A Cipheriv or Decipheriv is a Transform. */ + if (value->kind == SCR_DYN_HANDLE) return scr_stream_of_handle(value); if (value->kind != SCR_DYN_TYPED_REF || value->v.typed_ref.type_key_len <= 7 || memcmp(value->v.typed_ref.type_key, "object:", 7) != 0) return NULL; diff --git a/packages/runtime/src/scr_webcrypto.c b/packages/runtime/src/scr_webcrypto.c index 546b24151..4d2c27be4 100644 --- a/packages/runtime/src/scr_webcrypto.c +++ b/packages/runtime/src/scr_webcrypto.c @@ -1,22 +1,27 @@ -/* WebCrypto's HMAC slice: crypto.subtle.importKey (raw HMAC keys), - * sign, verify and the SHA-1/SHA-2 digests, plus the CryptoKey values - * importKey answers. +/* WebCrypto's secret-key slice: crypto.subtle.importKey, generateKey and + * exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF + * keys), HMAC sign and verify, the SHA-1/SHA-2 digests, PBKDF2 and HKDF + * deriveBits and deriveKey, the argument and key handling of AES encrypt + * and decrypt (whose cipher runs in scr_webcrypto_aes.c), and the + * CryptoKey values those methods answer. * * This unit is only the WebCrypto layer, modeled on Node 24's - * lib/internal/crypto/webcrypto.js and mac.js: Web IDL argument - * conversion, algorithm normalization, the CryptoKey handle, promise - * wrapping and Node's error shapes. The MACs and digests themselves come - * from node:crypto's digest core (scr_crypto_hmac_raw and - * scr_crypto_digest_raw in scr_lib.c), and results settle through the - * deferred crypto queue that node:crypto's callback forms use, so sign, - * verify and digest settle on a later loop turn like Node's thread-pool - * jobs while importKey settles at once. + * lib/internal/crypto/webcrypto.js, mac.js, aes.js and keys.js: Web IDL + * argument conversion, algorithm normalization, the CryptoKey handle, the + * JSON Web Key forms, promise wrapping and Node's error shapes. The MACs + * and digests themselves come from node:crypto's digest core + * (scr_crypto_hmac_raw and scr_crypto_digest_raw in scr_lib.c), and + * results settle through the deferred crypto queue that node:crypto's + * callback forms use, so sign, verify, digest and generateKey settle on a + * later loop turn like Node's thread-pool jobs while importKey and + * exportKey settle at once. * * Every argument and validation failure rejects the returned promise. * Algorithms Node recognizes but this slice does not implement (RSA, - * ECDSA, AES, SHA-3, ...) and the 'jwk' key format reject with a coded - * SC2020 Error instead of silently diverging. The compiler links the unit - * only into programs that call these methods (the webcrypto feature). */ + * ECDSA, AES-OCB, SHA-3, ...) and the key formats of asymmetric keys + * reject with a coded SC2020 Error instead of silently diverging. The + * compiler links the unit only into programs that call these methods + * (the webcrypto feature). */ #include "scr_runtime.h" #include @@ -28,8 +33,9 @@ /* ── algorithm registry ─────────────────────────────────────────────── * The names Node 24 registers per operation (util.js's * kAlgorithmDefinitions with the conditional algorithms its OpenSSL - * build enables), matched case-insensitively. Only HMAC and the SHA-1/ - * SHA-2 digests are implemented. */ + * build enables), matched case-insensitively. HMAC, AES-CBC/CTR/GCM/KW + * keys, PBKDF2 and HKDF key imports and the SHA-1/SHA-2 digests are + * implemented. */ static const char *const scr_wc_import_names[] = { "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", "Argon2d", "Argon2i", "Argon2id", "ChaCha20-Poly1305", @@ -43,6 +49,15 @@ static const char *const scr_wc_sign_names[] = { "ECDSA", "Ed25519", "Ed448", "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87", "RSA-PSS", "RSASSA-PKCS1-v1_5", }; +/* generateKey and exportKey register the same algorithms. */ +static const char *const scr_wc_key_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", + "ChaCha20-Poly1305", "ECDH", "ECDSA", "Ed25519", "Ed448", + "HMAC", "KMAC128", "KMAC256", "ML-DSA-44", "ML-DSA-65", + "ML-DSA-87", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024", "MLKEM768-P256", + "MLKEM768-X25519", "MLKEM1024-P384", "RSA-OAEP", "RSA-PSS", "RSASSA-PKCS1-v1_5", + "X25519", "X448", +}; static const char *const scr_wc_digest_names[] = { "SHA-1", "SHA-256", "SHA-384", "SHA-512", "SHA3-256", "SHA3-384", "SHA3-512", "cSHAKE128", "cSHAKE256", "KT128", "KT256", "TurboSHAKE128", @@ -193,7 +208,24 @@ static const char *const scr_wc_usages[] = { "deriveKey", "deriveBits", "wrapKey", "unwrapKey", "encapsulateKey", "encapsulateBits", "decapsulateKey", "decapsulateBits", }; -enum { SCR_WC_USAGE_SIGN = 1u << 2, SCR_WC_USAGE_VERIFY = 1u << 3 }; +enum { + SCR_WC_USAGE_ENCRYPT = 1u << 0, + SCR_WC_USAGE_DECRYPT = 1u << 1, + SCR_WC_USAGE_SIGN = 1u << 2, + SCR_WC_USAGE_VERIFY = 1u << 3, + SCR_WC_USAGE_DERIVE_KEY = 1u << 4, + SCR_WC_USAGE_DERIVE_BITS = 1u << 5, + SCR_WC_USAGE_WRAP_KEY = 1u << 6, + SCR_WC_USAGE_UNWRAP_KEY = 1u << 7, +}; + +/* The usage index of a KeyUsage string, or -1. */ +static int scr_wc_usage_index(const ScrStr *s) { + for (size_t k = 0; k < SCR_WC_COUNT(scr_wc_usages); k++) + if (s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0) + return (int)k; + return -1; +} /* sequence over an array: the usage mask (-1 on failure). */ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { @@ -215,11 +247,8 @@ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { scr_dyn_release(plain); return -1; } - size_t k = 0; - while (k < SCR_WC_COUNT(scr_wc_usages) && - !(s->len == strlen(scr_wc_usages[k]) && memcmp(s->data, scr_wc_usages[k], s->len) == 0)) - k++; - if (k == SCR_WC_COUNT(scr_wc_usages)) { + int k = scr_wc_usage_index(s); + if (k < 0) { scr_wc_type_error("ERR_INVALID_ARG_VALUE", "%s: %s argument[%.0f] '%.*s' is not a valid enum value of type KeyUsage.", prefix, scr_wc_ordinals[index], i, (int)(s->len > 200 ? 200 : s->len), @@ -237,10 +266,10 @@ static long scr_wc_key_usages(const char *prefix, size_t index, ScrDyn *value) { /* ── algorithm normalization ────────────────────────────────────────── * normalizeAlgorithm(algorithm, op) for the members this slice reads: - * the canonical name, and for HMAC imports the hash and length. */ + * the canonical name, the HMAC hash and length, and AES key lengths. */ typedef struct { const char *name; /* canonical registered name */ - const char *hash; /* HMAC import: the normalized hash name */ + const char *hash; /* HMAC: the normalized hash name */ bool has_length; double length; } ScrWcAlgorithm; @@ -261,6 +290,28 @@ static ScrDyn *scr_wc_member(const ScrDyn *dict, const char *key) { return v ? v : scr_dyn_retain(scr_dyn_undefined()); } +/* An [EnforceRange] integer conversion: the truncated value in [0, max], + * or false with Node's TypeError pending. `context` names the value + * ("length in passed algorithm", "3rd argument"). */ +static bool scr_wc_enforce_range(const char *prefix, const char *context, const ScrDyn *value, + double max, double *out) { + double x; + if (!scr_dyn_number_coerce_js(value, &x)) return false; + if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: %s is not a finite number.", prefix, context); + return false; + } + x = trunc(x); + if (x == 0) x = 0; + if (x < 0 || x > max) { + scr_wc_type_error("ERR_OUT_OF_RANGE", "%s: %s is outside the expected range of 0 to %.0f.", + prefix, context, max); + return false; + } + *out = x; + return true; +} + /* The name an AlgorithmIdentifier carries (+1), with the Algorithm * dictionary's missing-name TypeError. `dict` names the dictionary the * value converts to. */ @@ -295,11 +346,13 @@ static const char *scr_wc_digest_algorithm(ScrDyn *identifier, const char *metho return canonical; } -/* HmacImportParams over an object: hash (required, a SHA name), then the - * optional [EnforceRange] unsigned long length. */ -static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { +/* HmacImportParams / HmacKeyGenParams (`dict`) over an object: hash + * (required, a SHA name), then the optional [EnforceRange] unsigned long + * length, which cannot be 0 (`zero_error` names that DOMException). */ +static bool scr_wc_hmac_params(ScrDyn *identifier, const char *dict, const char *zero_error, + const char *method, ScrWcAlgorithm *out) { if (!scr_wc_is_object(identifier)) { - scr_wc_missing_member("HmacImportParams", "hash"); + scr_wc_missing_member(dict, "hash"); return false; } ScrDyn *plain = scr_wc_plain(identifier); @@ -307,7 +360,7 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { ScrDyn *hash = scr_wc_member(plain, "hash"); bool ok = true; if (hash->kind == SCR_DYN_UNDEF) { - scr_wc_missing_member("HmacImportParams", "hash"); + scr_wc_missing_member(dict, "hash"); ok = false; } ScrDyn *hash_value = NULL; @@ -335,7 +388,9 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { (hash_name->data[2] | 32) == 'a'; if (hash_name) scr_str_release(hash_name); if (ok && !sha) { - scr_throw_domex("NotSupportedError", "Only SHA hashes are supported in HmacImportParams"); + char message[96]; + snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict); + scr_throw_domex("NotSupportedError", message); ok = false; } } @@ -343,34 +398,23 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { ScrDyn *length = scr_wc_member(plain, "length"); if (length->kind != SCR_DYN_UNDEF) { double x; - if (!scr_dyn_number_coerce_js(length, &x)) ok = false; - else if (x != x || x == 1.0 / 0.0 || x == -1.0 / 0.0) { - scr_wc_type_error("ERR_INVALID_ARG_TYPE", - "%s: length in passed algorithm is not a finite number.", - scr_wc_normalize_prefix); + if (!scr_wc_enforce_range(scr_wc_normalize_prefix, "length in passed algorithm", length, + 4294967295.0, &x)) + ok = false; + else if (x == 0) { + char message[96]; + snprintf(message, sizeof message, "%s.length cannot be 0", dict); + scr_throw_domex(zero_error, message); ok = false; } else { - x = trunc(x); - if (x == 0) x = 0; - if (x < 0 || x > 4294967295.0) { - scr_wc_type_error("ERR_OUT_OF_RANGE", - "%s: length in passed algorithm is outside the expected range of 0 to " - "4294967295.", - scr_wc_normalize_prefix); - ok = false; - } else if (x == 0) { - scr_throw_domex("DataError", "HmacImportParams.length cannot be 0"); - ok = false; - } else { - out->has_length = true; - out->length = x; - } + out->has_length = true; + out->length = x; } } scr_dyn_release(length); } if (ok) { - out->hash = scr_wc_digest_algorithm(hash_value, "importKey"); + out->hash = scr_wc_digest_algorithm(hash_value, method); if (!out->hash) ok = false; } if (hash_value) scr_dyn_release(hash_value); @@ -379,11 +423,51 @@ static bool scr_wc_hmac_import_params(ScrDyn *identifier, ScrWcAlgorithm *out) { return ok; } -/* normalizeAlgorithm for importKey, sign/verify (`names`) — false with - * the error pending. Only HMAC parameters are read; any other recognized - * algorithm is refused by the caller. */ -static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, - bool import, ScrWcAlgorithm *out) { +/* AesKeyGenParams / AesDerivedKeyParams (`dict`): the required + * [EnforceRange] unsigned short length, 128, 192 or 256. */ +static bool scr_wc_aes_length_params(ScrDyn *identifier, const char *dict, ScrWcAlgorithm *out) { + ScrDyn *plain = scr_wc_plain(identifier); + if (!plain) return false; + ScrDyn *length = scr_wc_member(plain, "length"); + bool ok = false; + double x; + if (length->kind == SCR_DYN_UNDEF) scr_wc_missing_member(dict, "length"); + else if (scr_wc_enforce_range(scr_wc_normalize_prefix, "length in passed algorithm", length, + 65535.0, &x)) { + if (x != 128 && x != 192 && x != 256) { + scr_throw_domex("OperationError", "AES key length must be 128, 192, or 256 bits"); + } else { + out->has_length = true; + out->length = x; + ok = true; + } + } + scr_dyn_release(length); + scr_dyn_release(plain); + return ok; +} + +static bool scr_wc_is_aes(const char *name) { + return strcmp(name, "AES-CBC") == 0 || strcmp(name, "AES-CTR") == 0 || + strcmp(name, "AES-GCM") == 0 || strcmp(name, "AES-KW") == 0; +} + +static bool scr_wc_is_kdf(const char *name) { + return strcmp(name, "PBKDF2") == 0 || strcmp(name, "HKDF") == 0; +} + +typedef enum { + SCR_WC_OP_IMPORT, + SCR_WC_OP_SIGN, + SCR_WC_OP_GENERATE, +} ScrWcOp; + +/* normalizeAlgorithm for importKey, sign/verify and generateKey (`names`) + * — false with the error pending. Only the dictionaries of the + * implemented algorithms are read; any other recognized algorithm is + * refused by the caller. */ +static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_t count, ScrWcOp op, + ScrWcAlgorithm *out) { memset(out, 0, sizeof *out); ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); if (!name) return false; @@ -393,19 +477,27 @@ static bool scr_wc_normalize(ScrDyn *identifier, const char *const *names, size_ scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); return false; } - if (import && strcmp(out->name, "HMAC") == 0) return scr_wc_hmac_import_params(identifier, out); + bool hmac = strcmp(out->name, "HMAC") == 0; + if (op == SCR_WC_OP_IMPORT && hmac) + return scr_wc_hmac_params(identifier, "HmacImportParams", "DataError", "importKey", out); + if (op == SCR_WC_OP_GENERATE && hmac) + return scr_wc_hmac_params(identifier, "HmacKeyGenParams", "OperationError", "generateKey", out); + if (op == SCR_WC_OP_GENERATE && scr_wc_is_aes(out->name)) + return scr_wc_aes_length_params(identifier, "AesKeyGenParams", out); return true; } /* ── CryptoKey ───────────────────────────────────────────────────────── - * A secret HMAC key: the bytes, the hash, the bit length, the usage mask - * and the extractable flag. The algorithm and usages getters answer the - * same object on every read (Node caches its clones), built on first - * read. The bytes are cleared when the last reference goes. */ + * A secret key: the algorithm name, the bytes, the HMAC hash, the bit + * length (HMAC and AES keys), the usage mask and the extractable flag. + * The algorithm and usages getters answer the same object on every read + * (Node caches its clones), built on first read. The bytes are cleared + * when the last reference goes. */ typedef struct ScrCryptoKey { size_t rc; - const char *hash; /* WebCrypto hash name */ - double length; /* bits */ + const char *name; /* canonical algorithm name */ + const char *hash; /* HMAC: WebCrypto hash name */ + double length; /* HMAC and AES: bits */ unsigned usages; bool extractable; ScrDyn *algorithm; /* cached getter results, or NULL */ @@ -446,26 +538,34 @@ static ScrDyn *scr_wc_cstr(const char *s) { return out; } +/* Node's algorithm objects: { name, length, hash } for HMAC keys, + * { name, length } for AES keys and { name } for PBKDF2 and HKDF keys. */ static ScrDyn *scr_crypto_key_algorithm(ScrCryptoKey *key) { if (!key->algorithm) { - ScrDyn *hash = scr_dyn_new_obj(); - scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); ScrDyn *algorithm = scr_dyn_new_obj(); - scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr("HMAC")); - scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); - scr_dyn_obj_set(algorithm, "hash", 4, hash); + scr_dyn_obj_set(algorithm, "name", 4, scr_wc_cstr(key->name)); + if (!scr_wc_is_kdf(key->name)) + scr_dyn_obj_set(algorithm, "length", 6, scr_dyn_new_num(key->length)); + if (key->hash) { + ScrDyn *hash = scr_dyn_new_obj(); + scr_dyn_obj_set(hash, "name", 4, scr_wc_cstr(key->hash)); + scr_dyn_obj_set(algorithm, "hash", 4, hash); + } key->algorithm = algorithm; } return scr_dyn_retain(key->algorithm); } +/* The usages as a fresh array in Node's canonical order (+1). */ +static ScrDyn *scr_wc_usage_array(unsigned usages) { + ScrDyn *list = scr_dyn_new_arr(); + for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) + if (usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); + return list; +} + static ScrDyn *scr_crypto_key_usages(ScrCryptoKey *key) { - if (!key->usage_list) { - ScrDyn *list = scr_dyn_new_arr(); - for (size_t i = 0; i < SCR_WC_COUNT(scr_wc_usages); i++) - if (key->usages & (1u << i)) scr_dyn_arr_push(list, scr_wc_cstr(scr_wc_usages[i])); - key->usage_list = list; - } + if (!key->usage_list) key->usage_list = scr_wc_usage_array(key->usages); return scr_dyn_retain(key->usage_list); } @@ -540,8 +640,9 @@ static const ScrDynHandleOps scr_crypto_key_ops = { .symbol_get = scr_crypto_key_symbol_get, }; -static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *hash, - double length, unsigned usages, bool extractable) { +static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const char *name, + const char *hash, double length, unsigned usages, + bool extractable) { static SCR_TL bool installed; if (!installed) { installed = true; @@ -550,6 +651,7 @@ static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const c ScrCryptoKey *key = malloc(sizeof *key + (len ? len : 1)); if (!key) scr_trap("scriptc: out of memory\n"); key->rc = 1; + key->name = name; key->hash = hash; key->length = length; key->usages = usages; @@ -563,6 +665,208 @@ static ScrDyn *scr_crypto_key_new(const unsigned char *data, size_t len, const c return box; } +/* numBitsToBytes: whole bytes for `bits`, rounding up. */ +static size_t scr_wc_bits_to_bytes(double bits) { + return (size_t)(floor(bits / 8) + floor((7 + fmod(bits, 8)) / 8)); +} + +/* Clears the unused low bits of the final byte of a `bits`-long key + * (truncateToBitLength). */ +static void scr_wc_truncate_bits(unsigned char *data, size_t len, double bits) { + int extra = (int)fmod(bits, 8); + if (extra && len) data[len - 1] = (unsigned char)(data[len - 1] & (0xff << (8 - extra))); +} + +/* The usages each key algorithm accepts. */ +static unsigned scr_wc_allowed_usages(const char *name) { + if (strcmp(name, "HMAC") == 0) return SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY; + if (strcmp(name, "AES-KW") == 0) return SCR_WC_USAGE_WRAP_KEY | SCR_WC_USAGE_UNWRAP_KEY; + if (scr_wc_is_kdf(name)) return SCR_WC_USAGE_DERIVE_KEY | SCR_WC_USAGE_DERIVE_BITS; + return SCR_WC_USAGE_ENCRYPT | SCR_WC_USAGE_DECRYPT | SCR_WC_USAGE_WRAP_KEY | + SCR_WC_USAGE_UNWRAP_KEY; +} + +/* validateKeyUsages: SyntaxError for a usage outside the algorithm's. */ +static bool scr_wc_check_usages(const char *name, long usages) { + if (!(usages & ~(long)scr_wc_allowed_usages(name))) return true; + char message[96]; + snprintf(message, sizeof message, "Unsupported key usage for %s%s key", + scr_wc_is_kdf(name) ? "a " : "", name); + scr_throw_domex("SyntaxError", message); + return false; +} + +/* The JWK "alg" of an HMAC hash or an AES key, or NULL. */ +static const char *scr_wc_jwk_alg(const char *name, const char *hash, double length, char *buf, + size_t size) { + if (strcmp(name, "HMAC") == 0) { + if (strcmp(hash, "SHA-1") == 0) return "HS1"; + if (strcmp(hash, "SHA-256") == 0) return "HS256"; + if (strcmp(hash, "SHA-384") == 0) return "HS384"; + if (strcmp(hash, "SHA-512") == 0) return "HS512"; + return NULL; + } + if (!scr_wc_is_aes(name)) return NULL; + snprintf(buf, size, "A%.0f%s", length, name + 4); + return buf; +} + +/* ── JSON Web Keys ───────────────────────────────────────────────────── + * The JsonWebKey dictionary members a secret key reads, converted in Web + * IDL's member order (every DOMString member converts, so a Symbol or a + * throwing toString in any of them rejects like Node). */ +typedef struct { + ScrStr *alg, *k, *kty, *use; + bool has_ext, ext; + ScrDyn *key_ops; /* array of strings, or NULL */ +} ScrWcJwk; + +static void scr_wc_jwk_drop(ScrWcJwk *jwk) { + if (jwk->alg) scr_str_release(jwk->alg); + if (jwk->k) scr_str_release(jwk->k); + if (jwk->kty) scr_str_release(jwk->kty); + if (jwk->use) scr_str_release(jwk->use); + if (jwk->key_ops) scr_dyn_release(jwk->key_ops); + memset(jwk, 0, sizeof *jwk); +} + +/* A DOMString dictionary member: NULL with *ok when absent; NULL with + * !*ok and the TypeError pending on failure. */ +static ScrStr *scr_wc_jwk_string(const char *prefix, const ScrDyn *dict, const char *key, bool *ok) { + ScrDyn *value = scr_wc_member(dict, key); + ScrStr *out = NULL; + if (value->kind == SCR_DYN_SYMBOL) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s in 2nd argument is a Symbol and cannot be converted to a string.", + prefix, key); + *ok = false; + } else if (value->kind != SCR_DYN_UNDEF) { + out = scr_wc_dom_string(value); + if (!out) *ok = false; + } + scr_dyn_release(value); + return out; +} + +static bool scr_wc_jwk_convert(const char *prefix, ScrDyn *value, ScrWcJwk *out) { + memset(out, 0, sizeof *out); + if (value->kind == SCR_DYN_UNDEF || value->kind == SCR_DYN_NULL) return true; + if (!scr_wc_is_object(value)) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument cannot be converted to a dictionary", + prefix); + return false; + } + ScrDyn *dict = scr_wc_plain(value); + if (!dict) return false; + static const char *const members[] = {"alg", "crv", "d", "dp", "dq", "e", "ext", + "k", "key_ops", "kty", "n", "p", "priv", "pub", + "q", "qi", "use", "x", "y"}; + bool ok = true; + for (size_t i = 0; ok && i < SCR_WC_COUNT(members); i++) { + const char *key = members[i]; + if (strcmp(key, "ext") == 0) { + ScrDyn *ext = scr_wc_member(dict, key); + if (ext->kind != SCR_DYN_UNDEF) { + out->has_ext = true; + out->ext = scr_dyn_truthy(ext); + } + scr_dyn_release(ext); + } else if (strcmp(key, "key_ops") == 0) { + ScrDyn *ops = scr_wc_member(dict, key); + if (ops->kind != SCR_DYN_UNDEF) { + ScrDyn *list = scr_wc_plain(ops); + if (!list) ok = false; + else if (list->kind != SCR_DYN_ARR) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: key_ops in 2nd argument cannot be converted to sequence.", prefix); + scr_dyn_release(list); + ok = false; + } else { + ScrDyn *strings = scr_dyn_new_arr(); + double n = scr_dyn_arr_len(list); + for (double j = 0; ok && j < n; j++) { + ScrDyn *item = scr_dyn_arr_at(list, j); + ScrStr *s = scr_wc_dom_string(item); + scr_dyn_release(item); + if (!s) ok = false; + else { + scr_dyn_arr_push(strings, scr_dyn_new_str(s)); + scr_str_release(s); + } + } + scr_dyn_release(list); + if (ok) out->key_ops = strings; + else scr_dyn_release(strings); + } + } + scr_dyn_release(ops); + } else { + ScrStr *s = scr_wc_jwk_string(prefix, dict, key, &ok); + if (strcmp(key, "alg") == 0) out->alg = s; + else if (strcmp(key, "k") == 0) out->k = s; + else if (strcmp(key, "kty") == 0) out->kty = s; + else if (strcmp(key, "use") == 0) out->use = s; + else if (s) scr_str_release(s); + } + } + scr_dyn_release(dict); + if (!ok) scr_wc_jwk_drop(out); + return ok; +} + +static bool scr_wc_str_is(const ScrStr *s, const char *literal) { + return s && s->len == strlen(literal) && memcmp(s->data, literal, s->len) == 0; +} + +/* validateJwk for an oct key, then the key bytes (+1): k decodes with + * Node's lenient base64 decoder, so any string answers bytes. */ +static ScrBytes *scr_wc_jwk_secret(const ScrWcJwk *jwk, bool extractable, long usages, + const char *expected_use) { + if (!jwk->kty) { + scr_throw_domex("DataError", "Invalid keyData"); + return NULL; + } + if (!scr_wc_str_is(jwk->kty, "oct")) { + scr_throw_domex("DataError", "Invalid JWK \"kty\" Parameter"); + return NULL; + } + if (!jwk->k) { + scr_throw_domex("DataError", "Invalid keyData"); + return NULL; + } + if (usages != 0 && jwk->use && !scr_wc_str_is(jwk->use, expected_use)) { + scr_throw_domex("DataError", "Invalid JWK \"use\" Parameter"); + return NULL; + } + if (jwk->key_ops) { + long seen = 0; + double n = scr_dyn_arr_len(jwk->key_ops); + for (double i = 0; i < n; i++) { + ScrDyn *op = scr_dyn_arr_at(jwk->key_ops, i); + int k = scr_wc_usage_index(op->v.str); + scr_dyn_release(op); + if (k < 0) continue; + if (seen & (1L << k)) { + scr_throw_domex("DataError", "Duplicate key operation"); + return NULL; + } + seen |= 1L << k; + } + if ((seen & usages) != usages) { + scr_throw_domex("DataError", "Key operations and usage mismatch"); + return NULL; + } + } + if (jwk->has_ext && !jwk->ext && extractable) { + scr_throw_domex("DataError", "JWK \"ext\" Parameter and extractable mismatch"); + return NULL; + } + ScrStr *encoding = scr_str_new("base64url", 9); + ScrBytes *bytes = scr_bytes_from_str(jwk->k, encoding); + scr_str_release(encoding); + return bytes; +} + /* ── the operations ─────────────────────────────────────────────────── * Each takes its argument vector as one dyn array (borrowed) and answers * a +1 promise. */ @@ -573,6 +877,19 @@ static ScrPromise *scr_wc_rejected(void) { return p; } +/* A promise already fulfilled with `value` (moves). */ +static ScrPromise *scr_wc_resolved(ScrDyn *value) { + return scr_promise_settled_ref(value, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); +} + +/* A promise that fulfills with `value` (moves) on a later loop turn, like + * Node's crypto jobs. */ +static ScrPromise *scr_wc_deferred(ScrDyn *value) { + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_dyn(value, NULL, scr_crypto_promise_callback(p), scr_crypto_promise_settle_dyn); + return p; +} + /* The argument count check every method starts with (after the receiver * check the compiler's call shape already guarantees). */ static bool scr_wc_required(const char *prefix, const ScrDyn *args, size_t required) { @@ -595,9 +912,158 @@ static void scr_wc_args_drop(ScrWcArgs *a, size_t n) { for (size_t i = 0; i < n; i++) scr_dyn_release(a->v[i]); } +/* AlgorithmIdentifier: an object, or a value converted to a DOMString. */ +static bool scr_wc_algorithm_identifier(ScrDyn *value) { + if (scr_wc_is_object(value)) return true; + ScrStr *s = scr_wc_dom_string(value); + if (!s) return false; + scr_str_release(s); + return true; +} + +/* macImportKey: the usage check, the format, then normalizeKeyLength. */ +static ScrDyn *scr_wc_import_hmac(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const ScrWcAlgorithm *algorithm, bool extractable, long usages, + bool *unsupported) { + if (!scr_wc_check_usages("HMAC", usages)) return NULL; + ScrBytes *bytes = NULL; + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + bytes = scr_bytes_retain(data); + } else if (strcmp(format, "jwk") == 0) { + bytes = scr_wc_jwk_secret(jwk, extractable, usages, "sig"); + if (!bytes) return NULL; + char buf[16]; + const char *expected = scr_wc_jwk_alg("HMAC", algorithm->hash, 0, buf, sizeof buf); + if (jwk->alg && expected && !scr_wc_str_is(jwk->alg, expected)) { + scr_bytes_release(bytes); + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + return NULL; + } + } else { + *unsupported = true; + return NULL; + } + ScrDyn *result = NULL; + { + ScrBytes *view = bytes; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + double length = (double)len * 8; + if (len == 0) { + scr_throw_domex("DataError", "Zero-length key is not supported"); + goto done; + } + if (algorithm->has_length) { + if (scr_wc_bits_to_bytes(algorithm->length) != len) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + length = algorithm->length; + } + result = scr_crypto_key_new(view->data, len, "HMAC", algorithm->hash, length, + (unsigned)usages, extractable); + ScrCryptoKey *key = scr_crypto_key_of(result); + scr_wc_truncate_bits(key->data, key->len, length); + } +done: + scr_bytes_release(bytes); + return result; +} + +/* aesImportKey: the usage check, then the format and the key length. */ +static ScrDyn *scr_wc_import_aes(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const char *name, bool extractable, long usages, + bool *unsupported) { + if (!scr_wc_check_usages(name, usages)) return NULL; + ScrBytes *bytes = NULL; + bool from_jwk = strcmp(format, "jwk") == 0; + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + bytes = scr_bytes_retain(data); + } else if (from_jwk) { + bytes = scr_wc_jwk_secret(jwk, extractable, usages, "enc"); + if (!bytes) return NULL; + } else { + *unsupported = true; + return NULL; + } + ScrDyn *result = NULL; + { + ScrBytes *view = bytes; + SCR_BYTES_SNAPSHOT(view); + size_t len = (size_t)scr_bytes_byte_len(view); + double length = (double)len * 8; + if (length != 128 && length != 192 && length != 256) { + scr_throw_domex("DataError", "Invalid key length"); + goto done; + } + char buf[16]; + if (from_jwk && jwk->alg && + !scr_wc_str_is(jwk->alg, scr_wc_jwk_alg(name, NULL, length, buf, sizeof buf))) { + scr_throw_domex("DataError", "JWK \"alg\" does not match the requested algorithm"); + goto done; + } + result = scr_crypto_key_new(view->data, len, name, NULL, length, (unsigned)usages, extractable); + } +done: + scr_bytes_release(bytes); + return result; +} + +/* importGenericSecretKey (PBKDF2, HKDF): never extractable, derivation + * usages only, raw bytes of any length. */ +static ScrDyn *scr_wc_import_kdf(const char *format, ScrBytes *data, const char *name, + bool extractable, long usages, bool *unsupported) { + if (extractable) { + char message[64]; + snprintf(message, sizeof message, "%s keys are not extractable", name); + scr_throw_domex("SyntaxError", message); + return NULL; + } + if (!scr_wc_check_usages(name, usages)) return NULL; + if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { + *unsupported = true; + return NULL; + } + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + return scr_crypto_key_new(view->data, (size_t)scr_bytes_byte_len(view), name, NULL, 0, + (unsigned)usages, false); +} + +/* importKeySync over converted arguments: the per-algorithm import, the + * unsupported-format NotSupportedError, then the empty-usages check. */ +static ScrDyn *scr_wc_import_key_sync(const char *format, ScrBytes *data, const ScrWcJwk *jwk, + const ScrWcAlgorithm *algorithm, bool extractable, + long usages) { + bool unsupported = false; + ScrDyn *key; + if (strcmp(algorithm->name, "HMAC") == 0) + key = scr_wc_import_hmac(format, data, jwk, algorithm, extractable, usages, &unsupported); + else if (scr_wc_is_aes(algorithm->name)) + key = scr_wc_import_aes(format, data, jwk, algorithm->name, extractable, usages, &unsupported); + else if (scr_wc_is_kdf(algorithm->name)) + key = scr_wc_import_kdf(format, data, algorithm->name, extractable, usages, &unsupported); + else { + scr_wc_unsupported_algorithm("importKey", algorithm->name); + return NULL; + } + if (unsupported) { + char message[96]; + snprintf(message, sizeof message, "Unable to import %s using %s format", algorithm->name, + format); + scr_throw_domex("NotSupportedError", message); + return NULL; + } + if (key && usages == 0) { + scr_dyn_release(key); + scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); + return NULL; + } + return key; +} + /* importKey(format, keyData, algorithm, extractable, keyUsages): Node's - * conversion order, then macImportKey's checks and importKeySync's empty - * usage check. */ + * conversion order, then normalization and importKeySync. */ static ScrDyn *scr_wc_import(const ScrDyn *args) { static const char prefix[] = "Failed to execute 'importKey' on 'SubtleCrypto'"; if (!scr_wc_required(prefix, args, 5)) return NULL; @@ -605,77 +1071,28 @@ static ScrDyn *scr_wc_import(const ScrDyn *args) { scr_wc_args_take(&a, args, 5); ScrDyn *result = NULL; ScrBytes *data = NULL; + ScrWcJwk jwk; + memset(&jwk, 0, sizeof jwk); const char *format = scr_wc_key_format(prefix, a.v[0]); if (!format) goto done; - bool jwk = strcmp(format, "jwk") == 0; - /* The JsonWebKey dictionary is not implemented; that format is refused - * after the remaining arguments convert. */ - if (!jwk) { + if (strcmp(format, "jwk") == 0) { + if (!scr_wc_jwk_convert(prefix, a.v[1], &jwk)) goto done; + } else { data = scr_wc_buffer_source(prefix, 1, a.v[1]); if (!data) goto done; } - if (!scr_wc_is_object(a.v[2])) { - ScrStr *s = scr_wc_dom_string(a.v[2]); - if (!s) goto done; - scr_str_release(s); - } + if (!scr_wc_algorithm_identifier(a.v[2])) goto done; bool extractable = scr_dyn_truthy(a.v[3]); long usages = scr_wc_key_usages(prefix, 4, a.v[4]); if (usages < 0) goto done; ScrWcAlgorithm algorithm; - if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), true, - &algorithm)) + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), + SCR_WC_OP_IMPORT, &algorithm)) goto done; - if (strcmp(algorithm.name, "HMAC") != 0) { - scr_wc_unsupported_algorithm("importKey", algorithm.name); - goto done; - } - if (usages & ~(long)(SCR_WC_USAGE_SIGN | SCR_WC_USAGE_VERIFY)) { - scr_throw_domex("SyntaxError", "Unsupported key usage for HMAC key"); - goto done; - } - if (jwk) { - scr_wc_unsupported("importKey", "the 'jwk' key format"); - goto done; - } - if (strcmp(format, "raw") != 0 && strcmp(format, "raw-secret") != 0) { - char message[96]; - snprintf(message, sizeof message, "Unable to import HMAC using %s format", format); - scr_throw_domex("NotSupportedError", message); - goto done; - } - { - ScrBytes *view = data; - SCR_BYTES_SNAPSHOT(view); - size_t len = (size_t)scr_bytes_byte_len(view); - if (len == 0) { - scr_throw_domex("DataError", "Zero-length key is not supported"); - goto done; - } - double length = (double)len * 8; - unsigned char last = view->data[len - 1]; - if (algorithm.has_length) { - double bytes = floor(algorithm.length / 8) + floor((7 + fmod(algorithm.length, 8)) / 8); - if (bytes != (double)len) { - scr_throw_domex("DataError", "Invalid key length"); - goto done; - } - /* A length that is not a whole number of bytes clears the unused - * low bits of the final byte (truncateToBitLength). */ - int extra = (int)fmod(algorithm.length, 8); - if (extra) last = (unsigned char)(last & (0xff << (8 - extra))); - length = algorithm.length; - } - if (usages == 0) { - scr_throw_domex("SyntaxError", "Usages cannot be empty when importing a secret key."); - goto done; - } - result = scr_crypto_key_new(view->data, len, algorithm.hash, length, (unsigned)usages, - extractable); - scr_crypto_key_of(result)->data[len - 1] = last; - } + result = scr_wc_import_key_sync(format, data, &jwk, &algorithm, extractable, usages); done: if (data) scr_bytes_release(data); + scr_wc_jwk_drop(&jwk); scr_wc_args_drop(&a, 5); return result; } @@ -683,7 +1100,118 @@ done: ScrPromise *scr_webcrypto_import_key(ScrDyn *args) { ScrDyn *key = scr_wc_import(args); if (!key) return scr_wc_rejected(); - return scr_promise_settled_ref(key, &scr_dyn_retain_v, &scr_dyn_release_v, NULL); + return scr_wc_resolved(key); +} + +/* generateKey(algorithm, extractable, keyUsages) for HMAC and AES keys: + * random bytes of the requested length (an HMAC key defaults to its + * hash's block size; a length that is not a whole number of bytes clears + * the final byte's unused low bits). */ +ScrPromise *scr_webcrypto_generate_key(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'generateKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + ScrDyn *key = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + bool extractable = scr_dyn_truthy(a.v[1]); + long usages = scr_wc_key_usages(prefix, 2, a.v[2]); + if (usages < 0) goto done; + ScrWcAlgorithm algorithm; + if (!scr_wc_normalize(a.v[0], scr_wc_key_names, SCR_WC_COUNT(scr_wc_key_names), + SCR_WC_OP_GENERATE, &algorithm)) + goto done; + bool hmac = strcmp(algorithm.name, "HMAC") == 0; + if (!hmac && !scr_wc_is_aes(algorithm.name)) { + scr_wc_unsupported_algorithm("generateKey", algorithm.name); + goto done; + } + if (!scr_wc_check_usages(algorithm.name, usages)) goto done; + if (usages == 0) { + scr_throw_domex("SyntaxError", "Usages cannot be empty when creating a key."); + goto done; + } + double length = algorithm.has_length ? algorithm.length + : strcmp(algorithm.hash, "SHA-384") == 0 || strcmp(algorithm.hash, "SHA-512") == 0 + ? 1024 + : 512; + size_t len = scr_wc_bits_to_bytes(length); + unsigned char *bytes = malloc(len ? len : 1); + if (!bytes) scr_trap("scriptc: out of memory\n"); + arc4random_buf(bytes, len); + scr_wc_truncate_bits(bytes, len, length); + key = scr_crypto_key_new(bytes, len, algorithm.name, hmac ? algorithm.hash : NULL, length, + (unsigned)usages, extractable); + volatile unsigned char *wipe = bytes; + for (size_t i = 0; i < len; i++) wipe[i] = 0; + free(bytes); +done: + scr_wc_args_drop(&a, 3); + if (!key) return scr_wc_rejected(); + return scr_wc_deferred(key); +} + +/* exportKeySync for secret keys: raw and raw-secret answer an ArrayBuffer + * of the key bytes, jwk an object { key_ops, ext, alg, kty, k }. */ +static ScrDyn *scr_wc_export(const ScrDyn *args) { + static const char prefix[] = "Failed to execute 'exportKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return NULL; + ScrWcArgs a; + scr_wc_args_take(&a, args, 2); + ScrDyn *result = NULL; + const char *format = scr_wc_key_format(prefix, a.v[0]); + if (!format) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (scr_wc_is_kdf(key->name)) { + char message[64]; + snprintf(message, sizeof message, "%s key export is not supported", key->name); + scr_throw_domex("NotSupportedError", message); + goto done; + } + if (!key->extractable) { + scr_throw_domex("InvalidAccessError", "key is not extractable"); + goto done; + } + if (strcmp(format, "raw") == 0 || strcmp(format, "raw-secret") == 0) { + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(bytes->data, key->data, key->len); + result = scr_array_buffer_from_bytes(bytes); + scr_bytes_release(bytes); + } else if (strcmp(format, "jwk") == 0) { + char buf[16]; + const char *alg = scr_wc_jwk_alg(key->name, key->hash, key->length, buf, sizeof buf); + ScrBytes *bytes = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(bytes->data, key->data, key->len); + ScrStr *encoding = scr_str_new("base64url", 9); + ScrStr *k = scr_bytes_to_str(bytes, encoding); + scr_str_release(encoding); + scr_bytes_release(bytes); + result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "key_ops", 7, scr_wc_usage_array(key->usages)); + scr_dyn_obj_set(result, "ext", 3, scr_dyn_new_bool(key->extractable)); + if (alg) scr_dyn_obj_set(result, "alg", 3, scr_wc_cstr(alg)); + scr_dyn_obj_set(result, "kty", 3, scr_wc_cstr("oct")); + scr_dyn_obj_set(result, "k", 1, scr_dyn_new_str(k)); + scr_str_release(k); + } else { + char message[96]; + snprintf(message, sizeof message, "Unable to export %s secret key using %s format", key->name, + format); + scr_throw_domex("NotSupportedError", message); + } +done: + scr_wc_args_drop(&a, 2); + return result; +} + +ScrPromise *scr_webcrypto_export_key(ScrDyn *args) { + ScrDyn *result = scr_wc_export(args); + if (!result) return scr_wc_rejected(); + return scr_wc_resolved(result); } /* sign(algorithm, key, data) / verify(algorithm, key, signature, data): @@ -717,13 +1245,17 @@ static bool scr_wc_sign_verify(const ScrDyn *args, bool verify, unsigned char ma data = scr_wc_buffer_source(prefix, verify ? 3 : 2, a.v[verify ? 3 : 2]); if (!data) goto done; ScrWcAlgorithm algorithm; - if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), false, - &algorithm)) + if (!scr_wc_normalize(a.v[0], scr_wc_sign_names, SCR_WC_COUNT(scr_wc_sign_names), + SCR_WC_OP_SIGN, &algorithm)) goto done; if (strcmp(algorithm.name, "HMAC") != 0) { scr_wc_unsupported_algorithm(method, algorithm.name); goto done; } + if (strcmp(key->name, algorithm.name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } if (!(key->usages & (verify ? SCR_WC_USAGE_VERIFY : SCR_WC_USAGE_SIGN))) { char message[64]; snprintf(message, sizeof message, "Unable to use this key to %s", method); @@ -827,3 +1359,563 @@ done: scr_crypto_promise_settle_array_buffer); return p; } + +/* ── encrypt and decrypt ────────────────────────────────────────────── + * The WebCrypto half of encrypt and decrypt: argument conversion, the + * AesCbcParams, AesCtrParams and AeadParams dictionaries, and the key + * checks. The cipher itself runs in scr_webcrypto_aes.c, which links + * with the mbedTLS archive and passes it in. */ +static const char *const scr_wc_cipher_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-OCB", "ChaCha20-Poly1305", "RSA-OAEP", +}; + +/* Node's limit on the data, IV and additional data (kMaxBufferLength). */ +static const double scr_wc_max_buffer = 2147483647.0; + +/* A BufferSource member of an algorithm dictionary: false with the error + * pending; *out stays NULL when an optional member is absent. */ +static bool scr_wc_member_buffer(const ScrDyn *dict, const char *dict_name, const char *key, + bool required, ScrBytes **out) { + ScrDyn *value = scr_wc_member(dict, key); + bool ok = true; + if (value->kind == SCR_DYN_UNDEF) { + if (required) { + scr_wc_missing_member(dict_name, key); + ok = false; + } + } else { + ScrDyn *plain = scr_wc_plain(value); + if (!plain) ok = false; + else if (plain->kind == SCR_DYN_BYTES || scr_array_buffer_is(plain)) { + *out = scr_bytes_buffer_source(plain); + } else { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", + "%s: %s in passed algorithm is not instance of ArrayBuffer, Buffer, " + "TypedArray, or DataView.", + scr_wc_normalize_prefix, key); + ok = false; + } + if (plain) scr_dyn_release(plain); + } + scr_dyn_release(value); + return ok; +} + +/* validateByteLength / validateMaxBufferLength over a converted member. */ +static bool scr_wc_exact_length(const ScrBytes *bytes, const char *name, double length) { + if (scr_bytes_byte_len(bytes) == length) return true; + char message[96]; + snprintf(message, sizeof message, "%s must contain exactly %.0f bytes", name, length); + scr_throw_domex("OperationError", message); + return false; +} + +static bool scr_wc_max_length(const ScrBytes *bytes, const char *name) { + if (scr_bytes_byte_len(bytes) <= scr_wc_max_buffer) return true; + char message[96]; + snprintf(message, sizeof message, "%s must be at most %.0f bytes", name, scr_wc_max_buffer); + scr_throw_domex("OperationError", message); + return false; +} + +typedef struct { + ScrBytes *iv; /* the IV or the CTR counter block */ + ScrBytes *aad; + double tag_bits; + double counter_bits; +} ScrWcCipherParams; + +static void scr_wc_cipher_params_drop(ScrWcCipherParams *params) { + if (params->iv) scr_bytes_release(params->iv); + if (params->aad) scr_bytes_release(params->aad); + params->iv = params->aad = NULL; +} + +/* An optional or required [EnforceRange] octet member: false with the + * error pending; *present says whether it was given. */ +static bool scr_wc_member_octet(const ScrDyn *dict, const char *dict_name, const char *key, + bool required, bool *present, double *out) { + ScrDyn *value = scr_wc_member(dict, key); + bool ok = true; + *present = value->kind != SCR_DYN_UNDEF; + if (!*present) { + if (required) { + scr_wc_missing_member(dict_name, key); + ok = false; + } + } else { + char context[48]; + snprintf(context, sizeof context, "%s in passed algorithm", key); + ok = scr_wc_enforce_range(scr_wc_normalize_prefix, context, value, 255, out); + } + scr_dyn_release(value); + return ok; +} + +/* normalizeAlgorithm(algorithm, 'encrypt' / 'decrypt'): the canonical + * name and the AES dictionaries' members, in Web IDL member order with + * each member's validator right after its conversion. */ +static const char *scr_wc_normalize_cipher(ScrDyn *identifier, const char *method, + ScrWcCipherParams *params) { + memset(params, 0, sizeof *params); + params->tag_bits = 128; + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return NULL; + const char *canonical = + scr_wc_lookup(scr_wc_cipher_names, SCR_WC_COUNT(scr_wc_cipher_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return NULL; + } + if (!scr_wc_is_aes(canonical)) { + scr_wc_unsupported_algorithm(method, canonical); + return NULL; + } + ScrDyn *dict = scr_wc_is_object(identifier) ? scr_wc_plain(identifier) + : scr_dyn_retain(scr_dyn_undefined()); + if (!dict) return NULL; + bool ok = true; + bool present; + if (strcmp(canonical, "AES-CBC") == 0) { + ok = scr_wc_member_buffer(dict, "AesCbcParams", "iv", true, ¶ms->iv) && + scr_wc_exact_length(params->iv, "algorithm.iv", 16); + } else if (strcmp(canonical, "AES-CTR") == 0) { + ok = scr_wc_member_buffer(dict, "AesCtrParams", "counter", true, ¶ms->iv) && + scr_wc_exact_length(params->iv, "algorithm.counter", 16) && + scr_wc_member_octet(dict, "AesCtrParams", "length", true, &present, + ¶ms->counter_bits); + if (ok && (params->counter_bits == 0 || params->counter_bits > 128)) { + scr_throw_domex("OperationError", "AES-CTR algorithm.length must be between 1 and 128"); + ok = false; + } + } else { + ok = scr_wc_member_buffer(dict, "AeadParams", "additionalData", false, ¶ms->aad) && + (!params->aad || scr_wc_max_length(params->aad, "algorithm.additionalData")) && + scr_wc_member_buffer(dict, "AeadParams", "iv", true, ¶ms->iv) && + scr_wc_max_length(params->iv, "algorithm.iv") && + scr_wc_member_octet(dict, "AeadParams", "tagLength", false, &present, ¶ms->tag_bits); + if (ok && !present) params->tag_bits = 128; + double t = params->tag_bits; + if (ok && t != 32 && t != 64 && t != 96 && t != 104 && t != 112 && t != 120 && t != 128) { + char message[64]; + snprintf(message, sizeof message, "%.0f is not a valid AES-GCM tag length", t); + scr_throw_domex("OperationError", message); + ok = false; + } + } + scr_dyn_release(dict); + if (!ok) { + scr_wc_cipher_params_drop(params); + return NULL; + } + return canonical; +} + +static void *scr_wc_caught_retain(void *c) { return scr_caught_retain(c); } +static void scr_wc_caught_release(void *c) { scr_caught_release(c); } + +static void scr_wc_settle_rethrow(ScrClosure *cb, ScrError *error, ScrBytes *value) { + (void)error; + (void)value; + ScrPromise *p; + ScrCaught *caught; + memcpy(&p, &cb->caps[0]->slot, sizeof p); + memcpy(&caught, &cb->caps[1]->slot, sizeof caught); + scr_rethrow(caught); + scr_promise_reject_pending(p); +} + +/* A job failure: a fresh promise that rejects with the pending exception + * on a later loop turn, as Node's job promises do (+1). */ +static ScrPromise *scr_wc_deferred_rejection(void) { + ScrPromise *p = scr_promise_new(); + ScrClosure *cb = scr_closure_new(NULL, 2); + cb->caps[0] = scr_box_new_obj(scr_promise_retain_v, scr_promise_release_v, NULL); + scr_box_set_ref(cb->caps[0], scr_promise_retain(p)); + cb->caps[1] = scr_box_new_obj(scr_wc_caught_retain, scr_wc_caught_release, scr_caught_trace_v); + scr_box_set_ref(cb->caps[1], scr_exc_take()); + scr_crypto_defer_bytes(NULL, NULL, cb, scr_wc_settle_rethrow); + return p; +} + +/* A promise that fulfills with an ArrayBuffer of `bytes` (moves) on a + * later loop turn. */ +static ScrPromise *scr_wc_deferred_buffer(ScrBytes *bytes) { + ScrPromise *p = scr_promise_new(); + scr_crypto_defer_bytes(bytes, NULL, scr_crypto_promise_callback(p), + scr_crypto_promise_settle_array_buffer); + return p; +} + +ScrPromise *scr_webcrypto_cipher(ScrDyn *args, bool decrypt, ScrWebCryptoAesFn aes) { + const char *method = decrypt ? "decrypt" : "encrypt"; + const char *prefix = decrypt ? "Failed to execute 'decrypt' on 'SubtleCrypto'" + : "Failed to execute 'encrypt' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 3)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 3); + ScrBytes *data = NULL; + ScrWcCipherParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + data = scr_wc_buffer_source(prefix, 2, a.v[2]); + if (!data) goto done; + const char *name = scr_wc_normalize_cipher(a.v[0], method, ¶ms); + if (!name) goto done; + if (strcmp(key->name, name) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + goto done; + } + if (!(key->usages & (decrypt ? SCR_WC_USAGE_DECRYPT : SCR_WC_USAGE_ENCRYPT))) { + char message[64]; + snprintf(message, sizeof message, "Unable to use this key to %s", method); + scr_throw_domex("InvalidAccessError", message); + goto done; + } + if (!scr_wc_max_length(data, "data")) goto done; + { + ScrBytes *view = data; + SCR_BYTES_SNAPSHOT(view); + ScrBytes *iv = params.iv; + SCR_BYTES_SNAPSHOT(iv); + if (!params.aad) params.aad = scr_bytes_new(SCR_BYTES_U8, 0); + ScrBytes *aad = params.aad; + SCR_BYTES_SNAPSHOT(aad); + ScrWebCryptoAesParams run = { + .iv = iv->data, + .iv_len = (size_t)scr_bytes_byte_len(iv), + .aad = aad->data, + .aad_len = (size_t)scr_bytes_byte_len(aad), + .tag_len = (size_t)(params.tag_bits / 8), + .counter_bits = (unsigned)params.counter_bits, + }; + ScrBytes *out = aes(name, decrypt, key->data, key->len, &run, view->data, + (size_t)scr_bytes_byte_len(view)); + if (out) { + result = scr_wc_deferred_buffer(out); + } else { + scr_throw_domex("OperationError", "The operation failed for an operation-specific reason"); + result = scr_wc_deferred_rejection(); + } + } +done: + if (data) scr_bytes_release(data); + scr_wc_cipher_params_drop(¶ms); + scr_wc_args_drop(&a, 3); + return result ? result : scr_wc_rejected(); +} + +/* ── deriveBits and deriveKey ───────────────────────────────────────── + * PBKDF2 and HKDF over node:crypto's KDF core (scr_crypto_pbkdf2 and + * scr_crypto_hkdf_bytes in scr_lib.c), with Node's Pbkdf2Params and + * HkdfParams dictionaries, length rules and key checks. deriveKey derives + * the bits the derived key type needs and imports them as a raw-secret + * key of that type. */ +static const char *const scr_wc_derive_names[] = { + "Argon2d", "Argon2i", "Argon2id", "ECDH", "HKDF", "PBKDF2", "X25519", "X448", +}; +/* The algorithms 'get key length' registers. */ +static const char *const scr_wc_length_names[] = { + "AES-CBC", "AES-CTR", "AES-GCM", "AES-KW", "AES-OCB", "Argon2d", "Argon2i", + "Argon2id", "ChaCha20-Poly1305", "HKDF", "HMAC", "KMAC128", "KMAC256", "PBKDF2", +}; + +typedef struct { + const char *name; + const char *hash; + double iterations; + ScrBytes *salt; + ScrBytes *info; +} ScrWcKdfParams; + +static void scr_wc_kdf_params_drop(ScrWcKdfParams *params) { + if (params->salt) scr_bytes_release(params->salt); + if (params->info) scr_bytes_release(params->info); + params->salt = params->info = NULL; +} + +/* A HashAlgorithmIdentifier member with ensureSHA (`dict` names the + * dictionary); *hash is the member value for the later digest + * normalization (+1). */ +static bool scr_wc_member_hash(const ScrDyn *dict, const char *dict_name, ScrDyn **hash) { + ScrDyn *value = scr_wc_member(dict, "hash"); + if (value->kind == SCR_DYN_UNDEF) { + scr_dyn_release(value); + scr_wc_missing_member(dict_name, "hash"); + return false; + } + ScrStr *name = NULL; + bool ok = true; + if (scr_wc_is_object(value)) { + ScrDyn *plain = scr_wc_plain(value); + ScrDyn *member = plain ? scr_wc_member(plain, "name") : NULL; + if (plain) scr_dyn_release(plain); + if (!member) ok = false; + else { + if (member->kind == SCR_DYN_STR) name = scr_str_retain(member->v.str); + scr_dyn_release(member); + } + *hash = scr_dyn_retain(value); + } else { + name = scr_wc_dom_string(value); + if (!name) ok = false; + *hash = name ? scr_dyn_new_str(name) : NULL; + } + scr_dyn_release(value); + bool sha = name && name->len >= 3 && (name->data[0] | 32) == 's' && + (name->data[1] | 32) == 'h' && (name->data[2] | 32) == 'a'; + if (name) scr_str_release(name); + if (ok && !sha) { + char message[96]; + snprintf(message, sizeof message, "Only SHA hashes are supported in %s", dict_name); + scr_throw_domex("NotSupportedError", message); + ok = false; + } + if (!ok && *hash) { + scr_dyn_release(*hash); + *hash = NULL; + } + return ok; +} + +/* normalizeAlgorithm(algorithm, 'deriveBits') for PBKDF2 and HKDF. */ +static bool scr_wc_normalize_kdf(ScrDyn *identifier, const char *method, ScrWcKdfParams *params) { + memset(params, 0, sizeof *params); + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + params->name = scr_wc_lookup(scr_wc_derive_names, SCR_WC_COUNT(scr_wc_derive_names), name); + scr_str_release(name); + if (!params->name) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + if (!scr_wc_is_kdf(params->name)) { + scr_wc_unsupported_algorithm(method, params->name); + return false; + } + bool pbkdf2 = strcmp(params->name, "PBKDF2") == 0; + const char *dict_name = pbkdf2 ? "Pbkdf2Params" : "HkdfParams"; + ScrDyn *dict = scr_wc_is_object(identifier) ? scr_wc_plain(identifier) + : scr_dyn_retain(scr_dyn_undefined()); + if (!dict) return false; + ScrDyn *hash = NULL; + bool ok = scr_wc_member_hash(dict, dict_name, &hash); + if (ok && pbkdf2) { + ScrDyn *iterations = scr_wc_member(dict, "iterations"); + if (iterations->kind == SCR_DYN_UNDEF) { + scr_wc_missing_member(dict_name, "iterations"); + ok = false; + } else if (!scr_wc_enforce_range(scr_wc_normalize_prefix, "iterations in passed algorithm", + iterations, 4294967295.0, ¶ms->iterations)) { + ok = false; + } else if (params->iterations == 0) { + scr_throw_domex("OperationError", "iterations cannot be zero"); + ok = false; + } else if (params->iterations > 2147483647.0) { + scr_throw_domex("NotSupportedError", "iterations exceeds the implementation limit"); + ok = false; + } + scr_dyn_release(iterations); + ok = ok && scr_wc_member_buffer(dict, dict_name, "salt", true, ¶ms->salt); + } else if (ok) { + ok = scr_wc_member_buffer(dict, dict_name, "info", true, ¶ms->info); + if (ok && scr_bytes_byte_len(params->info) > 1024) { + scr_throw_domex("OperationError", "algorithm.info must be at most 1024 bytes"); + ok = false; + } + ok = ok && scr_wc_member_buffer(dict, dict_name, "salt", true, ¶ms->salt); + } + scr_dyn_release(dict); + if (ok) { + params->hash = scr_wc_digest_algorithm(hash, method); + if (!params->hash) ok = false; + } + if (hash) scr_dyn_release(hash); + if (!ok) scr_wc_kdf_params_drop(params); + return ok; +} + +/* The length rules of pbkdf2DeriveBits and hkdfDeriveBits; `null_length` + * says the length was null. */ +static bool scr_wc_kdf_length(const ScrWcKdfParams *params, bool null_length, double length) { + if (null_length) { + scr_throw_domex("OperationError", "length cannot be null"); + return false; + } + if (fmod(length, 8) != 0) { + scr_throw_domex("OperationError", "length must be a multiple of 8"); + return false; + } + if (strcmp(params->name, "HKDF") == 0) { + double digest = strcmp(params->hash, "SHA-1") == 0 ? 20 + : strcmp(params->hash, "SHA-256") == 0 ? 32 + : strcmp(params->hash, "SHA-384") == 0 ? 48 + : 64; + if (length > 255 * digest * 8) { + scr_throw_domex("OperationError", "length exceeds the maximum derived bit length"); + return false; + } + } + return true; +} + +/* The derived bytes (+1); length is a nonzero multiple of 8. */ +static ScrBytes *scr_wc_kdf_derive(const ScrWcKdfParams *params, const ScrCryptoKey *key, + double length) { + ScrBytes *secret = scr_bytes_new(SCR_BYTES_U8, (double)key->len); + if (key->len) memcpy(secret->data, key->data, key->len); + const char *native = scr_wc_native_hash(params->hash); + ScrStr *digest = scr_str_new(native, strlen(native)); + ScrBytes *out; + if (strcmp(params->name, "PBKDF2") == 0) { + out = scr_crypto_pbkdf2(secret, params->salt, params->iterations, length / 8, digest); + } else { + out = scr_crypto_hkdf_bytes(digest, secret, params->salt, params->info, length / 8); + } + scr_str_release(digest); + volatile unsigned char *wipe = secret->data; + for (size_t i = 0; i < key->len; i++) wipe[i] = 0; + scr_bytes_release(secret); + return out; +} + +/* The base key checks deriveBits and deriveKey share. */ +static bool scr_wc_check_base_key(const ScrCryptoKey *key, const char *algorithm, unsigned usage, + const char *usage_name) { + if (!(key->usages & usage)) { + char message[64]; + snprintf(message, sizeof message, "baseKey does not have %s usage", usage_name); + scr_throw_domex("InvalidAccessError", message); + return false; + } + if (strcmp(key->name, algorithm) != 0) { + scr_throw_domex("InvalidAccessError", "Key algorithm mismatch"); + return false; + } + return true; +} + +/* deriveBits(algorithm, baseKey[, length]). */ +ScrPromise *scr_webcrypto_derive_bits(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'deriveBits' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 2)) return scr_wc_rejected(); + size_t argc = (size_t)scr_dyn_arr_len(args); + ScrWcArgs a; + size_t n = argc > 3 ? 3 : argc; + scr_wc_args_take(&a, args, n); + ScrWcKdfParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + /* length defaults to null when absent or undefined. */ + bool null_length = n < 3 || a.v[2]->kind == SCR_DYN_UNDEF || a.v[2]->kind == SCR_DYN_NULL; + double length = 0; + if (!null_length && !scr_wc_enforce_range(prefix, "3rd argument", a.v[2], 4294967295.0, &length)) + goto done; + if (!scr_wc_normalize_kdf(a.v[0], "deriveBits", ¶ms)) goto done; + if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_BITS, "deriveBits")) goto done; + if (!scr_wc_kdf_length(¶ms, null_length, length)) goto done; + if (length == 0) { + ScrBytes *empty = scr_bytes_new(SCR_BYTES_U8, 0); + result = scr_wc_resolved(scr_array_buffer_from_bytes(empty)); + scr_bytes_release(empty); + goto done; + } + ScrBytes *bits = scr_wc_kdf_derive(¶ms, key, length); + if (bits) result = scr_wc_deferred_buffer(bits); +done: + scr_wc_kdf_params_drop(¶ms); + scr_wc_args_drop(&a, n); + return result ? result : scr_wc_rejected(); +} + +/* getKeyLength for the derived key type: the bits to derive, or *null + * for the KDF types (whose derivation then rejects). */ +static bool scr_wc_derived_length(ScrDyn *identifier, const ScrWcAlgorithm *imported, + bool *null_length, double *length) { + ScrStr *name = scr_wc_algorithm_name(identifier, "Algorithm"); + if (!name) return false; + const char *canonical = + scr_wc_lookup(scr_wc_length_names, SCR_WC_COUNT(scr_wc_length_names), name); + scr_str_release(name); + if (!canonical) { + scr_throw_domex("NotSupportedError", "Unrecognized algorithm name"); + return false; + } + *null_length = false; + if (scr_wc_is_aes(canonical)) { + ScrWcAlgorithm aes; + memset(&aes, 0, sizeof aes); + if (!scr_wc_aes_length_params(identifier, "AesDerivedKeyParams", &aes)) return false; + *length = aes.length; + return true; + } + if (strcmp(canonical, "HMAC") == 0 && imported->hash) { + *length = imported->has_length ? imported->length + : strcmp(imported->hash, "SHA-384") == 0 || strcmp(imported->hash, "SHA-512") == 0 + ? 1024 + : 512; + return true; + } + *null_length = true; + return true; +} + +/* deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages): + * the derivation's errors reject at once; the import of the derived bits + * settles, or rejects, on a later loop turn. */ +ScrPromise *scr_webcrypto_derive_key(ScrDyn *args) { + static const char prefix[] = "Failed to execute 'deriveKey' on 'SubtleCrypto'"; + if (!scr_wc_required(prefix, args, 5)) return scr_wc_rejected(); + ScrWcArgs a; + scr_wc_args_take(&a, args, 5); + ScrWcKdfParams params; + memset(¶ms, 0, sizeof params); + ScrPromise *result = NULL; + if (!scr_wc_algorithm_identifier(a.v[0])) goto done; + ScrCryptoKey *key = scr_crypto_key_of(a.v[1]); + if (!key) { + scr_wc_type_error("ERR_INVALID_ARG_TYPE", "%s: 2nd argument is not of type CryptoKey.", prefix); + goto done; + } + if (!scr_wc_algorithm_identifier(a.v[2])) goto done; + bool extractable = scr_dyn_truthy(a.v[3]); + long usages = scr_wc_key_usages(prefix, 4, a.v[4]); + if (usages < 0) goto done; + if (!scr_wc_normalize_kdf(a.v[0], "deriveKey", ¶ms)) goto done; + ScrWcAlgorithm imported; + if (!scr_wc_normalize(a.v[2], scr_wc_import_names, SCR_WC_COUNT(scr_wc_import_names), + SCR_WC_OP_IMPORT, &imported)) + goto done; + bool null_length; + double length; + if (!scr_wc_derived_length(a.v[2], &imported, &null_length, &length)) goto done; + if (strcmp(imported.name, "HMAC") != 0 && !scr_wc_is_aes(imported.name) && + !scr_wc_is_kdf(imported.name)) { + scr_wc_unsupported_algorithm("deriveKey", imported.name); + goto done; + } + if (!scr_wc_check_base_key(key, params.name, SCR_WC_USAGE_DERIVE_KEY, "deriveKey")) goto done; + if (!scr_wc_kdf_length(¶ms, null_length, length)) goto done; + ScrBytes *bits = scr_wc_kdf_derive(¶ms, key, length); + if (!bits) goto done; + ScrDyn *derived = scr_wc_import_key_sync("raw-secret", bits, NULL, &imported, extractable, usages); + scr_bytes_release(bits); + result = derived ? scr_wc_deferred(derived) : scr_wc_deferred_rejection(); +done: + scr_wc_kdf_params_drop(¶ms); + scr_wc_args_drop(&a, 5); + return result ? result : scr_wc_rejected(); +} diff --git a/packages/runtime/src/scr_webcrypto_aes.c b/packages/runtime/src/scr_webcrypto_aes.c new file mode 100644 index 000000000..7cb63f561 --- /dev/null +++ b/packages/runtime/src/scr_webcrypto_aes.c @@ -0,0 +1,137 @@ +/* WebCrypto's AES ciphers: crypto.subtle.encrypt and decrypt with AES-CBC, + * AES-CTR and AES-GCM keys, over the vendored mbedTLS block cipher. + * + * scr_webcrypto.c converts and validates the arguments, normalizes the + * algorithm and checks the key; this unit runs the cipher itself, the way + * Node 24's src/crypto/crypto_aes.cc does: CBC with PKCS#7 padding, CTR + * with a counter that wraps within its low `length` bits (and fails when + * the input needs more blocks than those bits can count), and GCM with a + * 12 to 128 byte IV and a 4 to 16 byte tag appended to the ciphertext. + * The unit links only into programs that call these methods, alongside + * the cipher feature's mbedTLS archive. */ +#include "scr_runtime.h" + +#include +#include + +#include "mbedtls/aes.h" +#include "mbedtls/gcm.h" +#include "mbedtls/platform_util.h" + +static ScrBytes *scr_wc_aes_gcm(bool decrypt, const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + if (p->iv_len < 12 || p->iv_len > 128) return NULL; + if (decrypt && len < p->tag_len) return NULL; + size_t out_len = decrypt ? len - p->tag_len : len + p->tag_len; + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)out_len); + mbedtls_gcm_context gcm; + mbedtls_gcm_init(&gcm); + int rc = mbedtls_gcm_setkey(&gcm, MBEDTLS_CIPHER_ID_AES, key, (unsigned)(key_len * 8)); + if (rc == 0 && decrypt) { + rc = mbedtls_gcm_auth_decrypt(&gcm, out_len, p->iv, p->iv_len, p->aad, p->aad_len, + in + out_len, p->tag_len, in, out->data); + } else if (rc == 0) { + rc = mbedtls_gcm_crypt_and_tag(&gcm, MBEDTLS_GCM_ENCRYPT, len, p->iv, p->iv_len, p->aad, + p->aad_len, in, out->data, p->tag_len, out->data + len); + } + mbedtls_gcm_free(&gcm); + if (rc != 0) { + mbedtls_platform_zeroize(out->data, out_len); + scr_bytes_release(out); + return NULL; + } + return out; +} + +static ScrBytes *scr_wc_aes_cbc(bool decrypt, const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + if (decrypt && (len == 0 || len % 16 != 0)) return NULL; + size_t padded = decrypt ? len : (len / 16 + 1) * 16; + unsigned char *buf = malloc(padded); + if (!buf) scr_trap("scriptc: out of memory\n"); + memcpy(buf, in, len); + if (!decrypt) memset(buf + len, (int)(padded - len), padded - len); + unsigned char iv[16]; + memcpy(iv, p->iv, 16); + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + int rc = decrypt ? mbedtls_aes_setkey_dec(&aes, key, (unsigned)(key_len * 8)) + : mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8)); + if (rc == 0) + rc = mbedtls_aes_crypt_cbc(&aes, decrypt ? MBEDTLS_AES_DECRYPT : MBEDTLS_AES_ENCRYPT, padded, + iv, buf, buf); + mbedtls_aes_free(&aes); + size_t out_len = padded; + if (rc == 0 && decrypt) { + /* PKCS#7: the final byte names 1 to 16 bytes of padding, each of + * which holds that value. Checked without data-dependent branches. */ + unsigned char pad = buf[padded - 1]; + unsigned char diff = (unsigned char)-(pad == 0 || pad > 16); + for (unsigned i = 1; i <= 16; i++) + diff |= (unsigned char)((buf[padded - i] ^ pad) & (unsigned char)-(i <= pad)); + if (diff) rc = -1; + else out_len = padded - pad; + } + ScrBytes *out = NULL; + if (rc == 0) { + out = scr_bytes_new(SCR_BYTES_U8, (double)out_len); + memcpy(out->data, buf, out_len); + } + mbedtls_platform_zeroize(buf, padded); + free(buf); + return out; +} + +/* Adds one to the low `bits` bits of a big-endian counter block, leaving + * the bits above them unchanged. */ +static void scr_wc_ctr_increment(unsigned char block[16], unsigned bits) { + for (int i = 15; i >= 0 && bits > 0; i--) { + unsigned width = bits >= 8 ? 8 : bits; + unsigned mask = (1u << width) - 1; + unsigned low = ((unsigned)block[i] + 1) & mask; + block[i] = (unsigned char)((block[i] & ~mask) | low); + bits -= width; + if (low != 0) return; + } +} + +static ScrBytes *scr_wc_aes_ctr(const unsigned char *key, size_t key_len, + const ScrWebCryptoAesParams *p, const unsigned char *in, + size_t len) { + size_t blocks = (len + 15) / 16; + if (p->counter_bits < 64 && (double)blocks > (double)(1ull << p->counter_bits)) return NULL; + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + if (mbedtls_aes_setkey_enc(&aes, key, (unsigned)(key_len * 8)) != 0) { + mbedtls_aes_free(&aes); + return NULL; + } + ScrBytes *out = scr_bytes_new(SCR_BYTES_U8, (double)len); + unsigned char counter[16], stream[16]; + memcpy(counter, p->iv, 16); + for (size_t off = 0; off < len; off += 16) { + mbedtls_aes_crypt_ecb(&aes, MBEDTLS_AES_ENCRYPT, counter, stream); + size_t n = len - off < 16 ? len - off : 16; + for (size_t i = 0; i < n; i++) out->data[off + i] = (unsigned char)(in[off + i] ^ stream[i]); + scr_wc_ctr_increment(counter, p->counter_bits); + } + mbedtls_platform_zeroize(stream, sizeof stream); + mbedtls_aes_free(&aes); + return out; +} + +/* The cipher scr_webcrypto_cipher runs: +1 output bytes, or NULL when the + * operation fails (Node's OperationError). */ +static ScrBytes *scr_wc_aes(const char *name, bool decrypt, const unsigned char *key, + size_t key_len, const ScrWebCryptoAesParams *p, + const unsigned char *in, size_t len) { + if (strcmp(name, "AES-GCM") == 0) return scr_wc_aes_gcm(decrypt, key, key_len, p, in, len); + if (strcmp(name, "AES-CBC") == 0) return scr_wc_aes_cbc(decrypt, key, key_len, p, in, len); + return scr_wc_aes_ctr(key, key_len, p, in, len); +} + +ScrPromise *scr_webcrypto_encrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, false, scr_wc_aes); } + +ScrPromise *scr_webcrypto_decrypt(ScrDyn *args) { return scr_webcrypto_cipher(args, true, scr_wc_aes); } diff --git a/packages/runtime/src/scr_webcrypto_object.c b/packages/runtime/src/scr_webcrypto_object.c new file mode 100644 index 000000000..c20ef13dc --- /dev/null +++ b/packages/runtime/src/scr_webcrypto_object.c @@ -0,0 +1,53 @@ +/* The crypto global as a value: node:crypto's dynamic Crypto object + * (scr_crypto_native in scr_async_dyn.c, which carries subtle.digest) with + * the Crypto object's own randomUUID and getRandomValues methods, for + * untyped reads such as uuid's `crypto.randomUUID && crypto.randomUUID()`. + * Node's receiver check (ERR_INVALID_THIS on a detached call) is not + * modeled. There is one object per thread, so every spelling of the + * global (crypto, globalThis.crypto, node:crypto's webcrypto) is the same + * value; it is released at exit. Linked only into programs that read the + * global as a value. */ +#include "scr_runtime.h" + +static ScrDyn *scr_webcrypto_random_uuid_call(ScrClosure *closure, ScrDyn *const *args, size_t argc) { + (void)closure; + (void)args; + (void)argc; + ScrStr *uuid = scr_crypto_random_uuid(); + ScrDyn *result = scr_dyn_new_str(uuid); + scr_str_release(uuid); + return result; +} + +static ScrDyn *scr_webcrypto_get_random_values_call(ScrClosure *closure, ScrDyn *const *args, + size_t argc) { + (void)closure; + if (argc == 0) { + static const char message[] = + "Failed to execute 'getRandomValues' on 'Crypto': 1 argument required, but only 0 present."; + scr_throw_error_msg_code(SCR_ERR_TYPE, message, sizeof message - 1, "ERR_MISSING_ARGS"); + return NULL; + } + return scr_crypto_get_random_values_dyn(args[0]); +} + +static SCR_TL ScrDyn *scr_webcrypto_object_value; + +static void scr_webcrypto_object_cleanup(void) { + if (scr_webcrypto_object_value) scr_dyn_release(scr_webcrypto_object_value); + scr_webcrypto_object_value = NULL; +} + +ScrDyn *scr_webcrypto_object(void) { + if (scr_webcrypto_object_value) return scr_dyn_retain(scr_webcrypto_object_value); + ScrDyn *result = scr_crypto_native(); + scr_dyn_obj_set(result, "randomUUID", 10, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_webcrypto_random_uuid_call, 0, + "crypto.randomUUID", "randomUUID")); + scr_dyn_obj_set(result, "getRandomValues", 15, + scr_dyn_new_func(scr_closure_new(NULL, 0), scr_webcrypto_get_random_values_call, 1, + "crypto.getRandomValues", "getRandomValues")); + scr_webcrypto_object_value = result; + scr_atexit(scr_webcrypto_object_cleanup); + return scr_dyn_retain(result); +} diff --git a/packages/runtime/src/scr_zlib_stream.c b/packages/runtime/src/scr_zlib_stream.c index 4ed717d53..54ffacf02 100644 --- a/packages/runtime/src/scr_zlib_stream.c +++ b/packages/runtime/src/scr_zlib_stream.c @@ -104,7 +104,8 @@ static void scr_zs_finish(ScrZlibStream *zs, ScrStream *s) { zs->flushing = false; scr_stream_flush_done(s, NULL, NULL, NULL); } else { - scr_stream_transform_done(s, NULL, NULL, NULL); + /* scr_zs_complete has applied Transform._write's callback rule. */ + scr_stream_write_done(s, NULL); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3974f346e..2404c3885 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: 3.22.2 version: 3.22.2 js-yaml: - specifier: 4.3.0 - version: 4.3.0 + specifier: 4.3.2 + version: 4.3.2 oxfmt: specifier: 0.71.0 version: 0.71.0 @@ -41,6 +41,9 @@ importers: rollup: specifier: 4.62.2 version: 4.62.2 + smol-toml: + specifier: 1.7.1 + version: 1.7.1 three: specifier: 0.186.1 version: 0.186.1 @@ -2089,8 +2092,8 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true json-schema-to-ts@1.6.4: @@ -2458,6 +2461,10 @@ packages: resolution: {integrity: sha512-QlaZEqcAH3/RtNyet1IPIYPsEWAaYyXXv1Krsi+1L/QHppjX4Ifm8MQsBISz9vE8cHicIq3clogsheili5vhaQ==} engines: {node: '>= 18'} + smol-toml@1.7.1: + resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==} + engines: {node: '>= 18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -4305,7 +4312,7 @@ snapshots: dependencies: argparse: 2.0.1 - js-yaml@4.3.0: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -4694,6 +4701,8 @@ snapshots: smol-toml@1.5.2: {} + smol-toml@1.7.1: {} + source-map-js@1.2.1: {} srvx@0.11.16: {} diff --git a/tests/corpus/6826-map-primitive-constructors.ts b/tests/corpus/6826-map-primitive-constructors.ts new file mode 100644 index 000000000..f6d61bd48 --- /dev/null +++ b/tests/corpus/6826-map-primitive-constructors.ts @@ -0,0 +1,25 @@ +// The String, Number and Boolean constructors passed as callbacks: over +// unknown arrays they convert any element, over typed arrays String converts +// each element as String(value) does, and the boxed values keep identity. +const values: unknown[] = [1, "b", true, null, undefined, Symbol("s"), [1, 2], { a: 1 }, 2n]; +console.log(values.map(String).join("|")); +console.log(values.slice(0, 5).map(Number), values.map(Boolean)); + +function joinAll(...parts: unknown[]): string { + return parts.map(String).join(","); +} +console.log(joinAll(1, "a", false, null)); + +const numbers = [1, 2.5, -0, NaN, Infinity]; +console.log(numbers.map(String)); +const mixed: (number | string | boolean)[] = [1, "x", false]; +console.log(mixed.map(String)); +const symbols: (symbol | string)[] = [Symbol("q"), "r", Symbol()]; +console.log(symbols.map(String)); +console.log([10n, -3n].map(String)); +console.log(["1", " 2 ", "x", ""].map(Number), ["", "0", "a"].map(Boolean)); + +const boxed: unknown = String; +const table: { [name: string]: unknown } = { s: String, n: Number, b: Boolean }; +console.log(boxed === String, table.n === Number, table.b === String, typeof boxed); +console.log(values.filter(Boolean).length, values.some(Boolean), numbers.every(Boolean)); diff --git a/tests/corpus/6827-recursive-union-nested-literals.ts b/tests/corpus/6827-recursive-union-nested-literals.ts new file mode 100644 index 000000000..900bbf977 --- /dev/null +++ b/tests/corpus/6827-recursive-union-nested-literals.ts @@ -0,0 +1,29 @@ +// A recursive JSON-like union with an array arm and an index-signature +// record arm: nested literals mix records into arrays and arrays into +// records at every depth. +type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +function show(value: Json): string { + return JSON.stringify(value); +} + +const config: Json = { list: [1, "two", null, { nested: [true, { deeper: [] }] }], flag: false }; +console.log(show(config)); +console.log(show({ list: [{ n: true }] })); +console.log(show([1, { k: null }, [{ a: [2] }]])); +console.log(show({ a: { b: [{ c: "x" }, 3] } })); + +const rows: Json[] = [{ id: 1, tags: ["a", { t: 2 }] }, [null, { x: [] }]]; +console.log(rows.map(show).join(" | ")); + +let value: Json = []; +value = { replaced: [1, { again: [true] }] }; +console.log(show(value)); + +const doc: { [key: string]: Json } = { items: [{ name: "x", parts: [1, { p: "q" }] }] }; +console.log(show(doc)); +const items = doc["items"]; +if (Array.isArray(items)) { + items.push({ added: [9] }); + console.log(items.length, show(items)); +} diff --git a/tests/corpus/6828-recursive-union-object-walkers.ts b/tests/corpus/6828-recursive-union-object-walkers.ts new file mode 100644 index 000000000..5c854eb94 --- /dev/null +++ b/tests/corpus/6828-recursive-union-object-walkers.ts @@ -0,0 +1,83 @@ +// Recursive walkers over a JSON-like union: the array branch calls the +// walker for each element, and the record branch reads the narrowed record +// with Object.values, Object.keys and Object.entries. +type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +function depth(value: Json): number { + if (Array.isArray(value)) return 1 + Math.max(0, ...value.map(depth)); + if (value !== null && typeof value === "object") return 1 + Math.max(0, ...Object.values(value).map(depth)); + return 0; +} + +function count(value: Json): number { + let total = 1; + if (Array.isArray(value)) { + for (const item of value) total += count(item); + } else if (value !== null && typeof value === "object") { + for (const item of Object.values(value)) total += count(item); + } + return total; +} + +function paths(value: Json, prefix: string): string[] { + if (Array.isArray(value)) return value.flatMap((item, index) => paths(item, `${prefix}[${index}]`)); + if (value !== null && typeof value === "object") { + const out: string[] = []; + for (const key of Object.keys(value)) out.push(...paths(value[key]!, `${prefix}.${key}`)); + for (const [key, item] of Object.entries(value)) if (item === null) out.push(`${prefix}.${key}=null`); + return out; + } + return [`${prefix}=${String(value)}`]; +} + +const doc: Json = JSON.parse('{"a":[1,{"b":[true,null]}],"c":{"d":"x"},"e":[]}'); +console.log(depth(doc), count(doc)); +console.log(paths(doc, "$").join(" ")); +console.log(depth(3), count([[[]]]), paths([], "$")); + +// A computed-key read or write in a branch that an array hole reaches throws +// Node's TypeError after the key (and, for a write, the value) evaluates. +type Bag = number | string | number[] | { [key: string]: number }; +const keys: string[] = []; +function keyOf(k: string): string { + keys.push(k); + return k; +} +function isBag(value: Bag): value is { [key: string]: number } { + return !Array.isArray(value) && typeof value !== "string" && typeof value !== "number"; +} +function bump(value: Bag, k: string): string { + if (isBag(value)) { + value[keyOf(k)] = (value[keyOf(k)] ?? 0) + 1; + return String(value[keyOf(k)]); + } + return "-"; +} +function put(value: Bag, k: string): string { + if (isBag(value)) { + value[keyOf(k)] = keys.length; + return "put"; + } + return "-"; +} +function peek(value: Bag, k: string): string { + if (isBag(value)) return typeof value[keyOf(k)]; + return "-"; +} +const bags: Bag[] = [{ a: 1 }, "s", [1]]; +bags.length = 4; +for (const which of ["bump", "put", "peek"]) { + const out: string[] = []; + for (let i = 0; i < bags.length; i++) { + try { + const k = "a" + which.length; + if (which === "bump") out.push(bump(bags[i], k)); + else if (which === "put") out.push(put(bags[i], k)); + else out.push(peek(bags[i], k)); + } catch (e) { + out.push(e instanceof TypeError ? e.message : "other"); + } + } + console.log(out.join(" | ")); +} +console.log(keys.join(","), JSON.stringify(bags)); diff --git a/tests/corpus/6862-readline-chained-listeners.cjs b/tests/corpus/6862-readline-chained-listeners.cjs new file mode 100644 index 000000000..78c05063f --- /dev/null +++ b/tests/corpus/6862-readline-chained-listeners.cjs @@ -0,0 +1,29 @@ +// Chained readline listener registration: on(), once() and addListener() +// return the interface, so `createInterface(...).on('line', f).on('close', g)` +// registers both listeners, in order, on the same interface; a chain's value +// is the interface itself. +'use strict'; +const readline = require('node:readline'); +const { Readable } = require('node:stream'); + +const lines = []; +readline + .createInterface({ input: Readable.from(['alpha\nbeta\n', 'gamma\n']), terminal: false }) + .on('line', (line) => lines.push(line)) + .once('line', (line) => console.log('first line', line)) + .on('close', () => console.log('lines', lines.length, lines.join(','))); + +const rl = readline.createInterface({ input: Readable.from(['one\ntwo']) }); +const same = rl.on('line', (line) => console.log('rl line', line)).addListener('close', () => { + console.log('rl close'); +}); +console.log('chain returns the interface', same === rl); + +let count = 0; +readline + .createInterface({ input: Readable.from(['x\ny\nz\n']) }) + .on('line', () => { + count++; + }) + .on('pause', () => console.log('pause after', count)) + .on('close', () => console.log('count', count)); diff --git a/tests/corpus/6865-number-parseint-default-radix.ts b/tests/corpus/6865-number-parseint-default-radix.ts new file mode 100644 index 000000000..dc6d2a4ad --- /dev/null +++ b/tests/corpus/6865-number-parseint-default-radix.ts @@ -0,0 +1,16 @@ +// Number.parseInt without a radix parses as the global parseInt does: radix +// 10, or 16 after a 0x prefix, after leading whitespace and a sign, stopping +// at the first character that is not a digit. +const inputs = ["42", " 17px", "0x1F", "-0x10", "0X0a", "08", "010", "1e3", "", " ", "abc", " -0", "+12", "9007199254740993", "0b11", "0o17", "\t\n 99 bottles", "١٢", "-", "0x"]; +for (const text of inputs) { + const parsed = Number.parseInt(text); + console.log(JSON.stringify(text), Object.is(parsed, -0) ? "-0" : String(parsed), parsed === parseInt(text)); +} + +function depth(arg: string | undefined): number { + return Math.max(6, Number.parseInt(arg ?? "10")); +} +console.log("depth", depth(process.argv[2]), depth("21"), depth("3")); + +const sizes = "640x480".split("x").map((part) => Number.parseInt(part)); +console.log("sizes", sizes[0]! * sizes[1]!); diff --git a/tests/corpus/6866-number-parseint-untyped.js b/tests/corpus/6866-number-parseint-untyped.js new file mode 100644 index 000000000..48223bd9b --- /dev/null +++ b/tests/corpus/6866-number-parseint-untyped.js @@ -0,0 +1,9 @@ +// Number.parseInt over untyped values in JavaScript, with and without a +// radix: the argument converts with ToString first ("undefined", "null", +// "1,2" for an array), then parses as the global parseInt does. +const config = JSON.parse('{"port":"8080","hex":"0xff","list":[3,4],"flag":true,"none":null}'); +for (const key of ["port", "hex", "list", "flag", "none", "missing"]) { + const value = config[key]; + console.log(key, Number.parseInt(value), Number.parseInt(value, 16), Number.parseInt(value, 36)); +} +console.log("argv", Number.parseInt(process.argv[2])); diff --git a/tests/corpus/6867-string-match-optional-regexp.js b/tests/corpus/6867-string-match-optional-regexp.js new file mode 100644 index 000000000..e847bda36 --- /dev/null +++ b/tests/corpus/6867-string-match-optional-regexp.js @@ -0,0 +1,20 @@ +// String match and search in JavaScript with a pattern that may be undefined +// (a Map lookup or an array read): a RegExp matches as usual, and an +// undefined pattern is `new RegExp(undefined)`, the empty pattern. +const text = 'agggtaaa tttaccct agggtaaa'; +const patterns = new Map([ + ['forward', /agggtaaa/g], + ['either', /agggtaaa|tttaccct/gi], + ['first', /t+a/], +]); +for (const name of ['forward', 'either', 'first', 'missing']) { + const re = patterns.get(name); + const m = text.match(re); + console.log(name, m === null ? null : m.length, m === null ? '' : JSON.stringify([...m]), text.search(re)); +} + +const list = [/a(g+)/, /x/]; +for (let i = 0; i < 3; i++) { + const m = text.match(list[i]); + console.log(i, m === null ? 'null' : `${m[0]} at ${m.index} ${m[1] ?? ''}`, text.search(list[i])); +} diff --git a/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs new file mode 100644 index 000000000..ff2c59a46 --- /dev/null +++ b/tests/corpus/6868-buffer-from-untyped-array-buffer.cjs @@ -0,0 +1,56 @@ +// Buffer.from over an untyped ArrayBuffer or SharedArrayBuffer (read from a +// Map, an untyped object field or a worker message): the Buffer covers the +// buffer from its byte offset (the second argument) and shares its memory, in +// both directions, as in Node. An encoding name or NaN in the second argument +// is offset 0; an offset past the end throws ERR_BUFFER_OUT_OF_BOUNDS. +'use strict'; +const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); + +if (isMainThread) { + const store = new Map(); + store.set('plain', new ArrayBuffer(6)); + store.set('shared', new SharedArrayBuffer(6)); + for (const key of ['plain', 'shared']) { + const storage = store.get(key); + const buf = Buffer.from(storage); + buf[1] = 0x41; + new Uint8Array(storage)[2] = 0x42; + console.log(key, buf.length, Buffer.isBuffer(buf), buf.toString('hex'), new Uint8Array(storage).join(',')); + const encoded = Buffer.from(storage, 'utf8'); + console.log(key, 'with encoding', encoded.length, encoded.toString('hex')); + } + + const offsets = new Map(); + offsets.set('storage', new ArrayBuffer(8)); + const storage = offsets.get('storage'); + for (const offset of [2, 1.5, '3', NaN, null, undefined, true, 8, 9, -1, -0.5, -1.5, -1e21, -Number.MAX_VALUE, -Infinity]) { + try { + const view = Buffer.from(storage, offset); + view.fill(offset === 2 ? 0x11 : 0); + console.log('offset', String(offset), view.byteOffset, view.length, new Uint8Array(storage).join(',')); + } catch (error) { + console.log('offset', String(offset), error.name, error.code, error.message); + } + } + const sharedOffsets = new Map([['s', new SharedArrayBuffer(6)]]); + const tail = Buffer.from(sharedOffsets.get('s'), 4); + tail[0] = 0x7f; + console.log('shared offset', tail.length, new Uint8Array(sharedOffsets.get('s')).join(',')); + + const holder = JSON.parse('{}'); + holder.data = new ArrayBuffer(3); + const view = Buffer.from(holder.data); + view.write('xyz'); + console.log('field', Buffer.from(holder.data).toString()); + + const sab = new SharedArrayBuffer(8); + const worker = new Worker(__filename, { workerData: sab }); + worker.on('message', (message) => { + const seen = Buffer.from(message.storage); + console.log('worker wrote', seen.toString('latin1'), new Uint8Array(sab).join(',')); + }); +} else { + const out = Buffer.from(workerData); + out.write('shared!'); + parentPort.postMessage({ storage: workerData }); +} diff --git a/tests/corpus/6869-atomics-untyped-arrays.cjs b/tests/corpus/6869-atomics-untyped-arrays.cjs new file mode 100644 index 000000000..18fe07319 --- /dev/null +++ b/tests/corpus/6869-atomics-untyped-arrays.cjs @@ -0,0 +1,65 @@ +// Atomics over untyped arrays in JavaScript: a worker's workerData carries +// Int32Array and Uint8Array views of a SharedArrayBuffer, and load, store, +// add, sub, and, or, xor, exchange, compareExchange, wait and notify run on +// them as on typed bindings. Other values throw Node's +// TypeError, and an index outside the array Node's RangeError. The results +// go to an untyped array. +'use strict'; +const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads'); + +const lines = []; +function attempt(label, run) { + try { + run(); + lines.push(`${label} ok`); + } catch (error) { + lines.push(`${label} ${error.name} ${error.message}`); + } +} + +if (isMainThread) { + const sab = new SharedArrayBuffer(16); + const counters = new Int32Array(sab); + const worker = new Worker(__filename, { workerData: { counters, bytes: new Uint8Array(sab, 8, 8) } }); + worker.on('message', (message) => { + if (message === 'ready') { + Atomics.store(counters, 1, 42); + console.log('notified', Atomics.notify(counters, 1, 1) >= 0); + return; + } + for (const line of message.lines) console.log(line); + console.log('worker results', JSON.stringify(message.out)); + console.log('counters', Array.from(counters).join(',')); + }); +} else { + const counters = workerData.counters; + const bytes = workerData.bytes; + const out = []; + out.push(Atomics.add(counters, 0, 5), Atomics.sub(counters, 0, 2), Atomics.load(counters, 0)); + out.push(Atomics.or(counters, 2, 6), Atomics.and(counters, 2, 3), Atomics.xor(counters, 2, 1), Atomics.load(counters, 2)); + out.push(Atomics.exchange(counters, 3, 9), Atomics.compareExchange(counters, 3, 9, 11), Atomics.compareExchange(counters, 3, 0, 1)); + out.push(Atomics.add(bytes, 0, 300), Atomics.load(bytes, 0), Atomics.sub(bytes, 1, 1), Atomics.load(bytes, 1)); + out.push(Atomics.load(counters, 1.9), Atomics.load(counters, NaN)); + attempt('index past the end', () => { + Atomics.load(counters, 4); + }); + attempt('negative index', () => { + Atomics.add(bytes, -1, 1); + }); + const values = [[], {}, undefined, null, 5, 'text', new Float64Array(2), new Map(), new ArrayBuffer(4), new DataView(new ArrayBuffer(4))]; + for (const value of values) { + attempt('load', () => { + Atomics.load(value, 0); + }); + attempt('notify', () => { + Atomics.notify(value, 0); + }); + } + attempt('wait on Uint8Array', () => { + Atomics.wait(bytes, 0, 0, 0); + }); + out.push(Atomics.wait(counters, 1, 0, 0)); + parentPort.postMessage('ready'); + out.push(Atomics.wait(counters, 1, 0, 5000) === 'ok' || Atomics.load(counters, 1) === 42); + parentPort.postMessage({ out, lines }); +} diff --git a/tests/corpus/6912-extend-factory-local-class.ts b/tests/corpus/6912-extend-factory-local-class.ts new file mode 100644 index 000000000..e33a59c77 --- /dev/null +++ b/tests/corpus/6912-extend-factory-local-class.ts @@ -0,0 +1,99 @@ +// A generic factory returns its own local class through a declared +// constructor type, and a class extends the factory's result. The local +// class's static methods read the factory's parameter, inherit into the +// subclass, and call each other through `this`. +type Struct = (abstract new () => S) & { readonly decode: (value: unknown) => S }; +function struct(value: S): Struct { + abstract class Base { + static decode(_input: unknown): S { + return value; + } + } + return Base as Struct; +} +class Counter extends struct({ value: 42 }) {} +console.log(Counter.decode(null).value); + +type Named = (new (label: string) => { label: string; payload: T }) & { + readonly describe: () => string; + readonly make: (label: string) => { label: string; payload: T }; +}; +function named(kind: string, payload: T): Named { + class Entry { + label: string; + payload: T; + constructor(label: string) { + this.label = label; + this.payload = payload; + } + static describe(): string { + return `entry of ${kind}`; + } + static make(label: string): Entry { + return new this(label); + } + } + return Entry; +} +class Tag extends named("tag", [1, 2, 3]) { + shout(): string { + return this.label.toUpperCase(); + } +} +const tag = new Tag("hello"); +console.log(tag.shout(), tag.payload.join("+"), Tag.describe()); +const made = Tag.make("made"); +console.log(made.label, made.payload.length); + +const Flags = named("flag", { on: true }); +const flag = new Flags("first"); +console.log(flag.label, flag.payload.on, Flags.describe()); +const Other = named("other", 5); +console.log(Other.describe(), new Other("x").payload, Flags.describe()); + +// A function-local class's static methods read the function's parameter and +// call each other through `this`. +function twice(n: number): number { + class K { + static double(): number { + return n * 2; + } + static quad(): number { + return this.double() * 2; + } + } + return K.double() + K.quad(); +} +console.log(twice(3), twice(5)); + +// The factory returns an anonymous class expression, directly or through a +// `const` binding. +function anonymous(value: S): Struct { + return class { + static decode(_input: unknown): S { + return value; + } + } as unknown as Struct; +} +class Anonymous extends anonymous({ tag: "anon" }) {} +console.log(Anonymous.decode(null).tag); + +type Holder = (new (held: T) => { held: T }) & { readonly seed: () => T }; +function holder(initial: T): Holder { + const Hold = class { + held: T; + constructor(held: T) { + this.held = held; + } + static seed(): T { + return initial; + } + }; + return Hold; +} +class Boxed extends holder(7) { + twice(): number { + return this.held * 2; + } +} +console.log(new Boxed(4).twice(), Boxed.seed()); diff --git a/tests/corpus/6994-uint8-clamped-store-rounding.ts b/tests/corpus/6994-uint8-clamped-store-rounding.ts new file mode 100644 index 000000000..5fc3295da --- /dev/null +++ b/tests/corpus/6994-uint8-clamped-store-rounding.ts @@ -0,0 +1,77 @@ +// Uint8ClampedArray stores: NaN and values at most 0 store 0, values at +// least 255 store 255, and values between round to the nearest integer, +// ties to even. Covers single stores and loops, every tie between 0 and +// 255 with the doubles on either side of it, and values far outside. + +/** The largest power of two at most `x` (x > 0). */ +function floorPower(x: number): number { + let p = 1; + while (p * 2 <= x) p *= 2; + while (p > x) p /= 2; + return p; +} + +/** The doubles just above and below `x` (x >= 0). */ +function nextUp(x: number): number { + return x === 0 ? 5e-324 : x + floorPower(x) * 2 ** -52; +} + +function nextDown(x: number): number { + if (x === 0) return -5e-324; + const p = floorPower(x); + return x - (p === x ? p * 2 ** -53 : p * 2 ** -52); +} + +function storeAll(values: Float64Array, out: Uint8ClampedArray): void { + const n = out.length; + for (let i = 0; i < n; i++) out[i] = values[i]!; +} + +function scale(px: Uint8ClampedArray, k: number): void { + const n = px.length; + for (let i = 0; i < n; i++) px[i] = px[i]! * k; +} + +const special = [ + NaN, -NaN, Infinity, -Infinity, 0, -0, 5e-324, -5e-324, 2.2250738585072014e-308, 0.49999999999999994, + 0.5, 0.5000000000000001, 1, 1.5, 2.5, 3.5, 127.5, 128.5, 253.5, 254.49999999999997, 254.5, + 254.50000000000003, 254.99999999999997, 255, 255.00000000000003, 255.5, 256, -1, -0.5, -0.4, + 1e300, -1e300, 2 ** 31, 2 ** 32 + 0.5, 2 ** 52, 2 ** 52 + 1, 2 ** 53, -(2 ** 52), +]; +const one = new Uint8ClampedArray(1); +const singles: number[] = []; +for (const v of special) { + one[0] = v; + singles.push(one[0]!); +} +console.log("single", singles.join(",")); + +const ties: number[] = []; +for (let k = 0; k <= 255; k++) { + ties.push(k + 0.5, nextDown(k + 0.5), nextUp(k + 0.5), k, nextDown(k), nextUp(k)); +} +const values = new Float64Array(ties.length + special.length); +values.set(ties); +values.set(special, ties.length); +const stored = new Uint8ClampedArray(values.length); +storeAll(values, stored); +const expected = new Uint8ClampedArray(values.length); +for (let i = 0; i < values.length; i++) expected[i] = values[i]!; +let mismatches = 0; +for (let i = 0; i < values.length; i++) if (stored[i] !== expected[i]) mismatches++; +console.log("loop", values.length, "mismatches", mismatches); +let digest = 0; +for (let i = 0; i < stored.length; i++) digest = (Math.imul(digest, 31) + stored[i]!) | 0; +console.log("digest", digest, stored.subarray(0, 24).join(",")); +console.log("tail", stored.subarray(ties.length).join(",")); + +const px = new Uint8ClampedArray(64); +for (let i = 0; i < px.length; i++) px[i] = i * 4.25; +scale(px, 1.37); +console.log("scaled", px.join(",")); +scale(px, -1); +console.log("negated", px.join(",")); +const fromInts = new Uint8ClampedArray(8); +const ints = new Int32Array([-300, -1, 0, 1, 254, 255, 256, 2147483647]); +for (let i = 0; i < fromInts.length; i++) fromInts[i] = ints[i]!; +console.log("ints", fromInts.join(",")); diff --git a/tests/corpus/6995-worker-typed-array-loop-versions.ts b/tests/corpus/6995-worker-typed-array-loop-versions.ts new file mode 100644 index 000000000..5b85271b9 --- /dev/null +++ b/tests/corpus/6995-worker-typed-array-loop-versions.ts @@ -0,0 +1,74 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +// Typed-array loops in a worker-enabled program, over views of a thread's +// own ArrayBuffer, views of a SharedArrayBuffer, and both in one loop. Each +// loop gives the same results whichever kind of buffer its views use, and +// writes through shared views are visible to the other thread. + +function scale(src: Float64Array, dst: Float32Array, k: number): void { + for (let i = 0; i < dst.length; i++) dst[i] = src[i]! * k; +} + +function countAbove(view: Int16Array, limit: number): number { + let n = 0; + for (let i = 0; i < view.length; i++) if (view[i]! > limit) n++; + return n; +} + +function mix(a: Uint8Array, b: Uint8Array, out: Uint8ClampedArray): void { + const n = out.length; + for (let i = 0; i < n; i++) out[i] = a[i]! * 2 - b[i]!; +} + +function bump(view: Int32Array, by: number): void { + for (let i = 0; i < view.length; i++) view[i] = (view[i]! + by) | 0; +} + +function report(label: string, src: Float64Array, dst: Float32Array, ints: Int16Array, a: Uint8Array, b: Uint8Array, out: Uint8ClampedArray): string { + for (let i = 0; i < src.length; i++) src[i] = i * 1.5 - 4; + for (let i = 0; i < ints.length; i++) ints[i] = (i * 7919) % 2001 - 1000; + for (let i = 0; i < a.length; i++) { + a[i] = i * 37; + b[i] = 255 - i * 11; + } + scale(src, dst, 0.1); + mix(a, b, out); + return `${label}: ${dst.join(",")} | ${countAbove(ints, 250)} ${countAbove(ints.subarray(3), -1)} | ${out.join(",")}`; +} + +function views(buffer: ArrayBuffer | SharedArrayBuffer, other: ArrayBuffer | SharedArrayBuffer): string { + const src = new Float64Array(buffer, 0, 12); + const dst = new Float32Array(other, 0, 10); + const ints = new Int16Array(buffer, 96, 40); + const a = new Uint8Array(other, 40, 16); + const b = new Uint8Array(buffer, 176, 16); + const out = new Uint8ClampedArray(other, 56, 16); + return report("pair", src, dst, ints, a, b, out); +} + +if (isMainThread) { + // Bytes 224-255 of the shared buffer hold counters the views never use. + const shared = new SharedArrayBuffer(256); + console.log(views(new ArrayBuffer(256), new ArrayBuffer(256))); + console.log(views(shared, new ArrayBuffer(256))); + console.log(views(new ArrayBuffer(256), shared)); + console.log(views(shared, new SharedArrayBuffer(256))); + const own = new Int32Array(8); + bump(own, 5); + bump(new Int32Array(shared, 224, 8), 1); + const worker = new Worker(new URL(import.meta.url), { workerData: shared }); + worker.on("message", (line: string) => console.log(line)); + worker.on("exit", (code: number) => { + const seen = new Int32Array(shared, 224, 8); + bump(seen, 1000); + console.log(`exit ${code}: ${seen.join(",")} own ${own.join(",")}`); + console.log(`dst after worker: ${new Float32Array(shared, 0, 10).join(",")}`); + }); +} else { + const shared = workerData as SharedArrayBuffer; + const counters = new Int32Array(shared, 224, 8); + bump(counters, 10); + bump(counters.subarray(2, 5), -3); + parentPort!.postMessage(`worker: ${counters.join(",")}`); + parentPort!.postMessage(views(new ArrayBuffer(256), shared)); +} diff --git a/tests/corpus/6996-typed-array-index-temps.ts b/tests/corpus/6996-typed-array-index-temps.ts new file mode 100644 index 000000000..a19561ef0 --- /dev/null +++ b/tests/corpus/6996-typed-array-index-temps.ts @@ -0,0 +1,134 @@ +// Constant index temps derived from a loop counter (`const j = i * 4`) in +// typed-array loops: as indices and as numbers, in loops whose bounds guard +// passes or fails, nested, captured by closures, read after an await or a +// yield, and in loops whose counter reaches large values. + +function gray(px: Uint8Array, out: Uint8Array): void { + for (let i = 0; i < out.length; i++) { + const j = i * 4; + out[i] = (px[j]! * 77 + px[j + 1]! * 150 + px[j + 2]! * 29) >> 8; + } +} + +function interleave(a: Float32Array, b: Float32Array, out: Float32Array): void { + for (let i = 0; i < a.length; i++) { + const at = 2 * i; + out[at] = a[i]!; + out[at + 1] = b[i]!; + } +} + +function strided(src: Int16Array, out: Int32Array): string { + const seen: number[] = []; + for (let i = 0; i < out.length; i++) { + const at = i * 3 + 2; + out[i] = src[at]! - src[at - 2]!; + if (i % 5 === 0) seen.push(at, at / 2, -at, at % 4); + } + return seen.join(","); +} + +function previous(src: Float64Array, out: Float64Array): string { + const edges: string[] = []; + for (let i = 0; i < out.length; i++) { + const before = i - 1; + out[i] = src[before]! + 1; + if (before < 1) edges.push(`${before}:${Object.is(before * 0, -0)}:${1 / (before * 0)}`); + } + return edges.join(" "); +} + +function rows(cells: Uint32Array, width: number, sums: Float64Array): void { + for (let r = 0; r < sums.length; r++) { + const base = r * width; + let total = 0; + for (let c = 0; c < width; c++) { + const at = base + c; + total += cells[at]!; + } + sums[r] = total; + } +} + +function captured(values: Uint8Array): number { + const readers: (() => number)[] = []; + for (let i = 0; i < values.length; i++) { + const at = i * 2; + readers.push(() => at + values[i]!); + } + let sum = 0; + for (const read of readers) sum += read(); + return sum; +} + +function wide(start: number, count: number): string { + const out: number[] = []; + for (let i = start; i < start + count; i++) { + const next = i + 1; + const twice = i * 2; + out.push(next, twice, next - i); + } + return out.join(","); +} + +async function afterAwait(values: Float64Array): Promise { + const out: number[] = []; + for (let i = 0; i < values.length; i++) { + const at = i * 2; + await null; + out.push(at, values[at] ?? -1); + } + return out.join(","); +} + +function* acrossYield(values: Int8Array): Generator { + for (let i = 0; i < values.length; i++) { + const at = i + 1; + yield values[i]!; + yield at * 10 + (values[at] ?? 0); + } +} + +const px = new Uint8Array(64); +for (let i = 0; i < px.length; i++) px[i] = (i * 41 + 7) & 255; +const g = new Uint8Array(16); +gray(px, g); +console.log("gray", g.join(",")); +const tall = new Uint8Array(20); +gray(px, tall); +console.log("gray past input", tall.join(",")); + +const fa = new Float32Array([1.5, -2.25, 3.125, 0.1]); +const fb = new Float32Array([10, 20, 30, 40]); +const pairs = new Float32Array(8); +interleave(fa, fb, pairs); +console.log("interleave", pairs.join(",")); +const short = new Float32Array(5); +interleave(fa, fb, short); +console.log("interleave short", short.join(",")); + +const src16 = new Int16Array(40); +for (let i = 0; i < src16.length; i++) src16[i] = (i * 9973) % 32768 - 16384; +const diff = new Int32Array(12); +console.log("strided", strided(src16, diff), diff.join(",")); +const far = new Int32Array(20); +console.log("strided past input", strided(src16, far), far.join(",")); + +const src64 = new Float64Array([0.5, 1.5, 2.5]); +const shifted = new Float64Array(4); +console.log("previous", previous(src64, shifted), shifted.join(",")); + +const cells = new Uint32Array(30); +for (let i = 0; i < cells.length; i++) cells[i] = (i * 2654435761) >>> 0; +const sums = new Float64Array(5); +rows(cells, 6, sums); +console.log("rows", sums.join(",")); +const partial = new Float64Array(6); +rows(cells, 6, partial); +console.log("rows past input", partial.join(",")); + +console.log("captured", captured(new Uint8Array([5, 6, 7]))); +console.log("wide", wide(2 ** 53 - 4, 3)); +console.log("wide negative", wide(-3, 4)); +console.log("across yield", [...acrossYield(new Int8Array([3, -4, 5]))].join(",")); +afterAwait(new Float64Array([0.25, 1.25, 2.25, 3.25, 4.25])).then((line) => console.log("after await", line)); diff --git a/tests/corpus/7040-es5-error-constructor.mjs b/tests/corpus/7040-es5-error-constructor.mjs new file mode 100644 index 000000000..977567125 --- /dev/null +++ b/tests/corpus/7040-es5-error-constructor.mjs @@ -0,0 +1,79 @@ +// A constructor function that inherits from Error.prototype, the pre-class +// way of declaring an error type. +function ParseFailure(reason, position) { + Error.call(this); + this.name = "ParseFailure"; + this.reason = reason; + this.position = position; + this.message = reason + (position === undefined ? "" : " at " + position); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } else { + this.stack = new Error().stack || ""; + } +} +ParseFailure.prototype = Object.create(Error.prototype); +ParseFailure.prototype.constructor = ParseFailure; +ParseFailure.prototype.toString = function toString(compact) { + return this.name + ": " + this.reason + (compact ? "" : " (position " + this.position + ")"); +}; + +const failure = new ParseFailure("unexpected token", 7); +console.log(failure instanceof ParseFailure, failure instanceof Error, failure.constructor === ParseFailure); +console.log(failure.name, "|", failure.message, "|", failure.reason, failure.position); +console.log(failure.toString(), "|", failure.toString(true), "|", String(failure), "|", `${failure}`); +console.log(Object.keys(failure), JSON.stringify(failure)); +console.log(typeof failure.stack, failure.stack.split("\n")[0], Object.keys(failure).includes("stack")); +console.log(Object.prototype.toString.call(failure), Error.isError(failure)); + +try { + throw new ParseFailure("thrown"); +} catch (caught) { + console.log(caught instanceof Error, caught instanceof ParseFailure, caught.reason, caught.message); +} +const values = [new ParseFailure("in a list"), new Error("plain"), { name: "Fake", message: "x" }, "text"]; +console.log(values.map((value) => value instanceof Error)); + +// The inherited members of Error.prototype. +const bare = Object.create(Error.prototype); +console.log(bare instanceof Error, bare.name, JSON.stringify(bare.message), String(bare), Object.keys(bare)); +console.log(Error.prototype.name, JSON.stringify(Error.prototype.message), Error.prototype instanceof Error); +const record = Object.create(Error.prototype); +record.name = "N"; +record.message = "M"; +console.log(String(record), `${Object.create(record)}`); + +// captureStackTrace on other objects: the header follows name and message. +const target = { name: "Custom", message: "details", toString() { return "ignored"; } }; +Error.captureStackTrace(target); +console.log(target.stack.split("\n")[0], Object.keys(target)); +const empty = {}; +Error.captureStackTrace(empty); +console.log(empty.stack.split("\n")[0], typeof Error.captureStackTrace); + +// A subtype of the subtype. +function StrictFailure(reason) { + ParseFailure.call(this, reason, 0); + this.name = "StrictFailure"; +} +StrictFailure.prototype = Object.create(ParseFailure.prototype); +StrictFailure.prototype.constructor = StrictFailure; +const strict = new StrictFailure("strict mode"); +console.log(strict instanceof StrictFailure, strict instanceof ParseFailure, strict instanceof Error, strict.toString(true)); + +// A class that extends Error calls it too, directly or through `?.`. +class TokenError extends Error { + constructor(message) { + super(message); + this.name = "TokenError"; + Error.captureStackTrace(this, TokenError); + } +} +class LexError extends TokenError { + constructor(message) { + super(message), (this.name = "LexError"), Error.captureStackTrace?.(this, this.constructor); + } +} +for (const error of [new TokenError("bad token"), new LexError("bad char")]) { + console.log(error.name, error.message, error.stack.split("\n")[0], error instanceof TokenError, Object.keys(error)); +} diff --git a/tests/corpus/7041-error-called-without-new.mjs b/tests/corpus/7041-error-called-without-new.mjs new file mode 100644 index 000000000..bf1df8e99 --- /dev/null +++ b/tests/corpus/7041-error-called-without-new.mjs @@ -0,0 +1,40 @@ +// The standard error constructors called as functions construct errors too, +// and Function.prototype.call ignores the receiver it is given. +const plain = Error("plain"); +const typed = TypeError("typed"); +const range = RangeError(); +const withCause = Error("wrapped", { cause: plain }); +console.log(plain instanceof Error, plain.message, String(plain)); +console.log(typed instanceof TypeError, typed instanceof Error, typed.name, typed.message); +console.log(range instanceof RangeError, JSON.stringify(range.message), String(range)); +console.log(withCause.cause === plain, withCause.message); +console.log(SyntaxError("s").name, ReferenceError("r").name, EvalError("e").name, URIError("u").name); + +const receiver = { marker: true }; +const called = Error.call(receiver, "via call"); +console.log(called instanceof Error, called.message, Object.keys(receiver), called === receiver); +const typedCall = TypeError.call(null, "typed via call"); +console.log(typedCall instanceof TypeError, typedCall.message); +const noArguments = Error.call(); +console.log(noArguments instanceof Error, JSON.stringify(noArguments.message)); + +let evaluated = 0; +function effect(value) { + evaluated++; + return value; +} +const ordered = Error.call(effect({}), effect("ordered")); +console.log(evaluated, ordered.message); + +function Wrapper(message) { + const base = Error.call(this, message); + this.message = base.message; + this.kind = "wrapper"; +} +const wrapped = new Wrapper("kept"); +console.log(wrapped.message, wrapped.kind, wrapped instanceof Wrapper, wrapped instanceof Error); +try { + throw Error("thrown without new"); +} catch (error) { + console.log(error instanceof Error, error.message); +} diff --git a/tests/corpus/7042-string-method-names-on-objects.mjs b/tests/corpus/7042-string-method-names-on-objects.mjs new file mode 100644 index 000000000..c623f5417 --- /dev/null +++ b/tests/corpus/7042-string-method-names-on-objects.mjs @@ -0,0 +1,70 @@ +// Objects whose members share names with String.prototype methods, filled +// in after their declaration. A call dispatches on the receiver's runtime +// kind: a string runs the String method, any other value its own member. +var helpers = {}; +var initialized; +function setup() { + if (initialized) return helpers; + initialized = true; + function repeat(text, count) { + let out = ""; + for (let i = 0; i < count; i += 1) out += text; + return out; + } + function padStart(text, width, fill) { + let out = String(text); + while (out.length < width) out = fill + out; + return out; + } + helpers.repeat = repeat; + helpers.padStart = padStart; + helpers.trim = function (value) { + return "<" + value + ">"; + }; + helpers.split = (value, separator) => [value, separator, "own"]; + helpers.toUpperCase = () => "own upper"; + return helpers; +} + +const h = setup(); +console.log("[" + h.repeat("ab", 3) + "]", "[" + h.repeat("-", 0) + "]"); +console.log(h.padStart(7, 3, "0"), h.padStart("long", 2, "*")); +console.log(h.trim(" x "), h.split("a,b", ","), h.toUpperCase()); + +const indent = h.repeat(" ", 2); +console.log("[" + indent + "]", indent.length); +const lines = ["one", "two"].map((line, index) => h.padStart(index + 1, 3, " ") + " | " + line); +console.log(lines.join("\n")); + +// The same names on a missing member throw Node's TypeError. +var partial = {}; +function fillPartial() { + partial.present = true; +} +fillPartial(); +try { + partial.repeat("x", 2); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} +try { + partial.trim(); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} + +// Arguments are evaluated once, in order, after the member is read. +const order = []; +var traced = {}; +function fillTraced() { + traced.replace = (a, b) => { + order.push("call"); + return a + b; + }; +} +fillTraced(); +function arg(value) { + order.push("arg " + value); + return value; +} +console.log(traced.replace(arg(1), arg(2)), order.join(", ")); diff --git a/tests/corpus/7043-date-set-time.mjs b/tests/corpus/7043-date-set-time.mjs new file mode 100644 index 000000000..a673c58dc --- /dev/null +++ b/tests/corpus/7043-date-set-time.mjs @@ -0,0 +1,28 @@ +// Date.prototype.setTime on Dates a JavaScript file constructs: the Date +// is one mutable object, so every reference sees the new time value. +function shifted(year, month, day, minutes) { + const date = new Date(Date.UTC(year, month, day, 12, 30, 15, 250)); + if (minutes) date.setTime(date.getTime() - minutes * 60000); + return date; +} +const unchanged = shifted(2001, 1, 3, 0); +const moved = shifted(2001, 1, 3, 90); +console.log(unchanged.toISOString(), moved.toISOString()); + +const alias = moved; +const record = { when: moved }; +console.log(moved.setTime(1000), alias.getTime(), record.when.toISOString()); +console.log(moved.setTime("2500"), moved.getTime(), moved.setTime(1.9), moved.setTime(-1.9)); +console.log(moved.setTime(NaN), moved.getTime(), String(moved), moved.toJSON()); +console.log(moved.setTime(8.64e15), moved.setTime(8.64e15 + 1), moved.setTime(-8.64e15 - 1)); +console.log(Object.is(moved.setTime(-0), 0), moved.setTime(), moved.setTime(true), moved.setTime(null)); +console.log(moved.setTime({ valueOf: () => 86400000 }), moved.toISOString()); +try { + moved.setTime({ valueOf: () => { throw new Error("no time"); } }); +} catch (error) { + console.log(error.message, moved.toISOString()); +} +console.log(JSON.stringify({ at: shifted(1999, 11, 31, 1) })); +const dates = [shifted(2020, 0, 1, 0), shifted(2020, 0, 1, 0)]; +dates.forEach((date, index) => date.setTime(date.getTime() + index * 1000)); +console.log(dates.map((date) => date.getUTCSeconds())); diff --git a/tests/corpus/7044-nested-empty-object-maps.mjs b/tests/corpus/7044-nested-empty-object-maps.mjs new file mode 100644 index 000000000..e8d330f20 --- /dev/null +++ b/tests/corpus/7044-nested-empty-object-maps.mjs @@ -0,0 +1,46 @@ +// Object literals whose fields start as empty objects, used as maps the +// program adds keys to, including chained assignment into two of them. +function index(entries) { + const table = { + first: {}, + second: {}, + all: {}, + lists: { first: [], second: [], all: [] }, + }; + function add(entry) { + if (entry.many) { + table.lists[entry.group].push(entry.name); + table.lists["all"].push(entry.name); + } else { + table[entry.group][entry.name] = table["all"][entry.name] = entry; + } + } + entries.forEach(add); + return table; +} +const table = index([ + { group: "first", name: "a" }, + { group: "second", name: "b" }, + { group: "first", name: "c", many: true }, + { group: "second", name: "a" }, +]); +console.log(Object.keys(table.first), Object.keys(table.second), Object.keys(table.all), table.lists); +console.log(table.all.a === table.second.a, table.first.a.group, table.all.b.name, "c" in table.first); +console.log(JSON.stringify(table)); + +function counters(words) { + const result = { seen: {}, firstAt: {} }; + words.forEach((word, position) => { + result.seen[word] = (result.seen[word] || 0) + 1; + if (!(word in result.firstAt)) result.firstAt[word] = position; + }); + return result; +} +console.log(JSON.stringify(counters(["x", "y", "x", "z", "y", "x"]))); + +const settings = { values: {}, names: {} }; +const key = String(Math.round(1.6)); +settings.values[key] = 2; +settings["names"][key + "!"] = settings.values[key] * 10; +delete settings.values[key]; +console.log(JSON.stringify(settings), Object.keys(settings.values).length); diff --git a/tests/corpus/7045-instanceof-constructor-values.mjs b/tests/corpus/7045-instanceof-constructor-values.mjs new file mode 100644 index 000000000..6b41cfdca --- /dev/null +++ b/tests/corpus/7045-instanceof-constructor-values.mjs @@ -0,0 +1,43 @@ +// `instanceof` against constructors held as values: a property holding a +// global constructor, and constructor functions with prototype chains. +const kinds = [ + { name: "Date", type: Date }, + { name: "RegExp", type: RegExp }, + { name: "Map", type: Map }, + { name: "Set", type: Set }, + { name: "WeakMap", type: WeakMap }, + { name: "Promise", type: Promise }, + { name: "Array", type: Array }, +]; +function classify(value) { + return kinds + .filter((kind) => typeof value === "object" && value !== null && value instanceof kind.type) + .map((kind) => kind.name) + .join("+") || "none"; +} +const samples = [new Date(0), /x/g, new Map([[1, 2]]), new Set([1]), new WeakMap(), Promise.resolve(1), [1, 2], { a: 1 }, null, "text", 4]; +console.log(samples.map(classify).join(" ")); + +function Shape(sides) { + this.sides = sides; +} +Shape.prototype.describe = function () { + return this.sides + " sides"; +}; +function Square() { + Shape.call(this, 4); +} +Square.prototype = Object.create(Shape.prototype); +Square.prototype.constructor = Square; +const square = new Square(); +const shape = new Shape(3); +const types = { Shape, Square }; +console.log(square instanceof types.Shape, square instanceof types.Square, shape instanceof types.Square); +console.log(square.describe(), shape.describe(), Object.create(Square.prototype) instanceof types.Shape); +console.log([square, shape, {}, []].map((value) => value instanceof types.Shape)); +const notCallable = JSON.parse("{}"); +try { + console.log(square instanceof notCallable); +} catch (error) { + console.log(error instanceof TypeError, error.message); +} diff --git a/tests/corpus/7046-parse-float-extra-arguments.mjs b/tests/corpus/7046-parse-float-extra-arguments.mjs new file mode 100644 index 000000000..9046a9145 --- /dev/null +++ b/tests/corpus/7046-parse-float-extra-arguments.mjs @@ -0,0 +1,9 @@ +// parseFloat reads one argument; Node ignores any others. +const radix = 10; +const inputs = ["3.25", " -0.5e2xyz", "0x10", "Infinityx", ".5", "-.e1", "", "1_000"]; +console.log(inputs.map((text) => parseFloat(text, 10))); +console.log(inputs.map((text) => parseFloat(text, radix, "ignored"))); +const fromJson = JSON.parse('{"value":"12.5kg","n":7}'); +console.log(parseFloat(fromJson.value, 10), parseFloat(fromJson.n, radix), Object.is(parseFloat("-0", 10), -0)); +const values = ["1.5", "2.25"].map((text) => parseFloat(text.replace(/_/g, ""), 10)); +console.log(values, values[0] + values[1]); diff --git a/tests/corpus/7047-dynamic-string-index.mjs b/tests/corpus/7047-dynamic-string-index.mjs new file mode 100644 index 000000000..fc81db3ce --- /dev/null +++ b/tests/corpus/7047-dynamic-string-index.mjs @@ -0,0 +1,21 @@ +// Indexing a string with a key whose type is only known at run time reads +// the property the key names: a code unit, the length, or undefined. +function skipSpaces(text, at) { + let c; + while ((c = text[at]) === " " || c === "\t") at++; + return [at, c]; +} +const where = JSON.parse('{"at":2,"name":"length","past":9,"text":"1","neg":-1,"frac":1.5}'); +console.log(skipSpaces("ab \tcd", where.at), skipSpaces("ab", where.past)); +const s = "hello"; +console.log(s[where.at], s[where.name], s[where.past], s[where.text], s[where.neg], s[where.frac]); +console.log(typeof s[where.past], typeof s[where.name], s[where.at] === "l"); +function scan(text, start) { + let out = ""; + for (let i = start; text[i] !== undefined && text[i] !== ";"; i++) out += text[i]; + return out; +} +console.log(scan("key=value;rest", JSON.parse("4")), scan("abc", JSON.parse("1"))); +const codes = []; +for (let i = JSON.parse("0"); i < s.length; i++) codes.push(s[i].charCodeAt(0)); +console.log(codes); diff --git a/tests/corpus/7048-unary-number-coercions.mjs b/tests/corpus/7048-unary-number-coercions.mjs new file mode 100644 index 000000000..4154ae79e --- /dev/null +++ b/tests/corpus/7048-unary-number-coercions.mjs @@ -0,0 +1,34 @@ +// Unary plus and minus on booleans and on values that may be strings or +// undefined, and += on a binding that holds a number or a string. +function step(text, at) { + const open = text[at] === "["; + at += +open; + return [at, +open + +true + +false, -open]; +} +const at = JSON.parse("1"); +console.log(step("a[b", at), step("ab", at)); +let total = 0; +for (const flag of [true, false, true]) total += +flag; +console.log(total, +true, -true, +(1 > 2), -(2 > 1)); + +const match = "2024-01-02".match(/^(\d+)-(\d+)-(\d+)(?:T(\d+))?$/); +console.log(+match[1], +match[4], -match[2], +match[3] + 1, typeof +match[4], -match[4]); +const optional = [undefined, "7", " 8 ", "", "x"]; +console.log(optional.map((value) => +value), optional.map((value) => -value)); + +let fraction = 0; +const digits = "5"; +if (digits) { + fraction = digits.slice(0, 3); + while (fraction.length < 3) fraction += "0"; + fraction = +fraction; +} +console.log(fraction, typeof fraction); +let width = 0; +width = digits.repeat(2); +width += 1; +console.log(width, typeof width); +let label = "x"; +label += 5; +label += true; +console.log(label); diff --git a/tests/corpus/7049-member-assignment-values.mjs b/tests/corpus/7049-member-assignment-values.mjs new file mode 100644 index 000000000..b614ba308 --- /dev/null +++ b/tests/corpus/7049-member-assignment-values.mjs @@ -0,0 +1,20 @@ +// Member assignments used as values: the receiver, the key and the value are +// evaluated in order, and the expression yields the assigned value. +const log = []; +const holder = { get inner() { log.push("receiver"); return this._inner; }, _inner: {} }; +function value(v) { log.push("value"); return v; } +function key(k) { log.push("key"); return k; } +const r = holder.inner[key("k")] = value(5); +console.log(r, log.join(","), holder._inner.k); +const store = JSON.parse('{"a":{},"b":{}}'); +const t = { id: "x" }; +const out = store["a"][t.id] = store.b[t.id] = t; +console.log(out === t, JSON.stringify(store)); +let n; +const arr = [0, 0, 0]; +n = arr[1] = arr[2] = 7; +console.log(n, arr); +function Counter() { this.count = 0; } +Counter.prototype.bump = function () { return (this.count = this.count + 1); }; +const c = new Counter(); +console.log(c.bump(), c.bump(), c.count); diff --git a/tests/corpus/7050-date-subclass.mjs b/tests/corpus/7050-date-subclass.mjs new file mode 100644 index 000000000..b5edb38e4 --- /dev/null +++ b/tests/corpus/7050-date-subclass.mjs @@ -0,0 +1,78 @@ +// A class that extends Date: private fields beside the time value, super() +// with each Date constructor form, an overriding toISOString that +// JSON.stringify reaches through Date.prototype.toJSON, and the inherited +// Date methods. +class Moment extends Date { + #zone = "Z"; + #precise = true; + constructor(value, zone) { + if (value === undefined) super(0); + else super(value); + if (zone !== undefined) this.#zone = zone; + } + get zone() { + return this.#zone; + } + coarse() { + this.#precise = false; + return this; + } + toISOString() { + const iso = super.toISOString(); + const base = this.#precise ? iso : iso.slice(0, 19) + "Z"; + return this.#zone === "Z" ? base : base.slice(0, -1) + this.#zone; + } +} + +const fromString = new Moment("2001-02-03T04:05:06.789Z"); +const fromNumber = new Moment(86400000, "+01:00"); +const fromDate = new Moment(new Date(Date.UTC(1999, 11, 31, 23, 59, 59))); +const fromMoment = new Moment(fromString); +const none = new Moment(); +console.log(fromString.toISOString(), fromNumber.toISOString(), fromDate.toISOString()); +console.log(fromMoment.getTime() === fromString.getTime(), none.getTime(), fromNumber.zone); +console.log(fromString.getUTCFullYear(), fromString.getUTCMonth(), fromString.getUTCDate(), fromString.getUTCHours(), fromString.getUTCMilliseconds()); +console.log(fromString instanceof Date, fromString instanceof Moment, new Date(0) instanceof Moment); +console.log(JSON.stringify({ fromString, fromNumber, list: [fromDate.coarse()] })); +console.log(fromNumber.toJSON(), JSON.stringify(fromNumber)); +console.log(String(fromDate).slice(0, 15), `${fromDate}`.slice(0, 15), fromDate.toUTCString()); +console.log(+fromNumber, +fromNumber - 1000, fromNumber.valueOf(), fromNumber + "" === String(fromNumber)); +console.log(fromString > fromNumber, fromString < fromNumber, fromString >= fromMoment); +console.log(fromNumber.setTime(1000), fromNumber.getTime(), fromNumber.toISOString()); +console.log(new Date(fromString).toISOString(), new Date(fromNumber).getTime()); +console.log(fromString, [fromNumber], { d: fromDate }); + +const invalid = new Moment("not a date"); +console.log(Number.isNaN(invalid.getTime()), JSON.stringify({ invalid }), invalid.toJSON(), String(invalid)); +console.log(invalid); +try { + invalid.toISOString(); +} catch (error) { + console.log(error instanceof RangeError, error.message); +} + +class LocalMoment extends Moment { + constructor(value) { + super(value, "local"); + } + toISOString() { + return super.toISOString().replace("local", ""); + } +} +const local = new LocalMoment(0); +console.log(local.toISOString(), JSON.stringify([local]), local instanceof Moment, local.zone); + +// The field constructor and Date's statics, which the subclass inherits. +class Plain extends Date {} +const fields = new Plain(2020, 0, 15, 10, 30); +console.log(fields.getFullYear(), fields.getMonth(), fields.getDate(), fields.getHours(), fields.getMinutes()); +console.log(Plain.UTC(2000, 0, 1), Plain.parse("2000-01-01T00:00:00Z"), typeof Plain.now(), Object.keys(fields)); + +// Date held as a value answers instanceof for subclass instances. +const constructors = { date: Date }; +console.log(fields instanceof constructors.date, local instanceof constructors.date, {} instanceof constructors.date); + +// setTime evaluates every argument before it converts the first one. +let base = 1000; +const delayed = { valueOf: () => base }; +console.log(fields.setTime(delayed, (base = 2000)), fields.getTime()); diff --git a/tests/corpus/7051-date-subclass-typed.ts b/tests/corpus/7051-date-subclass-typed.ts new file mode 100644 index 000000000..e39b5f534 --- /dev/null +++ b/tests/corpus/7051-date-subclass-typed.ts @@ -0,0 +1,47 @@ +// A TypeScript class that extends Date, and a subclass of it: typed calls +// of the inherited Date methods, conversions, comparisons, JSON and +// console output, with the toISOString override visible to toJSON. +class Stamp extends Date { + #precise: boolean; + label = "stamp"; + constructor(value: number | string, precise = false) { + super(value); + this.#precise = precise; + } + override toISOString(): string { + const iso = super.toISOString(); + return this.#precise ? iso : iso.slice(0, 19) + "Z"; + } + shifted(ms: number): Stamp { + return new Stamp(this.getTime() + ms, this.#precise); + } +} + +class Tick extends Stamp { + constructor(value: number) { + super(value, true); + } + seconds(): number { + return this.getUTCSeconds() + this.getUTCMilliseconds() / 1000; + } +} + +const a = new Stamp("2001-02-03T04:05:06.789Z"); +const b = new Tick(1500); +console.log(a.toISOString(), b.toISOString(), a.getTime(), b.seconds()); +console.log(a instanceof Date, b instanceof Stamp, b instanceof Date, a.shifted(1000).toISOString()); +console.log(JSON.stringify({ a, b, list: [a] }), JSON.stringify(a), a.toJSON(), b.toJSON()); +console.log(String(a).slice(0, 15), `${b}`.slice(0, 15), +a, b.valueOf(), a.getTime() - b.getTime()); +console.log(a < b, a > b, a >= b, a == a, a + "!" === String(a) + "!"); +console.log(a); +console.log([b], { b }); +console.log(a.setTime(5000), a.getTime(), a.toISOString(), a.label); + +const items: Stamp[] = [a, b, new Tick(0)]; +items.sort((x, y) => x.getTime() - y.getTime()); +console.log(items.map((s) => s.toISOString()).join(",")); +console.log(items.filter((s) => s instanceof Tick).length); +function describe(value: Stamp | string): string { + return typeof value === "string" ? value : value.toISOString(); +} +console.log(describe(b), describe("text")); diff --git a/tests/corpus/7052-date-subclass-dynamic.mjs b/tests/corpus/7052-date-subclass-dynamic.mjs new file mode 100644 index 000000000..267c01ded --- /dev/null +++ b/tests/corpus/7052-date-subclass-dynamic.mjs @@ -0,0 +1,43 @@ +// Instances of a class that extends Date held in values typed only at run +// time: method calls, method values, conversions and instanceof through +// those values, and JSON and console output of the containers. +class Day extends Date { + #label; + constructor(value, label) { + super(value); + this.#label = label; + } + get label() { + return this.#label; + } + toISOString() { + return super.toISOString().slice(0, 10); + } +} +class Plain extends Date {} + +const box = JSON.parse('{"items":[],"byName":{}}'); +box.items.push(new Day(Date.UTC(2020, 1, 29), "leap"), new Plain(0), new Date(1000), "text"); +box.byName.first = box.items[0]; + +function inspect(value) { + if (!(value instanceof Date)) return "not a date: " + value; + return [value.toISOString(), value.getTime(), value.getUTCDate(), value instanceof Day, value.toJSON()].join(" | "); +} +for (const item of box.items) console.log(inspect(item)); +console.log(JSON.stringify(box)); +console.log(box.byName.first.label, box.byName.first.getUTCFullYear(), String(box.items[1]).slice(0, 15)); +console.log(+box.items[0], box.items[0] - box.items[1], box.items[0] > box.items[2], `${box.items[1]}`.slice(0, 3)); + +const getTime = box.items[0].getTime; +console.log(getTime.call(box.items[1]), getTime.call(box.items[0]), typeof getTime); +const toISO = box.items[0].toISOString; +console.log(toISO.call(box.items[0])); +console.log(box.items[1].setTime(5000), box.items[1].getTime(), box.items[1].toISOString()); +try { + getTime.call({}); +} catch (error) { + console.log(error instanceof TypeError); +} +console.log(box.items); +console.log(new Date(box.items[0]).toISOString(), new Date(box.items[1]).getTime()); diff --git a/tests/corpus/7053-private-fields-untyped-receivers.mjs b/tests/corpus/7053-private-fields-untyped-receivers.mjs new file mode 100644 index 000000000..815f612b0 --- /dev/null +++ b/tests/corpus/7053-private-fields-untyped-receivers.mjs @@ -0,0 +1,72 @@ +// Private fields read and written through receivers whose type is known only +// at run time: static factories and helpers that take the instance as an +// argument, compound assignments and updates, `#x in value`, subclasses, and +// Node's TypeErrors for values that do not carry the field. +class Counter { + #count = 0; + #label = null; + constructor(label) { + this.#label = label; + } + static labeled(label, start) { + let counter = new Counter("pending"); + counter.#label = label; + counter.#count = start; + return counter; + } + static read(counter) { + return [counter.#label, counter.#count]; + } + static bump(counter, by) { + counter.#count += by; + counter.#count++; + ++counter.#count; + return counter.#count; + } + static clear(counter) { + counter.#label = null; + return counter; + } + static owns(value) { + return #count in value; + } + describe() { + return `${this.#label}:${this.#count}`; + } +} +class LimitedCounter extends Counter { + #limit = 3; + static limit(value) { + return value.#limit; + } +} + +const made = Counter.labeled("made", 5); +console.log(made.describe(), Counter.read(made), Counter.bump(made, 10), made.describe()); +const held = JSON.parse('{"list":[]}'); +held.list.push(new Counter("held"), new LimitedCounter("limited")); +for (const item of held.list) console.log(Counter.read(item), Counter.bump(item, 1), item.describe(), Counter.owns(item)); +console.log(LimitedCounter.limit(held.list[1]), Counter.clear(held.list[0]).describe()); +for (const value of [{}, [], null, undefined, 1, "text", held.list[0]]) { + try { + console.log(Counter.read(value)); + } catch (error) { + console.log("read", error instanceof TypeError, error.message); + } + try { + Counter.bump(value, 1); + console.log("bumped"); + } catch (error) { + console.log("bump", error instanceof TypeError, error.message); + } + try { + console.log(LimitedCounter.limit(value)); + } catch (error) { + console.log("limit", error instanceof TypeError, error.message); + } + try { + console.log("owns", Counter.owns(value)); + } catch (error) { + console.log("owns", error instanceof TypeError, error.message); + } +} diff --git a/tests/corpus/7054-let-bindings-with-null-writes.mjs b/tests/corpus/7054-let-bindings-with-null-writes.mjs new file mode 100644 index 000000000..c8ca26a9d --- /dev/null +++ b/tests/corpus/7054-let-bindings-with-null-writes.mjs @@ -0,0 +1,37 @@ +// `let` bindings initialized with a string, number or boolean and later +// assigned values that can be null or undefined. JavaScript files are +// checked without strict null checks, so the declared type of each binding +// is the primitive alone. +const PATTERN = /^(\d+)(?:\.(\d+))?([+-]\d\d:\d\d)?$/; +function parts(text) { + let offset = "Z"; + let fraction = 0; + let signed = false; + const match = text.match(PATTERN); + if (match) { + offset = match[3] || null; + fraction = match[2] ? +match[2] : undefined; + signed = match[3] ? match[3][0] === "-" : null; + } + return [offset, fraction, signed]; +} +console.log(parts("12"), parts("12.5"), parts("12.25-01:00"), parts("x")); + +function pick(flag) { + let name = "default"; + name = flag ? name.toUpperCase() : void 0; + let count = 1; + count = flag && count + 1; + let ready = true; + ready = flag === undefined ? undefined : !flag; + return [name, count, ready, typeof name, typeof count]; +} +console.log(pick(true), pick(false), pick(undefined)); + +let last = "start"; +const seen = []; +for (const value of ["a", null, "b", undefined]) { + last = value ?? null; + seen.push(last === null ? "null" : last.length); +} +console.log(seen, last); diff --git a/tests/corpus/7055-cipher-streams.ts b/tests/corpus/7055-cipher-streams.ts new file mode 100644 index 000000000..8f9e0d1ea --- /dev/null +++ b/tests/corpus/7055-cipher-streams.ts @@ -0,0 +1,222 @@ +// Cipheriv and Decipheriv as Transforms: pipeline() and stream/promises +// pipeline stages, pipe() chains, for await, write()/end() with 'data' +// events, setEncoding(), finished(), compose(), addAbortSignal(), +// stream/consumers, the stream properties, and the cipher's state after +// its stream flushed. A Hash reads through stream/consumers and for await +// the same way. +import * as crypto from "node:crypto"; +import { + PassThrough, + Readable, + Transform, + Writable, + addAbortSignal, + compose, + finished, + pipeline, +} from "node:stream"; +import { text } from "node:stream/consumers"; +import { pipeline as pipelinePromise } from "node:stream/promises"; + +const key = Buffer.alloc(32, 7); +const iv = Buffer.alloc(16, 3); + +function collector(into: Buffer[]): Writable { + return new Writable({ + write(chunk: Buffer, _enc: string, cb: () => void) { + into.push(chunk); + cb(); + }, + }); +} + +async function viaCallbackPipeline(): Promise { + const out: Buffer[] = []; + await new Promise((resolve) => { + pipeline( + Readable.from(["hello ", "cipher ", "streams"]), + crypto.createCipheriv("aes-256-cbc", key, iv), + crypto.createDecipheriv("aes-256-cbc", key, iv), + collector(out), + (err: Error | null) => { + console.log("callback pipeline:", err ?? null, Buffer.concat(out).toString()); + resolve(); + }, + ); + }); +} + +async function viaPromisePipeline(): Promise { + const hash = crypto.createHash("sha256"); + await pipelinePromise( + Readable.from([Buffer.from("abc"), Buffer.from("def")]), + crypto.createCipheriv("aes-256-ctr", key, iv), + new PassThrough(), + hash, + ); + console.log("promise pipeline digest:", hash.digest("hex")); +} + +async function viaPipeAndForAwait(): Promise { + const cipher = crypto.createCipheriv("aes-256-cbc", key, iv); + const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv); + const chain = Readable.from(["piped ", "through ", "two ciphers"]).pipe(cipher).pipe(decipher); + let text = ""; + for await (const chunk of chain) text += chunk; + console.log("pipe + for await:", text); +} + +async function viaPipeIntoTransform(): Promise { + const cipher = crypto.createCipheriv("aes-256-cbc", key, iv); + const upper = new Transform({ + transform(chunk: Buffer, _enc: string, cb: (err: Error | null, data?: Buffer) => void) { + cb(null, Buffer.from(chunk.toString("hex").toUpperCase())); + }, + }); + const out = cipher.pipe(upper); + console.log("pipe answers its destination:", out === upper); + cipher.end("into a transform"); + let hex = ""; + for await (const chunk of out) hex += chunk; + console.log("pipe into transform:", hex); +} + +async function viaPipelineArgumentOrder(): Promise { + const order: string[] = []; + const source = (): string[] => { + order.push("source"); + return ["x", "y"]; + }; + const cipherStage = (): crypto.Cipheriv => { + order.push("cipher"); + return crypto.createCipheriv("aes-256-ctr", key, iv); + }; + const hash = crypto.createHash("sha256"); + await pipelinePromise( + source(), + async function* (src: AsyncIterable) { + for await (const chunk of src) yield chunk.toUpperCase(); + }, + cipherStage(), + hash, + ); + console.log("pipeline argument order:", order.join(" "), hash.digest("hex")); +} + +async function viaEvents(): Promise { + const cipher = crypto.createCipheriv("aes-128-cbc", Buffer.alloc(16, 1), Buffer.alloc(16, 2)); + cipher.setEncoding("hex"); + let hex = ""; + cipher.on("data", (chunk: string) => { + hex += chunk; + }); + await new Promise((resolve) => { + cipher.on("end", () => { + console.log("events:", typeof hex, hex); + resolve(); + }); + console.log("write returns:", cipher.write("first block of text")); + cipher.end("and the rest"); + }); + // The stream's flush ran final(): the cipher is finished. + try { + cipher.update("more"); + } catch (e) { + console.log("update after the stream:", (e as Error).message); + } + try { + cipher.final(); + } catch (e) { + console.log("final after the stream:", (e as NodeJS.ErrnoException).code, (e as Error).message); + } +} + +async function gcm(): Promise { + const gcmKey = Buffer.alloc(32, 9); + const nonce = Buffer.alloc(12, 4); + const enc = crypto.createCipheriv("aes-256-gcm", gcmKey, nonce); + const parts: Buffer[] = []; + enc.on("data", (chunk: Buffer) => parts.push(chunk)); + await new Promise((resolve) => { + enc.on("end", () => resolve()); + enc.end("authenticated message"); + }); + const tag = enc.getAuthTag(); + console.log("gcm tag length:", tag.length); + const dec = crypto.createDecipheriv("aes-256-gcm", gcmKey, nonce); + dec.setAuthTag(tag); + let plain = ""; + for await (const chunk of Readable.from([Buffer.concat(parts)]).pipe(dec)) plain += chunk; + console.log("gcm round trip:", plain); +} + +async function viaFinishedAndProperties(): Promise { + const c = crypto.createDecipheriv("aes-256-ctr", key, iv); + console.log("before:", c.readable, c.writable, c.writableEnded, c.destroyed, c.writableLength); + const done = new Promise((resolve) => { + finished(c, (err?: NodeJS.ErrnoException | null) => { + console.log("finished:", err ?? null, c.readableEnded, c.writableFinished); + resolve(); + }); + }); + const parts: string[] = []; + const reading = (async () => { + for await (const chunk of c) parts.push(chunk.toString("hex")); + })(); + c.write(Buffer.from("00112233", "hex")); + c.end(Buffer.from("44556677", "hex")); + console.log("ended:", c.writableEnded, c.writable); + await reading; + await done; + console.log("for await over the cipher:", parts.join("")); + await new Promise((resolve) => setImmediate(resolve)); + console.log("after close:", c.destroyed, c.closed); +} + +async function viaCompositions(): Promise { + const both = compose( + crypto.createCipheriv("aes-256-cbc", key, iv), + crypto.createDecipheriv("aes-256-cbc", key, iv), + ); + let composed = ""; + both.on("data", (chunk: Buffer) => { + composed += chunk.toString(); + }); + await new Promise((resolve) => { + both.on("end", () => resolve()); + both.end("composed"); + }); + console.log("compose:", composed); + + const ac = new AbortController(); + const aborted = addAbortSignal(ac.signal, crypto.createCipheriv("aes-256-ctr", key, iv)); + aborted.on("error", (e: Error) => console.log("addAbortSignal:", e.name)); + const closed = new Promise((resolve) => aborted.on("close", () => resolve())); + ac.abort(); + await closed; + console.log("aborted cipher destroyed:", aborted.destroyed); + + const sealed = crypto.createCipheriv("aes-256-ctr", key, iv).update("through consumers"); + const plain = await text(Readable.from([sealed]).pipe(crypto.createDecipheriv("aes-256-ctr", key, iv))); + console.log("consumers text:", plain); + + const hash = crypto.createHash("sha1"); + hash.setEncoding("hex"); + console.log("hash text:", await text(Readable.from(["abc"]).pipe(hash))); + const md5 = crypto.createHash("md5"); + md5.end("xyz"); + for await (const digest of md5) console.log("hash for await:", (digest as Buffer).toString("hex")); +} + +async function main(): Promise { + await viaFinishedAndProperties(); + await viaCompositions(); + await viaCallbackPipeline(); + await viaPromisePipeline(); + await viaPipeAndForAwait(); + await viaPipeIntoTransform(); + await viaPipelineArgumentOrder(); + await viaEvents(); + await gcm(); +} +main(); diff --git a/tests/corpus/7056-cipher-stream-errors.ts b/tests/corpus/7056-cipher-stream-errors.ts new file mode 100644 index 000000000..e3e653091 --- /dev/null +++ b/tests/corpus/7056-cipher-stream-errors.ts @@ -0,0 +1,111 @@ +// Cipher stream errors, close ordering and backpressure: a bad decrypt +// through pipeline() destroys every stage with the same error, a failed +// GCM authentication reaches 'error' after the data, destroy() closes the +// cipher's stream, and write() reports the high-water mark with 'drain'. +import * as crypto from "node:crypto"; +import { Readable, Writable, pipeline } from "node:stream"; + +const key = Buffer.alloc(32, 5); +const iv = Buffer.alloc(16, 6); + +function sealed(text: string): Buffer { + const c = crypto.createCipheriv("aes-256-cbc", key, iv); + return Buffer.concat([c.update(text), c.final()]); +} + +async function badDecrypt(): Promise { + const log: string[] = []; + const source = Readable.from([sealed("secret message")]); + const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.alloc(32, 8), iv); + const sink = new Writable({ + write(_chunk: Buffer, _enc: string, cb: () => void) { + cb(); + }, + }); + source.on("close", () => log.push("source close")); + decipher.on("error", (e: NodeJS.ErrnoException) => log.push(`decipher error ${e.code}`)); + decipher.on("close", () => log.push("decipher close")); + sink.on("error", (e: NodeJS.ErrnoException) => log.push(`sink error ${e.code}`)); + sink.on("close", () => log.push("sink close")); + await new Promise((resolve) => { + pipeline(source, decipher, sink, (err: NodeJS.ErrnoException | null) => { + log.push(`callback ${err?.code} ${err?.message}`); + setImmediate(() => { + console.log(log.join("\n")); + console.log("destroyed:", source.destroyed, decipher.destroyed, sink.destroyed); + resolve(); + }); + }); + }); +} + +async function gcmWithoutTag(): Promise { + const d = crypto.createDecipheriv("aes-256-gcm", key, Buffer.alloc(12)); + await new Promise((resolve) => { + d.on("data", (chunk: Buffer) => console.log("gcm data", chunk.length)); + d.on("error", (e: Error) => console.log("gcm error:", e.message)); + d.on("close", () => { + console.log("gcm close"); + resolve(); + }); + d.end(Buffer.alloc(5)); + }); +} + +async function destroyMidway(): Promise { + const c = crypto.createCipheriv("aes-256-ctr", key, iv); + await new Promise((resolve) => { + c.on("data", (chunk: Buffer) => { + console.log("ctr data", chunk.length); + c.destroy(new Error("stop")); + }); + c.on("error", (e: Error) => console.log("ctr error:", e.message)); + c.on("close", () => { + console.log("ctr close, destroyed:", c.destroyed); + resolve(); + }); + c.write("0123456789"); + c.write("abcdef"); + }); +} + +async function backpressure(): Promise { + const c = crypto.createCipheriv("aes-256-ctr", key, iv); + let received = 0; + const slow = new Writable({ + highWaterMark: 1024, + write(chunk: Buffer, _enc: string, cb: () => void) { + received += chunk.length; + setImmediate(cb); + }, + }); + c.pipe(slow); + const block = Buffer.alloc(4096, 1); + let writes = 0; + let fullAt = -1; + while (writes < 64) { + writes++; + if (!c.write(block)) { + fullAt = writes; + break; + } + } + console.log("write returned false at", fullAt); + await new Promise((resolve) => c.once("drain", () => resolve())); + console.log("drained"); + await new Promise((resolve) => { + slow.on("finish", () => { + console.log("received", received); + resolve(); + }); + c.end(); + }); +} + +async function main(): Promise { + await badDecrypt(); + await gcmWithoutTag(); + await destroyMidway(); + await backpressure(); +} +main(); diff --git a/tests/corpus/7057-cipher-streams.mjs b/tests/corpus/7057-cipher-streams.mjs new file mode 100644 index 000000000..2173d29aa --- /dev/null +++ b/tests/corpus/7057-cipher-streams.mjs @@ -0,0 +1,85 @@ +// Cipheriv and Decipheriv streams from JavaScript: pipeline() and pipe() +// with cipher stages, a cipher passed through untyped helpers that call +// its stream members dynamically, and cipher stages that reach pipeline() +// through an untyped parameter. +import crypto from "node:crypto"; +import { pipeline, Readable, Writable } from "node:stream"; + +const key = Buffer.alloc(24, 1); +const iv = Buffer.alloc(16, 2); + +function attach(stream, label, log) { + stream.on("data", (chunk) => log.push(`${label} data ${chunk.length}`)); + stream.on("end", () => log.push(`${label} end`)); + stream.on("close", () => log.push(`${label} close`)); + return stream; +} + +function feed(stream, parts) { + for (const part of parts) stream.write(part); + stream.end(); +} + +function state(stream) { + return [stream.readable, stream.writable, stream.writableEnded, stream.destroyed, stream.readableLength].join(","); +} + +const log = []; +const cipher = attach(crypto.createCipheriv("aes-192-cbc", key, iv), "cipher", log); +const collected = []; +cipher.on("data", (chunk) => collected.push(chunk)); +cipher.on("end", () => { + console.log("state at end:", state(cipher)); + const sealed = Buffer.concat(collected); + const out = []; + pipeline( + Readable.from([sealed.subarray(0, 7), sealed.subarray(7)]), + crypto.createDecipheriv("aes-192-cbc", key, iv), + new Writable({ + write(chunk, enc, cb) { + out.push(chunk); + cb(); + }, + }), + (err) => { + console.log(log.join("\n")); + console.log("pipeline:", err ?? null, Buffer.concat(out).toString()); + const piped = Readable.from([sealed]).pipe(crypto.createDecipheriv("aes-192-cbc", key, iv)); + piped.setEncoding("utf8"); + let text = ""; + piped.on("data", (s) => (text += s)); + piped.on("end", () => { + console.log("pipe:", text); + throughHelper(sealed); + }); + }, + ); +}); + +// The stages arrive as untyped values. +function run(stages, done) { + const out = []; + pipeline( + stages[0], + stages[1], + new Writable({ + write(chunk, enc, cb) { + out.push(chunk); + cb(); + }, + }), + (err) => done(err, Buffer.concat(out)), + ); +} + +function throughHelper(sealed) { + run([Readable.from([sealed]), crypto.createDecipheriv("aes-192-cbc", key, iv)], (err, plain) => { + console.log("untyped stage:", err ?? null, plain.toString()); + run([Readable.from([sealed]), crypto.createDecipheriv("aes-192-cbc", Buffer.alloc(24, 9), iv)], (err2) => { + console.log("untyped stage, wrong key:", err2.code, err2.message); + }); + }); +} +console.log("state before:", state(cipher)); +feed(cipher, ["one ", "two ", "three"]); +console.log("state after end():", state(cipher)); diff --git a/tests/corpus/7058-pipeline-function-stages.mjs b/tests/corpus/7058-pipeline-function-stages.mjs new file mode 100644 index 000000000..f75abac39 --- /dev/null +++ b/tests/corpus/7058-pipeline-function-stages.mjs @@ -0,0 +1,180 @@ +// pipeline() function stages from JavaScript: runs of async generator +// stages between streams, stages named by a function declaration and a +// const, sources that are arrays, strings, synchronous generators and +// generator functions (with the ticks Node's async-from-sync iteration +// takes), the string and Buffer methods of generator-fed chunks, and an +// error thrown in a stage after two streams. +import { pipeline, PassThrough, Transform, Writable } from "node:stream"; +import { pipeline as pipelinePromise } from "node:stream/promises"; +import zlib from "node:zlib"; + +function sink(into) { + return new Writable({ + write(chunk, enc, cb) { + into.push(chunk.toString()); + cb(); + }, + }); +} + +async function* upper(source) { + for await (const chunk of source) yield chunk.toString().toUpperCase(); +} + +const bracket = async function* (source) { + for await (const chunk of source) yield `[${chunk.trim().replaceAll("\n", "|")}]`; +}; + +function* letters() { + yield "a"; + yield "b"; + yield "c"; +} + +// Stages on both sides of two zlib streams, and a named stage. +{ + const out = []; + await pipelinePromise( + async function* () { + for (let i = 0; i < 3; i++) yield `line ${i}\n`; + }, + zlib.createGzip(), + zlib.createGunzip(), + upper, + bracket, + sink(out), + ); + console.log("split runs:", out.join("")); +} + +// Iterable sources, and the destination value that reaches the callback. +await new Promise((resolve) => { + pipeline( + ["x", "y", "z"], + zlib.createDeflate(), + zlib.createInflate(), + async function (source) { + let text = ""; + for await (const chunk of source) text += chunk; + return text; + }, + (err, value) => { + console.log("array source:", err ?? null, value); + resolve(undefined); + }, + ); +}); +{ + const out = []; + await pipelinePromise("str", upper, sink(out)); + console.log("string source:", out.join(",")); + out.length = 0; + await pipelinePromise(letters, upper, sink(out)); + console.log("generator function source:", out.join(",")); + out.length = 0; + await pipelinePromise(letters(), new PassThrough(), sink(out)); + console.log("generator source:", out.join(",")); +} + +// Generator-fed chunks keep their string and Buffer methods. +{ + const out = []; + await pipelinePromise( + async function* () { + yield Buffer.from("buf"); + yield Buffer.from("fers"); + }, + async function* (source) { + for await (const chunk of source) yield chunk.toString("hex"); + }, + async function* (source) { + for await (const chunk of source) yield chunk.padStart(10, "-"); + }, + sink(out), + ); + console.log("chunk methods:", out.join(" ")); +} + +// The ticks of iterable sources match Node's for-await over them. +function ticker(log) { + let on = true; + (async () => { + for (let i = 0; on && i < 12; i++) { + log.push(`t${i}`); + await null; + } + })(); + return () => { + on = false; + }; +} +{ + const log = []; + const stop = ticker(log); + await new Promise((resolve) => + pipeline(["a", "b"], new PassThrough(), sink(log), () => { + stop(); + resolve(undefined); + }), + ); + console.log("array ticks:", log.join(" ")); +} +{ + const log = []; + const stop = ticker(log); + await new Promise((resolve) => + pipeline( + letters, + async function* (source) { + for await (const chunk of source) { + log.push(`stage ${chunk}`); + yield chunk; + } + }, + sink(log), + () => { + stop(); + resolve(undefined); + }, + ), + ); + console.log("generator function ticks:", log.join(" ")); +} + +// An error thrown in a stage after two streams destroys both with it. +{ + const log = []; + const first = new PassThrough(); + const second = new Transform({ + transform(chunk, enc, cb) { + cb(null, chunk.toString().toUpperCase()); + }, + }); + first.on("close", () => log.push("first close")); + second.on("close", () => log.push("second close")); + second.on("error", (e) => log.push(`second error ${e.name}`)); + await new Promise((resolve) => { + pipeline( + async function* () { + yield "one\n"; + yield "two\n"; + }, + first, + second, + async function* (source) { + for await (const chunk of source) { + log.push(`stage got ${JSON.stringify(chunk)}`); + if (chunk.length > 100) yield chunk; + throw new Error("stage failed"); + } + }, + sink([]), + (err) => { + log.push(`callback ${err.message}`); + setImmediate(resolve); + }, + ); + }); + console.log(log.join("\n")); + console.log("destroyed:", first.destroyed, second.destroyed); +} diff --git a/tests/corpus/7059-transform-backpressure.ts b/tests/corpus/7059-transform-backpressure.ts new file mode 100644 index 000000000..d19a41eb8 --- /dev/null +++ b/tests/corpus/7059-transform-backpressure.ts @@ -0,0 +1,90 @@ +// Transform write backpressure and typed pipeline stages. A Transform +// holds a write's callback while its readable side is at the high-water +// mark, so write() returns false once both buffers fill and 'drain' comes +// after the reader catches up. Typed stages: named async generator +// functions on both sides of zlib streams, an array source, and an async +// function destination whose value resolves the promise pipeline. +import { PassThrough, Transform, Writable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import * as zlib from "node:zlib"; + +class Copy extends Transform { + override _transform(chunk: Buffer, _enc: BufferEncoding, cb: (err?: Error | null) => void): void { + this.push(chunk); + cb(); + } +} + +async function measure(label: string, t: Transform): Promise { + let received = 0; + const slow = new Writable({ + highWaterMark: 1024, + write(chunk: Buffer, _enc: string, cb: () => void) { + received += chunk.length; + setImmediate(cb); + }, + }); + t.pipe(slow); + const block = Buffer.alloc(4096, 120); + let falseAt = -1; + for (let i = 1; i <= 64; i++) { + if (!t.write(block)) { + falseAt = i; + break; + } + } + console.log(label, "write() false at", falseAt, t.writableLength, t.readableLength); + await new Promise((resolve) => t.once("drain", () => resolve())); + console.log(label, "drained", t.writableLength); + await new Promise((resolve) => { + slow.on("finish", () => resolve()); + t.end(); + }); + console.log(label, "received", received); +} + +async function* numbered(): AsyncGenerator { + for (let i = 0; i < 4; i++) yield `row ${i}\n`; +} + +async function* shout(source: AsyncIterable): AsyncGenerator { + for await (const chunk of source) yield chunk.toString().toUpperCase(); +} + +async function count(source: AsyncIterable): Promise { + let n = 0; + for await (const chunk of source) n += chunk.split("\n").length - 1; + return n; +} + +async function main(): Promise { + await measure("passthrough", new PassThrough()); + await measure("subclass", new Copy()); + await measure( + "options", + new Transform({ + transform(chunk: Buffer, _enc: string, cb: (err?: Error | null, data?: Buffer) => void) { + cb(null, chunk); + }, + }), + ); + + const out: string[] = []; + await pipeline( + numbered, + zlib.createGzip(), + zlib.createGunzip(), + shout, + new Writable({ + write(chunk: Buffer, _enc: string, cb: () => void) { + out.push(chunk.toString()); + cb(); + }, + }), + ); + console.log("named stages:", JSON.stringify(out.join(""))); + + const rows = await pipeline(["a\n", "b\n", "c\n"], zlib.createGzip(), zlib.createGunzip(), shout, count); + console.log("array source, rows:", rows); +} +main(); diff --git a/tests/corpus/7109-uninitialized-let-assignment-expressions.ts b/tests/corpus/7109-uninitialized-let-assignment-expressions.ts new file mode 100644 index 000000000..1354ab100 --- /dev/null +++ b/tests/corpus/7109-uninitialized-let-assignment-expressions.ts @@ -0,0 +1,63 @@ +// A module `let` declared without an initializer is initialized (to +// undefined) when its declaration runs. Assignments in expression +// position store into it like assignment statements do. + +// The deferred-resolver pattern: an expression-bodied executor. +let resolve!: (value: number) => void; +const pending = new Promise((r) => (resolve = r)); +resolve(1); +console.log("resolved", await pending); + +// An expression-bodied setter, and the assignment's value. +let handler!: (n: number) => string; +const setHandler = (h: (n: number) => string) => (handler = h); +const returned = setHandler((n) => `handled ${n}`); +console.log(returned(2), handler(3)); + +// The value held in a binding first. +let next!: () => string; +const install = (f: () => string) => { + const r = (next = f); + return r; +}; +console.log(install(() => "installed")(), next()); + +// Chained, conditional and argument positions. +let first!: (n: number) => number; +let second!: (n: number) => number; +first = second = (n) => n + 1; +console.log("chained", first(1), second(2)); +let picked!: () => string; +const choose = (flag: boolean) => (flag ? (picked = () => "yes") : (picked = () => "no")); +choose(false); +console.log("conditional", picked()); +const call = (f: () => string) => f(); +let passed!: () => string; +console.log("argument", call((passed = () => "passed")), passed()); + +// Reassignment releases the earlier function and keeps the new one. +let counter!: () => number; +const make = (start: number) => { + let n = start; + return (counter = () => ++n); +}; +make(10); +make(20); +console.log("reassigned", counter(), counter()); + +// An optional binding behaves the same way. +let maybe: ((s: string) => string) | undefined; +const setMaybe = (f: (s: string) => string) => (maybe = f); +console.log("optional", setMaybe((s) => s.toUpperCase())("a"), maybe?.("b")); + +// A write that runs before the declaration still throws. +function early(): () => number { + return (late = () => 1); +} +try { + early(); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} +let late!: () => number; +console.log("after declaration", early()(), late()); diff --git a/tests/corpus/7120-webcrypto-random-uuid.ts b/tests/corpus/7120-webcrypto-random-uuid.ts new file mode 100644 index 000000000..d35df97cc --- /dev/null +++ b/tests/corpus/7120-webcrypto-random-uuid.ts @@ -0,0 +1,22 @@ +// crypto.randomUUID() on the WebCrypto global, directly and through +// globalThis: version-4 UUIDs, distinct on every call, and inside +// closures and default parameters the way request-id middleware uses it. +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +const direct: string = crypto.randomUUID(); +console.log(direct.length, shape.test(direct)); +const viaGlobalThis = globalThis.crypto.randomUUID(); +console.log(viaGlobalThis.length, shape.test(viaGlobalThis), viaGlobalThis !== direct); + +const seen = new Set(); +for (let i = 0; i < 100; i++) seen.add(crypto.randomUUID()); +console.log("distinct", seen.size); + +function requestId(generator: () => string = () => crypto.randomUUID()): string { + return generator(); +} +console.log(shape.test(requestId()), requestId(() => "fixed")); + +const ids = ["a", "b", "c"].map((name) => ({ name, id: crypto.randomUUID() })); +console.log(ids.every((entry) => shape.test(entry.id)), ids[0]!.id !== ids[1]!.id); +console.log(typeof crypto.randomUUID()); diff --git a/tests/corpus/7121-webcrypto-crypto-object.mjs b/tests/corpus/7121-webcrypto-crypto-object.mjs new file mode 100644 index 000000000..058e72153 --- /dev/null +++ b/tests/corpus/7121-webcrypto-crypto-object.mjs @@ -0,0 +1,35 @@ +// The crypto global from JavaScript: uuid's feature test +// (`crypto.randomUUID && crypto.randomUUID()`), the methods read as values, +// and getRandomValues through the dynamic object, including its +// missing-argument and non-integer-view errors. +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +function v4(options) { + if (!options && crypto.randomUUID) { + return crypto.randomUUID(); + } + return "fallback"; +} +console.log(shape.test(v4()), v4({})); +console.log(shape.test(globalThis.crypto.randomUUID())); +console.log(typeof crypto.randomUUID, typeof crypto.getRandomValues, typeof crypto.subtle); +console.log(crypto.randomUUID.name, crypto.getRandomValues.name, crypto.getRandomValues.length); + +const native = typeof crypto !== "undefined" && crypto.randomUUID ? { randomUUID: crypto.randomUUID.bind(crypto) } : {}; +console.log(shape.test(native.randomUUID())); + +const rng = (n) => crypto.getRandomValues(new Uint8Array(n)); +console.log(rng(16).length, rng(16) instanceof Uint8Array); +const words = new Uint32Array(4); +console.log(crypto.getRandomValues(words) === words); + +function attempt(label, run) { + try { + console.log(label, run()); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} +attempt("no argument", () => crypto.getRandomValues()); +attempt("float view", () => crypto.getRandomValues(new Float64Array(2))); +attempt("too long", () => crypto.getRandomValues(new Uint8Array(65537))); diff --git a/tests/corpus/7122-webcrypto-node-crypto.ts b/tests/corpus/7122-webcrypto-node-crypto.ts new file mode 100644 index 000000000..c0ada7939 --- /dev/null +++ b/tests/corpus/7122-webcrypto-node-crypto.ts @@ -0,0 +1,38 @@ +// WebCrypto reached through node:crypto: the webcrypto export (named and +// through the namespace), the module's own subtle export, and their +// identity with the crypto global. Each form runs the same HMAC and +// digest calls as the global. +import * as nodeCrypto from "node:crypto"; +import { subtle, webcrypto } from "node:crypto"; + +const encoder = new TextEncoder(); +const message = encoder.encode("node:crypto webcrypto"); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +console.log(webcrypto === crypto, webcrypto === globalThis.crypto, nodeCrypto.webcrypto === crypto); +console.log(hex(await webcrypto.subtle.digest("SHA-256", message))); +console.log(hex(await subtle.digest("SHA-1", message))); +console.log(hex(await nodeCrypto.subtle.digest({ name: "SHA-384" }, message))); +console.log(hex(await nodeCrypto.webcrypto.subtle.digest("SHA-512", message))); + +const secret = encoder.encode("shared secret"); +const params = { name: "HMAC", hash: "SHA-256" }; +const signer = await webcrypto.subtle.importKey("raw", secret, params, false, ["sign"]); +const verifier = await subtle.importKey("raw", secret, params, false, ["verify"]); +const signature = await nodeCrypto.subtle.sign("HMAC", signer, message); +console.log(hex(signature)); +console.log(await nodeCrypto.webcrypto.subtle.verify("HMAC", verifier, signature, message)); +console.log(await subtle.verify("HMAC", verifier, signature, encoder.encode("other"))); +console.log(signer instanceof CryptoKey, signer.algorithm.name, verifier.usages.join(",")); + +const shape = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +console.log(shape.test(webcrypto.randomUUID()), shape.test(nodeCrypto.webcrypto.randomUUID())); +console.log(webcrypto.getRandomValues(new Uint32Array(4)).length); +try { + await subtle.digest("MD5", message); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} diff --git a/tests/corpus/7123-webcrypto-node-crypto.cjs b/tests/corpus/7123-webcrypto-node-crypto.cjs new file mode 100644 index 000000000..b11131d4b --- /dev/null +++ b/tests/corpus/7123-webcrypto-node-crypto.cjs @@ -0,0 +1,31 @@ +// node:crypto's WebCrypto from CommonJS: destructured webcrypto and +// subtle, the module object's members, the +// `globalThis.crypto ?? require("crypto").webcrypto` fallback libraries +// use to find a Crypto object, and crypto destructured from globalThis, +// all the same object. +const nodeCrypto = require("node:crypto"); +const { webcrypto, subtle } = require("crypto"); + +const encoder = new TextEncoder(); +const hex = (buffer) => Buffer.from(new Uint8Array(buffer)).toString("hex"); + +function cryptoObject() { + return globalThis.crypto ?? nodeCrypto.webcrypto; +} + +async function main() { + console.log(webcrypto === globalThis.crypto, cryptoObject() === crypto, typeof subtle); + console.log(hex(await webcrypto.subtle.digest("SHA-256", encoder.encode("abc")))); + console.log(hex(await subtle.digest("SHA-1", encoder.encode("abc")))); + console.log(hex(await nodeCrypto.subtle.digest("SHA-256", encoder.encode("abc")))); + const key = await nodeCrypto.webcrypto.subtle.importKey("raw", encoder.encode("k"), { name: "HMAC", hash: "SHA-1" }, true, ["sign"]); + console.log(hex(await subtle.sign("HMAC", key, encoder.encode("abc")))); + const wc = cryptoObject(); + console.log(wc.randomUUID().length, wc.getRandomValues(new Uint8Array(8)).length); + console.log(typeof webcrypto.randomUUID, typeof nodeCrypto.webcrypto.getRandomValues); + const { crypto: destructured } = globalThis; + console.log(destructured === webcrypto, destructured.randomUUID().length); + console.log(hex(await destructured.subtle.digest("SHA-1", encoder.encode("abc")))); +} + +main(); diff --git a/tests/corpus/7124-webcrypto-secret-keys.ts b/tests/corpus/7124-webcrypto-secret-keys.ts new file mode 100644 index 000000000..8f1f5d601 --- /dev/null +++ b/tests/corpus/7124-webcrypto-secret-keys.ts @@ -0,0 +1,91 @@ +// crypto.crypto.subtle.generateKey, exportKey and importKey for secret keys: +// HMAC and AES keys generated at every length, exported as raw bytes and +// as JSON Web Keys, imported back from both forms, and PBKDF2 and HKDF +// base keys imported from raw bytes. Random key bytes are checked by +// length and round trip, not by value. +const encoder = new TextEncoder(); + +type Key = Awaited>; + +function describe(key: Key): string { + return JSON.stringify({ + type: key.type, + extractable: key.extractable, + algorithm: key.algorithm, + usages: key.usages, + }); +} + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +async function show(label: string, run: () => Promise): Promise { + try { + console.log(label, await run()); + } catch (e) { + const error = e as Error & { code?: string }; + console.log(label, "rejects", error.name, error.code, error.message); + } +} + +for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + await show(`hmac ${hash}`, async () => { + const key = await crypto.subtle.generateKey({ name: "HMAC", hash }, true, ["sign", "verify"]); + const raw = await crypto.subtle.exportKey("raw", key); + return `${describe(key)} ${raw.byteLength}`; + }); +} +await show("hmac length 100", async () => { + const key = await crypto.subtle.generateKey({ name: "HMAC", hash: { name: "SHA-512" }, length: 100 }, true, ["sign"]); + const raw = new Uint8Array(await crypto.subtle.exportKey("raw", key)); + return `${describe(key)} ${raw.length} ${raw[12]! & 0x0f}`; +}); +for (const name of ["AES-GCM", "AES-CBC", "AES-CTR", "AES-KW"]) { + for (const length of [128, 192, 256]) { + await show(`${name} ${length}`, async () => { + const usages: ("wrapKey" | "unwrapKey" | "encrypt" | "decrypt")[] = + name === "AES-KW" ? ["wrapKey", "unwrapKey"] : ["encrypt", "decrypt"]; + const key = await crypto.subtle.generateKey({ name, length }, true, usages); + const jwk = await crypto.subtle.exportKey("jwk", key); + return `${describe(key)} ${jwk.alg} ${jwk.kty} ${jwk.k!.length} ${JSON.stringify(jwk.key_ops)} ${jwk.ext}`; + }); + } +} + +// Fixed key bytes: the exported forms are deterministic. +const bytes = new Uint8Array(32); +for (let i = 0; i < bytes.length; i++) bytes[i] = i * 7 + 1; +const hmac = await crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA-384" }, true, ["verify", "sign"]); +const hmacJwk = await crypto.subtle.exportKey("jwk", hmac); +console.log(JSON.stringify(hmacJwk), Object.keys(hmacJwk).join(",")); +console.log(hex(await crypto.subtle.exportKey("raw", hmac)) === Buffer.from(bytes).toString("hex")); +const reimported = await crypto.subtle.importKey("jwk", hmacJwk, { name: "HMAC", hash: "SHA-384" }, true, ["sign"]); +console.log(describe(reimported)); +const signature = await crypto.subtle.sign("HMAC", reimported, encoder.encode("payload")); +console.log(await crypto.subtle.verify("HMAC", hmac, signature, encoder.encode("payload"))); + +const short = await crypto.subtle.importKey("raw", bytes.subarray(0, 20), { name: "HMAC", hash: "SHA-1", length: 156 }, true, ["sign"]); +console.log(describe(short), JSON.stringify(await crypto.subtle.exportKey("jwk", short))); +console.log(hex(await crypto.subtle.exportKey("raw-secret", short))); + +for (const length of [16, 24, 32]) { + const key = await crypto.subtle.importKey("raw", bytes.subarray(0, length), "AES-GCM", true, ["encrypt"]); + const jwk = await crypto.subtle.exportKey("jwk", key); + console.log(describe(key), JSON.stringify(jwk)); + const back = await crypto.subtle.importKey("jwk", jwk, { name: "AES-GCM" }, false, ["encrypt"]); + console.log(describe(back)); +} +const cbc = await crypto.subtle.importKey("raw-secret", bytes.subarray(0, 16), "aes-cbc", true, ["decrypt", "unwrapKey"]); +console.log(describe(cbc), hex(await crypto.subtle.exportKey("raw", cbc))); +const wrap = await crypto.subtle.importKey("raw", bytes.subarray(0, 24), "AES-KW", true, ["unwrapKey", "wrapKey"]); +console.log(describe(wrap), JSON.stringify(await crypto.subtle.exportKey("jwk", wrap))); + +const password = encoder.encode("correct horse battery staple"); +const pbkdf2 = await crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveBits", "deriveKey"]); +console.log(describe(pbkdf2)); +const hkdf = await crypto.subtle.importKey("raw-secret", bytes, { name: "hkdf" }, false, ["deriveKey"]); +console.log(describe(hkdf)); +const empty = await crypto.subtle.importKey("raw", new Uint8Array(0), "PBKDF2", false, ["deriveBits"]); +console.log(describe(empty)); +console.log(hmac instanceof CryptoKey, pbkdf2 instanceof CryptoKey); diff --git a/tests/corpus/7125-webcrypto-key-errors.mjs b/tests/corpus/7125-webcrypto-key-errors.mjs new file mode 100644 index 000000000..7f14ae0d7 --- /dev/null +++ b/tests/corpus/7125-webcrypto-key-errors.mjs @@ -0,0 +1,95 @@ +// Rejections of crypto.crypto.subtle.generateKey, exportKey and importKey for +// secret keys, in Node's order: argument conversion, algorithm +// normalization, usages, then the per-algorithm checks, and the JSON Web +// Key validation of oct keys. + +async function attempt(label, run) { + try { + const value = await run(); + const shown = value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value instanceof CryptoKey ? JSON.stringify(value.algorithm) : JSON.stringify(value); + console.log(label, "resolves", shown); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +await attempt("generate too few", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true)); +await attempt("generate unknown", () => crypto.subtle.generateKey({ name: "FOO" }, true, ["sign"])); +await attempt("generate PBKDF2", () => crypto.subtle.generateKey({ name: "PBKDF2" }, false, ["deriveBits"])); +await attempt("generate no name", () => crypto.subtle.generateKey({ hash: "SHA-1" }, true, ["sign"])); +await attempt("generate hmac string", () => crypto.subtle.generateKey("HMAC", true, ["sign"])); +await attempt("generate hmac no hash", () => crypto.subtle.generateKey({ name: "HMAC" }, true, ["sign"])); +await attempt("generate hmac md5", () => crypto.subtle.generateKey({ name: "HMAC", hash: "MD5" }, true, ["sign"])); +await attempt("generate hmac bogus hash", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-999" }, true, ["sign"])); +await attempt("generate hmac length 0", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: 0 }, true, ["sign"])); +await attempt("generate hmac length NaN", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: NaN }, true, ["sign"])); +await attempt("generate hmac length -1", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1", length: -1 }, true, ["sign"])); +await attempt("generate hmac encrypt", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, ["encrypt"])); +await attempt("generate hmac no usages", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, [])); +await attempt("generate bad usage", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, ["fly"])); +await attempt("generate usages string", () => crypto.subtle.generateKey({ name: "HMAC", hash: "SHA-1" }, true, "sign")); +await attempt("generate aes no length", () => crypto.subtle.generateKey({ name: "AES-GCM" }, true, ["encrypt"])); +await attempt("generate aes length 100", () => crypto.subtle.generateKey({ name: "AES-GCM", length: 100 }, true, ["encrypt"])); +await attempt("generate aes length 70000", () => crypto.subtle.generateKey({ name: "AES-CBC", length: 70000 }, true, ["encrypt"])); +await attempt("generate aes sign", () => crypto.subtle.generateKey({ name: "AES-CTR", length: 128 }, true, ["sign"])); +await attempt("generate aes-kw encrypt", () => crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["encrypt"])); +await attempt("generate aes no usages", () => crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, true, [])); +await attempt("generate aes string length", async () => (await crypto.subtle.generateKey({ name: "aes-gcm", length: "256" }, true, ["encrypt"])).algorithm); + +const hmac = await crypto.subtle.importKey("raw", new Uint8Array(20), { name: "HMAC", hash: "SHA-256" }, true, ["sign"]); +const locked = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-GCM", false, ["encrypt"]); +const pbkdf2 = await crypto.subtle.importKey("raw", new Uint8Array(4), "PBKDF2", false, ["deriveBits"]); +await attempt("export too few", () => crypto.subtle.exportKey("raw")); +await attempt("export bad format", () => crypto.subtle.exportKey("pem", hmac)); +await attempt("export not a key", () => crypto.subtle.exportKey("raw", { type: "secret" })); +await attempt("export spki", () => crypto.subtle.exportKey("spki", hmac)); +await attempt("export pkcs8", () => crypto.subtle.exportKey("pkcs8", hmac)); +await attempt("export raw-public", () => crypto.subtle.exportKey("raw-public", hmac)); +await attempt("export non-extractable", () => crypto.subtle.exportKey("raw", locked)); +await attempt("export pbkdf2", () => crypto.subtle.exportKey("raw", pbkdf2)); +await attempt("export pbkdf2 jwk", () => crypto.subtle.exportKey("jwk", pbkdf2)); + +const sixteen = new Uint8Array(16); +await attempt("import aes 15 bytes", () => crypto.subtle.importKey("raw", new Uint8Array(15), "AES-GCM", true, ["encrypt"])); +await attempt("import aes sign", () => crypto.subtle.importKey("raw", sixteen, "AES-GCM", true, ["sign"])); +await attempt("import aes-kw encrypt", () => crypto.subtle.importKey("raw", sixteen, "AES-KW", true, ["encrypt"])); +await attempt("import aes no usages", () => crypto.subtle.importKey("raw", sixteen, "AES-CBC", true, [])); +await attempt("import aes spki", () => crypto.subtle.importKey("spki", sixteen, "AES-CBC", true, ["encrypt"])); +await attempt("import aes length ignored", () => crypto.subtle.importKey("raw", sixteen, { name: "AES-CTR", length: 256 }, true, ["encrypt"])); +await attempt("import pbkdf2 extractable", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", true, ["deriveBits"])); +await attempt("import pbkdf2 sign", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", false, ["sign"])); +await attempt("import pbkdf2 no usages", () => crypto.subtle.importKey("raw", sixteen, "PBKDF2", false, [])); +await attempt("import hkdf jwk", () => crypto.subtle.importKey("jwk", { kty: "oct", k: "AAAA" }, "HKDF", false, ["deriveKey"])); +await attempt("import hkdf pkcs8", () => crypto.subtle.importKey("pkcs8", sixteen, "HKDF", false, ["deriveKey"])); +await attempt("sign with aes key", () => crypto.subtle.sign("HMAC", locked, sixteen)); + +const hs256 = { name: "HMAC", hash: "SHA-256" }; +const jwk = (data, alg = hs256, extractable = true, usages = ["sign"]) => crypto.subtle.importKey("jwk", data, alg, extractable, usages); +await attempt("jwk ok", () => jwk({ kty: "oct", k: "AAECAwQFBgc" })); +await attempt("jwk alg match", () => jwk({ kty: "oct", k: "AAECAwQFBgc", alg: "HS256" })); +await attempt("jwk alg mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgc", alg: "HS512" })); +await attempt("jwk no kty", () => jwk({ k: "AAECAwQFBgc" })); +await attempt("jwk kty RSA", () => jwk({ kty: "RSA", k: "AAECAwQFBgc" })); +await attempt("jwk no k", () => jwk({ kty: "oct" })); +await attempt("jwk k number", () => jwk({ kty: "oct", k: 5 })); +await attempt("jwk k empty", () => jwk({ kty: "oct", k: "" })); +await attempt("jwk k standard alphabet", () => jwk({ kty: "oct", k: "AA+/AA==" })); +await attempt("jwk k junk", () => jwk({ kty: "oct", k: "!!!!" })); +await attempt("jwk use enc", () => jwk({ kty: "oct", k: "AAECAwQFBgc", use: "enc" })); +await attempt("jwk use sig", () => jwk({ kty: "oct", k: "AAECAwQFBgc", use: "sig" })); +await attempt("jwk key_ops superset", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["sign", "verify", "fly"] })); +await attempt("jwk key_ops missing", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["verify"] })); +await attempt("jwk key_ops duplicate", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: ["sign", "sign"] })); +await attempt("jwk key_ops string", () => jwk({ kty: "oct", k: "AAECAwQFBgc", key_ops: "sign" })); +await attempt("jwk ext false", () => jwk({ kty: "oct", k: "AAECAwQFBgc", ext: false })); +await attempt("jwk ext false locked", () => jwk({ kty: "oct", k: "AAECAwQFBgc", ext: false }, hs256, false)); +await attempt("jwk not an object", () => jwk("key")); +await attempt("jwk null", () => jwk(null)); +await attempt("jwk kty symbol", () => jwk({ kty: Symbol("oct"), k: "AA" })); +await attempt("jwk hmac length", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, { name: "HMAC", hash: "SHA-1", length: 60 })); +await attempt("jwk hmac length mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, { name: "HMAC", hash: "SHA-1", length: 128 })); +await attempt("jwk no usages", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, hs256, true, [])); +await attempt("jwk aes", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", alg: "A128GCM" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes alg mismatch", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", alg: "A256GCM" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes use sig", () => jwk({ kty: "oct", k: "AAECAwQFBgcICQoLDA0ODw", use: "sig" }, "AES-GCM", true, ["encrypt"])); +await attempt("jwk aes length", () => jwk({ kty: "oct", k: "AAECAwQFBgc" }, "AES-CBC", true, ["encrypt"])); diff --git a/tests/corpus/7126-webcrypto-aes.ts b/tests/corpus/7126-webcrypto-aes.ts new file mode 100644 index 000000000..0e7be7548 --- /dev/null +++ b/tests/corpus/7126-webcrypto-aes.ts @@ -0,0 +1,77 @@ +// crypto.crypto.subtle.encrypt and decrypt with AES-GCM, AES-CBC and AES-CTR: +// fixed keys, IVs and inputs give Node's exact ciphertexts, every key +// length and GCM tag length round-trips, and CTR counters wrap within +// their low `length` bits. +const encoder = new TextEncoder(); +const decoder = new TextDecoder(); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +function bytes(length: number, seed: number): Uint8Array { + const out = new Uint8Array(length); + for (let i = 0; i < length; i++) out[i] = (i * 31 + seed) & 0xff; + return out; +} + +const plaintext = encoder.encode("The quick brown fox jumps over the lazy dog"); +for (const length of [16, 24, 32]) { + const raw = bytes(length, length); + const gcm = await crypto.subtle.importKey("raw", raw, "AES-GCM", false, ["encrypt", "decrypt"]); + const cbc = await crypto.subtle.importKey("raw", raw, "AES-CBC", false, ["encrypt", "decrypt"]); + const ctr = await crypto.subtle.importKey("raw", raw, "AES-CTR", false, ["encrypt", "decrypt"]); + const iv = bytes(12, 7); + const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, gcm, plaintext); + console.log("gcm", length, sealed.byteLength, hex(sealed)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, gcm, sealed))); + const aad = encoder.encode("header"); + const tagged = await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData: aad, tagLength: 96 }, gcm, plaintext); + console.log("gcm aad", length, hex(tagged)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv, additionalData: aad, tagLength: 96 }, gcm, tagged))); + const cbcIv = bytes(16, 3); + const padded = await crypto.subtle.encrypt({ name: "AES-CBC", iv: cbcIv }, cbc, plaintext); + console.log("cbc", length, padded.byteLength, hex(padded)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-CBC", iv: cbcIv }, cbc, padded))); + const counter = bytes(16, 9); + const streamed = await crypto.subtle.encrypt({ name: "AES-CTR", counter, length: 64 }, ctr, plaintext); + console.log("ctr", length, hex(streamed)); + console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-CTR", counter, length: 64 }, ctr, streamed))); +} + +const key = await crypto.subtle.importKey("raw", bytes(16, 1), "AES-GCM", true, ["encrypt", "decrypt"]); +for (const tagLength of [32, 64, 96, 104, 112, 120, 128]) { + const iv = bytes(16, tagLength); + const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv, tagLength }, key, plaintext.subarray(0, 5)); + const opened = await crypto.subtle.decrypt({ name: "AES-GCM", iv, tagLength }, key, sealed); + console.log("tag", tagLength, sealed.byteLength, hex(sealed), decoder.decode(opened)); +} +console.log("gcm empty", hex(await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(0)))); +console.log("gcm long iv", hex(await crypto.subtle.encrypt({ name: "AES-GCM", iv: bytes(128, 5) }, key, plaintext.subarray(0, 3)))); + +const cbcKey = await crypto.subtle.importKey("raw", bytes(16, 2), "AES-CBC", false, ["encrypt", "decrypt"]); +for (const length of [0, 1, 15, 16, 17, 32]) { + const sealed = await crypto.subtle.encrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, cbcKey, bytes(length, 4)); + const opened = await crypto.subtle.decrypt({ name: "AES-CBC", iv: new Uint8Array(16) }, cbcKey, sealed); + console.log("cbc length", length, sealed.byteLength, opened.byteLength, hex(opened) === hex(bytes(length, 4).buffer as ArrayBuffer)); +} + +const ctrKey = await crypto.subtle.importKey("raw", new Uint8Array(16), "AES-CTR", false, ["encrypt"]); +const allOnes = new Uint8Array(16).fill(0xff); +const zeros = new Uint8Array(64); +console.log("ctr wrap 4", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 4 }, ctrKey, zeros))); +console.log("ctr wrap 128", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 128 }, ctrKey, zeros))); +console.log("ctr wrap 2", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: allOnes, length: 2 }, ctrKey, zeros))); +console.log("ctr 1 bit, 2 blocks", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: new Uint8Array(16), length: 1 }, ctrKey, zeros.subarray(0, 32)))); +console.log("ctr partial", hex(await crypto.subtle.encrypt({ name: "AES-CTR", counter: bytes(16, 6), length: 128 }, ctrKey, zeros.subarray(0, 21)))); + +const generated = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); +const iv = crypto.getRandomValues(new Uint8Array(12)); +const message = encoder.encode("generated key round trip"); +const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, generated, message); +console.log(sealed.byteLength, decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, generated, sealed))); +const exported = await crypto.subtle.exportKey("jwk", generated); +const reimported = await crypto.subtle.importKey("jwk", exported, "AES-GCM", false, ["decrypt"]); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, reimported, sealed))); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, generated, new DataView(sealed)))); +console.log(decoder.decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv.buffer as ArrayBuffer }, generated, Buffer.from(sealed)))); diff --git a/tests/corpus/7127-webcrypto-aes-errors.mjs b/tests/corpus/7127-webcrypto-aes-errors.mjs new file mode 100644 index 000000000..7ce51230a --- /dev/null +++ b/tests/corpus/7127-webcrypto-aes-errors.mjs @@ -0,0 +1,71 @@ +// Rejections of crypto.crypto.subtle.encrypt and decrypt in Node's order: +// argument conversion, the AesCbcParams, AesCtrParams and AeadParams +// dictionaries, the key checks, then the cipher's own OperationErrors +// (bad tags, bad padding, IV and counter limits). + +async function attempt(label, run) { + try { + const value = await run(); + console.log(label, "resolves", value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const raw = new Uint8Array(16).map((_, i) => i); +const gcm = await crypto.subtle.importKey("raw", raw, "AES-GCM", true, ["encrypt", "decrypt"]); +const cbc = await crypto.subtle.importKey("raw", raw, "AES-CBC", true, ["encrypt", "decrypt"]); +const ctr = await crypto.subtle.importKey("raw", raw, "AES-CTR", true, ["encrypt", "decrypt"]); +const encryptOnly = await crypto.subtle.importKey("raw", raw, "AES-GCM", true, ["encrypt"]); +const hmac = await crypto.subtle.importKey("raw", raw, { name: "HMAC", hash: "SHA-1" }, true, ["sign"]); +const iv12 = new Uint8Array(12); +const iv16 = new Uint8Array(16); +const data = new TextEncoder().encode("hello world"); + +await attempt("too few", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm)); +await attempt("decrypt too few", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 })); +await attempt("not a key", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, JSON.parse("{}"), data)); +await attempt("data string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm, "hello")); +await attempt("unknown algorithm", () => crypto.subtle.encrypt({ name: "AES-XTS" }, gcm, data)); +await attempt("AES-KW", () => crypto.subtle.encrypt({ name: "AES-KW" }, gcm, data)); +await attempt("no name", () => crypto.subtle.encrypt({ iv: iv12 }, gcm, data)); +await attempt("string algorithm", () => crypto.subtle.encrypt("AES-GCM", gcm, data)); +await attempt("gcm no iv", () => crypto.subtle.encrypt({ name: "AES-GCM" }, gcm, data)); +await attempt("gcm iv string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: "abc" }, gcm, data)); +await attempt("gcm aad string", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, additionalData: "x" }, gcm, data)); +await attempt("gcm aad null", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, additionalData: null }, gcm, data)); +await attempt("gcm tag 100", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: 100 }, gcm, data)); +await attempt("gcm tag 300", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: 300 }, gcm, data)); +await attempt("gcm tag NaN", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12, tagLength: NaN }, gcm, data)); +await attempt("gcm tag string", () => crypto.subtle.encrypt({ name: "aes-gcm", iv: iv12, tagLength: "96" }, gcm, data)); +await attempt("gcm iv 11 bytes", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(11) }, gcm, data)); +await attempt("gcm iv 129 bytes", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(129) }, gcm, data)); +await attempt("gcm iv empty", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(0) }, gcm, data)); +const sealed = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, gcm, data)); +const tampered = sealed.slice(); +tampered[0] ^= 1; +await attempt("gcm tampered", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, tampered)); +await attempt("gcm wrong aad", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12, additionalData: data }, gcm, sealed)); +await attempt("gcm wrong tag length", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12, tagLength: 96 }, gcm, sealed)); +await attempt("gcm shorter than tag", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, new Uint8Array(15))); +await attempt("gcm tag only", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, gcm, new Uint8Array(16))); +await attempt("cbc no iv", () => crypto.subtle.encrypt({ name: "AES-CBC" }, cbc, data)); +await attempt("cbc iv 12 bytes", () => crypto.subtle.encrypt({ name: "AES-CBC", iv: iv12 }, cbc, data)); +await attempt("cbc decrypt 15 bytes", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, new Uint8Array(15))); +await attempt("cbc decrypt empty", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, new Uint8Array(0))); +const block = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-CBC", iv: iv16 }, cbc, data)); +const badPad = block.slice(); +badPad[15] ^= 0x55; +await attempt("cbc bad padding", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: iv16 }, cbc, badPad)); +await attempt("cbc other iv", () => crypto.subtle.decrypt({ name: "AES-CBC", iv: new Uint8Array(16).fill(1) }, cbc, block)); +await attempt("ctr no counter", () => crypto.subtle.encrypt({ name: "AES-CTR", length: 64 }, ctr, data)); +await attempt("ctr no length", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16 }, ctr, data)); +await attempt("ctr length 0", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 0 }, ctr, data)); +await attempt("ctr length 129", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 129 }, ctr, data)); +await attempt("ctr length 300", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 300 }, ctr, data)); +await attempt("ctr counter 8 bytes", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: new Uint8Array(8), length: 64 }, ctr, data)); +await attempt("ctr 1 bit, 3 blocks", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 1 }, ctr, new Uint8Array(48))); +await attempt("ctr empty", () => crypto.subtle.encrypt({ name: "AES-CTR", counter: iv16, length: 1 }, ctr, new Uint8Array(0))); +await attempt("algorithm mismatch", () => crypto.subtle.encrypt({ name: "AES-CBC", iv: iv16 }, gcm, data)); +await attempt("hmac key", () => crypto.subtle.encrypt({ name: "AES-GCM", iv: iv12 }, hmac, data)); +await attempt("missing usage", () => crypto.subtle.decrypt({ name: "AES-GCM", iv: iv12 }, encryptOnly, sealed)); diff --git a/tests/corpus/7128-webcrypto-derive.ts b/tests/corpus/7128-webcrypto-derive.ts new file mode 100644 index 000000000..4badedb66 --- /dev/null +++ b/tests/corpus/7128-webcrypto-derive.ts @@ -0,0 +1,45 @@ +// crypto.crypto.subtle.deriveBits and deriveKey with PBKDF2 and HKDF: Node's +// exact derived bytes for every SHA digest, zero-length derivations, the +// SCRAM-style PBKDF2-then-HMAC chain, and derived AES and HMAC keys that +// encrypt and sign. +const encoder = new TextEncoder(); + +function hex(buffer: ArrayBuffer): string { + return Buffer.from(buffer).toString("hex"); +} + +const password = await crypto.subtle.importKey("raw", encoder.encode("pencil"), "PBKDF2", false, ["deriveBits", "deriveKey"]); +const ikm = await crypto.subtle.importKey("raw", encoder.encode("input keying material"), { name: "HKDF" }, false, ["deriveBits", "deriveKey"]); +const salt = encoder.encode("QSXCR+Q6sek8bf92"); +const info = encoder.encode("context"); + +for (const hash of ["SHA-1", "SHA-256", "SHA-384", "SHA-512"]) { + console.log("pbkdf2", hash, hex(await crypto.subtle.deriveBits({ name: "PBKDF2", hash, salt, iterations: 4096 }, password, 256))); + console.log("hkdf", hash, hex(await crypto.subtle.deriveBits({ name: "HKDF", hash: { name: hash }, salt, info }, ikm, 256))); +} +console.log("pbkdf2 0", (await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1 }, password, 0)).byteLength); +console.log("hkdf 0", (await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt, info }, ikm, 0)).byteLength); +console.log("hkdf empty salt and info", hex(await crypto.subtle.deriveBits({ name: "hkdf", hash: "SHA-256", salt: new Uint8Array(0), info: new Uint8Array(0) }, ikm, 128))); +console.log("hkdf max", (await crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-1", salt, info }, ikm, 255 * 20 * 8)).byteLength); + +// SCRAM-SHA-256's client proof inputs, the way database drivers compute them. +const saltedPassword = await crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 4096 }, password, 256); +const salted = await crypto.subtle.importKey("raw", saltedPassword, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const clientKey = await crypto.subtle.sign("HMAC", salted, encoder.encode("Client Key")); +console.log("client key", hex(clientKey)); +console.log("stored key", hex(await crypto.subtle.digest("SHA-256", clientKey))); + +const aes = await crypto.subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1000 }, password, { name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]); +console.log(JSON.stringify(aes.algorithm), aes.usages.join(","), aes.extractable); +console.log("aes key", hex(await crypto.subtle.exportKey("raw", aes))); +const iv = new Uint8Array(12); +const sealed = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, aes, encoder.encode("derived")); +console.log(hex(sealed), new TextDecoder().decode(await crypto.subtle.decrypt({ name: "AES-GCM", iv }, aes, sealed))); + +const mac = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-256", salt, info }, ikm, { name: "HMAC", hash: "SHA-384" }, false, ["sign", "verify"]); +console.log(JSON.stringify(mac.algorithm), mac.extractable); +console.log("mac", hex(await crypto.subtle.sign("HMAC", mac, encoder.encode("message")))); +const shortMac = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-1", salt, info }, ikm, { name: "HMAC", hash: "SHA-1", length: 128 }, true, ["sign"]); +console.log(JSON.stringify(shortMac.algorithm), hex(await crypto.subtle.exportKey("raw", shortMac))); +const wrap = await crypto.subtle.deriveKey({ name: "HKDF", hash: "SHA-512", salt, info }, ikm, { name: "AES-KW", length: 128 }, true, ["wrapKey"]); +console.log(JSON.stringify(wrap.algorithm), JSON.stringify(await crypto.subtle.exportKey("jwk", wrap))); diff --git a/tests/corpus/7129-webcrypto-derive-errors.mjs b/tests/corpus/7129-webcrypto-derive-errors.mjs new file mode 100644 index 000000000..83149b61b --- /dev/null +++ b/tests/corpus/7129-webcrypto-derive-errors.mjs @@ -0,0 +1,72 @@ +// Rejections of crypto.crypto.subtle.deriveBits and deriveKey in Node's order: +// argument conversion, the Pbkdf2Params and HkdfParams dictionaries, the +// base key's usages and algorithm, the length rules, and for deriveKey +// the derived key type's normalization and import. +const encoder = new TextEncoder(); + +async function attempt(label, run) { + try { + const value = await run(); + const shown = value instanceof ArrayBuffer ? `ArrayBuffer(${value.byteLength})` : value instanceof CryptoKey ? JSON.stringify(value.algorithm) : value; + console.log(label, "resolves", shown); + } catch (e) { + console.log(label, e.name, e.code, e.message); + } +} + +const password = await crypto.subtle.importKey("raw", encoder.encode("password"), "PBKDF2", false, ["deriveBits", "deriveKey"]); +const ikm = await crypto.subtle.importKey("raw", encoder.encode("input key"), "HKDF", false, ["deriveBits", "deriveKey"]); +const bitsOnly = await crypto.subtle.importKey("raw", encoder.encode("x"), "PBKDF2", false, ["deriveBits"]); +const keyOnly = await crypto.subtle.importKey("raw", encoder.encode("x"), "PBKDF2", false, ["deriveKey"]); +const hmac = await crypto.subtle.importKey("raw", encoder.encode("x"), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]); +const salt = encoder.encode("salt"); +const pbkdf2 = (extra = {}) => ({ name: "PBKDF2", hash: "SHA-256", salt, iterations: 1000, ...extra }); +const hkdf = (extra = {}) => ({ name: "HKDF", hash: "SHA-256", salt, info: encoder.encode("info"), ...extra }); + +await attempt("bits too few", () => crypto.subtle.deriveBits(pbkdf2())); +await attempt("bits not a key", () => crypto.subtle.deriveBits(pbkdf2(), JSON.parse("{}"), 64)); +await attempt("bits length null", () => crypto.subtle.deriveBits(pbkdf2(), password, null)); +await attempt("bits length undefined", () => crypto.subtle.deriveBits(pbkdf2(), password, undefined)); +await attempt("bits length absent", () => crypto.subtle.deriveBits(pbkdf2(), password)); +await attempt("bits length 12", () => crypto.subtle.deriveBits(pbkdf2(), password, 12)); +await attempt("bits length -1", () => crypto.subtle.deriveBits(pbkdf2(), password, -1)); +await attempt("bits length NaN", () => crypto.subtle.deriveBits(pbkdf2(), password, NaN)); +await attempt("bits length string", () => crypto.subtle.deriveBits(pbkdf2(), password, "64")); +await attempt("bits unknown", () => crypto.subtle.deriveBits({ name: "SCRYPT" }, password, 64)); +await attempt("bits no name", () => crypto.subtle.deriveBits({ hash: "SHA-1" }, password, 64)); +await attempt("pbkdf2 iterations 0", () => crypto.subtle.deriveBits(pbkdf2({ iterations: 0 }), password, 64)); +await attempt("pbkdf2 iterations 2^31", () => crypto.subtle.deriveBits(pbkdf2({ iterations: 2 ** 31 }), password, 64)); +await attempt("pbkdf2 iterations -1", () => crypto.subtle.deriveBits(pbkdf2({ iterations: -1 }), password, 64)); +await attempt("pbkdf2 no iterations", () => crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", salt }, password, 64)); +await attempt("pbkdf2 no salt", () => crypto.subtle.deriveBits({ name: "PBKDF2", hash: "SHA-256", iterations: 1 }, password, 64)); +await attempt("pbkdf2 no hash", () => crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: 1 }, password, 64)); +await attempt("pbkdf2 string", () => crypto.subtle.deriveBits("PBKDF2", password, 64)); +await attempt("pbkdf2 md5", () => crypto.subtle.deriveBits(pbkdf2({ hash: "MD5" }), password, 64)); +await attempt("pbkdf2 bogus sha", () => crypto.subtle.deriveBits(pbkdf2({ hash: "SHA-999" }), password, 64)); +await attempt("pbkdf2 salt string", () => crypto.subtle.deriveBits(pbkdf2({ salt: "salt" }), password, 64)); +await attempt("pbkdf2 hkdf key", () => crypto.subtle.deriveBits(pbkdf2(), ikm, 64)); +await attempt("pbkdf2 hmac key", () => crypto.subtle.deriveBits(pbkdf2(), hmac, 64)); +await attempt("pbkdf2 no deriveBits usage", () => crypto.subtle.deriveBits(pbkdf2(), keyOnly, 64)); +await attempt("hkdf no info", () => crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt }, ikm, 64)); +await attempt("hkdf no salt", () => crypto.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", info: salt }, ikm, 64)); +await attempt("hkdf info 1025 bytes", () => crypto.subtle.deriveBits(hkdf({ info: new Uint8Array(1025) }), ikm, 64)); +await attempt("hkdf over maximum", () => crypto.subtle.deriveBits(hkdf({ hash: "SHA-1" }), ikm, 255 * 20 * 8 + 8)); +await attempt("hkdf length 4", () => crypto.subtle.deriveBits(hkdf(), ikm, 4)); +await attempt("hkdf null", () => crypto.subtle.deriveBits(hkdf(), ikm, null)); + +const aes = { name: "AES-GCM", length: 128 }; +await attempt("key too few", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true)); +await attempt("key not a key", () => crypto.subtle.deriveKey(pbkdf2(), "key", aes, true, ["encrypt"])); +await attempt("key no deriveKey usage", () => crypto.subtle.deriveKey(pbkdf2(), bitsOnly, aes, true, ["encrypt"])); +await attempt("key mismatch", () => crypto.subtle.deriveKey(hkdf(), password, aes, true, ["encrypt"])); +await attempt("key aes no length", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "AES-GCM" }, true, ["encrypt"])); +await attempt("key aes length 100", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "AES-CBC", length: 100 }, true, ["encrypt"])); +await attempt("key aes sign usage", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true, ["sign"])); +await attempt("key aes no usages", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, true, [])); +await attempt("key hmac no hash", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC" }, true, ["sign"])); +await attempt("key hmac length 0", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC", hash: "SHA-1", length: 0 }, true, ["sign"])); +await attempt("key hmac length 100", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "HMAC", hash: "SHA-1", length: 100 }, true, ["sign"])); +await attempt("key hkdf type", () => crypto.subtle.deriveKey(pbkdf2(), password, "HKDF", false, ["deriveBits"])); +await attempt("key unknown type", () => crypto.subtle.deriveKey(pbkdf2(), password, { name: "TWOFISH" }, true, ["encrypt"])); +await attempt("key pbkdf2 type", () => crypto.subtle.deriveKey(hkdf(), ikm, { name: "PBKDF2" }, false, ["deriveKey"])); +await attempt("key ok", () => crypto.subtle.deriveKey(pbkdf2(), password, aes, false, ["encrypt"])); diff --git a/tests/corpus/7183-local-class-heritage-effects.ts b/tests/corpus/7183-local-class-heritage-effects.ts new file mode 100644 index 000000000..514449fdb --- /dev/null +++ b/tests/corpus/7183-local-class-heritage-effects.ts @@ -0,0 +1,54 @@ +// A class defined inside a function evaluates its `extends` expression +// every time the definition runs, even when the base it names is fixed: +// calls in the heritage run in order, and a heritage call that throws +// stops the definition. +class Base { + tag(): string { + return "base"; + } +} +const log: string[] = []; +function select(note: string): typeof Base { + log.push(`select ${note}`); + return Base; +} + +function define(round: number): string { + class Local extends select(`declaration ${round}`) {} + return new Local().tag(); +} +for (let round = 0; round < 2; round++) log.push(define(round)); + +function expressionForm(): string { + const Made = class extends select("expression") {}; + return new Made().tag(); +} +log.push(expressionForm()); + +function nested(): string { + class Outer extends select("outer") {} + class Inner extends Outer {} + return new Inner().tag(); +} +log.push(nested()); + +function refuse(): typeof Base { + throw new Error("heritage failed"); +} +function failing(): void { + try { + class Never extends refuse() {} + log.push(new Never().tag()); + } catch (error) { + log.push((error as Error).message); + } +} +failing(); + +const arrow = (n: number): string => { + class InArrow extends select(`arrow ${n}`) {} + return new InArrow().tag(); +}; +log.push(arrow(1)); + +console.log(log.join("\n")); diff --git a/tests/diagnostics/webcrypto-refusals.ts b/tests/diagnostics/webcrypto-refusals.ts index d162872fb..309e70c4e 100644 --- a/tests/diagnostics/webcrypto-refusals.ts +++ b/tests/diagnostics/webcrypto-refusals.ts @@ -1,17 +1,8 @@ -// The native WebCrypto surface is HMAC importKey('raw'), sign and verify, -// and the SHA-1/SHA-2 digests, called directly on crypto.subtle. The 'jwk' -// key format and crypto.subtle or its methods taken as values are refused -// by name; the bundled declarations omit the other SubtleCrypto methods -// (node-types/fenced.ts pins their fence under @types/node). -const fromJwk = await crypto.subtle.importKey( - "jwk", - { kty: "oct", k: "c2VjcmV0" }, - { name: "HMAC", hash: "SHA-256" }, - false, - ["sign"], -); -console.log(fromJwk.type); - +// The native WebCrypto surface is crypto.subtle's secret-key slice, called +// directly on crypto.subtle. crypto.subtle or its methods taken as values +// are refused by name; the bundled declarations omit the other +// SubtleCrypto methods (node-types/fenced.ts pins their fence under +// @types/node). const subtle = crypto.subtle; console.log(typeof subtle); diff --git a/tests/fixtures/node-types/fenced.ts b/tests/fixtures/node-types/fenced.ts index 16a74081f..274d57272 100644 --- a/tests/fixtures/node-types/fenced.ts +++ b/tests/fixtures/node-types/fenced.ts @@ -93,23 +93,19 @@ void inspectReadableStream; import { callbackify } from "node:util"; const callbackified = callbackify(async (n: number) => n); void callbackified; -/* Cipheriv values support update(), final(), setAAD(), setAutoPadding(), - * getAuthTag(), and setAuthTag(); the stream.Transform members are fenced. */ -const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); -cipherStream.on("data", () => {}); /* ECDH on a curve Node knows but the runtime does not implement, and * computeSecret(view, encoding), which answers a Buffer where @types/node * declares a string. */ import { createECDH } from "node:crypto"; createECDH("sect163k1"); createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); -/* WebCrypto beyond the HMAC slice: @types/node declares the whole - * SubtleCrypto surface, and the methods outside importKey, sign, verify - * and digest fence by name. */ -async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); +/* WebCrypto beyond the secret-key slice: @types/node declares the whole + * SubtleCrypto surface, and the methods outside the slice, such as + * wrapKey, fence by name. */ +async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); } -void encryptWithWebCrypto; +void wrapWithWebCrypto; /* Node's navigator global: typeof navigator and navigator.userAgent * compile; the other members fence by name. */ const cores = navigator.hardwareConcurrency; diff --git a/tests/fixtures/node-types/webcrypto-keys.ts b/tests/fixtures/node-types/webcrypto-keys.ts new file mode 100644 index 000000000..be16271d8 --- /dev/null +++ b/tests/fixtures/node-types/webcrypto-keys.ts @@ -0,0 +1,32 @@ +/* WebCrypto secret keys under @types/node: generateKey, exportKey and + * importKey for HMAC and AES keys, and PBKDF2 key imports, lower to the + * same native values as under the bundled declarations. @types/node + * names the dictionary, key and JSON Web Key types in node:crypto's + * webcrypto namespace. */ +import type { webcrypto } from "node:crypto"; + +async function main(): Promise { + const hmacParams: webcrypto.HmacKeyGenParams = { name: "HMAC", hash: "SHA-256", length: 256 }; + const hmac: webcrypto.CryptoKey = await crypto.subtle.generateKey(hmacParams, true, ["sign", "verify"]); + const hmacAlgorithm = hmac.algorithm as webcrypto.HmacKeyAlgorithm; + console.log(hmac.type, hmacAlgorithm.name, hmacAlgorithm.length, hmacAlgorithm.hash.name, hmac.usages.join(",")); + const raw: ArrayBuffer = await crypto.subtle.exportKey("raw", hmac); + console.log(raw.byteLength); + + const aesParams: webcrypto.AesKeyGenParams = { name: "AES-GCM", length: 192 }; + const aes = await crypto.subtle.generateKey(aesParams, true, ["encrypt", "decrypt"]); + const aesAlgorithm = aes.algorithm as webcrypto.AesKeyAlgorithm; + console.log(aesAlgorithm.name, aesAlgorithm.length, aes.extractable); + const jwk: webcrypto.JsonWebKey = await crypto.subtle.exportKey("jwk", aes); + console.log(jwk.kty, jwk.alg, jwk.ext, jwk.key_ops?.join(","), jwk.k?.length); + const back = await crypto.subtle.importKey("jwk", jwk, "AES-GCM", false, ["decrypt"]); + console.log(back.algorithm.name, back.extractable, back.usages.join(",")); + + const fixed = new Uint8Array(16).fill(9); + const imported = await crypto.subtle.importKey("raw", fixed, { name: "AES-CBC" }, true, ["encrypt"]); + console.log(Buffer.from(await crypto.subtle.exportKey("raw", imported)).toString("hex")); + const password = await crypto.subtle.importKey("raw", new TextEncoder().encode("pw"), "PBKDF2", false, ["deriveBits"]); + console.log(password.algorithm.name, password.extractable, password.usages.join(",")); +} + +void main(); diff --git a/tests/fixtures/npm-static/js-yaml-cli.mjs b/tests/fixtures/npm-static/js-yaml-cli.mjs new file mode 100644 index 000000000..8c958cf80 --- /dev/null +++ b/tests/fixtures/npm-static/js-yaml-cli.mjs @@ -0,0 +1,63 @@ +// The published js-yaml package compiled as static modules: loading, dumping, +// schemas, custom types and the YAMLException errors it throws. +import yaml from "js-yaml"; +const text = ` +# config +name: demo +version: 1.2 +count: 0x1F +octal: 0o17 +neg: -12_000 +flt: 6.02e+23 +inf: .inf +nan: .NaN +on: yes +off: false +nil: ~ +date: 2001-12-14 +stamp: 2001-12-14t21:59:43.10-05:00 +str: "quoted \\u263A \\t tab" +single: 'it''s' +multi: | + line one + line two +folded: >- + folded + text +base: &base { a: 1, b: [x, y] } +derived: + <<: *base + c: 3 +list: + - one + - { k: v } + - [1, 2, 3] + - !!str 123 +set: !!set { a, b } +omap: !!omap [ { x: 1 }, { y: 2 } ] +binary: !!binary aGVsbG8= +`; +const doc = yaml.load(text); +console.log(JSON.stringify(doc, (k, v) => (v instanceof Uint8Array ? Array.from(v) : v))); +console.log(doc.stamp instanceof Date, doc.stamp.toISOString(), doc.date.toISOString(), Object.is(doc.nan, NaN) || Number.isNaN(doc.nan)); +const docs = yaml.loadAll("---\na: 1\n---\nb: 2\n...\n"); +console.log(JSON.stringify(docs)); +const items = []; +yaml.loadAll("--- 1\n--- two\n", (d) => items.push(d)); +console.log(items); +console.log(yaml.dump(doc, { sortKeys: true, lineWidth: 40, skipInvalid: true })); +console.log(yaml.dump({ a: "multi\nline\n", b: " lead", c: "yes", d: "123", e: null, f: [], g: {}, h: "x: y", i: -0, j: 1e21 }, { indent: 4 })); +console.log(yaml.dump([{ deep: { er: [1, { z: true }] } }], { flowLevel: 2 })); +console.log(yaml.dump({ s: "a'b\"c", u: "é☺", long: "word ".repeat(30).trim() }, { quotingType: '"', forceQuotes: true })); +for (const bad of ["a: [1", "a: b: c", "- a\nb: 1", "key: 'open", "a: !!int x", "? a\n? a\n", "&x a: *y"]) { + try { yaml.load(bad); console.log("no error", bad); } + catch (e) { console.log(e.name, "|", e.reason, "|", e.mark && e.mark.line, e.mark && e.mark.column, "|", e.message.split("\n")[0]); console.log(e.toString(true)); console.log(String(e).split("\n").length); } +} +try { yaml.load("a: 1\na: 2"); } catch (e) { console.log(e instanceof yaml.YAMLException, e instanceof Error, e.message); } +console.log(JSON.stringify(yaml.load("a: 1\na: 2", { json: true }))); +console.log(JSON.stringify(yaml.load("x: 1", { schema: yaml.JSON_SCHEMA })), JSON.stringify(yaml.load("x: yes", { schema: yaml.FAILSAFE_SCHEMA })), JSON.stringify(yaml.load("x: yes", { schema: yaml.CORE_SCHEMA }))); +const Point = new yaml.Type("!point", { kind: "sequence", construct: (d) => ({ x: d[0], y: d[1] }), instanceOf: Object, represent: (p) => [p.x, p.y] }); +const schema = yaml.DEFAULT_SCHEMA.extend([Point]); +console.log(JSON.stringify(yaml.load("p: !point [3, 4]", { schema }))); +try { yaml.dump({ f() {} }); } catch (e) { console.log(e.name, e.message); } +console.log(yaml.dump({ f() {}, u: undefined, k: 1 }, { skipInvalid: true })); diff --git a/tests/fixtures/npm-static/smol-toml-cli.mjs b/tests/fixtures/npm-static/smol-toml-cli.mjs new file mode 100644 index 000000000..453e16801 --- /dev/null +++ b/tests/fixtures/npm-static/smol-toml-cli.mjs @@ -0,0 +1,68 @@ +// The published smol-toml package compiled as static modules: parsing every +// value kind, the TomlDate subclass of Date, stringify and TomlError. +import { parse, stringify, TomlDate, TomlError } from "smol-toml"; +const text = ` +# a document +title = "TOML \\"example\\"" +int = +99 +neg = -17 +hex = 0xDEADBEEF +oct = 0o755 +bin = 0b1101 +under = 1_000_000 +float = 6.626e-34 +inf = inf +nan = nan +bool = true +odt1 = 1979-05-27T07:32:00Z +odt2 = 1979-05-27T00:32:00-07:00 +odt3 = 1979-05-27T00:32:00.999999-07:00 +ldt = 1979-05-27T07:32:00 +ld = 1979-05-27 +lt = 07:32:00.123 +space = 1979-05-27 07:32:00Z +literal = 'C:\\Users\\nodejs\\templates' +multi = """ +Roses are red +Violets are blue""" +lmulti = ''' +The first newline is +trimmed in raw strings.''' + +[owner] +name = "Tom" +"quoted key" = 1 +dotted.inner.key = "x" + +[database] +ports = [ 8000, 8001, 8002 ] +data = [ ["delta", "phi"], [3.14] ] +temp_targets = { cpu = 79.5, case = 72.0 } + +[[products]] +name = "Hammer" +sku = 738594937 + +[[products]] + +[[products]] +name = "Nail" +color = "gray" +`; +const doc = parse(text); +console.log(JSON.stringify(doc, null, 1)); +for (const key of ["odt1", "odt2", "odt3", "ldt", "ld", "lt", "space"]) { + const d = doc[key]; + console.log(key, d instanceof Date, d instanceof TomlDate, d.isDateTime(), d.isLocal(), d.isDate(), d.isTime(), d.isValid(), d.toISOString(), Number.isNaN(d.getTime()) ? "NaN" : d.getTime()); +} +console.log(Number.isNaN(doc.nan), doc.inf, typeof doc.hex, doc.under); +console.log(stringify(doc)); +console.log(stringify({ a: new Date(0), b: TomlDate.wrapAsLocalDate(new Date(86400000)), c: new TomlDate("10:20:30"), n: [1, [2, 3]], t: { u: { v: "w" } }, s: "quote\" and \\ backslash", f: 1.5, i: 2, big: 2n ** 40n })); +const d = new TomlDate("2020-02-29T12:00:00+01:00"); +console.log(d.toISOString(), JSON.stringify({ d }), String(d.getUTCHours()), d.valueOf()); +const bad = new TomlDate("not a date"); +console.log(bad.isValid(), Number.isNaN(bad.getTime())); +for (const src of ["a = ", "a = 1\na = 2", "[t]\n[t]", "x = [1,", "k = 1979-13-01", "s = \"unterminated", "= 1", "a = 25:00:00"]) { + try { parse(src); console.log("parsed", JSON.stringify(src)); } + catch (e) { console.log(e instanceof TomlError, e instanceof Error, e.name, e.line, e.column, e.message.split("\n")[0]); } +} diff --git a/tests/harness/__snapshots__/node-types-fenced.txt b/tests/harness/__snapshots__/node-types-fenced.txt index 0bd13963d..3198db366 100644 --- a/tests/harness/__snapshots__/node-types-fenced.txt +++ b/tests/harness/__snapshots__/node-types-fenced.txt @@ -205,47 +205,38 @@ fenced.ts:94:23 - error SC2020: 'util.callbackify into a callback whose error pa hint: Node calls back with a null error on success; type the binding's callback as (err: NodeJS.ErrnoException | null, ...) => void -fenced.ts:99:1 - error SC2020: 'Cipheriv/Decipheriv.on' is part of the standard library types but has no scriptc lowering yet +fenced.ts:100:12 - error SC2020: 'crypto.createECDH with curve 'sect163k1'' is part of the standard library types but has no scriptc lowering yet - 98 | const cipherStream = createCipheriv("aes-128-cbc", Buffer.alloc(16), Buffer.alloc(16)); - 99 | cipherStream.on("data", () => {}); - | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 100 | /* ECDH on a curve Node knows but the runtime does not implement, and - - hint: update(), final(), setAAD(), setAutoPadding(), getAuthTag(), and setAuthTag() are supported; cipher streams are not - -fenced.ts:104:12 - error SC2020: 'crypto.createECDH with curve 'sect163k1'' is part of the standard library types but has no scriptc lowering yet - - 103 | import { createECDH } from "node:crypto"; - 104 | createECDH("sect163k1"); + 99 | import { createECDH } from "node:crypto"; + 100 | createECDH("sect163k1"); | ^~~~~~~~~~~ - 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); + 101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); hint: the supported curves are prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1 -fenced.ts:105:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer or view key' is part of the standard library types but has no scriptc lowering yet +fenced.ts:101:1 - error SC2020: 'ECDH.computeSecret(key, encoding) with a Buffer or view key' is part of the standard library types but has no scriptc lowering yet - 104 | createECDH("sect163k1"); - 105 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); + 100 | createECDH("sect163k1"); + 101 | createECDH("prime256v1").computeSecret(Buffer.alloc(65), "hex"); | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 106 | /* WebCrypto beyond the HMAC slice: @types/node declares the whole + 102 | /* WebCrypto beyond the secret-key slice: @types/node declares the whole hint: Node reads the second argument as the key's input encoding and answers a Buffer; pass the output encoding third: computeSecret(key, undefined, encoding) -fenced.ts:110:9 - error SC2020: 'crypto.subtle.encrypt' is part of the standard library types but has no scriptc lowering yet +fenced.ts:106:9 - error SC2020: 'crypto.subtle.wrapKey' is part of the standard library types but has no scriptc lowering yet - 109 | async function encryptWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { - 110 | await crypto.subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, key, new Uint8Array(1)); + 105 | async function wrapWithWebCrypto(key: import("node:crypto").webcrypto.CryptoKey): Promise { + 106 | await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); | ^~~~~~~~~~~~~~~~~~~~~ - 111 | } + 107 | } - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -fenced.ts:115:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet +fenced.ts:111:15 - error SC2020: 'navigator.hardwareConcurrency' is typed by @types/node but has no scriptc lowering yet - 114 | * compile; the other members fence by name. */ - 115 | const cores = navigator.hardwareConcurrency; + 110 | * compile; the other members fence by name. */ + 111 | const cores = navigator.hardwareConcurrency; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 116 | void cores; + 112 | void cores; hint: of Node's navigator global only typeof navigator and navigator.userAgent compile \ No newline at end of file diff --git a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt index 521edff3c..e46099f69 100644 --- a/tests/harness/__snapshots__/webcrypto-refusals.ts.txt +++ b/tests/harness/__snapshots__/webcrypto-refusals.ts.txt @@ -1,26 +1,17 @@ -webcrypto-refusals.ts:7:3 - error SC2020: 'crypto.subtle.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet +webcrypto-refusals.ts:6:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet - 6 | const fromJwk = await crypto.subtle.importKey( - 7 | "jwk", - | ^~~~~ - 8 | { kty: "oct", k: "c2VjcmV0" }, + 5 | // @types/node). + 6 | const subtle = crypto.subtle; + | ^~~~~~~~~~~~~ + 7 | console.log(typeof subtle); - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle -webcrypto-refusals.ts:15:16 - error SC2020: 'crypto.subtle as a value' is part of the standard library types but has no scriptc lowering yet +webcrypto-refusals.ts:9:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet - 14 | - 15 | const subtle = crypto.subtle; - | ^~~~~~~~~~~~~ - 16 | console.log(typeof subtle); - - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle - -webcrypto-refusals.ts:18:20 - error SC2020: 'crypto.subtle.sign as a value' is part of the standard library types but has no scriptc lowering yet - - 17 | - 18 | console.log(typeof crypto.subtle.sign); + 8 | + 9 | console.log(typeof crypto.subtle.sign); | ^~~~~~~~~~~~~~~~~~ - 19 | + 10 | - hint: the native WebCrypto surface is crypto.subtle.importKey('raw', ...) for HMAC keys, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file + hint: the native WebCrypto surface is crypto.subtle's importKey, generateKey and exportKey for HMAC and AES keys (importKey also for PBKDF2 and HKDF keys), encrypt and decrypt with AES-GCM, AES-CBC or AES-CTR, deriveBits and deriveKey with PBKDF2 or HKDF, sign and verify with HMAC, and digest with SHA-1, SHA-256, SHA-384 or SHA-512, each called directly on crypto.subtle \ No newline at end of file diff --git a/tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json b/tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json new file mode 100644 index 000000000..d2c795829 --- /dev/null +++ b/tests/harness/corpus-costs/6826-map-primitive-constructors.ts.json @@ -0,0 +1 @@ +{ "plain": 3700, "san": 3500 } diff --git a/tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json b/tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json new file mode 100644 index 000000000..f44e89d4f --- /dev/null +++ b/tests/harness/corpus-costs/6827-recursive-union-nested-literals.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 3400 } diff --git a/tests/harness/corpus-costs/6828-recursive-union-object-walkers.ts.json b/tests/harness/corpus-costs/6828-recursive-union-object-walkers.ts.json new file mode 100644 index 000000000..77ddc81f8 --- /dev/null +++ b/tests/harness/corpus-costs/6828-recursive-union-object-walkers.ts.json @@ -0,0 +1 @@ +{ "plain": 4200, "san": 5800 } diff --git a/tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json b/tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json new file mode 100644 index 000000000..5c8f4dafd --- /dev/null +++ b/tests/harness/corpus-costs/6862-readline-chained-listeners.cjs.json @@ -0,0 +1 @@ +{ "plain": 1930, "san": 4280 } diff --git a/tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json b/tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json new file mode 100644 index 000000000..ad095e0af --- /dev/null +++ b/tests/harness/corpus-costs/6865-number-parseint-default-radix.ts.json @@ -0,0 +1 @@ +{ "plain": 1940, "san": 4550 } diff --git a/tests/harness/corpus-costs/6866-number-parseint-untyped.js.json b/tests/harness/corpus-costs/6866-number-parseint-untyped.js.json new file mode 100644 index 000000000..03bfc0b22 --- /dev/null +++ b/tests/harness/corpus-costs/6866-number-parseint-untyped.js.json @@ -0,0 +1 @@ +{ "plain": 1830, "san": 3890 } diff --git a/tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json b/tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json new file mode 100644 index 000000000..f6a23dfa9 --- /dev/null +++ b/tests/harness/corpus-costs/6867-string-match-optional-regexp.js.json @@ -0,0 +1 @@ +{ "plain": 1960, "san": 4170 } diff --git a/tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json b/tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json new file mode 100644 index 000000000..a1967e528 --- /dev/null +++ b/tests/harness/corpus-costs/6868-buffer-from-untyped-array-buffer.cjs.json @@ -0,0 +1 @@ +{ "plain": 2070, "san": 4110 } diff --git a/tests/harness/corpus-costs/6869-atomics-untyped-arrays.cjs.json b/tests/harness/corpus-costs/6869-atomics-untyped-arrays.cjs.json new file mode 100644 index 000000000..114626b53 --- /dev/null +++ b/tests/harness/corpus-costs/6869-atomics-untyped-arrays.cjs.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 4710 } diff --git a/tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json b/tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json new file mode 100644 index 000000000..a44d195f2 --- /dev/null +++ b/tests/harness/corpus-costs/6912-extend-factory-local-class.ts.json @@ -0,0 +1 @@ +{ "plain": 3633, "san": 5578 } diff --git a/tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json b/tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json new file mode 100644 index 000000000..a52f2c454 --- /dev/null +++ b/tests/harness/corpus-costs/6994-uint8-clamped-store-rounding.ts.json @@ -0,0 +1 @@ +{ "plain": 2400, "san": 3300 } diff --git a/tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json b/tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json new file mode 100644 index 000000000..c0aef3bb7 --- /dev/null +++ b/tests/harness/corpus-costs/6995-worker-typed-array-loop-versions.ts.json @@ -0,0 +1 @@ +{ "plain": 2700, "san": 3900 } diff --git a/tests/harness/corpus-costs/6996-typed-array-index-temps.ts.json b/tests/harness/corpus-costs/6996-typed-array-index-temps.ts.json new file mode 100644 index 000000000..4dc6b88cb --- /dev/null +++ b/tests/harness/corpus-costs/6996-typed-array-index-temps.ts.json @@ -0,0 +1 @@ +{ "plain": 3500, "san": 4600 } diff --git a/tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json b/tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json new file mode 100644 index 000000000..40dc942dc --- /dev/null +++ b/tests/harness/corpus-costs/7040-es5-error-constructor.mjs.json @@ -0,0 +1 @@ +{ "plain": 3648, "san": 32315 } diff --git a/tests/harness/corpus-costs/7041-error-called-without-new.mjs.json b/tests/harness/corpus-costs/7041-error-called-without-new.mjs.json new file mode 100644 index 000000000..d5a7a927c --- /dev/null +++ b/tests/harness/corpus-costs/7041-error-called-without-new.mjs.json @@ -0,0 +1 @@ +{ "plain": 2293, "san": 3259 } diff --git a/tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json b/tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json new file mode 100644 index 000000000..e85a6cc3e --- /dev/null +++ b/tests/harness/corpus-costs/7042-string-method-names-on-objects.mjs.json @@ -0,0 +1 @@ +{ "plain": 2776, "san": 20012 } diff --git a/tests/harness/corpus-costs/7043-date-set-time.mjs.json b/tests/harness/corpus-costs/7043-date-set-time.mjs.json new file mode 100644 index 000000000..cdb4d4f92 --- /dev/null +++ b/tests/harness/corpus-costs/7043-date-set-time.mjs.json @@ -0,0 +1 @@ +{ "plain": 2423, "san": 3371 } diff --git a/tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json b/tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json new file mode 100644 index 000000000..a7951b37e --- /dev/null +++ b/tests/harness/corpus-costs/7044-nested-empty-object-maps.mjs.json @@ -0,0 +1 @@ +{ "plain": 2671, "san": 3931 } diff --git a/tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json b/tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json new file mode 100644 index 000000000..1776c882d --- /dev/null +++ b/tests/harness/corpus-costs/7045-instanceof-constructor-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 2726, "san": 4330 } diff --git a/tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json b/tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json new file mode 100644 index 000000000..2e8f43953 --- /dev/null +++ b/tests/harness/corpus-costs/7046-parse-float-extra-arguments.mjs.json @@ -0,0 +1 @@ +{ "plain": 2204, "san": 3269 } diff --git a/tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json b/tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json new file mode 100644 index 000000000..d73f3cccc --- /dev/null +++ b/tests/harness/corpus-costs/7047-dynamic-string-index.mjs.json @@ -0,0 +1 @@ +{ "plain": 2451, "san": 3411 } diff --git a/tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json b/tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json new file mode 100644 index 000000000..710783451 --- /dev/null +++ b/tests/harness/corpus-costs/7048-unary-number-coercions.mjs.json @@ -0,0 +1 @@ +{ "plain": 2478, "san": 3675 } diff --git a/tests/harness/corpus-costs/7049-member-assignment-values.mjs.json b/tests/harness/corpus-costs/7049-member-assignment-values.mjs.json new file mode 100644 index 000000000..551cfbf7b --- /dev/null +++ b/tests/harness/corpus-costs/7049-member-assignment-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 2456, "san": 3617 } diff --git a/tests/harness/corpus-costs/7050-date-subclass.mjs.json b/tests/harness/corpus-costs/7050-date-subclass.mjs.json new file mode 100644 index 000000000..e21289735 --- /dev/null +++ b/tests/harness/corpus-costs/7050-date-subclass.mjs.json @@ -0,0 +1 @@ +{ "plain": 2932, "san": 3727 } diff --git a/tests/harness/corpus-costs/7051-date-subclass-typed.ts.json b/tests/harness/corpus-costs/7051-date-subclass-typed.ts.json new file mode 100644 index 000000000..fad282486 --- /dev/null +++ b/tests/harness/corpus-costs/7051-date-subclass-typed.ts.json @@ -0,0 +1 @@ +{ "plain": 2533, "san": 3429 } diff --git a/tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json b/tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json new file mode 100644 index 000000000..cabbe475e --- /dev/null +++ b/tests/harness/corpus-costs/7052-date-subclass-dynamic.mjs.json @@ -0,0 +1 @@ +{ "plain": 2740, "san": 3689 } diff --git a/tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json b/tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json new file mode 100644 index 000000000..8ebe6846a --- /dev/null +++ b/tests/harness/corpus-costs/7053-private-fields-untyped-receivers.mjs.json @@ -0,0 +1 @@ +{ "plain": 2415, "san": 3338 } diff --git a/tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json b/tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json new file mode 100644 index 000000000..d313a64f1 --- /dev/null +++ b/tests/harness/corpus-costs/7054-let-bindings-with-null-writes.mjs.json @@ -0,0 +1 @@ +{ "plain": 2293, "san": 3012 } diff --git a/tests/harness/corpus-costs/7055-cipher-streams.ts.json b/tests/harness/corpus-costs/7055-cipher-streams.ts.json new file mode 100644 index 000000000..7dda3fe10 --- /dev/null +++ b/tests/harness/corpus-costs/7055-cipher-streams.ts.json @@ -0,0 +1 @@ +{ "plain": 4395, "san": 6600 } diff --git a/tests/harness/corpus-costs/7056-cipher-stream-errors.ts.json b/tests/harness/corpus-costs/7056-cipher-stream-errors.ts.json new file mode 100644 index 000000000..b3e89de2a --- /dev/null +++ b/tests/harness/corpus-costs/7056-cipher-stream-errors.ts.json @@ -0,0 +1 @@ +{ "plain": 2558, "san": 4456 } diff --git a/tests/harness/corpus-costs/7057-cipher-streams.mjs.json b/tests/harness/corpus-costs/7057-cipher-streams.mjs.json new file mode 100644 index 000000000..1edb6e866 --- /dev/null +++ b/tests/harness/corpus-costs/7057-cipher-streams.mjs.json @@ -0,0 +1 @@ +{ "plain": 2780, "san": 5329 } diff --git a/tests/harness/corpus-costs/7058-pipeline-function-stages.mjs.json b/tests/harness/corpus-costs/7058-pipeline-function-stages.mjs.json new file mode 100644 index 000000000..d4709c5d6 --- /dev/null +++ b/tests/harness/corpus-costs/7058-pipeline-function-stages.mjs.json @@ -0,0 +1 @@ +{ "plain": 3703, "san": 7279 } diff --git a/tests/harness/corpus-costs/7059-transform-backpressure.ts.json b/tests/harness/corpus-costs/7059-transform-backpressure.ts.json new file mode 100644 index 000000000..cc34bea0e --- /dev/null +++ b/tests/harness/corpus-costs/7059-transform-backpressure.ts.json @@ -0,0 +1 @@ +{ "plain": 2394, "san": 5656 } diff --git a/tests/harness/corpus-costs/7109-uninitialized-let-assignment-expressions.ts.json b/tests/harness/corpus-costs/7109-uninitialized-let-assignment-expressions.ts.json new file mode 100644 index 000000000..46c7f7832 --- /dev/null +++ b/tests/harness/corpus-costs/7109-uninitialized-let-assignment-expressions.ts.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 4500 } diff --git a/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json b/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json new file mode 100644 index 000000000..ce52548ca --- /dev/null +++ b/tests/harness/corpus-costs/7120-webcrypto-random-uuid.ts.json @@ -0,0 +1 @@ +{ "plain": 4431, "san": 4365 } diff --git a/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json b/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json new file mode 100644 index 000000000..db431c2ca --- /dev/null +++ b/tests/harness/corpus-costs/7121-webcrypto-crypto-object.mjs.json @@ -0,0 +1 @@ +{ "plain": 5043, "san": 4705 } diff --git a/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json b/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json new file mode 100644 index 000000000..46469f2a9 --- /dev/null +++ b/tests/harness/corpus-costs/7122-webcrypto-node-crypto.ts.json @@ -0,0 +1 @@ +{ "plain": 4805, "san": 6923 } diff --git a/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json b/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json new file mode 100644 index 000000000..682ba704a --- /dev/null +++ b/tests/harness/corpus-costs/7123-webcrypto-node-crypto.cjs.json @@ -0,0 +1 @@ +{ "plain": 4728, "san": 6725 } diff --git a/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json b/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json new file mode 100644 index 000000000..73f1ce847 --- /dev/null +++ b/tests/harness/corpus-costs/7124-webcrypto-secret-keys.ts.json @@ -0,0 +1 @@ +{ "plain": 5860, "san": 9241 } diff --git a/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json b/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json new file mode 100644 index 000000000..ceff344eb --- /dev/null +++ b/tests/harness/corpus-costs/7125-webcrypto-key-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 6582, "san": 9054 } diff --git a/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json b/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json new file mode 100644 index 000000000..b1680985c --- /dev/null +++ b/tests/harness/corpus-costs/7126-webcrypto-aes.ts.json @@ -0,0 +1 @@ +{ "plain": 4563, "san": 6500 } diff --git a/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json b/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json new file mode 100644 index 000000000..1ebb743b0 --- /dev/null +++ b/tests/harness/corpus-costs/7127-webcrypto-aes-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 4385, "san": 8939 } diff --git a/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json b/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json new file mode 100644 index 000000000..2888d22eb --- /dev/null +++ b/tests/harness/corpus-costs/7128-webcrypto-derive.ts.json @@ -0,0 +1 @@ +{ "plain": 5042, "san": 9143 } diff --git a/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json b/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json new file mode 100644 index 000000000..b2f70bf57 --- /dev/null +++ b/tests/harness/corpus-costs/7129-webcrypto-derive-errors.mjs.json @@ -0,0 +1 @@ +{ "plain": 5549, "san": 7441 } diff --git a/tests/harness/corpus-costs/7183-local-class-heritage-effects.ts.json b/tests/harness/corpus-costs/7183-local-class-heritage-effects.ts.json new file mode 100644 index 000000000..6bcb3bc35 --- /dev/null +++ b/tests/harness/corpus-costs/7183-local-class-heritage-effects.ts.json @@ -0,0 +1 @@ +{ "plain": 2573, "san": 3208 } diff --git a/tests/harness/errors.test.ts b/tests/harness/errors.test.ts index 8af6998ca..00ee40043 100644 --- a/tests/harness/errors.test.ts +++ b/tests/harness/errors.test.ts @@ -1634,14 +1634,14 @@ console.log(typeof promisePrototype, typeof promisePrototype.then, promiseProtot }); }); -// WebCrypto's native slice is HMAC importKey('raw'), sign and verify, and -// the SHA-1/SHA-2 digests. Algorithms Node recognizes outside that slice -// reject with a coded SC2020 Error instead of a Node-shaped success, and a -// JavaScript call with the 'jwk' format meets its named fence when it runs. +// WebCrypto's native slice is crypto.subtle's secret-key methods. +// Algorithms Node recognizes outside that slice reject with a coded SC2020 +// Error instead of a Node-shaped success, and a JavaScript call of a +// method outside it meets its named fence when it runs. // Node's own rejections (unknown names, bad usages) are corpus-pinned // (6292, 6294); these refusals have no Node twin, so they live here. describe("WebCrypto refusals (JS lane)", () => { - test("recognized algorithms outside the HMAC slice reject with SC2020", async () => { + test("recognized algorithms outside the secret-key slice reject with SC2020", async () => { const r = await compileAndRun( "webcrypto-unsupported-algorithms", `const bytes = new TextEncoder().encode("secret"); @@ -1662,10 +1662,10 @@ await attempt("sha3", () => crypto.subtle.digest("SHA3-256", bytes)); await attempt("sha3 hmac", () => crypto.subtle.importKey("raw", bytes, { name: "HMAC", hash: "SHA3-256" }, false, ["sign"]), ); -const format = JSON.parse('"jwk"'); -await attempt("computed jwk", () => - crypto.subtle.importKey(format, { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]), -); +await attempt("ed25519 key", () => crypto.subtle.generateKey({ name: "Ed25519" }, false, ["sign"])); +const aes = await crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, false, ["encrypt"]); +await attempt("aes-ocb", () => crypto.subtle.encrypt({ name: "AES-OCB", iv: new Uint8Array(12) }, aes, bytes)); +await attempt("ecdh bits", () => crypto.subtle.deriveBits({ name: "ECDH" }, key, 256)); `, "mjs", ); @@ -1676,22 +1676,25 @@ await attempt("computed jwk", () => "ecdsa Error SC2020 crypto.subtle.sign with the ECDSA algorithm is not supported yet\n" + "sha3 Error SC2020 crypto.subtle.digest with the SHA3-256 algorithm is not supported yet\n" + "sha3 hmac Error SC2020 crypto.subtle.importKey with the SHA3-256 algorithm is not supported yet\n" + - "computed jwk Error SC2020 crypto.subtle.importKey with the 'jwk' key format is not supported yet\n", + "ed25519 key Error SC2020 crypto.subtle.generateKey with the Ed25519 algorithm is not supported yet\n" + + "aes-ocb Error SC2020 crypto.subtle.encrypt with the AES-OCB algorithm is not supported yet\n" + + "ecdh bits Error SC2020 crypto.subtle.deriveBits with the ECDH algorithm is not supported yet\n", ); }); - test("a literal 'jwk' import is a named fence where it runs", async () => { + test("a method outside the slice is a named fence where it runs", async () => { const r = await compileAndRun( - "webcrypto-jwk-literal", + "webcrypto-wrap-key", `console.log("before"); -await crypto.subtle.importKey("jwk", { kty: "oct", k: "c2VjcmV0" }, { name: "HMAC", hash: "SHA-256" }, false, ["sign"]); +const key = await crypto.subtle.generateKey({ name: "AES-KW", length: 128 }, true, ["wrapKey"]); +await crypto.subtle.wrapKey("raw", key, key, "AES-KW"); `, "mjs", ); expect(r.exitCode).toBe(1); expect(r.stdout).toBe("before\n"); expect(r.stderr).toMatch( - /^Uncaught Error: 'crypto\.subtle\.importKey with the 'jwk' key format' is part of the standard library types but has no scriptc lowering yet.*\n$/, + /^Uncaught Error: 'crypto\.subtle\.wrapKey' is part of the standard library types but has no scriptc lowering yet.*\n$/, ); }); }); diff --git a/tests/harness/npm-static-costs.json b/tests/harness/npm-static-costs.json index 2f1cdde1e..1546f8d57 100644 --- a/tests/harness/npm-static-costs.json +++ b/tests/harness/npm-static-costs.json @@ -84,6 +84,8 @@ "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, + "the published js-yaml loads, dumps and reports errors statically and matches Node": 12728, + "the published smol-toml parses and stringifies statically and matches Node": 11649, "untyped package methods preserve virtual overrides (llvm)": 3065 }, "san": { @@ -171,6 +173,8 @@ "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, + "the published js-yaml loads, dumps and reports errors statically and matches Node": 35621, + "the published smol-toml parses and stringifies statically and matches Node": 25130, "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 df82efbcf..7ee76307f 100644 --- a/tests/harness/npm-static.test.ts +++ b/tests/harness/npm-static.test.ts @@ -1175,6 +1175,38 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => { effectBuildTimeout, ); + test("the published js-yaml loads, dumps and reports errors statically and matches Node", async () => { + const entry = join(pilotRoot, "js-yaml-cli.mjs"); + const { coverage } = analyze(entry, { npmStatic: ["js-yaml"] }); + expect(coverage.npmStatic).toEqual([{ package: "js-yaml", status: "static" }]); + expect(coverage.diagnostics).toHaveLength(0); + const binary = await buildStatic(entry, ["js-yaml"]); + const [reference, native] = await Promise.all([ + runBinary(process.execPath, [entry]), + runBinary(binary, []), + ]); + expect(native.stdout.toString()).toBe(reference.stdout.toString()); + expect(comparableStderr(native.stderr)).toEqual(reference.stderr); + expect(native.exitCode).toBe(reference.exitCode); + expect(native.exitCode).toBe(0); + }, 300_000); + + test("the published smol-toml parses and stringifies statically and matches Node", async () => { + const entry = join(pilotRoot, "smol-toml-cli.mjs"); + const { coverage } = analyze(entry, { npmStatic: ["smol-toml"] }); + expect(coverage.npmStatic).toEqual([{ package: "smol-toml", status: "static" }]); + expect(coverage.diagnostics).toHaveLength(0); + const binary = await buildStatic(entry, ["smol-toml"]); + const [reference, native] = await Promise.all([ + runBinary(process.execPath, [entry]), + runBinary(binary, []), + ]); + expect(native.stdout.toString()).toBe(reference.stdout.toString()); + expect(comparableStderr(native.stderr)).toEqual(reference.stderr); + expect(native.exitCode).toBe(reference.exitCode); + expect(native.exitCode).toBe(0); + }, 300_000); + test("inferred variadic factories preserve checked callable results", async () => { const entry = join(pilotRoot, "variadic-factory-cli.ts"); const binary = await buildStatic(entry, ["variadic-factory"]); diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index dabb9b0bf..8532ad6e9 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -1132,6 +1132,24 @@ test("node-types: WebCrypto HMAC matches Node under @types/node", async () => { expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: WebCrypto secret keys match Node under @types/node", async () => { + const outDir = outDirFor("node-webcrypto-keys"); + const entry = join(nodeTypesDir, "webcrypto-keys.ts"); + const result = await compile(entry, { + outPath: join(outDir, "webcrypto-keys"), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: the navigator global's runtime probe matches Node under @types/node", async () => { const outDir = outDirFor("node-navigator"); const entry = join(nodeTypesDir, "navigator.ts"); diff --git a/tests/harness/self-hosting-llvm-emitter.test.ts b/tests/harness/self-hosting-llvm-emitter.test.ts index 9c24d50cb..cb9cd69c4 100644 --- a/tests/harness/self-hosting-llvm-emitter.test.ts +++ b/tests/harness/self-hosting-llvm-emitter.test.ts @@ -161,7 +161,7 @@ for (const backend of ["llvm"] as const) { name: string, request = llvmEmitterRequest(), ): Promise => { - writeFileSync(input, serializeModule(mod)); + writeFileSync(input, serializeModule(mod, true)); writeFileSync(config, JSON.stringify(request)); const result = await execFileAsync(stage, [input, output, config], { ...runOptions, @@ -195,7 +195,7 @@ for (const backend of ["llvm"] as const) { loc: { file: rejected.sourceFile, start: 0, end: 0 }, }, ]; - writeFileSync(input, serializeModule(rejected)); + writeFileSync(input, serializeModule(rejected, true)); writeFileSync(config, JSON.stringify(llvmEmitterRequest())); writeFileSync(output, "untouched"); const args = [input, output, config]; diff --git a/tests/harness/self-hosting-native-toolchain.test.ts b/tests/harness/self-hosting-native-toolchain.test.ts index 7da7626b7..c01b52595 100644 --- a/tests/harness/self-hosting-native-toolchain.test.ts +++ b/tests/harness/self-hosting-native-toolchain.test.ts @@ -105,7 +105,7 @@ for (const backend of ["llvm"] as const) { object_format: target.objectFormat, minimum_os: target.minimumOs, }, - runtime_abi: { version: 24, marker: "scr_runtime_abi_v24" }, + runtime_abi: { version: 25, marker: "scr_runtime_abi_v25" }, compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple }, macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" }, flavors: {