Merge main

This commit is contained in:
Chris Tate
2026-10-22 04:52:19 -05:00
320 changed files with 10411 additions and 1186 deletions
+1 -1
View File
@@ -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 |
@@ -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}`);
@@ -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.
@@ -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.
@@ -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.
+41 -41
View File
@@ -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"
}
},
{
@@ -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",
+1 -1
View File
@@ -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.";
+10 -7
View File
@@ -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"] },
+3 -2
View File
@@ -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",
+2 -2
View File
@@ -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");
});
});
+95 -15
View File
@@ -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<ArrayBuffer>;
deriveBits(
algorithm: Pbkdf2Params | HkdfParams,
baseKey: CryptoKey,
length?: number | null,
): Promise<ArrayBuffer>;
deriveKey(
algorithm: Pbkdf2Params | HkdfParams,
baseKey: CryptoKey,
derivedKeyType: AlgorithmIdentifier | AesDerivedKeyParams | HmacImportParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
digest(algorithm: AlgorithmIdentifier, data: BufferSource): Promise<ArrayBuffer>;
encrypt(
algorithm: AesGcmParams | AesCbcParams | AesCtrParams,
key: CryptoKey,
data: BufferSource,
): Promise<ArrayBuffer>;
exportKey(format: "jwk", key: CryptoKey): Promise<JsonWebKey>;
exportKey(format: Exclude<KeyFormat, "jwk">, key: CryptoKey): Promise<ArrayBuffer>;
generateKey(
algorithm: AesKeyGenParams | HmacKeyGenParams,
extractable: boolean,
keyUsages: readonly KeyUsage[],
): Promise<CryptoKey>;
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<T extends RandomValuesArray>(typedArray: T): T;
export const webcrypto: {
getRandomValues<T extends RandomValuesArray>(typedArray: T): T;
};
/* The WebCrypto global's object and its SubtleCrypto. */
export const webcrypto: Crypto;
export const subtle: SubtleCrypto;
export function randomFillSync<T extends ArrayBufferView | ArrayBufferLike>(
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;
@@ -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 => {
@@ -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<Node>;
/** Worker programs: typed-array accesses whose views the guard proves
* thread-owned (no shared storage). */
owned: ReadonlySet<Node>;
/** 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<string>;
/** Accesses already proved in bounds without a guard. */
proven: ReadonlySet<Node>;
/** 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<Node>;
}
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<number, Node[]>();
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<IrExpr>();
@@ -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<Node>();
const ownedNodes = new Set<Node>();
const used = new Set<number>();
for (const [receiver, nodes] of ownedKept) {
used.add(receiver);
for (const node of nodes) ownedNodes.add(node);
}
const ranges = new Map<IrExpr, IntegerRange>();
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}`);
@@ -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
@@ -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<LlEmitter["binding"]>,
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;
+20 -2
View File
@@ -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<IrExpr>();
bytesBounds: ReadonlySet<IrExpr | IrStmt> = new Set();
/** Worker programs: typed-array accesses whose views an enclosing version
* guard proved thread-owned. */
bytesOwned: ReadonlySet<IrExpr | IrStmt> = new Set();
integerViews = new Map<string, string>();
/** 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 {
@@ -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(
@@ -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();
@@ -448,6 +448,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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<string, string> = {
"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<string>([
// 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",
@@ -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<string, ("r" | "w" | "f" | "d" | "t" | "l")[]> = {
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})`);
@@ -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. */
@@ -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). */
@@ -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.
+2 -2
View File
@@ -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;
@@ -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: {
@@ -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",
@@ -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)",
+2 -1
View File
@@ -174,7 +174,8 @@ class ContractChecker {
return `${owner}.${method[2]}`;
}
if (fn?.sourceName !== undefined && fn.sourceName !== "<anonymous>") 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(/^%/, "");
}
@@ -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);
@@ -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);
@@ -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;
@@ -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({
@@ -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,
@@ -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 };
@@ -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 };
@@ -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);
@@ -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<string> = 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,
},
];
}
@@ -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<string, IrLibFn> = 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
@@ -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);
@@ -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<string> = 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
@@ -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++) {
@@ -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),
};
}
@@ -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 = {
@@ -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 =
@@ -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 => {
@@ -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({
@@ -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,
@@ -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 }));
@@ -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;
@@ -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}`,
@@ -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 };
@@ -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 };
@@ -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));
}
@@ -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",
@@ -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<string, IrLibFn> = 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.<method>(...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)
@@ -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(
@@ -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,
@@ -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 }];
@@ -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 [
@@ -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 =
@@ -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);
@@ -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 };
@@ -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
@@ -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) => ({
@@ -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 };
@@ -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"
@@ -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);
@@ -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);
@@ -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 },
@@ -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));
}
@@ -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);
@@ -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 });
}
@@ -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,
},
@@ -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({
@@ -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));
}
@@ -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));
}
@@ -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);
@@ -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,
@@ -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);
}
@@ -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);
@@ -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<string>();
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(
@@ -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<T>(
export function withStreamReceiver<T>(
lowerer: Lowerer,
node: ts.Expression,
value: IrExpr,
@@ -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<n>`) takes its own name, else the name it is
* bound to, else `anonymous function`. */
* function expression (`%fn.<hash>`, with `_<name>` 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";
}
@@ -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.
@@ -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,
@@ -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,
);
@@ -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<number>();
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)
);
}
@@ -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);
@@ -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.
@@ -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.<name>` 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);
@@ -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 };
@@ -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",
@@ -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<ts.Symbol, ts.Type>;
/** Rendered type arguments ("<number, string>") 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<ts.Symbol, ts.Type>();
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<S>`) 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<S>`).
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<S>`, `new () => S`) whose
// every return gives back the one local class, through parentheses and
// assertions (`return Base as Struct<S>`).
// 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<ts.Symbol>();
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<T>(
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,
@@ -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;
}
@@ -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,
@@ -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;
@@ -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, {
@@ -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;
@@ -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);

Some files were not shown because too many files have changed in this diff Show More