mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Merge main
This commit is contained in:
@@ -4,7 +4,7 @@ This is the only agent guide for the repository. Its rules apply everywhere, inc
|
||||
|
||||
## Markdown style
|
||||
|
||||
Write prose paragraphs and list items as single source lines and let editors and renderers wrap them naturally. Do not hard-wrap Markdown at a fixed column; preserve separate lines only where Markdown syntax or intentional formatting requires them, such as headings, lists, tables, blockquotes, and code blocks.
|
||||
Write prose paragraphs and list items as single source lines and let editors and renderers wrap them naturally. Do not hard-wrap Markdown at a fixed column; preserve separate lines only where Markdown syntax or intentional formatting requires them, such as headings, lists, tables, blockquotes, and code blocks. The Limitations page partials under `docs/content/limitations/` are the exception: they keep one sentence per line (see Limitations page partials).
|
||||
|
||||
## Build and test
|
||||
|
||||
@@ -140,10 +140,11 @@ Never hand-edit these files:
|
||||
|
||||
- `packages/compiler/surface-manifest.json`
|
||||
- `internal/compatibility/generated/node-v24-internal.json`
|
||||
- `internal/compatibility/generated/node-v24-backlog.json`
|
||||
- `docs/src/generated/node-v24-compatibility.json`
|
||||
|
||||
Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, changes.
|
||||
Change compiler decision sources or the implementation-owned compatibility manifests, then regenerate. Use `pnpm manifest` when compiler decision tables change and `pnpm node-compat` whenever the Node pin, compatibility manifests, compiler surface manifest, changes. `pnpm node-compat --local` reclassifies the committed ledger without fetching Node's docs; it writes the same files as `pnpm node-compat` while the Node pin is unchanged.
|
||||
|
||||
Never resolve a merge conflict in these files by hand. Resolve the source conflicts, then run `node scripts/regen-generated.mjs`: it takes the merged-in branch's version of each conflicted generated file, regenerates all of them from the merged sources, and stages the ones that conflicted.
|
||||
|
||||
The public docs artifact must not expose repository test paths, internal source identifiers, engineering evidence bookkeeping, documentation/metadata headings, command-line or configuration-only entries, or rows that are N/A in the native tier. Those remain in the complete internal census under `internal/compatibility/generated/`.
|
||||
|
||||
@@ -158,7 +159,7 @@ For each API family:
|
||||
5. Add differential evidence that executes under Node and as a compiled binary. Cover stdout, stderr, exit status, success behavior, and important error shapes. A load-only export check does not prove behavior.
|
||||
6. Update the implementation-owned source or manifest. Positive `supported` and `partial` mappings must name existing test evidence.
|
||||
7. Run `pnpm manifest` if compiler surface tables changed, then run `pnpm node-compat`. Never patch generated JSON or headers.
|
||||
8. Inspect the internal ledger, public artifact, and backlog diff. Confirm the intended API family changed and unrelated rows did not.
|
||||
8. Inspect the internal ledger and public artifact diff, and compare `pnpm node-compat:backlog` output before and after. Confirm the intended API family changed and unrelated rows did not.
|
||||
9. Run focused tests, `pnpm node-compat:check`, and the full validation gate required for the change. If docs output changed, also run the docs gate.
|
||||
|
||||
Compatibility commands:
|
||||
@@ -196,7 +197,7 @@ The docs site is a standalone pnpm workspace under `docs/`. It uses Next.js App
|
||||
- A labeled bullet (`- **Crypto:** ...`) is `docs/content/limitations/<section>/<label>.mdx`, for example `builtin-apis/crypto.mdx`. Edit that file to change the bullet.
|
||||
- The other text of a section (paragraphs, code blocks, unlabeled lists) is `docs/content/limitations/<section>.mdx`; text before the first heading is `introduction.mdx`.
|
||||
- `<section>` and `<label>` are the heading and label text in lowercase with each run of other characters replaced by `-`.
|
||||
- Write one sentence per line; a bullet's continuation lines are indented two spaces. Markdown joins the lines, and edits to different sentences don't conflict.
|
||||
- Write one sentence per line; a bullet's continuation lines are indented two spaces. Markdown joins the lines, and edits to different sentences don't conflict. `pnpm lint` fails on a line that holds two sentences, and `node docs/scripts/limitations-partials.mjs fix` splits such lines.
|
||||
- To add a bullet, add its file and an include line at its position in `limitations.mdx`, with a blank line on each side. Consecutive bullet includes render as one list.
|
||||
- To add a section, add the heading to `limitations.mdx` and include the section's partial below it.
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
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.
|
||||
`createECDH` and `ECDH.convertKey` support prime192v1, secp224r1, prime256v1, secp384r1, secp521r1, secp192k1, secp224k1, secp256k1, brainpoolP256r1, brainpoolP384r1, and brainpoolP512r1, on native targets, through `generateKeys()`, `computeSecret()`, `getPublicKey()`, `getPrivateKey()`, `setPrivateKey()`, and `setPublicKey()`, in compressed, uncompressed, and hybrid forms; the other curves `crypto.getCurves()` lists and wasm32-wasi builds are refused.
|
||||
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.
|
||||
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.
|
||||
Native derivations finish before their deferred callbacks, so computation blocks the caller.
|
||||
|
||||
@@ -8,10 +8,15 @@ Some builtins can be stored and passed as functions:
|
||||
In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles.
|
||||
Calls through these values bypass `node:test` mock timers.
|
||||
Other builtin globals stored in JavaScript remain opaque identity values.
|
||||
In JavaScript, `require("events")` and the `EventEmitter` class assigned to an untyped binding or stored in an object (`ReleaseEmitter = EventEmitter`) are the untyped `EventEmitter` constructor that constructor functions inherit from (see below): `new` through it builds such an emitter object, and identity and `instanceof` agree with the class.
|
||||
A class can extend `require("events")` directly.
|
||||
|
||||
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere.
|
||||
`util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
|
||||
|
||||
`util.promisify` over an untyped (`any`) function returns a native wrapper that follows Node's callback rules and carries the original's `name` and `length`.
|
||||
Such a function with other own properties, `util.promisify.custom` among them, throws `SC2020` where `util.promisify` runs, and the wrapper does not report Node's DEP0174 warning when the original returns a promise.
|
||||
|
||||
`util.styleText` supports standard formats and aliases, `#RGB` and `#RRGGBB` hex colors, arrays of formats, nested ANSI resets, and explicit color enabling with `{ validateStream: false }`.
|
||||
Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.
|
||||
|
||||
|
||||
@@ -2,3 +2,6 @@
|
||||
A function stored in a slot with a different signature remains the same function for equality, array searches, `length`, and own properties.
|
||||
Typed rest-parameter functions can be stored and called indirectly, including through a slot with fixed parameters, where the slot's arguments arrive in the rest array.
|
||||
Builtin function values have API-specific restrictions; see [Builtin function values](#builtin-function-values).
|
||||
- **JavaScript parameters:** In a JavaScript file TypeScript does not check, a prototype method (`C.prototype.m = function (...) {}`) takes whatever values its callers pass; its JSDoc types document the usual values without constraining them.
|
||||
A sloppy-mode function with named parameters may read `arguments` only where the aliasing of its parameters cannot show: it reads `arguments.length`, or reads elements and never writes a parameter or an element.
|
||||
Other uses of `arguments` in such a function are refused with `SC1090`; ES modules, `'use strict'` code, classes, and functions with default, rest, or destructured parameters have no such restriction.
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
`readable-stream` 2.x, 3.x and 4.x compile as Node.js's built-in `stream` module. For 2.x and 4.x, that is the package's own behavior when the `READABLE_STREAM` environment variable is `disable`. Without that variable, Node.js runs the package's JavaScript copy of the stream classes, so a native build can differ from Node.js wherever a program depends on that difference: `instanceof` checks against the built-in classes, class identity, error messages, and timing details. The package's `lib/_stream_*.js` files also compile as the matching built-in classes, although Node.js loads the package's own implementations through them in every environment. `readable-stream` 3.x is a copy of Node.js 10's streams. Its own `disable` mode throws on Node.js 24, so no setting makes Node.js run it against the built-in classes; a native build maps the members its entry exports (the stream classes, `Stream`, `finished` and `pipeline`) to the built-in ones.
|
||||
`readable-stream` 2.x, 3.x and 4.x compile as Node.js's built-in `stream` module.
|
||||
For 2.x and 4.x, that is the package's own behavior when the `READABLE_STREAM` environment variable is `disable`.
|
||||
Without that variable, Node.js runs the package's JavaScript copy of the stream classes, so a native build can differ from Node.js wherever a program depends on that difference: `instanceof` checks against the built-in classes, class identity, error messages, and timing details.
|
||||
The package's `lib/_stream_*.js` files also compile as the matching built-in classes, although Node.js loads the package's own implementations through them in every environment.
|
||||
`readable-stream` 3.x is a copy of Node.js 10's streams.
|
||||
Its own `disable` mode throws on Node.js 24, so no setting makes Node.js run it against the built-in classes; a native build maps the members its entry exports (the stream classes, `Stream`, `finished` and `pipeline`) to the built-in ones.
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
`socket.addMembership()` and `socket.dropMembership()` on a `dgram` socket that is not bound yet throw `SC2020`; Node binds such a socket to an ephemeral port first, so bind the socket before joining a group.
|
||||
The `SystemError` that the buffer size methods throw (`ERR_SOCKET_BUFFER_SIZE`, for example from `getRecvBufferSize()` before `bind()`) has Node's `message`, `code`, `errno` and `syscall`, but no `info` object.
|
||||
|
||||
In JavaScript, `socket.send()` also accepts lists of buffers, sends on a connected socket, and takes a completion callback, which receives `null` and the byte count, or the send's failure instead of an `'error'` event.
|
||||
The callback runs after the socket's pending events, where Node runs it on the next tick.
|
||||
In TypeScript, `send()` with a callback, a list, or on a connected socket fails to compile with `SC2020`.
|
||||
JavaScript can read a socket's `on`, `once`, `addListener`, `removeListener`, `off`, `emit`, `send`, `close`, `bind`, `connect`, `disconnect`, `address`, `ref`, and `unref` as function values, for example `socket.emit.bind(socket)`; reading other members as values is refused with `SC1090`.
|
||||
Listeners registered that way, or with untyped parameters, take only the `message`, `error`, `close`, `listening`, and `connect` events.
|
||||
When a socket closes, listeners registered from TypeScript are released, so an `emit()` from JavaScript after `'close'` reaches only listeners registered from JavaScript.
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
`server.listen(path)` and the `socketPath` option of `http.request()` and `https.request()` use Unix domain sockets on Linux and macOS. On Windows, where Node maps these paths to named pipes, they fail with `ENOTSUP`. `server.address()` on a server listening on a Unix socket returns the socket path, as in Node, when the result is typed `string | AddressInfo | null` (the @types/node declaration). Where it is typed as the `{ address, family, port }` record alone, which cannot hold the path, the call throws an error for such a server. `listen(path, backlog, callback)` is refused at compile time; pass `listen(path, callback)`.
|
||||
When the options of `http.request()` come from a variable, or from an object literal that spreads one, they are read at run time with Node's validation errors. The `auth`, `createConnection`, `family`, `hints`, `insecureHTTPParser`, `joinDuplicateHeaders`, `localAddress`, `localPort`, `lookup` and `uniqueHeaders` options then throw a runtime error when they hold a value, and so do `setHost: false` and `setDefaultHeaders: false`.
|
||||
`server.listen(path)` and the `socketPath` option of `http.request()` and `https.request()` use Unix domain sockets on Linux and macOS.
|
||||
On Windows, where Node maps these paths to named pipes, they fail with `ENOTSUP`.
|
||||
`server.address()` on a server listening on a Unix socket returns the socket path, as in Node, when the result is typed `string | AddressInfo | null` (the @types/node declaration).
|
||||
Where it is typed as the `{ address, family, port }` record alone, which cannot hold the path, the call throws an error for such a server.
|
||||
`listen(path, backlog, callback)` is refused at compile time; pass `listen(path, callback)`.
|
||||
When the options of `http.request()` come from a variable, or from an object literal that spreads one, they are read at run time with Node's validation errors.
|
||||
The `auth`, `createConnection`, `family`, `hints`, `insecureHTTPParser`, `joinDuplicateHeaders`, `localAddress`, `localPort`, `lookup` and `uniqueHeaders` options then throw a runtime error when they hold a value, and so do `setHost: false` and `setDefaultHeaders: false`.
|
||||
|
||||
@@ -21,7 +21,13 @@
|
||||
* node scripts/limitations-partials.mjs split <file> [page] write the page and partials from one file
|
||||
* node scripts/limitations-partials.mjs merge <base> <theirs>
|
||||
* three-way merge a branch's single-file page (<theirs>, forked from
|
||||
* <base>) into the current partials, then split the result */
|
||||
* <base>) into the current partials, then split the result
|
||||
* node scripts/limitations-partials.mjs check fail on a partial line that holds two sentences
|
||||
* node scripts/limitations-partials.mjs fix put each such sentence on its own line
|
||||
*
|
||||
* One sentence per line keeps changes to different sentences on different
|
||||
* lines, so concurrent edits merge. Markdown joins the lines with a space,
|
||||
* so the rendered page is the same. */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -139,6 +145,41 @@ function write(text, page = defaultPage) {
|
||||
console.error(`${relative(docsRoot, page)}: ${partials.size} partials in ${relative(docsRoot, partialsDir)}/`);
|
||||
}
|
||||
|
||||
// A sentence ends at `.`, `!` or `?` (after any closing `)`, `"` or `*`)
|
||||
// followed by a space and a capital letter, a code span, a link or bold
|
||||
// text. Abbreviations don't end one.
|
||||
const sentenceEnd = /(?<!\b(?:e\.g|i\.e|etc|vs|cf))([.!?][)"*]*) (?=[A-Z`[*])/g;
|
||||
|
||||
/** The partial text with each sentence of a prose line on its own line;
|
||||
* a bullet's sentences continue with the two-space indent. Code fences,
|
||||
* headings, tables, HTML and include lines are left alone, as are code
|
||||
* spans and link targets. */
|
||||
export function splitSentences(text) {
|
||||
let inFence = false;
|
||||
const out = [];
|
||||
for (const line of text.split("\n")) {
|
||||
if (/^\s*(```|~~~)/.test(line)) inFence = !inFence;
|
||||
if (inFence || /^\s*(```|~~~|#|<|\||$)/.test(line)) {
|
||||
out.push(line);
|
||||
continue;
|
||||
}
|
||||
const indent = /^\s*(?:[-*]|\d+\.) /.test(line) ? " " : line.match(/^\s*/)[0];
|
||||
// Mask code spans (all but the opening backtick, which can start a
|
||||
// sentence) and link targets, so their periods never split.
|
||||
const masked = line.replace(/`[^`]*`|\]\([^)]*\)/g, (m) => m[0] + "\0".repeat(m.length - 1));
|
||||
const pieces = [];
|
||||
let start = 0;
|
||||
for (const match of masked.matchAll(sentenceEnd)) {
|
||||
const end = match.index + match[1].length;
|
||||
pieces.push(line.slice(start, end));
|
||||
start = end + 1;
|
||||
}
|
||||
pieces.push(line.slice(start));
|
||||
out.push(pieces[0], ...pieces.slice(1).map((piece) => indent + piece));
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
function listPartials(dir = partialsDir) {
|
||||
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) =>
|
||||
entry.isDirectory() ? listPartials(join(dir, entry.name)) : [join(dir, entry.name)],
|
||||
@@ -175,9 +216,20 @@ if (command === "join") {
|
||||
} finally {
|
||||
rmSync(work, { recursive: true, force: true });
|
||||
}
|
||||
} else if (command === "check" || command === "fix") {
|
||||
let unsplit = 0;
|
||||
for (const file of listPartials().sort()) {
|
||||
const text = readFileSync(file, "utf8");
|
||||
const split = splitSentences(text);
|
||||
if (split === text) continue;
|
||||
unsplit++;
|
||||
if (command === "fix") writeFileSync(file, split);
|
||||
else console.error(`${relative(docsRoot, file)}: put each sentence on its own line (node docs/scripts/limitations-partials.mjs fix)`);
|
||||
}
|
||||
if (command === "check" && unsplit > 0) process.exitCode = 1;
|
||||
} else if (command === "list") {
|
||||
for (const file of listPartials().sort()) console.log(relative(docsRoot, file));
|
||||
} else {
|
||||
console.error("usage: limitations-partials.mjs join [page] | split <file> [page] | merge <base> <theirs> | list");
|
||||
console.error("usage: limitations-partials.mjs join [page] | split <file> [page] | merge <base> <theirs> | check | fix | list");
|
||||
process.exitCode = 2;
|
||||
}
|
||||
|
||||
@@ -71,841 +71,337 @@
|
||||
"slug": "assert",
|
||||
"title": "Assertion testing",
|
||||
"entries": 29,
|
||||
"apiEntries": 29,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 23,
|
||||
"refused": 0,
|
||||
"not-implemented": 6,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 29
|
||||
},
|
||||
{
|
||||
"slug": "async_context",
|
||||
"title": "Asynchronous context tracking",
|
||||
"entries": 20,
|
||||
"apiEntries": 20,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 7,
|
||||
"refused": 0,
|
||||
"not-implemented": 13,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 20
|
||||
},
|
||||
{
|
||||
"slug": "async_hooks",
|
||||
"title": "Async hooks",
|
||||
"entries": 20,
|
||||
"apiEntries": 20,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 4,
|
||||
"refused": 0,
|
||||
"not-implemented": 16,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 20
|
||||
},
|
||||
{
|
||||
"slug": "buffer",
|
||||
"title": "Buffer",
|
||||
"entries": 99,
|
||||
"apiEntries": 99,
|
||||
"static": {
|
||||
"supported": 16,
|
||||
"partial": 7,
|
||||
"refused": 0,
|
||||
"not-implemented": 76,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 99
|
||||
},
|
||||
{
|
||||
"slug": "addons",
|
||||
"title": "C++ addons",
|
||||
"entries": 3,
|
||||
"apiEntries": 3,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 3,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 3
|
||||
},
|
||||
{
|
||||
"slug": "n-api",
|
||||
"title": "C/C++ addons with Node-API",
|
||||
"entries": 192,
|
||||
"apiEntries": 192,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 192,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 192
|
||||
},
|
||||
{
|
||||
"slug": "child_process",
|
||||
"title": "Child processes",
|
||||
"entries": 39,
|
||||
"apiEntries": 39,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 18,
|
||||
"refused": 0,
|
||||
"not-implemented": 21,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 39
|
||||
},
|
||||
{
|
||||
"slug": "cluster",
|
||||
"title": "Cluster",
|
||||
"entries": 34,
|
||||
"apiEntries": 34,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 4,
|
||||
"refused": 0,
|
||||
"not-implemented": 30,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 34
|
||||
},
|
||||
{
|
||||
"slug": "console",
|
||||
"title": "Console",
|
||||
"entries": 24,
|
||||
"apiEntries": 24,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 23,
|
||||
"refused": 0,
|
||||
"not-implemented": 1,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 24
|
||||
},
|
||||
{
|
||||
"slug": "crypto",
|
||||
"title": "Crypto",
|
||||
"entries": 152,
|
||||
"apiEntries": 152,
|
||||
"static": {
|
||||
"supported": 6,
|
||||
"partial": 47,
|
||||
"refused": 20,
|
||||
"not-implemented": 79,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 152
|
||||
},
|
||||
{
|
||||
"slug": "diagnostics_channel",
|
||||
"title": "Diagnostics Channel",
|
||||
"entries": 71,
|
||||
"apiEntries": 71,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 8,
|
||||
"refused": 0,
|
||||
"not-implemented": 63,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 71
|
||||
},
|
||||
{
|
||||
"slug": "dns",
|
||||
"title": "DNS",
|
||||
"entries": 51,
|
||||
"apiEntries": 51,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 10,
|
||||
"refused": 0,
|
||||
"not-implemented": 41,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 51
|
||||
},
|
||||
{
|
||||
"slug": "domain",
|
||||
"title": "Domain",
|
||||
"entries": 12,
|
||||
"apiEntries": 12,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 12,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 12
|
||||
},
|
||||
{
|
||||
"slug": "environment_variables",
|
||||
"title": "Environment Variables",
|
||||
"entries": 2,
|
||||
"apiEntries": 2,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 2,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 2
|
||||
},
|
||||
{
|
||||
"slug": "errors",
|
||||
"title": "Errors",
|
||||
"entries": 478,
|
||||
"apiEntries": 478,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 10,
|
||||
"refused": 0,
|
||||
"not-implemented": 468,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 478
|
||||
},
|
||||
{
|
||||
"slug": "events",
|
||||
"title": "Events",
|
||||
"entries": 78,
|
||||
"apiEntries": 78,
|
||||
"static": {
|
||||
"supported": 18,
|
||||
"partial": 35,
|
||||
"refused": 0,
|
||||
"not-implemented": 25,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 78
|
||||
},
|
||||
{
|
||||
"slug": "fs",
|
||||
"title": "File system",
|
||||
"entries": 280,
|
||||
"apiEntries": 280,
|
||||
"static": {
|
||||
"supported": 7,
|
||||
"partial": 200,
|
||||
"refused": 0,
|
||||
"not-implemented": 73,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 280
|
||||
},
|
||||
{
|
||||
"slug": "globals",
|
||||
"title": "Globals",
|
||||
"entries": 92,
|
||||
"apiEntries": 92,
|
||||
"static": {
|
||||
"supported": 1,
|
||||
"partial": 16,
|
||||
"refused": 0,
|
||||
"not-implemented": 75,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 92
|
||||
},
|
||||
{
|
||||
"slug": "http",
|
||||
"title": "HTTP",
|
||||
"entries": 176,
|
||||
"apiEntries": 176,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 131,
|
||||
"refused": 0,
|
||||
"not-implemented": 45,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 176
|
||||
},
|
||||
{
|
||||
"slug": "http2",
|
||||
"title": "HTTP/2",
|
||||
"entries": 168,
|
||||
"apiEntries": 168,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 14,
|
||||
"refused": 2,
|
||||
"not-implemented": 152,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 168
|
||||
},
|
||||
{
|
||||
"slug": "https",
|
||||
"title": "HTTPS",
|
||||
"entries": 20,
|
||||
"apiEntries": 20,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 8,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 20
|
||||
},
|
||||
{
|
||||
"slug": "inspector",
|
||||
"title": "Inspector",
|
||||
"entries": 35,
|
||||
"apiEntries": 35,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 35,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 35
|
||||
},
|
||||
{
|
||||
"slug": "stream_iter",
|
||||
"title": "Iterable Streams API",
|
||||
"entries": 65,
|
||||
"apiEntries": 65,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 65,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 65
|
||||
},
|
||||
{
|
||||
"slug": "modules",
|
||||
"title": "Modules: CommonJS modules",
|
||||
"entries": 27,
|
||||
"apiEntries": 27,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 24,
|
||||
"refused": 0,
|
||||
"not-implemented": 1,
|
||||
"by-design": 0,
|
||||
"unreviewed": 2,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 27
|
||||
},
|
||||
{
|
||||
"slug": "esm",
|
||||
"title": "Modules: ECMAScript modules",
|
||||
"entries": 22,
|
||||
"apiEntries": 22,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 13,
|
||||
"refused": 0,
|
||||
"not-implemented": 9,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 22
|
||||
},
|
||||
{
|
||||
"slug": "module",
|
||||
"title": "Modules: node:module API",
|
||||
"entries": 30,
|
||||
"apiEntries": 30,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 5,
|
||||
"refused": 0,
|
||||
"not-implemented": 25,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 30
|
||||
},
|
||||
{
|
||||
"slug": "packages",
|
||||
"title": "Modules: Packages",
|
||||
"entries": 8,
|
||||
"apiEntries": 8,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 8,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 8
|
||||
},
|
||||
{
|
||||
"slug": "typescript",
|
||||
"title": "Modules: TypeScript",
|
||||
"entries": 2,
|
||||
"apiEntries": 2,
|
||||
"static": {
|
||||
"supported": 2,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 2
|
||||
},
|
||||
{
|
||||
"slug": "net",
|
||||
"title": "Net",
|
||||
"entries": 119,
|
||||
"apiEntries": 119,
|
||||
"static": {
|
||||
"supported": 16,
|
||||
"partial": 39,
|
||||
"refused": 0,
|
||||
"not-implemented": 64,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 119
|
||||
},
|
||||
{
|
||||
"slug": "os",
|
||||
"title": "OS",
|
||||
"entries": 24,
|
||||
"apiEntries": 24,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 24,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 24
|
||||
},
|
||||
{
|
||||
"slug": "path",
|
||||
"title": "Path",
|
||||
"entries": 17,
|
||||
"apiEntries": 17,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 17,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 17
|
||||
},
|
||||
{
|
||||
"slug": "perf_hooks",
|
||||
"title": "Performance hooks",
|
||||
"entries": 131,
|
||||
"apiEntries": 131,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 25,
|
||||
"refused": 0,
|
||||
"not-implemented": 106,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 131
|
||||
},
|
||||
{
|
||||
"slug": "permissions",
|
||||
"title": "Permissions",
|
||||
"entries": 4,
|
||||
"apiEntries": 4,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 4,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 4
|
||||
},
|
||||
{
|
||||
"slug": "process",
|
||||
"title": "Process",
|
||||
"entries": 120,
|
||||
"apiEntries": 120,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 51,
|
||||
"refused": 0,
|
||||
"not-implemented": 69,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 120
|
||||
},
|
||||
{
|
||||
"slug": "punycode",
|
||||
"title": "Punycode",
|
||||
"entries": 9,
|
||||
"apiEntries": 9,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 8,
|
||||
"refused": 0,
|
||||
"not-implemented": 1,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 9
|
||||
},
|
||||
{
|
||||
"slug": "querystring",
|
||||
"title": "Query strings",
|
||||
"entries": 7,
|
||||
"apiEntries": 7,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 7,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 7
|
||||
},
|
||||
{
|
||||
"slug": "readline",
|
||||
"title": "Readline",
|
||||
"entries": 43,
|
||||
"apiEntries": 43,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 42,
|
||||
"refused": 0,
|
||||
"not-implemented": 1,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 43
|
||||
},
|
||||
{
|
||||
"slug": "repl",
|
||||
"title": "REPL",
|
||||
"entries": 14,
|
||||
"apiEntries": 14,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 14,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 14
|
||||
},
|
||||
{
|
||||
"slug": "sqlite",
|
||||
"title": "SQLite",
|
||||
"entries": 52,
|
||||
"apiEntries": 52,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 52,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 52
|
||||
},
|
||||
{
|
||||
"slug": "stream",
|
||||
"title": "Stream",
|
||||
"entries": 140,
|
||||
"apiEntries": 140,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 53,
|
||||
"refused": 0,
|
||||
"not-implemented": 87,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 140
|
||||
},
|
||||
{
|
||||
"slug": "string_decoder",
|
||||
"title": "String decoder",
|
||||
"entries": 4,
|
||||
"apiEntries": 4,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 4,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 4
|
||||
},
|
||||
{
|
||||
"slug": "test",
|
||||
"title": "Test runner",
|
||||
"entries": 117,
|
||||
"apiEntries": 117,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 65,
|
||||
"refused": 0,
|
||||
"not-implemented": 52,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 117
|
||||
},
|
||||
{
|
||||
"slug": "timers",
|
||||
"title": "Timers",
|
||||
"entries": 25,
|
||||
"apiEntries": 25,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 22,
|
||||
"refused": 0,
|
||||
"not-implemented": 3,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 25
|
||||
},
|
||||
{
|
||||
"slug": "tls",
|
||||
"title": "TLS/SSL",
|
||||
"entries": 67,
|
||||
"apiEntries": 67,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 9,
|
||||
"refused": 4,
|
||||
"not-implemented": 54,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 67
|
||||
},
|
||||
{
|
||||
"slug": "tracing",
|
||||
"title": "Trace events",
|
||||
"entries": 9,
|
||||
"apiEntries": 9,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 9,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 9
|
||||
},
|
||||
{
|
||||
"slug": "tty",
|
||||
"title": "TTY",
|
||||
"entries": 19,
|
||||
"apiEntries": 19,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 7,
|
||||
"refused": 0,
|
||||
"not-implemented": 12,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 19
|
||||
},
|
||||
{
|
||||
"slug": "dgram",
|
||||
"title": "UDP/datagram",
|
||||
"entries": 38,
|
||||
"apiEntries": 38,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 12,
|
||||
"refused": 0,
|
||||
"not-implemented": 26,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 38
|
||||
},
|
||||
{
|
||||
"slug": "url",
|
||||
"title": "URL",
|
||||
"entries": 62,
|
||||
"apiEntries": 62,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 42,
|
||||
"refused": 0,
|
||||
"not-implemented": 20,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 62
|
||||
},
|
||||
{
|
||||
"slug": "util",
|
||||
"title": "Utilities",
|
||||
"entries": 109,
|
||||
"apiEntries": 109,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 72,
|
||||
"refused": 0,
|
||||
"not-implemented": 37,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 109
|
||||
},
|
||||
{
|
||||
"slug": "v8",
|
||||
"title": "V8",
|
||||
"entries": 68,
|
||||
"apiEntries": 68,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 4,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 64,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 68
|
||||
},
|
||||
{
|
||||
"slug": "vm",
|
||||
"title": "VM",
|
||||
"entries": 41,
|
||||
"apiEntries": 41,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 41,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 41
|
||||
},
|
||||
{
|
||||
"slug": "wasi",
|
||||
"title": "WASI",
|
||||
"entries": 7,
|
||||
"apiEntries": 7,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 7,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 7
|
||||
},
|
||||
{
|
||||
"slug": "webcrypto",
|
||||
"title": "Web Crypto API",
|
||||
"entries": 156,
|
||||
"apiEntries": 156,
|
||||
"static": {
|
||||
"supported": 2,
|
||||
"partial": 21,
|
||||
"refused": 0,
|
||||
"not-implemented": 133,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 156
|
||||
},
|
||||
{
|
||||
"slug": "webstreams",
|
||||
"title": "Web Streams API",
|
||||
"entries": 89,
|
||||
"apiEntries": 89,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 8,
|
||||
"refused": 1,
|
||||
"not-implemented": 80,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 89
|
||||
},
|
||||
{
|
||||
"slug": "worker_threads",
|
||||
"title": "Worker threads",
|
||||
"entries": 66,
|
||||
"apiEntries": 66,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 46,
|
||||
"refused": 0,
|
||||
"not-implemented": 20,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 66
|
||||
},
|
||||
{
|
||||
"slug": "zlib",
|
||||
"title": "Zlib",
|
||||
"entries": 94,
|
||||
"apiEntries": 94,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 52,
|
||||
"refused": 12,
|
||||
"not-implemented": 30,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
"apiEntries": 94
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
|
||||
@@ -5,14 +5,14 @@ This workspace package owns scriptc's Node.js parity system.
|
||||
- `node-v24.json` pins the Node release tag, commit, and canonical docs/source inputs.
|
||||
- `static-support.json` overlays dedicated compiler paths and their internal test evidence on the compiler's generated surface manifest.
|
||||
- `generated/node-v24-internal.json` is the engineering ledger, including implementation evidence and repository test paths.
|
||||
- `generated/node-v24-backlog.json` is the internal tier-specific work queue. It separates verification, implementation, explicit-refusal replacement, and partial-surface audit work, and prioritizes stable Node APIs above experimental, deprecated, and legacy APIs.
|
||||
- `pnpm node-compat:backlog` derives the internal tier-specific work queue from the ledger; it is not committed. It separates verification, implementation, explicit-refusal replacement, and partial-surface audit work, and prioritizes stable Node APIs above experimental, deprecated, and legacy APIs.
|
||||
- `docs/src/generated/node-v24-compatibility.json` is the stripped public artifact consumed by the docs website. It contains compiler-relevant runtime APIs, statuses, verification bases, and user-facing details; documentation, metadata, command-line/configuration entries, and rows that are N/A for native execution remain internal.
|
||||
|
||||
Statuses are evidence-aware: `supported` and `partial` require test evidence; `refused` requires an explicit compiler diagnostic or native throwing stub; `not-implemented` is the actionable backlog for an owned API family with no implementation match; `by-design` records an architectural exclusion; `unreviewed` still needs classification; and `not-applicable` is not a runtime API for that execution tier.
|
||||
|
||||
Run `pnpm node-compat:backlog` from the repository root for a summary. The filters `--tier`, `--action`, `--priority`, `--chapter`, and `--status` compose; use `--format=json` or `--format=tsv` for tooling.
|
||||
|
||||
Run `pnpm node-compat` from the repository root after changing the Node pin or the support manifest. `pnpm node-compat:check` is the offline drift/evidence gate; `pnpm --filter @internal/compatibility check:upstream` additionally verifies the pinned upstream inputs over the network.
|
||||
Run `pnpm node-compat` from the repository root after changing the Node pin or the support manifest. `pnpm node-compat --local` does the same without network access while the pin is unchanged, by reclassifying the committed ledger rows; after a merge, `node scripts/regen-generated.mjs` regenerates every generated file instead of a hand merge. `pnpm node-compat:check` is the offline drift/evidence gate; `pnpm --filter @internal/compatibility check:upstream` additionally verifies the pinned upstream inputs over the network.
|
||||
|
||||
## npm packages that compile as builtins
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,12 @@
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { buildBacklog } from "./generate.mjs";
|
||||
|
||||
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
||||
const backlog = JSON.parse(readFileSync(`${packageRoot}generated/node-v24-backlog.json`, "utf8"));
|
||||
const backlog = buildBacklog(
|
||||
JSON.parse(readFileSync(`${packageRoot}generated/node-v24-internal.json`, "utf8")),
|
||||
);
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
function option(name) {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
*
|
||||
* pnpm node-compat fetch latest-v24.x and rewrite the snapshot
|
||||
* pnpm node-compat --check fetch and fail if the snapshot would change
|
||||
* pnpm node-compat --local rewrite the snapshot without fetching: reclassify
|
||||
* the committed rows from the current sources
|
||||
* pnpm node-compat:check offline implementation/profile drift check
|
||||
*/
|
||||
|
||||
@@ -22,12 +24,15 @@ import { fileURLToPath } from "node:url";
|
||||
const packageRoot = fileURLToPath(new URL("../", import.meta.url));
|
||||
const root = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const internalOutputPath = `${packageRoot}generated/node-v24-internal.json`;
|
||||
const backlogOutputPath = `${packageRoot}generated/node-v24-backlog.json`;
|
||||
const publicOutputPath = `${root}docs/src/generated/node-v24-compatibility.json`;
|
||||
// The docs build derives this file's values from the public artifact; a
|
||||
// merge that keeps a branch's copy would bring back a file every
|
||||
// compatibility change rewrites.
|
||||
const retiredOutputPaths = [`${root}docs/src/generated/node-v24-compatibility-meta.json`];
|
||||
// Files derived from the ledger elsewhere: the docs build computes the
|
||||
// metadata from the public artifact, and `pnpm node-compat:backlog` builds
|
||||
// the backlog in memory. A merge that keeps a branch's copy would bring
|
||||
// back a file every compatibility change rewrites.
|
||||
const retiredOutputPaths = [
|
||||
`${root}docs/src/generated/node-v24-compatibility-meta.json`,
|
||||
`${packageRoot}generated/node-v24-backlog.json`,
|
||||
];
|
||||
const compilerManifestPath = `${root}packages/compiler/surface-manifest.json`;
|
||||
const compilerCompatPath = `${packageRoot}static-support.json`;
|
||||
const pinPath = `${packageRoot}node-v24.json`;
|
||||
@@ -447,12 +452,6 @@ function summarizeTier(rows, key, chapterPolicy) {
|
||||
return tier("unreviewed", "", { source: "derived:descendants" });
|
||||
}
|
||||
|
||||
function statusCounts(rows, key) {
|
||||
const counts = Object.fromEntries(STATUSES.map((status) => [status, 0]));
|
||||
for (const row of rows) if (row.scope === "api") counts[row[key].status] += 1;
|
||||
return counts;
|
||||
}
|
||||
|
||||
function verifiedAnchorIds(html) {
|
||||
return new Set(
|
||||
[...html.matchAll(/<a class="mark" href="#[^"]+" id="([^"]+)">#/g)].map((match) => match[1]),
|
||||
@@ -524,7 +523,6 @@ function buildSnapshot({ allJson, indexMarkdown, allSha256, indexSha256 }) {
|
||||
title: link.title,
|
||||
entries: chapterRows.length,
|
||||
apiEntries: chapterRows.filter((row) => row.scope === "api").length,
|
||||
static: statusCounts(chapterRows, "static"),
|
||||
});
|
||||
rows.push(...chapterRows);
|
||||
}
|
||||
@@ -873,7 +871,7 @@ function backlogAction(tier) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildBacklog(snapshot) {
|
||||
export function buildBacklog(snapshot) {
|
||||
const tasks = [];
|
||||
const tierItems = { static: {} };
|
||||
const priorities = { high: 0, normal: 0, low: 0 };
|
||||
@@ -969,7 +967,6 @@ function projectPublicMatrix(snapshot) {
|
||||
title: chapter.title,
|
||||
entries: chapterRows.length,
|
||||
apiEntries: chapterRows.length,
|
||||
static: statusCounts(chapterRows, "static"),
|
||||
});
|
||||
rows.push(...chapterRows);
|
||||
}
|
||||
@@ -1018,6 +1015,34 @@ function publicSnapshot(snapshot) {
|
||||
};
|
||||
}
|
||||
|
||||
/** The committed ledger with every row's classification recomputed from
|
||||
* the current sources. The rows themselves come from the pinned Node docs,
|
||||
* so while the pin is unchanged this is what a fetching run writes. */
|
||||
function reclassify(snapshot) {
|
||||
if (snapshot.nodeVersion !== pin.version || snapshot.nodeCommit !== pin.commit) {
|
||||
throw new Error(
|
||||
`the ledger targets Node ${snapshot.nodeVersion}, the pin ${pin.version}; run 'pnpm node-compat' to fetch the new docs`,
|
||||
);
|
||||
}
|
||||
const ctx = classificationContext();
|
||||
const ancestors = [];
|
||||
for (const row of snapshot.rows) {
|
||||
ancestors.length = row.depth;
|
||||
const parentSignature = ancestors[row.depth - 1]?.signature;
|
||||
row.static = classifyStatic(row, null, ctx, parentSignature);
|
||||
ancestors[row.depth] = row;
|
||||
}
|
||||
for (const chapter of snapshot.chapters) {
|
||||
const chapterRows = snapshot.rows.filter((row) => row.chapter === chapter.slug);
|
||||
chapterRows[0].static = summarizeTier(
|
||||
chapterRows,
|
||||
"static",
|
||||
ctx.compilerCompat.chapterPolicies[chapter.slug],
|
||||
);
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function checkLocal() {
|
||||
for (const path of retiredOutputPaths) {
|
||||
if (existsSync(path))
|
||||
@@ -1070,9 +1095,6 @@ function checkLocal() {
|
||||
throw new Error(`${row.id} exposes a row that is not applicable to native compilation`);
|
||||
}
|
||||
}
|
||||
if (readFileSync(backlogOutputPath, "utf8") !== render(buildBacklog(snapshot))) {
|
||||
throw new Error("the Node compatibility backlog is stale; run 'pnpm node-compat'");
|
||||
}
|
||||
if (readFileSync(publicOutputPath, "utf8") !== render(publicSnapshot(snapshot))) {
|
||||
throw new Error("the docs compatibility artifact is stale; run 'pnpm node-compat'");
|
||||
}
|
||||
@@ -1117,6 +1139,15 @@ async function fetchText(url) {
|
||||
async function main() {
|
||||
const args = new Set(process.argv.slice(2));
|
||||
if (args.has("--check-local")) return checkLocal();
|
||||
if (args.has("--local")) {
|
||||
const snapshot = reclassify(JSON.parse(readFileSync(internalOutputPath, "utf8")));
|
||||
writeFileSync(internalOutputPath, render(snapshot));
|
||||
writeFileSync(publicOutputPath, render(publicSnapshot(snapshot)));
|
||||
console.log(
|
||||
`reclassified the compatibility ledger and public artifact (${snapshot.rows.length} source rows, ${projectPublicMatrix(snapshot).rows.length} public matrix rows, ${pin.tag})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const [allText, indexMarkdown] = await Promise.all([
|
||||
fetchText(pin.allJsonUrl),
|
||||
@@ -1138,14 +1169,11 @@ async function main() {
|
||||
);
|
||||
verifyAnchors(snapshot, htmlByChapter);
|
||||
const internalRendered = render(snapshot);
|
||||
const backlogRendered = render(buildBacklog(snapshot));
|
||||
const publicRendered = render(publicSnapshot(snapshot));
|
||||
|
||||
if (args.has("--check")) {
|
||||
if (readFileSync(internalOutputPath, "utf8") !== internalRendered)
|
||||
throw new Error("internal Node compatibility snapshot is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(backlogOutputPath, "utf8") !== backlogRendered)
|
||||
throw new Error("Node compatibility backlog is stale; run 'pnpm node-compat'");
|
||||
if (readFileSync(publicOutputPath, "utf8") !== publicRendered)
|
||||
throw new Error("public Node compatibility snapshot is stale; run 'pnpm node-compat'");
|
||||
console.log(
|
||||
@@ -1154,11 +1182,11 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
writeFileSync(internalOutputPath, internalRendered);
|
||||
writeFileSync(backlogOutputPath, backlogRendered);
|
||||
writeFileSync(publicOutputPath, publicRendered);
|
||||
console.log(
|
||||
`wrote compatibility ledger, backlog, and public artifact (${snapshot.rows.length} source rows, ${projectPublicMatrix(snapshot).rows.length} public matrix rows, ${pin.tag})`,
|
||||
`wrote compatibility ledger and public artifact (${snapshot.rows.length} source rows, ${projectPublicMatrix(snapshot).rows.length} public matrix rows, ${pin.tag})`,
|
||||
);
|
||||
}
|
||||
|
||||
await main();
|
||||
// backlog.mjs imports buildBacklog; only running this file generates.
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) await main();
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const check = process.argv.slice(2).includes("--check");
|
||||
const args = check ? ["--check"] : [];
|
||||
const args = process.argv.slice(2).filter((arg) => arg === "--check" || arg === "--local");
|
||||
|
||||
for (const script of ["generate.mjs"]) {
|
||||
const result = spawnSync(process.execPath, [`src/${script}`, ...args], { stdio: "inherit" });
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
"test:sandbox": "node scripts/sandbox-test.mjs",
|
||||
"test:sandbox:image": "node scripts/sandbox-image.mjs",
|
||||
"test:watch": "vitest",
|
||||
"lint": "oxlint --deny-warnings packages scripts internal/compatibility/src tests/harness vitest.config.ts",
|
||||
"lint": "node docs/scripts/limitations-partials.mjs check && oxlint --deny-warnings packages scripts internal/compatibility/src tests/harness vitest.config.ts",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"format": "oxfmt --write \"{packages,scripts,internal/compatibility/src,tests/harness}/**/*.{js,mjs,cjs,ts,mts,cts,tsx}\" vitest.config.ts .oxlintrc.jsonc .oxfmtrc.jsonc",
|
||||
"format:check": "oxfmt --check \"{packages,scripts,internal/compatibility/src,tests/harness}/**/*.{js,mjs,cjs,ts,mts,cts,tsx}\" vitest.config.ts .oxlintrc.jsonc .oxfmtrc.jsonc",
|
||||
|
||||
@@ -328,15 +328,18 @@ export class Client extends DispatcherBase {
|
||||
}
|
||||
|
||||
__onTimeout(): void {
|
||||
this.__timerCancel = null;
|
||||
this.__timerRefresh = null;
|
||||
const socket = this.__socket;
|
||||
const parser = this.__parser;
|
||||
// A body timeout that fires while the response is paused keeps its
|
||||
// timer: resuming refreshes it, as undici's parser does.
|
||||
if (this.__timerType === TIMEOUT_BODY && parser !== null && parser.paused) return;
|
||||
this.__timerCancel = null;
|
||||
this.__timerRefresh = null;
|
||||
if (socket === null || parser === null) return;
|
||||
if (this.__timerType === TIMEOUT_HEADERS) {
|
||||
this.__destroySocket(socket, new HeadersTimeoutError());
|
||||
} else if (this.__timerType === TIMEOUT_BODY) {
|
||||
if (!parser.paused) this.__destroySocket(socket, new BodyTimeoutError());
|
||||
this.__destroySocket(socket, new BodyTimeoutError());
|
||||
} else if (this.__timerType === TIMEOUT_IDLE) {
|
||||
this.__destroySocket(socket, new InformationalError("socket idle timeout"));
|
||||
}
|
||||
@@ -782,14 +785,50 @@ function errorCode(err: Error): string {
|
||||
|
||||
const CRLF = Buffer.from("\r\n");
|
||||
|
||||
/** The value of an ASCII hex digit, or -1. */
|
||||
function hexValue(c: number): number {
|
||||
// Chunk-size line states (llhttp's chunk_size* and chunk_extension* nodes).
|
||||
const SIZE_FIRST = 0;
|
||||
const SIZE_DIGITS = 1;
|
||||
const SIZE_AFTER = 2;
|
||||
const SIZE_EXT = 3;
|
||||
const SIZE_EXT_NAME = 4;
|
||||
const SIZE_EXT_VALUE = 5;
|
||||
const SIZE_EXT_QUOTED = 6;
|
||||
const SIZE_EXT_PAIR = 7;
|
||||
const SIZE_EXT_QUOTED_DONE = 8;
|
||||
const SIZE_CR = 9;
|
||||
|
||||
function hexDigit(c: number): number {
|
||||
if (c >= 0x30 && c <= 0x39) return c - 0x30;
|
||||
if (c >= 0x41 && c <= 0x46) return c - 0x37;
|
||||
if (c >= 0x61 && c <= 0x66) return c - 0x57;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** tchar: ALPHA, DIGIT and !#$%&'*+-.^_`|~. */
|
||||
const TOKEN_BYTES = tokenBytes();
|
||||
|
||||
function tokenBytes(): Uint8Array {
|
||||
const table = new Uint8Array(256);
|
||||
const marks = "!#$%&'*+-.^_`|~";
|
||||
for (let i = 0; i < marks.length; i++) table[marks.charCodeAt(i)] = 1;
|
||||
for (let c = 0x30; c <= 0x39; c++) table[c] = 1;
|
||||
for (let c = 0x41; c <= 0x5a; c++) {
|
||||
table[c] = 1;
|
||||
table[c + 0x20] = 1;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
/** qdtext: HTAB, SP and 0x21-0xff but '"' and '\\'. */
|
||||
function isQuotedByte(c: number): boolean {
|
||||
return c === 0x09 || c === 0x20 || (c >= 0x21 && c !== 0x22 && c !== 0x5c);
|
||||
}
|
||||
|
||||
/** The byte after '\\' in a quoted-pair: HTAB, SP, VCHAR or obs-text. */
|
||||
function isQuotedPairByte(c: number): boolean {
|
||||
return c === 0x09 || c === 0x20 || (c >= 0x21 && c !== 0x7f);
|
||||
}
|
||||
|
||||
/** An incremental HTTP/1.1 response parser (the subset of llhttp undici
|
||||
* relies on): status line, raw header pairs, Content-Length, chunked and
|
||||
* read-until-close bodies, trailers, and pause/resume back-pressure. */
|
||||
@@ -816,14 +855,9 @@ class ResponseParser {
|
||||
bytesRead: number;
|
||||
trailers: Buffer[];
|
||||
skipBody: boolean;
|
||||
/** The chunk-size line read so far: its value, its digit count, whether
|
||||
* an extension or whitespace follows the digits, and whether its CR was
|
||||
* read. Extensions are skipped as they arrive, so the line keeps no
|
||||
* bytes. */
|
||||
sizeState: number;
|
||||
sizeDigits: number;
|
||||
chunkSize: number;
|
||||
chunkDigits: number;
|
||||
chunkExtension: boolean;
|
||||
chunkCR: boolean;
|
||||
|
||||
constructor(client: Client, socket: Socket) {
|
||||
this.client = client;
|
||||
@@ -846,10 +880,9 @@ class ResponseParser {
|
||||
this.bytesRead = 0;
|
||||
this.trailers = [];
|
||||
this.skipBody = false;
|
||||
this.sizeState = SIZE_FIRST;
|
||||
this.sizeDigits = 0;
|
||||
this.chunkSize = 0;
|
||||
this.chunkDigits = 0;
|
||||
this.chunkExtension = false;
|
||||
this.chunkCR = false;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
@@ -886,6 +919,7 @@ class ResponseParser {
|
||||
|
||||
resume(): void {
|
||||
if (!this.paused) return;
|
||||
if (this.client.__timerType === TIMEOUT_BODY) this.client.__refreshTimer();
|
||||
this.paused = false;
|
||||
this.socket.resume();
|
||||
this.run();
|
||||
@@ -903,18 +937,154 @@ class ResponseParser {
|
||||
}
|
||||
}
|
||||
|
||||
fail(message: string, code: string): void {
|
||||
// undici 7.x wraps llhttp's reason and leaves the code unset (its
|
||||
// createError indexes the error-name table with the error number);
|
||||
// 5.x reports the reason and its HPE_ code.
|
||||
const error =
|
||||
/** A parse error with llhttp's reason: undici 7 puts it inside its
|
||||
* protocol message and, as its error table maps names to numbers only,
|
||||
* sets no code; undici 5 uses the reason as the message and the code. */
|
||||
fail(reason: string, code: string): void {
|
||||
const err =
|
||||
this.client.__profile.major >= 7
|
||||
? new HTTPParserError(`Response does not match the HTTP/1.1 protocol (${message})`)
|
||||
: new HTTPParserError(message, code, "");
|
||||
this.client.__onParseError(this.socket, error);
|
||||
? new HTTPParserError(`Response does not match the HTTP/1.1 protocol (${reason})`)
|
||||
: new HTTPParserError(reason, code, "");
|
||||
this.client.__onParseError(this.socket, err);
|
||||
this.state = 99;
|
||||
}
|
||||
|
||||
/** Reads the chunk-size line one byte at a time as llhttp does, so a line
|
||||
* that never ends holds no memory: hex digits (past 16 significant ones
|
||||
* the size overflows), then extensions, checked and dropped, then CRLF.
|
||||
* The 7.x profile follows llhttp 9 (undici 7), the 5.x profile llhttp 8
|
||||
* in its lenient mode (undici 5). Returns true when the line is complete
|
||||
* and chunkSize holds its size; false when it needs more bytes or failed. */
|
||||
chunkSizeLine(): boolean {
|
||||
const buf = this.buf;
|
||||
if (this.sizeState === SIZE_FIRST) {
|
||||
// The common line, hex digits then CRLF in this buffer, in one pass;
|
||||
// any other line takes the byte loop below from its start.
|
||||
let size = 0;
|
||||
let digits = 0;
|
||||
let j = 0;
|
||||
for (; j < buf.length && j < 32; j++) {
|
||||
const d = hexDigit(buf[j]!);
|
||||
if (d === -1) break;
|
||||
if (digits > 0 || d !== 0) {
|
||||
digits++;
|
||||
size = size * 16 + d;
|
||||
}
|
||||
}
|
||||
if (j > 0 && digits <= 16 && j + 1 < buf.length && buf[j] === 0x0d && buf[j + 1] === 0x0a) {
|
||||
this.chunkSize = size;
|
||||
this.buf = buf.subarray(j + 2);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const v7 = this.client.__profile.major >= 7;
|
||||
let i = 0;
|
||||
while (i < buf.length) {
|
||||
const c = buf[i]!;
|
||||
const state = this.sizeState;
|
||||
if (state === SIZE_FIRST || state === SIZE_DIGITS) {
|
||||
const d = hexDigit(c);
|
||||
if (d === -1) {
|
||||
if (state === SIZE_FIRST) {
|
||||
this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE");
|
||||
return false;
|
||||
}
|
||||
this.sizeState = SIZE_AFTER;
|
||||
continue;
|
||||
}
|
||||
if (state === SIZE_FIRST) {
|
||||
this.chunkSize = 0;
|
||||
this.sizeDigits = 0;
|
||||
this.sizeState = SIZE_DIGITS;
|
||||
}
|
||||
if (this.sizeDigits > 0 || d !== 0) {
|
||||
if (this.sizeDigits === 16) {
|
||||
this.fail("Chunk size overflow", "INVALID_CHUNK_SIZE");
|
||||
return false;
|
||||
}
|
||||
this.sizeDigits++;
|
||||
this.chunkSize = this.chunkSize * 16 + d;
|
||||
}
|
||||
} else if (state === SIZE_AFTER) {
|
||||
if (c === 0x0d) this.sizeState = SIZE_CR;
|
||||
else if (c === 0x3b) this.sizeState = SIZE_EXT;
|
||||
else if (c === 0x0a && v7) {
|
||||
this.fail("Missing expected CR after chunk size", "CR_EXPECTED");
|
||||
return false;
|
||||
} else {
|
||||
this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE");
|
||||
return false;
|
||||
}
|
||||
} else if (state === SIZE_EXT) {
|
||||
if (c === 0x20 || c === 0x0d) {
|
||||
this.fail("Invalid character in chunk extensions", "STRICT");
|
||||
return false;
|
||||
}
|
||||
this.sizeState = SIZE_EXT_NAME;
|
||||
continue;
|
||||
} else if (state === SIZE_EXT_NAME) {
|
||||
if (c === 0x3d) this.sizeState = SIZE_EXT_VALUE;
|
||||
else if (c === 0x3b) this.sizeState = SIZE_EXT;
|
||||
else if (c === 0x0d) this.sizeState = SIZE_CR;
|
||||
else if (c === 0x0a && v7) {
|
||||
this.fail("Missing expected CR after chunk extension name", "CR_EXPECTED");
|
||||
return false;
|
||||
} else if (TOKEN_BYTES[c] !== 1) {
|
||||
this.fail("Invalid character in chunk extensions name", "STRICT");
|
||||
return false;
|
||||
}
|
||||
} else if (state === SIZE_EXT_VALUE) {
|
||||
if (c === 0x22) this.sizeState = SIZE_EXT_QUOTED;
|
||||
// llhttp 8 goes on with the chunk-size node after a value's ';'.
|
||||
else if (c === 0x3b) this.sizeState = v7 ? SIZE_EXT : SIZE_AFTER;
|
||||
else if (c === 0x0d) this.sizeState = SIZE_CR;
|
||||
else if (c === 0x0a && v7) {
|
||||
this.fail("Missing expected CR after chunk extension value", "CR_EXPECTED");
|
||||
return false;
|
||||
} else if (TOKEN_BYTES[c] !== 1) {
|
||||
this.fail("Invalid character in chunk extensions value", "STRICT");
|
||||
return false;
|
||||
}
|
||||
} else if (state === SIZE_EXT_QUOTED) {
|
||||
if (c === 0x22) this.sizeState = SIZE_EXT_QUOTED_DONE;
|
||||
else if (c === 0x5c && v7) this.sizeState = SIZE_EXT_PAIR;
|
||||
else if (!isQuotedByte(c)) {
|
||||
this.fail("Invalid character in chunk extensions quoted value", "STRICT");
|
||||
return false;
|
||||
}
|
||||
} else if (state === SIZE_EXT_PAIR) {
|
||||
if (!isQuotedPairByte(c)) {
|
||||
this.fail("Invalid quoted-pair in chunk extensions quoted value", "STRICT");
|
||||
return false;
|
||||
}
|
||||
this.sizeState = SIZE_EXT_QUOTED;
|
||||
} else if (state === SIZE_EXT_QUOTED_DONE) {
|
||||
if (c === 0x3b) this.sizeState = SIZE_EXT;
|
||||
else if (c === 0x0d) this.sizeState = SIZE_CR;
|
||||
else if (c === 0x0a && v7) {
|
||||
this.fail("Missing expected CR after chunk extension value", "CR_EXPECTED");
|
||||
return false;
|
||||
} else {
|
||||
this.fail("Invalid character in chunk extensions quote value", "STRICT");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// SIZE_CR: llhttp 9 needs the LF; llhttp 8's lenient mode skips
|
||||
// whatever byte follows the CR.
|
||||
if (c !== 0x0a && v7) {
|
||||
this.fail("Expected LF after chunk size", "STRICT");
|
||||
return false;
|
||||
}
|
||||
this.buf = buf.subarray(i + 1);
|
||||
this.sizeState = SIZE_FIRST;
|
||||
return true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
this.buf = Buffer.alloc(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
line(): Buffer | null {
|
||||
const idx = this.buf.indexOf(CRLF);
|
||||
if (idx === -1) return null;
|
||||
@@ -941,7 +1111,12 @@ class ResponseParser {
|
||||
const text = line.toString("latin1");
|
||||
const m = /^HTTP\/1\.([01]) (\d{3})(?: (.*))?$/.exec(text);
|
||||
if (m === null) {
|
||||
this.fail("Response does not match the HTTP/1.1 protocol", "INVALID_CONSTANT");
|
||||
const v7 = this.client.__profile.major >= 7;
|
||||
if (!text.startsWith("HTTP/")) {
|
||||
this.fail(v7 ? "Expected HTTP/, RTSP/ or ICE/" : "Expected HTTP/", "INVALID_CONSTANT");
|
||||
} else {
|
||||
this.fail(v7 ? "Invalid status code" : "Invalid response status", "INVALID_STATUS");
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.versionMinor = Number(m[1]);
|
||||
@@ -1055,55 +1230,15 @@ class ResponseParser {
|
||||
if (!this.complete()) return;
|
||||
}
|
||||
} else if (this.state === 3) {
|
||||
// Chunk size line: hex digits, then optional whitespace and
|
||||
// extensions, then CRLF. It is read byte by byte as it arrives.
|
||||
if (this.buf.length === 0) {
|
||||
if (this.more()) continue;
|
||||
// Chunk size line; a partial one continues with the next arrival.
|
||||
if (!this.chunkSizeLine()) {
|
||||
if (this.state === 3 && this.more()) continue;
|
||||
return;
|
||||
}
|
||||
let i = 0;
|
||||
let done = false;
|
||||
for (; i < this.buf.length && !done; i++) {
|
||||
const c = this.buf[i]!;
|
||||
if (this.chunkCR) {
|
||||
if (c !== 0x0a) {
|
||||
this.fail("Expected LF after chunk size", "STRICT");
|
||||
return;
|
||||
}
|
||||
done = true;
|
||||
} else if (c === 0x0d) {
|
||||
if (this.chunkDigits === 0) {
|
||||
this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE");
|
||||
return;
|
||||
}
|
||||
this.chunkCR = true;
|
||||
} else if (this.chunkExtension) {
|
||||
continue;
|
||||
} else if (hexValue(c) >= 0) {
|
||||
this.chunkSize = this.chunkSize * 16 + hexValue(c);
|
||||
this.chunkDigits++;
|
||||
if (this.chunkSize > Number.MAX_SAFE_INTEGER) {
|
||||
this.fail("Chunk size overflow", "INVALID_CHUNK_SIZE");
|
||||
return;
|
||||
}
|
||||
} else if (this.chunkDigits > 0 && (c === 0x3b || c === 0x20 || c === 0x09)) {
|
||||
this.chunkExtension = true;
|
||||
} else {
|
||||
this.fail("Invalid character in chunk size", "INVALID_CHUNK_SIZE");
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.buf = this.buf.subarray(i);
|
||||
if (!done) continue;
|
||||
const size = this.chunkSize;
|
||||
this.chunkSize = 0;
|
||||
this.chunkDigits = 0;
|
||||
this.chunkExtension = false;
|
||||
this.chunkCR = false;
|
||||
if (size === 0) {
|
||||
if (this.chunkSize === 0) {
|
||||
this.state = 5;
|
||||
} else {
|
||||
this.remaining = size;
|
||||
this.remaining = this.chunkSize;
|
||||
this.state = 4;
|
||||
}
|
||||
} else if (this.state === 4) {
|
||||
|
||||
@@ -8,7 +8,7 @@ export declare namespace DiagnosticsChannel {
|
||||
completed: boolean;
|
||||
method?: string;
|
||||
path: string;
|
||||
headers: string;
|
||||
headers: string | string[];
|
||||
addHeader(key: string, value: string): Request;
|
||||
}
|
||||
interface Response {
|
||||
|
||||
@@ -38,6 +38,12 @@ export interface ResponseData {
|
||||
const defaultProfile: Profile = { major: 7 };
|
||||
|
||||
export class Dispatcher extends EventEmitter {
|
||||
// An explicit constructor: the implicit one forwards its arguments to
|
||||
// EventEmitter's options-taking constructor, which stays a runtime fence.
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
dispatch(_options: DispatchOptions, _handler: DispatchHandlers): boolean {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
// program-installed global dispatcher) it is the runtime's own fetch, which
|
||||
// implements the same WHATWG algorithm. With one, the request goes through
|
||||
// that dispatcher (its connector, lookup and pool), following undici's
|
||||
// fetch for request headers, redirects and errors; the response body is
|
||||
// buffered into a native Response.
|
||||
// fetch for request headers, redirects and errors. The promise resolves
|
||||
// when the response headers arrive, and the Response body streams from the
|
||||
// connection with undici's back-pressure (see dispatchOnce).
|
||||
//
|
||||
// Known gaps: a Response built here reports `url` as "" and `redirected` as
|
||||
// false (the runtime's Response has no settable URL yet), and https requests
|
||||
// offer `gzip, deflate` rather than undici's `br, gzip, deflate` (the runtime
|
||||
// has no Brotli decoder).
|
||||
import { gunzipSync, inflateSync } from "node:zlib";
|
||||
import type { Transform } from "node:stream";
|
||||
import * as zlib from "node:zlib";
|
||||
import type { Dispatcher } from "./dispatcher.ts";
|
||||
import { getGlobalDispatcher, hasCustomGlobalDispatcher } from "./api.ts";
|
||||
import type { DispatchHandlers, Profile } from "./types.ts";
|
||||
@@ -115,7 +117,10 @@ interface RawResponse {
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: string[];
|
||||
body: Buffer;
|
||||
/** null when the response has no body to expose: a null-body status or
|
||||
* HEAD (its bytes, if any, are read and dropped), or a redirect fetch
|
||||
* follows or rejects (the request is aborted, closing its connection). */
|
||||
body: ReadableStream<Uint8Array> | null;
|
||||
}
|
||||
|
||||
const redirectStatuses = [301, 302, 303, 307, 308];
|
||||
@@ -125,7 +130,7 @@ export function undiciFetch(
|
||||
input: string | URL,
|
||||
init: UndiciRequestInit | undefined,
|
||||
createGlobal: () => Dispatcher,
|
||||
_profile: Profile,
|
||||
profile: Profile,
|
||||
): Promise<Response> {
|
||||
const explicit = init !== undefined ? init.dispatcher : undefined;
|
||||
if (explicit === undefined && !hasCustomGlobalDispatcher()) {
|
||||
@@ -139,7 +144,7 @@ export function undiciFetch(
|
||||
});
|
||||
}
|
||||
const dispatcher = explicit ?? getGlobalDispatcher(createGlobal);
|
||||
return fetchThrough(dispatcher, input, init ?? {});
|
||||
return fetchThrough(dispatcher, input, init ?? {}, profile);
|
||||
}
|
||||
|
||||
function normalizeMethod(method: string): string {
|
||||
@@ -153,6 +158,7 @@ async function fetchThrough(
|
||||
dispatcher: Dispatcher,
|
||||
input: string | URL,
|
||||
init: UndiciRequestInit,
|
||||
profile: Profile,
|
||||
): Promise<Response> {
|
||||
let url = typeof input === "string" ? new URL(input) : new URL(input.href);
|
||||
let method = normalizeMethod(init.method ?? "GET");
|
||||
@@ -197,7 +203,7 @@ async function fetchThrough(
|
||||
|
||||
let raw: RawResponse;
|
||||
try {
|
||||
raw = await dispatchOnce(dispatcher, url, method, pairs, body, signal);
|
||||
raw = await dispatchOnce(dispatcher, url, method, pairs, body, signal, redirectMode, profile);
|
||||
} catch (err) {
|
||||
if (signal !== null && signal.aborted) throw abortReason(signal);
|
||||
throw new TypeError("fetch failed", { cause: err });
|
||||
@@ -243,15 +249,11 @@ async function fetchThrough(
|
||||
continue;
|
||||
}
|
||||
|
||||
const nullBody = nullBodyStatuses.includes(raw.status) || method === "HEAD";
|
||||
return new Response(
|
||||
nullBody ? null : decode(raw.body, responseHeaders.get("content-encoding")),
|
||||
{
|
||||
status: raw.status,
|
||||
statusText: raw.statusText,
|
||||
headers: responseHeaders,
|
||||
},
|
||||
);
|
||||
return new Response(raw.body, {
|
||||
status: raw.status,
|
||||
statusText: raw.statusText,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,20 +326,28 @@ function headerListOf(
|
||||
return list;
|
||||
}
|
||||
|
||||
function decode(body: Buffer, encoding: string | null): Buffer {
|
||||
if (encoding === null || body.length === 0) return body;
|
||||
let out = body;
|
||||
const codings = encoding
|
||||
.toLowerCase()
|
||||
.split(",")
|
||||
.map((c) => c.trim())
|
||||
.reverse();
|
||||
/** undici 7's limit on a response's content codings, applied in both
|
||||
* profiles: undici 5 has none, and builds one decoder per listed coding. */
|
||||
const maxContentEncodings = 5;
|
||||
|
||||
/** Whether every coding has a decoder; otherwise the body passes through
|
||||
* as sent. */
|
||||
function decodable(codings: string[]): boolean {
|
||||
if (codings.length === 0) return false;
|
||||
for (const coding of codings) {
|
||||
if (coding === "gzip" || coding === "x-gzip") out = gunzipSync(out);
|
||||
else if (coding === "deflate") out = inflateSync(out);
|
||||
else return body;
|
||||
if (coding !== "gzip" && coding !== "x-gzip" && coding !== "deflate") return false;
|
||||
}
|
||||
return out;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** A decoder for one coding. undici 7 decodes with Z_SYNC_FLUSH, which
|
||||
* accepts a truncated stream; undici 5 uses zlib's defaults. */
|
||||
function decoderFor(coding: string, profile: Profile): Transform {
|
||||
const options =
|
||||
profile.major >= 7
|
||||
? { flush: zlib.constants.Z_SYNC_FLUSH, finishFlush: zlib.constants.Z_SYNC_FLUSH }
|
||||
: {};
|
||||
return coding === "deflate" ? zlib.createInflate(options) : zlib.createGunzip(options);
|
||||
}
|
||||
|
||||
function abortReason(signal: AbortSignal): Error {
|
||||
@@ -347,6 +357,16 @@ function abortReason(signal: AbortSignal): Error {
|
||||
: new DOMException("This operation was aborted", "AbortError");
|
||||
}
|
||||
|
||||
/** Unread bytes held beside the response stream before the connection
|
||||
* pauses: Node's Readable highWaterMark, which undici's fetch body uses. */
|
||||
const bodyHighWaterMark = 64 * 1024;
|
||||
|
||||
/** Dispatches one request and resolves when its response headers arrive.
|
||||
* The body streams as undici's fetch reads one: up to bodyHighWaterMark
|
||||
* unread bytes wait beside the stream, past that the connection (or the
|
||||
* last decoder) pauses until the reader pulls again; cancelling the stream
|
||||
* aborts the request, and an error after the headers errors the stream
|
||||
* with TypeError('terminated'). */
|
||||
function dispatchOnce(
|
||||
dispatcher: Dispatcher,
|
||||
url: URL,
|
||||
@@ -354,50 +374,252 @@ function dispatchOnce(
|
||||
headers: string[],
|
||||
body: Buffer | null,
|
||||
signal: AbortSignal | null,
|
||||
redirectMode: "follow" | "manual" | "error",
|
||||
profile: Profile,
|
||||
): Promise<RawResponse> {
|
||||
return new Promise<RawResponse>((resolve, reject) => {
|
||||
let status = 0;
|
||||
let statusText = "";
|
||||
let rawHeaders: string[] = [];
|
||||
const chunks: Buffer[] = [];
|
||||
let settled = false;
|
||||
let resolved = false;
|
||||
let done = false;
|
||||
let dump = false;
|
||||
let abortFn: ((err?: Error) => void) | null = null;
|
||||
const onAbort = (): void => {
|
||||
|
||||
// The response body: its stream controller, the unread bytes beside it,
|
||||
// and how to restart the paused producer.
|
||||
let controller: ReadableStreamDefaultController<Uint8Array> | null = null;
|
||||
let hasBody = false;
|
||||
const queue: Buffer[] = [];
|
||||
let queued = 0;
|
||||
let ended = false;
|
||||
let closed = false;
|
||||
let onDrain: (() => void) | null = null;
|
||||
// The decoders, first-applied last; their listeners hold this scope, so
|
||||
// releaseDecoders drops them once the body ends, fails or is cancelled.
|
||||
let decoding = false;
|
||||
let decoders: Transform[] = [];
|
||||
let first: Transform | null = null;
|
||||
let encodedBytes = 0;
|
||||
let resumeConnection: (() => void) | null = null;
|
||||
|
||||
const abortRequest = (err: Error): void => {
|
||||
const abort = abortFn;
|
||||
if (abort !== null && signal !== null) abort(abortReason(signal));
|
||||
if (abort !== null && !done) abort(err);
|
||||
};
|
||||
const onAbort = (): void => {
|
||||
if (signal === null) return;
|
||||
const reason = abortReason(signal);
|
||||
// After the headers undici 7 errors the body with the signal's
|
||||
// reason; undici 5 aborts its fetch controller without one, so the
|
||||
// body gets the controller's default AbortError.
|
||||
errorBody(
|
||||
profile.major >= 7 ? reason : new DOMException("The operation was aborted.", "AbortError"),
|
||||
);
|
||||
abortRequest(reason);
|
||||
};
|
||||
const stopListening = (): void => {
|
||||
if (signal !== null) signal.removeEventListener("abort", onAbort);
|
||||
};
|
||||
if (signal !== null) signal.addEventListener("abort", onAbort);
|
||||
const finish = (err: Error | null): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (signal !== null) signal.removeEventListener("abort", onAbort);
|
||||
if (err !== null) reject(err);
|
||||
else resolve({ status, statusText, headers: rawHeaders, body: Buffer.concat(chunks) });
|
||||
|
||||
const releaseDecoders = (): void => {
|
||||
const chain = decoders;
|
||||
decoders = [];
|
||||
first = null;
|
||||
for (const decoder of chain) {
|
||||
decoder.removeAllListeners();
|
||||
decoder.destroy();
|
||||
}
|
||||
};
|
||||
const closeBody = (): void => {
|
||||
closed = true;
|
||||
controller!.close();
|
||||
stopListening();
|
||||
};
|
||||
const errorBody = (err: unknown): void => {
|
||||
if (!hasBody || closed) return;
|
||||
closed = true;
|
||||
queue.length = 0;
|
||||
queued = 0;
|
||||
controller!.error(err);
|
||||
stopListening();
|
||||
releaseDecoders();
|
||||
};
|
||||
const endBody = (): void => {
|
||||
if (closed) return;
|
||||
ended = true;
|
||||
if (queue.length === 0) closeBody();
|
||||
};
|
||||
/** Takes a chunk; false when the producer should pause until onDrain. */
|
||||
const pushBody = (chunk: Buffer): boolean => {
|
||||
if (closed) return true;
|
||||
if (queue.length === 0 && (controller!.desiredSize ?? 0) > 0) {
|
||||
controller!.enqueue(new Uint8Array(chunk));
|
||||
return true;
|
||||
}
|
||||
queue.push(chunk);
|
||||
queued += chunk.length;
|
||||
return queued < bodyHighWaterMark;
|
||||
};
|
||||
const pullBody = (): void => {
|
||||
if (closed) return;
|
||||
const next = queue.shift();
|
||||
if (next !== undefined) {
|
||||
queued -= next.length;
|
||||
controller!.enqueue(new Uint8Array(next));
|
||||
}
|
||||
if (queue.length === 0 && ended) {
|
||||
closeBody();
|
||||
return;
|
||||
}
|
||||
const drain = onDrain;
|
||||
if (drain !== null && queued < bodyHighWaterMark) {
|
||||
onDrain = null;
|
||||
drain();
|
||||
}
|
||||
};
|
||||
const cancelBody = (reason: unknown): void => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
queue.length = 0;
|
||||
queued = 0;
|
||||
stopListening();
|
||||
releaseDecoders();
|
||||
abortRequest(
|
||||
reason instanceof Error
|
||||
? reason
|
||||
: new DOMException("The operation was aborted.", "AbortError"),
|
||||
);
|
||||
};
|
||||
const failBody = (err: Error): void => {
|
||||
errorBody(new TypeError("terminated", { cause: err }));
|
||||
abortRequest(err);
|
||||
};
|
||||
|
||||
const handler: DispatchHandlers = {
|
||||
onConnect: (abort: (err?: Error) => void): void => {
|
||||
abortFn = abort;
|
||||
if (signal !== null && signal.aborted) abort(abortReason(signal));
|
||||
},
|
||||
onHeaders: (
|
||||
statusCode: number,
|
||||
raw: Buffer[],
|
||||
_resume: () => void,
|
||||
text: string,
|
||||
): boolean => {
|
||||
onHeaders: (statusCode: number, raw: Buffer[], resume: () => void, text: string): boolean => {
|
||||
if (statusCode < 200) return true;
|
||||
status = statusCode;
|
||||
statusText = text;
|
||||
rawHeaders = [];
|
||||
for (const b of raw) rawHeaders.push(b.toString("latin1"));
|
||||
resolved = true;
|
||||
resumeConnection = resume;
|
||||
const rawHeaders: string[] = [];
|
||||
let location = false;
|
||||
let encoding: string | null = null;
|
||||
for (let i = 0; i + 1 < raw.length; i += 2) {
|
||||
const name = raw[i]!.toString("latin1");
|
||||
const value = raw[i + 1]!.toString("latin1");
|
||||
rawHeaders.push(name, value);
|
||||
const lower = name.toLowerCase();
|
||||
if (lower === "location") location = true;
|
||||
else if (lower === "content-encoding")
|
||||
encoding = encoding === null ? value : `${encoding}, ${value}`;
|
||||
}
|
||||
const follows =
|
||||
redirectStatuses.includes(statusCode) && location && redirectMode !== "manual";
|
||||
if (follows || nullBodyStatuses.includes(statusCode) || method === "HEAD") {
|
||||
dump = true;
|
||||
stopListening();
|
||||
resolve({ status: statusCode, statusText: text, headers: rawHeaders, body: null });
|
||||
// A redirect that is followed or rejected never reads its body:
|
||||
// abort the request so the connection is released instead of
|
||||
// draining a body that may never end.
|
||||
if (follows) abortRequest(new DOMException("The operation was aborted.", "AbortError"));
|
||||
return true;
|
||||
}
|
||||
const codings =
|
||||
encoding === null
|
||||
? []
|
||||
: encoding
|
||||
.toLowerCase()
|
||||
.split(",")
|
||||
.map((c) => c.trim());
|
||||
if (codings.length > maxContentEncodings) {
|
||||
dump = true;
|
||||
stopListening();
|
||||
reject(
|
||||
new Error(
|
||||
`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`,
|
||||
),
|
||||
);
|
||||
abortRequest(new DOMException("The operation was aborted.", "AbortError"));
|
||||
return true;
|
||||
}
|
||||
hasBody = true;
|
||||
const stream = new ReadableStream<Uint8Array>({
|
||||
start: (c: ReadableStreamDefaultController<Uint8Array>): void => {
|
||||
controller = c;
|
||||
},
|
||||
pull: (): void => pullBody(),
|
||||
cancel: (reason: unknown): void => cancelBody(reason),
|
||||
});
|
||||
if (decodable(codings)) {
|
||||
// Decoders run last-applied coding first, piped into each other;
|
||||
// the last one feeds the body.
|
||||
decoding = true;
|
||||
let last: Transform | null = null;
|
||||
for (let i = codings.length - 1; i >= 0; i--) {
|
||||
const decoder = decoderFor(codings[i]!, profile);
|
||||
decoder.on("error", (err: Error) => failBody(err));
|
||||
if (last === null) first = decoder;
|
||||
else last.pipe(decoder);
|
||||
decoders.push(decoder);
|
||||
last = decoder;
|
||||
}
|
||||
const output = last!;
|
||||
output.on("data", (chunk: Buffer) => {
|
||||
if (!pushBody(chunk)) {
|
||||
output.pause();
|
||||
onDrain = (): void => {
|
||||
output.resume();
|
||||
};
|
||||
}
|
||||
});
|
||||
output.on("end", () => {
|
||||
releaseDecoders();
|
||||
endBody();
|
||||
});
|
||||
first!.on("drain", () => resume());
|
||||
}
|
||||
resolve({ status: statusCode, statusText: text, headers: rawHeaders, body: stream });
|
||||
return true;
|
||||
},
|
||||
onData: (chunk: Buffer): boolean => {
|
||||
chunks.push(chunk);
|
||||
return true;
|
||||
if (dump || !hasBody || closed) return true;
|
||||
if (decoding) {
|
||||
const decoder = first;
|
||||
if (decoder === null) return true;
|
||||
encodedBytes += chunk.length;
|
||||
return decoder.write(chunk);
|
||||
}
|
||||
if (pushBody(chunk)) return true;
|
||||
onDrain = resumeConnection;
|
||||
return false;
|
||||
},
|
||||
onComplete: (_trailers: string[] | null): void => {
|
||||
done = true;
|
||||
if (!hasBody) {
|
||||
stopListening();
|
||||
return;
|
||||
}
|
||||
// zlib rejects an empty stream; undici ends such a body unread.
|
||||
const decoder = first;
|
||||
if (decoder !== null && encodedBytes > 0) {
|
||||
decoder.end();
|
||||
} else {
|
||||
releaseDecoders();
|
||||
endBody();
|
||||
}
|
||||
},
|
||||
onError: (err: Error): void => {
|
||||
done = true;
|
||||
if (!resolved) {
|
||||
stopListening();
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
errorBody(new TypeError("terminated", { cause: err }));
|
||||
},
|
||||
onComplete: (_trailers: string[] | null): void => finish(null),
|
||||
onError: (err: Error): void => finish(err),
|
||||
};
|
||||
dispatcher.dispatch(
|
||||
{
|
||||
|
||||
@@ -32,8 +32,10 @@ export class Request {
|
||||
error: Error | null;
|
||||
abortFn: ((err?: Error) => void) | null;
|
||||
handler: DispatchHandlers;
|
||||
/** The request as diagnostics_channel subscribers see it: one boxed
|
||||
* value per request, so its identity is stable across publications. */
|
||||
profile: Profile;
|
||||
/** The request as diagnostics_channel subscribers see it: one untyped
|
||||
* object per request, so its identity is stable across publications.
|
||||
* The request keeps its completed and headers fields current. */
|
||||
diagnostic: unknown;
|
||||
|
||||
constructor(origin: string, opts: DispatchOptions, handler: DispatchHandlers, profile: Profile) {
|
||||
@@ -91,6 +93,7 @@ export class Request {
|
||||
}
|
||||
|
||||
this.completed = false;
|
||||
this.profile = profile;
|
||||
this.aborted = false;
|
||||
this.idempotent = opts.idempotent ?? (method === "HEAD" || method === "GET");
|
||||
this.blocking = opts.blocking ?? (profile.major >= 7 ? method !== "HEAD" : false);
|
||||
@@ -131,18 +134,29 @@ export class Request {
|
||||
}
|
||||
|
||||
diagnosticView(): unknown {
|
||||
let view = this.diagnostic;
|
||||
if (view === null) {
|
||||
view = {
|
||||
if (this.diagnostic === null) {
|
||||
// addHeader adds a header as undici's Request.addHeader does (the
|
||||
// tracing instrumentations' header injection).
|
||||
const view: any = {
|
||||
origin: this.origin,
|
||||
completed: this.completed,
|
||||
method: this.method,
|
||||
path: this.path,
|
||||
headers: this.headers,
|
||||
headers: this.diagnosticHeaders(),
|
||||
addHeader: (key: string, value: string): unknown => {
|
||||
this.addHeader(key, value);
|
||||
return view;
|
||||
},
|
||||
};
|
||||
this.diagnostic = view;
|
||||
}
|
||||
return view;
|
||||
return this.diagnostic;
|
||||
}
|
||||
|
||||
/** The diagnostics view's headers: undici 7 publishes its header array,
|
||||
* undici 5 the raw block. */
|
||||
diagnosticHeaders(): string | string[] {
|
||||
return this.profile.major >= 7 ? this.headerPairs.slice() : this.headers;
|
||||
}
|
||||
|
||||
/** The DiagnosticsChannel.Request view's raw header block. */
|
||||
@@ -155,7 +169,9 @@ export class Request {
|
||||
}
|
||||
|
||||
addHeader(key: string, value: string): Request {
|
||||
this.headerPairs.push(key, value);
|
||||
processHeader(this, key, value, this.profile);
|
||||
const view: any = this.diagnostic;
|
||||
if (view !== null) view.headers = this.diagnosticHeaders();
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -207,6 +223,8 @@ export class Request {
|
||||
|
||||
onComplete(trailers: Buffer[]): void {
|
||||
this.completed = true;
|
||||
const view: any = this.diagnostic;
|
||||
if (view !== null) view.completed = true;
|
||||
if (channels.trailers.hasSubscribers)
|
||||
channels.trailers.publish({ request: this.diagnosticView(), trailers });
|
||||
if (this.handler.onComplete !== undefined) {
|
||||
@@ -227,10 +245,18 @@ export class Request {
|
||||
|
||||
function processHeader(request: Request, key: string, val: string, profile: Profile): void {
|
||||
const lower = key.toLowerCase();
|
||||
// undici 7 checks the name and every value before the special cases;
|
||||
// undici 5 checks the first Host value before storing it. Either way a
|
||||
// CR or LF in a value never reaches the request head.
|
||||
if (profile.major >= 7) {
|
||||
if (!tokenRegExp.test(lower)) throw new InvalidArgumentError("invalid header key");
|
||||
if (headerCharRegex.test(val)) throw new InvalidArgumentError(`invalid ${key} header`);
|
||||
}
|
||||
if (lower === "host") {
|
||||
// The client writes the Host value into the request head as given.
|
||||
if (headerCharRegex.test(val)) throw new InvalidArgumentError(`invalid ${key} header`);
|
||||
if (request.host === null) {
|
||||
if (headerCharRegex.test(val)) throw new InvalidArgumentError(`invalid ${key} header`);
|
||||
request.host = val;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ function onTick(): void {
|
||||
}
|
||||
}
|
||||
fastTimers.length = len;
|
||||
// An idle tick stops; the next timer schedules a new one.
|
||||
if (fastTimers.length !== 0) refreshTimeout();
|
||||
else refreshTick = null;
|
||||
}
|
||||
|
||||
function refreshTimeout(): void {
|
||||
|
||||
@@ -28,6 +28,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
...(features.dynTier ? { dynTier: true } : {}),
|
||||
...(features.streamDyn ? { streamDyn: true } : {}),
|
||||
...(features.handleProps ? { handleProps: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.dynBufferNum ? { dynBufferNum: true } : {}),
|
||||
...(features.builtinValues ? { builtinValues: true } : {}),
|
||||
@@ -37,6 +38,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
bigint: features.bigint,
|
||||
searchParams: features.searchParams,
|
||||
qs: features.qs,
|
||||
utilPromisify: features.utilPromisify,
|
||||
dynBuiltinClass: features.dynBuiltinClass,
|
||||
dynBuiltinStream: features.dynBuiltinStream,
|
||||
parseArgs: features.parseArgs,
|
||||
@@ -46,6 +48,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
http2: features.http2,
|
||||
dgram: features.dgram,
|
||||
dnsPromises: features.dnsPromises,
|
||||
dgramDyn: features.dgramDyn,
|
||||
watch: features.fsWatch,
|
||||
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
|
||||
nodeTest: features.nodeTest,
|
||||
|
||||
@@ -42,6 +42,7 @@ export async function compilePackedLibrary(
|
||||
bigint: options.bigint ?? false,
|
||||
searchParams: options.searchParams ?? false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
@@ -51,6 +52,7 @@ export async function compilePackedLibrary(
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -86,6 +86,7 @@ export const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
child: 16,
|
||||
fileHandle: 17,
|
||||
dir: 44,
|
||||
dgramSocket: 62,
|
||||
cryptoHash: 52,
|
||||
cryptoHmac: 53,
|
||||
};
|
||||
@@ -96,6 +97,15 @@ export const DYN_HANDLE_TAG_NUM: Record<string, number> = {
|
||||
const DYN_HANDLE_BOX_FN: Record<string, string> = {
|
||||
cryptoHash: "scr_crypto_hash_to_dyn",
|
||||
cryptoHmac: "scr_crypto_hash_to_dyn",
|
||||
dgramSocket: "scr_dgram_to_dyn",
|
||||
};
|
||||
|
||||
/** Handle kinds whose box holds a unit-owned wrapper rather than the
|
||||
* handle itself (a dgram socket's box carries its untyped listeners):
|
||||
* the unit's own function unwraps it, with the tag check and TypeError
|
||||
* of scr_dyn_handle_unbox. */
|
||||
const DYN_HANDLE_UNBOX_FN: Record<string, string> = {
|
||||
dgramSocket: "scr_dgram_from_dyn",
|
||||
};
|
||||
|
||||
export const DYN_KIND = {
|
||||
@@ -2492,11 +2502,17 @@ export class LlDyn {
|
||||
// TypeError on any other kind or tag).
|
||||
const h = DYN_HANDLE_KINDS.get(t.kind);
|
||||
if (h) {
|
||||
host.declare(`declare ptr @scr_dyn_handle_unbox(ptr, i32, ptr, ptr)`);
|
||||
const r = B.tmp();
|
||||
B.line(
|
||||
`${r} = call ptr @scr_dyn_handle_unbox(ptr %d, i32 ${DYN_HANDLE_TAG_NUM[t.kind]}, ptr %path, ptr ${want})`,
|
||||
);
|
||||
const unbox = DYN_HANDLE_UNBOX_FN[t.kind];
|
||||
if (unbox) {
|
||||
host.declare(`declare ptr @${unbox}(ptr, ptr, ptr)`);
|
||||
B.line(`${r} = call ptr @${unbox}(ptr %d, ptr %path, ptr ${want})`);
|
||||
} else {
|
||||
host.declare(`declare ptr @scr_dyn_handle_unbox(ptr, i32, ptr, ptr)`);
|
||||
B.line(
|
||||
`${r} = call ptr @scr_dyn_handle_unbox(ptr %d, i32 ${DYN_HANDLE_TAG_NUM[t.kind]}, ptr %path, ptr ${want})`,
|
||||
);
|
||||
}
|
||||
B.terminate(`ret ptr ${r}`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import { findMapEntryFusions } from "./map-entry-fusion.js";
|
||||
import { findLocalStackUnions } from "./stack-unions.js";
|
||||
import { findLocalStackObjects, findReassignedStackObjects } from "./stack-objects.js";
|
||||
import { findLocalUnionStorage } from "./local-union-storage.js";
|
||||
import { INT32_RANGE, analyzeIntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import { INT32_RANGE, analyzeIntegerRangeFacts } from "../../ir/integer-ranges.js";
|
||||
import { findBytesBounds } from "./bytes-bounds.js";
|
||||
import { findInt32Locals } from "./int32-locals.js";
|
||||
import { findIntegerViews } from "./integer-views.js";
|
||||
@@ -59,6 +59,7 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
const B = new BlockBuilder();
|
||||
emitter.B = B;
|
||||
emitter.currentFunctionName = fn.name;
|
||||
emitter.currentTier = fn.tier;
|
||||
// Fiber switches restore the active exception cell before control
|
||||
// returns to a synchronous frame (emitPendingCheck).
|
||||
B.excCellInvariant = fn.async !== true && fn.generator === undefined;
|
||||
@@ -220,7 +221,9 @@ export function emitFunction(emitter: LlEmitter, fn: IrFunction): string {
|
||||
emitter.localUnionStorage = new Map();
|
||||
emitter.walkBorrows = emitter.walkBorrowsByFunction.get(fn.name) ?? new Set();
|
||||
const slotFacts = emitter.int32Slots.facts(fn.name);
|
||||
emitter.integerRanges = analyzeIntegerRanges(numericFn, slotFacts);
|
||||
const rangeFacts = analyzeIntegerRangeFacts(numericFn, slotFacts);
|
||||
emitter.integerRanges = rangeFacts.ranges;
|
||||
emitter.integerLoopRanges = rangeFacts.loopRanges;
|
||||
emitter.int32Elements = slotFacts.elements ?? null;
|
||||
emitter.bytesBounds = findBytesBounds(numericFn, emitter.integerRanges);
|
||||
// Debug builds keep every number local a double for the debugger's
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */
|
||||
import { emitWasmModules } from "./wasm-modules.js";
|
||||
import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js";
|
||||
import { JS_IC_WORDS } from "./js-ic.js";
|
||||
import {
|
||||
mangleAsyncSpawn,
|
||||
mangleClassObj,
|
||||
@@ -559,6 +560,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
|
||||
out.push(
|
||||
`@sc_dyn_read_ic_${site} = internal ${tl}global { ptr, ${emitter.sizeType}, ${emitter.sizeType}, ptr } zeroinitializer`,
|
||||
);
|
||||
for (let site = 0; site < emitter.jsIcSiteCount; site++)
|
||||
out.push(`@sc_js_ic_${site} = internal ${tl}global [${JS_IC_WORDS} x i64] zeroinitializer`);
|
||||
}
|
||||
for (const table of emitter.constantNumericTables.values()) {
|
||||
const n = table.values.length;
|
||||
|
||||
@@ -56,7 +56,9 @@ import {
|
||||
SPECULATIVE_RANGE,
|
||||
emitSafeIntegerCheck,
|
||||
emitSpeculativeInteger,
|
||||
planProvenIntegerLoop,
|
||||
planSpeculativeLoop,
|
||||
type ProvenLoopPlan,
|
||||
speculativeReads,
|
||||
speculativeWriteTarget,
|
||||
} from "./speculative-integers.js";
|
||||
@@ -1802,7 +1804,9 @@ export function emitLoopCopies(
|
||||
)
|
||||
: null;
|
||||
if (!plan) {
|
||||
emitCopy(exit);
|
||||
const proven = allowed ? planProvenLoop(emitter, loop) : null;
|
||||
if (proven) emitProvenLoop(emitter, proven, exit, emitCopy);
|
||||
else emitCopy(exit);
|
||||
return;
|
||||
}
|
||||
const B = emitter.B;
|
||||
@@ -1881,7 +1885,7 @@ export function emitLoopCopies(
|
||||
const leave = B.newLabel("spec.leave");
|
||||
emitCopy(leave);
|
||||
B.startBlock(leave);
|
||||
emitter.writeBackSpeculativeGlobals(deferred);
|
||||
emitter.writeBackDeferred(deferred);
|
||||
B.br(exit);
|
||||
}
|
||||
} finally {
|
||||
@@ -1890,10 +1894,91 @@ export function emitLoopCopies(
|
||||
}
|
||||
}
|
||||
|
||||
/** A proven integer loop (speculative-integers.ts) for this loop, or null.
|
||||
* A shadowed local's double is stale until the loop is left, which only
|
||||
* the loop's exit, a return and an exception do: the loop is planned only
|
||||
* where an exception leaves the function (no enclosing try or finally, no
|
||||
* coroutine frame), so no handler reads the stale double. */
|
||||
function planProvenLoop(
|
||||
emitter: LlEmitter,
|
||||
loop: Extract<IrStmt, { kind: "while" | "doWhile" | "for" }>,
|
||||
): ProvenLoopPlan | null {
|
||||
if (
|
||||
loop.kind !== "for" ||
|
||||
!emitter.speculativeLoopsEnabled ||
|
||||
emitter.speculation !== null ||
|
||||
emitter.speculationResume !== null ||
|
||||
emitter.tryStack.length > 0 ||
|
||||
emitter.finallyStack.length > 0 ||
|
||||
emitter.currentCoro !== null
|
||||
)
|
||||
return null;
|
||||
const cursors = emitter.integerLoopRanges.get(loop);
|
||||
if (!cursors) return null;
|
||||
return planProvenIntegerLoop(
|
||||
loop,
|
||||
cursors,
|
||||
(id) => emitter.currentLocals.has(id) && emitter.speculativeLocal(id),
|
||||
(id) => emitter.integerLoopBindings.has(id) || emitter.int32Locals.has(id),
|
||||
(e) => {
|
||||
const range = emitter.integerRanges.get(e);
|
||||
return (
|
||||
range !== undefined &&
|
||||
range !== null &&
|
||||
range.min >= -Number.MAX_SAFE_INTEGER &&
|
||||
range.max <= Number.MAX_SAFE_INTEGER
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Emit a proven integer loop: its shadowed locals start from their
|
||||
* doubles (integers in their loop ranges on entry), its planned writes
|
||||
* compute integers into the shadows only, and leaving the loop through its
|
||||
* exit writes each double back. */
|
||||
function emitProvenLoop(
|
||||
emitter: LlEmitter,
|
||||
plan: ProvenLoopPlan,
|
||||
exit: string,
|
||||
emitCopy: (exit: string) => void,
|
||||
): void {
|
||||
const B = emitter.B;
|
||||
for (const [id, range] of plan.locals) {
|
||||
const shadow = B.slot();
|
||||
B.entryAllocas.push(
|
||||
`${shadow} = alloca i64 ; integer ${emitter.currentLocals.get(id)?.name ?? id}`,
|
||||
);
|
||||
const value = B.tmp(),
|
||||
integer = B.tmp();
|
||||
B.line(`${value} = load double, ptr ${emitter.binding(id).slot}`);
|
||||
B.line(`${integer} = fptosi double ${value} to i64`);
|
||||
B.line(`store i64 ${integer}, ptr ${shadow}`);
|
||||
emitter.integerLoopBindings.set(id, { slot: shadow, type: "i64", signed: true, range });
|
||||
}
|
||||
const deferred = new Set<string>();
|
||||
for (const id of plan.locals.keys()) deferred.add(id);
|
||||
emitter.speculation = { writes: plan.writes, resume: new Map(), deferred };
|
||||
try {
|
||||
const leave = B.newLabel("integer.leave");
|
||||
emitCopy(leave);
|
||||
B.startBlock(leave);
|
||||
emitter.writeBackDeferred(deferred);
|
||||
B.br(exit);
|
||||
} finally {
|
||||
emitter.speculation = null;
|
||||
for (const id of plan.locals.keys()) emitter.integerLoopBindings.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/** A speculated write in the fast copy: compute the value as a safe
|
||||
* integer, store it to the shadow and its exact double to the local.
|
||||
* Any failed check re-executes the statement in the ordinary copy. */
|
||||
export function emitSpeculativeWrite(emitter: LlEmitter, s: IrStmt, resume: string): void {
|
||||
* Any failed check re-executes the statement in the ordinary copy. A
|
||||
* proven integer loop's write (no `resume`) has nothing to check. */
|
||||
export function emitSpeculativeWrite(
|
||||
emitter: LlEmitter,
|
||||
s: IrStmt,
|
||||
resume: string | undefined,
|
||||
): void {
|
||||
const B = emitter.B;
|
||||
const target = speculativeWriteTarget(s)!;
|
||||
const binding = emitter.integerLoopBindings.get(target.localId)!;
|
||||
@@ -1901,7 +1986,7 @@ export function emitSpeculativeWrite(emitter: LlEmitter, s: IrStmt, resume: stri
|
||||
let bails = false;
|
||||
emitter.declare("declare i1 @llvm.expect.i1(i1, i1)");
|
||||
const bail = (condition: string): void => {
|
||||
if (condition === "false") return;
|
||||
if (condition === "false" || resume === undefined) return;
|
||||
bails = true;
|
||||
const expected = B.tmp();
|
||||
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${condition}, i1 false)`);
|
||||
@@ -1930,7 +2015,7 @@ export function emitSpeculativeWrite(emitter: LlEmitter, s: IrStmt, resume: stri
|
||||
B.line(`${biased} = add i64 ${result}, ${Number.MAX_SAFE_INTEGER}`);
|
||||
B.line(`${outside} = icmp ugt i64 ${biased}, ${2 * Number.MAX_SAFE_INTEGER}`);
|
||||
bail(outside);
|
||||
} else result = emitSpeculativeInteger(emitter, target.value, bail, reads);
|
||||
} else result = emitSpeculativeInteger(emitter, target.value, bail, reads, resume === undefined);
|
||||
// Speculable values read only numbers past this point.
|
||||
if (emitter.frames.pop()!.length > 0)
|
||||
throw new InternalCompilerError("llvm emitter bug: speculative write owns a temporary");
|
||||
@@ -1945,8 +2030,8 @@ export function emitSpeculativeWrite(emitter: LlEmitter, s: IrStmt, resume: stri
|
||||
const done = B.newLabel("spec.done");
|
||||
B.br(done);
|
||||
B.startBlock(failed);
|
||||
emitter.writeBackSpeculativeGlobals(deferred);
|
||||
B.br(resume);
|
||||
emitter.writeBackDeferred(deferred);
|
||||
B.br(resume!);
|
||||
B.startBlock(done);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { wasmBoundsChecks } from "./wasm-modules.js";
|
||||
import { jsTruthy } from "./jsval.js";
|
||||
import { JsIcSites } from "../../ir/js-ic.js";
|
||||
import { type ArraySearchKind } from "./array-search.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitThreadSlotsIntrinsic } from "./thread-slots.js";
|
||||
@@ -919,6 +920,12 @@ export class LlEmitter {
|
||||
readonly recordCloneShapes = new Set<string>();
|
||||
readonly tracedShapes: Set<string>;
|
||||
readonly tracedUnions: Set<string>;
|
||||
/** Inline-cache sites of dynamic-tier functions (js-ic.ts): one
|
||||
* zero-initialized ScrJsIc per site (scr_js.h). */
|
||||
jsIcSiteCount = 0;
|
||||
private jsIcSitesMemo: JsIcSites | undefined;
|
||||
/** The tier of the function being emitted. */
|
||||
currentTier: IrFunction["tier"] = undefined;
|
||||
/** The class graph (buildClassGraph): preorder numbering, hierarchy
|
||||
* membership, virtual slot lists. */
|
||||
readonly classMeta: Map<string, LlClassMeta>;
|
||||
@@ -1037,6 +1044,8 @@ export class LlEmitter {
|
||||
* not a constructor (where they change), not async or a generator. */
|
||||
finalFieldBorrows = false;
|
||||
integerRanges: IntegerRanges = new Map();
|
||||
/** The current function's bounded for-loop cursors (integer-ranges.ts). */
|
||||
integerLoopRanges: ReadonlyMap<IrStmt, ReadonlyMap<string, IntegerRange>> = new Map();
|
||||
/** Number arrays of the current function whose every present element is
|
||||
* an int32 (Int32[] contracts), or null when the module has none. */
|
||||
int32Elements: ((arr: IrExpr) => boolean) | null = null;
|
||||
@@ -2687,7 +2696,8 @@ export class LlEmitter {
|
||||
return emitLoopCopies(this, loop, allowed, exit, emitCopy);
|
||||
}
|
||||
|
||||
writeBackSpeculativeGlobals(deferred: Set<string>): void {
|
||||
/** Store the double of each deferred shadow back to its binding. */
|
||||
writeBackDeferred(deferred: Set<string>): void {
|
||||
const B = this.B;
|
||||
for (const id of deferred) {
|
||||
const integer = B.tmp(),
|
||||
@@ -2766,7 +2776,7 @@ export class LlEmitter {
|
||||
}
|
||||
return this.globalWriters.get(id) ?? new Set<string>();
|
||||
}
|
||||
private emitSpeculativeWrite(s: IrStmt, resume: string): void {
|
||||
private emitSpeculativeWrite(s: IrStmt, resume: string | undefined): void {
|
||||
return emitSpeculativeWrite(this, s, resume);
|
||||
}
|
||||
|
||||
@@ -4460,7 +4470,7 @@ export class LlEmitter {
|
||||
this.B.startBlock(resume.label);
|
||||
}
|
||||
if (this.speculation?.writes.has(s)) {
|
||||
this.emitSpeculativeWrite(s, this.speculation.resume.get(s)!);
|
||||
this.emitSpeculativeWrite(s, this.speculation.resume.get(s));
|
||||
return;
|
||||
}
|
||||
const previous = this.B.debugLocation;
|
||||
@@ -5555,6 +5565,15 @@ export class LlEmitter {
|
||||
return emitExpr(this, e);
|
||||
}
|
||||
|
||||
/** The helper classification of this module's inline-cache sites. */
|
||||
get jsIcSites(): JsIcSites {
|
||||
return (this.jsIcSitesMemo ??= new JsIcSites(this.mod));
|
||||
}
|
||||
|
||||
jsIcSite(): string {
|
||||
return `@sc_js_ic_${this.jsIcSiteCount++}`;
|
||||
}
|
||||
|
||||
/** A fresh read-site cache for one constant-key dyn read. */
|
||||
dynReadIcSite(): string {
|
||||
return `@sc_dyn_read_ic_${this.dynReadIcSites++}`;
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
inputSuffixWrites,
|
||||
} from "./borrowed-inputs.js";
|
||||
import type { Writes } from "./reference-writes.js";
|
||||
import { emitJsIcCall, jsIcSiteOf } from "./js-ic.js";
|
||||
import { emitClassMembership, emitDowncastGuard } from "./narrow-guards.js";
|
||||
|
||||
export function emitCallExpr(
|
||||
@@ -88,6 +89,12 @@ export function emitCallExpr(
|
||||
case "call": {
|
||||
const callee = host.fnByName.get(e.callee);
|
||||
if (!callee) throw new InternalCompilerError(`llvm emitter bug: unknown callee ${e.callee}`);
|
||||
if (!plainCall && !awaitEntry && !resultSlot && !stackResult) {
|
||||
// A property helper called from dynamic-tier code is an inline
|
||||
// cache site (two-tier design S4).
|
||||
const site = jsIcSiteOf(host, e);
|
||||
if (site) return emitJsIcCall(host, e, callee, site);
|
||||
}
|
||||
if (callee.fastRead === "index" && !plainCall && !awaitEntry && !resultSlot) {
|
||||
const indexed = emitNumericIndexRead(host, e, callee);
|
||||
if (indexed) return indexed;
|
||||
|
||||
@@ -28,6 +28,7 @@ import { elemAccess, vAdapters } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
import type { LlvmEmitterContext, ExprOf, LlValue } from "./expr-context.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
import { emitJsIcMethod, jsIcMethods } from "./js-ic.js";
|
||||
import { DYN_BUFFER_NUM_METHODS } from "../../ir/runtime-features.js";
|
||||
|
||||
export function emitDynamicExpr(
|
||||
@@ -207,10 +208,16 @@ export function emitDynamicExpr(
|
||||
// pending exception.
|
||||
const recv = emitBorrowedInput(host, e.recv);
|
||||
host.declare(`declare ptr @scr_dyn_prepare_method(ptr, ptr)`);
|
||||
const prepared = B.tmp();
|
||||
B.line(
|
||||
`${prepared} = call ptr @scr_dyn_prepare_method(ptr ${recv.name}, ptr ${host.cstr(e.method)})`,
|
||||
);
|
||||
let prepared: string;
|
||||
if (jsIcMethods(host)) {
|
||||
// Dynamic-tier code reads the method through the site's cache.
|
||||
prepared = emitJsIcMethod(host, e, recv.name);
|
||||
} else {
|
||||
prepared = B.tmp();
|
||||
B.line(
|
||||
`${prepared} = call ptr @scr_dyn_prepare_method(ptr ${recv.name}, ptr ${host.cstr(e.method)})`,
|
||||
);
|
||||
}
|
||||
host.own({ name: prepared, type: { kind: "dyn" } });
|
||||
host.emitPendingCheck();
|
||||
const args = e.args.map((a) => emitBorrowedInput(host, a));
|
||||
|
||||
@@ -0,0 +1,779 @@
|
||||
/* Inline-cache lowering of the dynamic tier (two-tier design stage S4;
|
||||
* runtime scr_js_object.c, sites ir/js-ic.ts).
|
||||
*
|
||||
* A property site in a dynamic-tier function owns one zeroed thread-local
|
||||
* ScrJsIc record (`@sc_js_ic_N`). The code at the site is fixed:
|
||||
*
|
||||
* load ordinary object whose shape is entry 0's own-property shape:
|
||||
* read entry `slot` inline unless it is an accessor; otherwise
|
||||
* scr_js_load_probe (polymorphic, prototype hits, misses)
|
||||
* store ordinary object whose shape is entry 0's replace shape, outside
|
||||
* an intact parse region, writable data entry: replace inline;
|
||||
* otherwise scr_js_store_probe (adds, polymorphic, misses)
|
||||
* keyed dense array element at an integral number key: inline;
|
||||
* otherwise scr_js_keyed_probe (string keys of ordinary objects)
|
||||
* method dynInvoke's method read: scr_js_load_probe
|
||||
*
|
||||
* A probe that cannot answer from plain data leaves everything untouched,
|
||||
* and the site calls its helper (or the generic runtime entry), which
|
||||
* defines the operation's semantics; nothing is ever patched.
|
||||
*
|
||||
* Layout facts (64-bit only; 32-bit targets keep the helper calls):
|
||||
* ScrDyn kind +8; v.obj {len +16, entries +32, region +72,
|
||||
* shape +80}; v.arr {len +16, items +32, presence +56};
|
||||
* v.num +16
|
||||
* ScrDynEntry 48 bytes: value +16, accessor bit 0 of +40, writable +41
|
||||
* ScrJsIc entries of 40 bytes {shape, target, holder, holder_shape,
|
||||
* slot}; 168 bytes in all */
|
||||
import { BOOL, DYN, F64, STRING, type IrExpr, type IrFunction } from "../../ir/ir.js";
|
||||
import { jsIcMethodSites, type JsIcSite } from "../../ir/js-ic.js";
|
||||
import { mangleBorrowedFunction } from "../mangle.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { DYN_KIND } from "./dyn.js";
|
||||
import type { ExprOf, LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { JS_BITS } from "./jsval-encoding.js";
|
||||
import { jsImmediates } from "./jsval.js";
|
||||
|
||||
type Host = LlvmEmitterContext;
|
||||
|
||||
/** Words of one ScrJsIc record. */
|
||||
export const JS_IC_WORDS = 21;
|
||||
|
||||
const ENTRY_STRIDE = 48;
|
||||
|
||||
/** The cache site a call is, when it is emitted in a dynamic-tier function
|
||||
* of a 64-bit target. */
|
||||
export function jsIcSiteOf(host: Host, e: ExprOf<"call">): JsIcSite | null {
|
||||
if (!jsImmediates(host) || host.currentTier !== "dynamic") return null;
|
||||
if (host.int32Abis.has(e.callee)) return null;
|
||||
return host.jsIcSites.site(e.callee);
|
||||
}
|
||||
|
||||
/** Whether method dispatch in the current function reads through a cache. */
|
||||
export function jsIcMethods(host: Host): boolean {
|
||||
return (
|
||||
jsImmediates(host) &&
|
||||
host.currentTier === "dynamic" &&
|
||||
jsIcMethodSites(host.currentFunctionName)
|
||||
);
|
||||
}
|
||||
|
||||
function gep(host: Host, base: string, offset: number): string {
|
||||
const p = host.B.tmp();
|
||||
host.B.line(`${p} = getelementptr inbounds i8, ptr ${base}, i64 ${offset}`);
|
||||
return p;
|
||||
}
|
||||
|
||||
function load(host: Host, type: string, base: string, offset: number): string {
|
||||
const v = host.B.tmp();
|
||||
host.B.line(`${v} = load ${type}, ptr ${offset === 0 ? base : gep(host, base, offset)}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
/** Branches to `yes` when the dyn cell `d` has kind `kind`. */
|
||||
function kindIs(host: Host, d: string, kind: number, yes: string, no: string): void {
|
||||
const k = load(host, "i32", d, 8);
|
||||
const is = host.B.tmp();
|
||||
host.B.line(`${is} = icmp eq i32 ${k}, ${kind}`);
|
||||
host.B.condBr(is, yes, no);
|
||||
}
|
||||
|
||||
/* How many cache entries a site tests inline, and whether those tests
|
||||
* cover prototype hits. Measured (two-tier S4): testing all four entries
|
||||
* inline made polymorphic reads 7-10% faster (the design prototype's W2/W3)
|
||||
* and monomorphic method code no faster, for 65-225 more bytes per site;
|
||||
* the prototype hit made method-heavy code 3-6% faster for 15-90 bytes.
|
||||
* So a site tests entry 0, own or prototype, and its probe does the rest.
|
||||
* (0 makes every access probe: with a runtime built with SCR_JS_VERIFY,
|
||||
* that checks every cached shape against its object.) */
|
||||
const INLINE_WAYS: number = 1;
|
||||
const INLINE_PROTO = true;
|
||||
|
||||
/** The address of entry `slot` of object `d`'s entries. */
|
||||
function entryAt(host: Host, d: string, slot: string): string {
|
||||
const B = host.B;
|
||||
const entries = load(host, "ptr", d, 32);
|
||||
const offset = B.tmp();
|
||||
const entry = B.tmp();
|
||||
B.line(`${offset} = mul i64 ${slot}, ${ENTRY_STRIDE}`);
|
||||
B.line(`${entry} = getelementptr inbounds i8, ptr ${entries}, i64 ${offset}`);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/** Branches to `miss` when the entry at `entry` is an accessor. */
|
||||
function dataEntry(host: Host, entry: string, miss: string): void {
|
||||
const B = host.B;
|
||||
const flags = load(host, "i8", entry, 40);
|
||||
const accessor = B.tmp();
|
||||
const isAccessor = B.tmp();
|
||||
B.line(`${accessor} = and i8 ${flags}, 1`);
|
||||
B.line(`${isAccessor} = icmp ne i8 ${accessor}, 0`);
|
||||
const data = B.newLabel("ic.data");
|
||||
B.condBr(isAccessor, miss, data);
|
||||
B.startBlock(data);
|
||||
}
|
||||
|
||||
/** The inline part of a load site, entered for an ordinary object `d`:
|
||||
* tests the first `ways` cache entries (own properties, and prototype hits
|
||||
* when `proto`); each hit converts the (borrowed) value with `result` and
|
||||
* branches to `join`, recording its phi input. Anything else branches to
|
||||
* `miss`. */
|
||||
function inlineLoad(
|
||||
host: Host,
|
||||
d: string,
|
||||
ic: string,
|
||||
ways: number,
|
||||
proto: boolean,
|
||||
result: (cell: string) => string,
|
||||
incoming: string[],
|
||||
join: string,
|
||||
miss: string,
|
||||
): void {
|
||||
const B = host.B;
|
||||
if (ways === 0) {
|
||||
B.br(miss);
|
||||
return;
|
||||
}
|
||||
const shape = load(host, "ptr", d, 80);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = icmp ne ptr ${shape}, null`);
|
||||
let next = B.newLabel("ic.w");
|
||||
B.condBr(present, next, miss);
|
||||
for (let way = 0; way < ways; way++) {
|
||||
const base = way * 40;
|
||||
B.startBlock(next);
|
||||
next = way + 1 < ways ? B.newLabel("ic.w") : miss;
|
||||
const cached = load(host, "ptr", ic, base);
|
||||
const same = B.tmp();
|
||||
B.line(`${same} = icmp eq ptr ${shape}, ${cached}`);
|
||||
const matched = B.newLabel("ic.m");
|
||||
B.condBr(same, matched, next);
|
||||
B.startBlock(matched);
|
||||
const target = load(host, "ptr", ic, base + 8);
|
||||
const own = B.tmp();
|
||||
B.line(`${own} = icmp eq ptr ${target}, null`);
|
||||
const ownLabel = B.newLabel("ic.own");
|
||||
const protoLabel = proto ? B.newLabel("ic.proto") : miss;
|
||||
B.condBr(own, ownLabel, protoLabel);
|
||||
const hit = (holder: string): void => {
|
||||
const slot = load(host, "i64", ic, base + 32);
|
||||
const entry = entryAt(host, holder, slot);
|
||||
dataEntry(host, entry, miss);
|
||||
const value = load(host, "ptr", entry, 16);
|
||||
const out = result(value);
|
||||
incoming.push(`[ ${out}, %${B.currentLabel()} ]`);
|
||||
B.br(join);
|
||||
};
|
||||
B.startBlock(ownLabel);
|
||||
hit(d);
|
||||
if (!proto) continue;
|
||||
B.startBlock(protoLabel);
|
||||
const holder = load(host, "ptr", ic, base + 16);
|
||||
const prototype = load(host, "ptr", d, 96);
|
||||
const linked = B.tmp();
|
||||
B.line(`${linked} = icmp eq ptr ${prototype}, ${holder}`);
|
||||
const isLinked = B.newLabel("ic.pl");
|
||||
B.condBr(linked, isLinked, miss);
|
||||
B.startBlock(isLinked);
|
||||
const holderObject = B.newLabel("ic.po");
|
||||
kindIs(host, holder, DYN_KIND.OBJ, holderObject, miss);
|
||||
B.startBlock(holderObject);
|
||||
const holderShape = load(host, "ptr", holder, 80);
|
||||
const current = B.tmp();
|
||||
B.line(`${current} = icmp eq ptr ${holderShape}, ${target}`);
|
||||
const holderHit = B.newLabel("ic.ph");
|
||||
B.condBr(current, holderHit, miss);
|
||||
B.startBlock(holderHit);
|
||||
hit(holder);
|
||||
}
|
||||
}
|
||||
|
||||
/** The inline part of a store site that replaces an own data property,
|
||||
* entered for an ordinary object `d` outside an intact parse region:
|
||||
* tests the first `ways` cache entries; a hit calls `store` with the
|
||||
* address of the entry's value word, which must end its block by
|
||||
* branching to the site's join. */
|
||||
function inlineReplace(
|
||||
host: Host,
|
||||
d: string,
|
||||
ic: string,
|
||||
ways: number,
|
||||
store: (slot: string) => void,
|
||||
miss: string,
|
||||
): void {
|
||||
const B = host.B;
|
||||
if (ways === 0) {
|
||||
B.br(miss);
|
||||
return;
|
||||
}
|
||||
const shape = load(host, "ptr", d, 80);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = icmp ne ptr ${shape}, null`);
|
||||
let next = B.newLabel("ic.w");
|
||||
B.condBr(present, next, miss);
|
||||
for (let way = 0; way < ways; way++) {
|
||||
const base = way * 40;
|
||||
B.startBlock(next);
|
||||
next = way + 1 < ways ? B.newLabel("ic.w") : miss;
|
||||
const cached = load(host, "ptr", ic, base);
|
||||
const target = load(host, "ptr", ic, base + 8);
|
||||
const same = B.tmp();
|
||||
const replace = B.tmp();
|
||||
const ok = B.tmp();
|
||||
B.line(`${same} = icmp eq ptr ${shape}, ${cached}`);
|
||||
B.line(`${replace} = icmp eq ptr ${target}, ${shape}`);
|
||||
B.line(`${ok} = and i1 ${same}, ${replace}`);
|
||||
const matched = B.newLabel("ic.m");
|
||||
B.condBr(ok, matched, next);
|
||||
B.startBlock(matched);
|
||||
const slot = load(host, "i64", ic, base + 32);
|
||||
const entry = entryAt(host, d, slot);
|
||||
const flags = load(host, "i8", entry, 40);
|
||||
const writable = load(host, "i8", entry, 41);
|
||||
const accessor = B.tmp();
|
||||
const data = B.tmp();
|
||||
const canWrite = B.tmp();
|
||||
const writes = B.tmp();
|
||||
B.line(`${accessor} = and i8 ${flags}, 1`);
|
||||
B.line(`${data} = icmp eq i8 ${accessor}, 0`);
|
||||
B.line(`${canWrite} = icmp ne i8 ${writable}, 0`);
|
||||
B.line(`${writes} = and i1 ${data}, ${canWrite}`);
|
||||
const hit = B.newLabel("ic.st");
|
||||
B.condBr(writes, hit, miss);
|
||||
B.startBlock(hit);
|
||||
store(gep(host, entry, 16));
|
||||
}
|
||||
}
|
||||
|
||||
/** A property value (a dyn cell, `owned` or borrowed from its entry) as
|
||||
* the site's result type, owned. A jsval result reads a number cell as an
|
||||
* immediate, so arithmetic on it takes no reference and a later store can
|
||||
* update an unshared number cell in place; any other cell is carried under
|
||||
* the cell tag. */
|
||||
function asResult(host: Host, cell: string, e: IrExpr, owned: boolean | string): string {
|
||||
const B = host.B;
|
||||
/** Retains the cell unless the caller owns it. */
|
||||
const hold = (): string => {
|
||||
if (owned === true) return cell;
|
||||
if (owned === false) return host.retainFrame(cell, DYN);
|
||||
const borrowed = B.newLabel("icr.b");
|
||||
const done = B.newLabel("icr.h");
|
||||
const from = B.currentLabel();
|
||||
B.condBr(owned, done, borrowed);
|
||||
B.startBlock(borrowed);
|
||||
const retained = host.retainFrame(cell, DYN);
|
||||
const retainedEnd = B.currentLabel();
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const held = B.tmp();
|
||||
B.line(`${held} = phi ptr [ ${cell}, %${from} ], [ ${retained}, %${retainedEnd} ]`);
|
||||
return held;
|
||||
};
|
||||
if (e.type.kind !== "jsval") return hold();
|
||||
const number = B.newLabel("icr.num");
|
||||
const other = B.newLabel("icr.cell");
|
||||
const join = B.newLabel("icr.j");
|
||||
kindIs(host, cell, DYN_KIND.NUM, number, other);
|
||||
B.startBlock(number);
|
||||
const d = load(host, "double", cell, 16);
|
||||
const raw = B.tmp();
|
||||
const nan = B.tmp();
|
||||
const bits = B.tmp();
|
||||
B.line(`${raw} = bitcast double ${d} to i64`);
|
||||
B.line(`${nan} = fcmp uno double ${d}, ${d}`);
|
||||
B.line(`${bits} = select i1 ${nan}, i64 ${JS_BITS.canonicalNan}, i64 ${raw}`);
|
||||
const immediate = B.tmp();
|
||||
B.line(`${immediate} = inttoptr i64 ${bits} to ptr`);
|
||||
if (owned === true) {
|
||||
host.releaseFrame(cell, DYN);
|
||||
} else if (owned !== false) {
|
||||
const release = B.newLabel("icr.r");
|
||||
const released = B.newLabel("icr.rd");
|
||||
B.condBr(owned, release, released);
|
||||
B.startBlock(release);
|
||||
host.releaseFrame(cell, DYN);
|
||||
B.br(released);
|
||||
B.startBlock(released);
|
||||
}
|
||||
const numberEnd = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(other);
|
||||
const held = hold();
|
||||
const cellBits = B.tmp();
|
||||
const word = B.tmp();
|
||||
const tagged = B.tmp();
|
||||
B.line(`${cellBits} = ptrtoint ptr ${held} to i64`);
|
||||
B.line(`${word} = or i64 ${cellBits}, ${JS_BITS.cellTag}`);
|
||||
B.line(`${tagged} = inttoptr i64 ${word} to ptr`);
|
||||
const otherEnd = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi ptr [ ${immediate}, %${numberEnd} ], [ ${tagged}, %${otherEnd} ]`);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The helper call of a site's slow path, with the site's evaluated
|
||||
* (borrowed) arguments. Ordinary parameters take a reference; borrowed
|
||||
* ones lend it. Returns the owned result, or "" for a void helper. */
|
||||
function helperCall(host: Host, e: ExprOf<"call">, callee: IrFunction, args: LlValue[]): string {
|
||||
const B = host.B;
|
||||
const borrowed = host.callLifetimes.borrowed.get(e.callee);
|
||||
const argList = args
|
||||
.map((a, i) => {
|
||||
const type = callee.params[i]!.type;
|
||||
const value = borrowed?.has(i) ? a.name : host.retainFrame(a.name, a.type);
|
||||
return `${host.llType(type)} ${value}`;
|
||||
})
|
||||
.join(", ");
|
||||
const target = `@${borrowed ? mangleBorrowedFunction(e.callee) : host.callTarget(e.callee)}`;
|
||||
// The helper is the site's cold path: keep its body out of line.
|
||||
if (e.type.kind === "void") {
|
||||
B.line(`call void ${target}(${argList}) noinline cold`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
return "";
|
||||
}
|
||||
let t = B.tmp();
|
||||
B.line(`${t} = call ${host.llType(e.type)} ${target}(${argList}) noinline cold`);
|
||||
if (host.mayThrow.has(e.callee)) host.emitPendingCheck();
|
||||
if (borrowed && host.borrowedReturns.has(e.callee)) t = host.retainFrame(t, e.type);
|
||||
return t;
|
||||
}
|
||||
|
||||
function keyString(host: Host, key: string, e: IrExpr): string {
|
||||
return host.emitExpr({ kind: "strLit", value: key, type: STRING, loc: e.loc }).name;
|
||||
}
|
||||
|
||||
/** Emits a call to a classified helper as a cache site. */
|
||||
export function emitJsIcCall(
|
||||
host: Host,
|
||||
e: ExprOf<"call">,
|
||||
callee: IrFunction,
|
||||
site: JsIcSite,
|
||||
): LlValue {
|
||||
switch (site.kind) {
|
||||
case "get":
|
||||
return emitGet(host, e, callee, site.key);
|
||||
case "set":
|
||||
return emitSet(host, e, callee, site.key);
|
||||
case "keyed":
|
||||
return emitKeyed(host, e, callee);
|
||||
}
|
||||
}
|
||||
|
||||
function emitGet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string): LlValue {
|
||||
const B = host.B;
|
||||
const receiver = emitBorrowedInput(host, e.args[0]!);
|
||||
const name = keyString(host, key, e);
|
||||
const ic = host.jsIcSite();
|
||||
const object = B.newLabel("icg.obj");
|
||||
const probe = B.newLabel("icg.probe");
|
||||
const slow = B.newLabel("icg.slow");
|
||||
const join = B.newLabel("icg.j");
|
||||
const incoming: string[] = [];
|
||||
if (key === "length") {
|
||||
// An array's length is its element count (the generic read answers
|
||||
// it before any property table).
|
||||
const array = B.newLabel("icg.arr");
|
||||
const other = B.newLabel("icg.other");
|
||||
kindIs(host, receiver.name, DYN_KIND.ARR, array, other);
|
||||
B.startBlock(array);
|
||||
const len = load(host, "i64", receiver.name, 16);
|
||||
const count = B.tmp();
|
||||
B.line(`${count} = uitofp i64 ${len} to double`);
|
||||
incoming.push(`[ ${numberResult(host, count, e)}, %${B.currentLabel()} ]`);
|
||||
B.br(join);
|
||||
B.startBlock(other);
|
||||
}
|
||||
// Inline hits (borrowed values) and probe hits (owned ones) share one
|
||||
// conversion to the result type.
|
||||
const hit = B.newLabel("icg.hit");
|
||||
const hits: string[] = [];
|
||||
kindIs(host, receiver.name, DYN_KIND.OBJ, object, probe);
|
||||
B.startBlock(object);
|
||||
inlineLoad(host, receiver.name, ic, INLINE_WAYS, INLINE_PROTO, (cell) => cell, hits, hit, probe);
|
||||
B.startBlock(probe);
|
||||
host.declare(`declare ptr @scr_js_load_probe(ptr, ptr, ptr)`);
|
||||
const probed = B.tmp();
|
||||
B.line(`${probed} = call ptr @scr_js_load_probe(ptr ${receiver.name}, ptr ${name}, ptr ${ic})`);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${probed}, null`);
|
||||
const probeEnd = B.currentLabel();
|
||||
B.condBr(found, hit, slow);
|
||||
B.startBlock(hit);
|
||||
const cell = B.tmp();
|
||||
const owned = B.tmp();
|
||||
const ownedIn = hits.map((h) => h.replace(/^\[ [^,]+,/, "[ false,"));
|
||||
B.line(`${cell} = phi ptr ${[...hits, `[ ${probed}, %${probeEnd} ]`].join(", ")}`);
|
||||
B.line(`${owned} = phi i1 ${[...ownedIn, `[ true, %${probeEnd} ]`].join(", ")}`);
|
||||
{
|
||||
const out = asResult(host, cell, e, owned);
|
||||
incoming.push(`[ ${out}, %${B.currentLabel()} ]`);
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(slow);
|
||||
const result = helperCall(host, e, callee, [receiver]);
|
||||
incoming.push(`[ ${result}, %${B.currentLabel()} ]`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi ptr ${incoming.join(", ")}`);
|
||||
return host.own({ name: out, type: e.type });
|
||||
}
|
||||
|
||||
/** A number as the site's result type: an immediate for a jsval result,
|
||||
* a fresh cell (as the generic read makes) for a dyn one. */
|
||||
function numberResult(host: Host, d: string, e: IrExpr): string {
|
||||
const B = host.B;
|
||||
if (e.type.kind === "jsval") {
|
||||
// Counts are never NaN: the bits are the word.
|
||||
const bits = B.tmp();
|
||||
const word = B.tmp();
|
||||
B.line(`${bits} = bitcast double ${d} to i64`);
|
||||
B.line(`${word} = inttoptr i64 ${bits} to ptr`);
|
||||
return word;
|
||||
}
|
||||
host.declare(`declare ptr @scr_dyn_new_num(double)`);
|
||||
const cell = B.tmp();
|
||||
B.line(`${cell} = call ptr @scr_dyn_new_num(double ${d})`);
|
||||
return cell;
|
||||
}
|
||||
|
||||
function emitSet(host: Host, e: ExprOf<"call">, callee: IrFunction, key: string): LlValue {
|
||||
const B = host.B;
|
||||
const receiver = emitBorrowedInput(host, e.args[0]!);
|
||||
// A value the jsval pass left as `js.toDyn(x)` is stored from the jsval
|
||||
// itself: a number replaces the value of an unshared number cell in
|
||||
// place instead of boxing a new one.
|
||||
const stored = e.args[1]!;
|
||||
const word =
|
||||
stored.kind === "libCall" && stored.fn === "js.toDyn"
|
||||
? host.emitExpr(stored.args[0]!).name
|
||||
: null;
|
||||
const value = word === null ? emitBorrowedInput(host, stored) : null;
|
||||
const name = keyString(host, key, e);
|
||||
const ic = host.jsIcSite();
|
||||
const object = B.newLabel("ics.obj");
|
||||
const probe = B.newLabel("ics.probe");
|
||||
const slow = B.newLabel("ics.slow");
|
||||
const join = B.newLabel("ics.j");
|
||||
// The cell the probe and the helper store when the value is a jsval;
|
||||
// the statement's frame releases it.
|
||||
let boxedSlot: string | null = null;
|
||||
if (word !== null) {
|
||||
boxedSlot = B.slot();
|
||||
B.entryAllocas.push(`${boxedSlot} = alloca ptr`);
|
||||
B.line(`store ptr null, ptr ${boxedSlot}`);
|
||||
host.ownSlot(boxedSlot, DYN);
|
||||
}
|
||||
kindIs(host, receiver.name, DYN_KIND.OBJ, object, probe);
|
||||
B.startBlock(object);
|
||||
const region = load(host, "ptr", receiver.name, 72);
|
||||
const outside = B.tmp();
|
||||
B.line(`${outside} = icmp eq ptr ${region}, null`);
|
||||
const unshared = B.newLabel("ics.r");
|
||||
B.condBr(outside, unshared, probe);
|
||||
B.startBlock(unshared);
|
||||
host.declare(`declare ptr @scr_js_to_dyn_borrow(ptr)`);
|
||||
inlineReplace(
|
||||
host,
|
||||
receiver.name,
|
||||
ic,
|
||||
INLINE_WAYS,
|
||||
(slot) => {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = load ptr, ptr ${slot}`);
|
||||
if (word === null) {
|
||||
B.line(`store ptr ${host.retainHeap(value!.name, DYN)}, ptr ${slot}`);
|
||||
host.releaseHeap(old, DYN);
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
const bits = B.tmp();
|
||||
const isDouble = B.tmp();
|
||||
B.line(`${bits} = ptrtoint ptr ${word} to i64`);
|
||||
B.line(`${isDouble} = icmp ult i64 ${bits}, ${JS_BITS.limit}`);
|
||||
const number = B.newLabel("ics.dbl");
|
||||
const box = B.newLabel("ics.box");
|
||||
B.condBr(isDouble, number, box);
|
||||
B.startBlock(number);
|
||||
const oldKind = load(host, "i32", old, 8);
|
||||
const oldRc = load(host, "i64", old, 0);
|
||||
const isNumber = B.tmp();
|
||||
const unshared = B.tmp();
|
||||
const inPlace = B.tmp();
|
||||
B.line(`${isNumber} = icmp eq i32 ${oldKind}, ${DYN_KIND.NUM}`);
|
||||
B.line(`${unshared} = icmp eq i64 ${oldRc}, 1`);
|
||||
B.line(`${inPlace} = and i1 ${isNumber}, ${unshared}`);
|
||||
const update = B.newLabel("ics.inplace");
|
||||
B.condBr(inPlace, update, box);
|
||||
B.startBlock(update);
|
||||
const d = B.tmp();
|
||||
B.line(`${d} = bitcast i64 ${bits} to double`);
|
||||
B.line(`store double ${d}, ptr ${gep(host, old, 16)}`);
|
||||
B.br(join);
|
||||
B.startBlock(box);
|
||||
const cell = B.tmp();
|
||||
B.line(`${cell} = call ptr @scr_js_to_dyn_borrow(ptr ${word})`);
|
||||
B.line(`store ptr ${cell}, ptr ${slot}`);
|
||||
host.releaseHeap(old, DYN);
|
||||
B.br(join);
|
||||
},
|
||||
probe,
|
||||
);
|
||||
B.startBlock(probe);
|
||||
let cell: string;
|
||||
if (word !== null) {
|
||||
cell = B.tmp();
|
||||
B.line(`${cell} = call ptr @scr_js_to_dyn_borrow(ptr ${word})`);
|
||||
B.line(`store ptr ${cell}, ptr ${boxedSlot}`);
|
||||
} else {
|
||||
cell = value!.name;
|
||||
}
|
||||
host.declare(`declare zeroext i1 @scr_js_store_probe(ptr, ptr, ptr, ptr)`);
|
||||
const done = B.tmp();
|
||||
B.line(
|
||||
`${done} = call zeroext i1 @scr_js_store_probe(ptr ${receiver.name}, ptr ${name}, ptr ${cell}, ptr ${ic})`,
|
||||
);
|
||||
B.condBr(done, join, slow);
|
||||
B.startBlock(slow);
|
||||
helperCall(host, e, callee, [receiver, { name: cell, type: DYN }]);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
|
||||
/** A keyed site's key when it is a static number boxed for the helper
|
||||
* (`dynFrom(n)`, or `dynFrom(u)` of a local union with a number arm): the
|
||||
* site indexes with the double and boxes the key only on a miss. */
|
||||
interface NumberKey {
|
||||
/** i1: whether `value` holds the key (always for a static number). */
|
||||
present: string;
|
||||
value: string;
|
||||
/** Emits the key's cell (+1), exactly as the helper argument boxes it. */
|
||||
box: () => string;
|
||||
}
|
||||
|
||||
function numberKey(host: Host, key: IrExpr): NumberKey | null {
|
||||
if (key.kind !== "dynFrom" || key.liveRef !== undefined || key.fnName !== undefined) return null;
|
||||
const source = key.value;
|
||||
if (source.type.kind === "f64") {
|
||||
const value = host.emitExpr(source).name;
|
||||
// Request the converter where the boxing it replaces did: converters
|
||||
// are numbered in request order.
|
||||
const conv = host.dyn.toDynHelper(source.type);
|
||||
return {
|
||||
present: "true",
|
||||
value,
|
||||
box: () => {
|
||||
const cell = host.B.tmp();
|
||||
host.B.line(`${cell} = call ptr @${conv}(double ${value})`);
|
||||
return cell;
|
||||
},
|
||||
};
|
||||
}
|
||||
if (source.type.kind !== "union" || source.kind !== "varRef") return null;
|
||||
const unionId = source.type.unionId;
|
||||
const tag = host.unionsById.get(unionId)?.arms.findIndex((arm) => arm.kind === "f64") ?? -1;
|
||||
if (tag < 0) return null;
|
||||
const B = host.B;
|
||||
const present = host.emitExpr({
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value: source,
|
||||
type: BOOL,
|
||||
loc: key.loc,
|
||||
}).name;
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca double`);
|
||||
B.line(`store double 0.0, ptr ${slot}`);
|
||||
const number = B.newLabel("ick.k");
|
||||
const done = B.newLabel("ick.kd");
|
||||
B.condBr(present, number, done);
|
||||
B.startBlock(number);
|
||||
// Temps of a conditional block are released in it.
|
||||
host.frames.push([]);
|
||||
const d = host.emitExpr({
|
||||
kind: "unionNarrow",
|
||||
unionId,
|
||||
tag,
|
||||
value: source,
|
||||
type: F64,
|
||||
loc: key.loc,
|
||||
});
|
||||
B.line(`store double ${d.name}, ptr ${slot}`);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const value = B.tmp();
|
||||
B.line(`${value} = load double, ptr ${slot}`);
|
||||
return {
|
||||
present,
|
||||
value,
|
||||
box: () => {
|
||||
// The local is a plain read: boxing it again evaluates nothing else.
|
||||
host.frames.push([]);
|
||||
const boxed = host.emitExpr(key);
|
||||
host.toFrame(boxed);
|
||||
host.releaseTemps(host.frames.pop()!);
|
||||
return boxed.name;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function emitKeyed(host: Host, e: ExprOf<"call">, callee: IrFunction): LlValue {
|
||||
const B = host.B;
|
||||
const receiver = emitBorrowedInput(host, e.args[0]!);
|
||||
const keyExpr = e.args[1]!;
|
||||
const numeric = numberKey(host, keyExpr);
|
||||
const key = numeric === null ? emitBorrowedInput(host, keyExpr) : null;
|
||||
const ic = host.jsIcSite();
|
||||
const array = B.newLabel("ick.arr");
|
||||
const number = B.newLabel("ick.num");
|
||||
const probe = B.newLabel("ick.probe");
|
||||
const slow = B.newLabel("ick.slow");
|
||||
const join = B.newLabel("ick.j");
|
||||
const incoming: string[] = [];
|
||||
// The boxed key of the miss paths when the site indexes with a double;
|
||||
// the statement's frame releases it.
|
||||
let keySlot: string | null = null;
|
||||
if (numeric !== null) {
|
||||
keySlot = B.slot();
|
||||
B.entryAllocas.push(`${keySlot} = alloca ptr`);
|
||||
B.line(`store ptr null, ptr ${keySlot}`);
|
||||
host.ownSlot(keySlot, DYN);
|
||||
}
|
||||
kindIs(host, receiver.name, DYN_KIND.ARR, array, probe);
|
||||
B.startBlock(array);
|
||||
let index: string;
|
||||
if (numeric === null) {
|
||||
kindIs(host, key!.name, DYN_KIND.NUM, number, probe);
|
||||
B.startBlock(number);
|
||||
index = load(host, "double", key!.name, 16);
|
||||
} else {
|
||||
B.condBr(numeric.present, number, probe);
|
||||
B.startBlock(number);
|
||||
index = numeric.value;
|
||||
}
|
||||
const len = load(host, "i64", receiver.name, 16);
|
||||
const i = B.tmp();
|
||||
const back = B.tmp();
|
||||
const integral = B.tmp();
|
||||
const inRange = B.tmp();
|
||||
const nonNegative = B.tmp();
|
||||
const ok1 = B.tmp();
|
||||
const ok2 = B.tmp();
|
||||
// fptoui of a negative or out-of-range double is poison: test the
|
||||
// double's range before trusting the integer.
|
||||
B.line(`${nonNegative} = fcmp oge double ${index}, 0.0`);
|
||||
const lenD = B.tmp();
|
||||
B.line(`${lenD} = uitofp i64 ${len} to double`);
|
||||
B.line(`${inRange} = fcmp olt double ${index}, ${lenD}`);
|
||||
B.line(`${ok1} = and i1 ${nonNegative}, ${inRange}`);
|
||||
const ranged = B.newLabel("ick.rng");
|
||||
B.condBr(ok1, ranged, probe);
|
||||
B.startBlock(ranged);
|
||||
B.line(`${i} = fptoui double ${index} to i64`);
|
||||
B.line(`${back} = uitofp i64 ${i} to double`);
|
||||
B.line(`${integral} = fcmp oeq double ${back}, ${index}`);
|
||||
const presence = load(host, "ptr", receiver.name, 56);
|
||||
const dense = B.tmp();
|
||||
B.line(`${dense} = icmp eq ptr ${presence}, null`);
|
||||
B.line(`${ok2} = and i1 ${integral}, ${dense}`);
|
||||
const hit = B.newLabel("ick.hit");
|
||||
B.condBr(ok2, hit, probe);
|
||||
B.startBlock(hit);
|
||||
const items = load(host, "ptr", receiver.name, 32);
|
||||
const itemPtr = B.tmp();
|
||||
B.line(`${itemPtr} = getelementptr inbounds ptr, ptr ${items}, i64 ${i}`);
|
||||
const item = B.tmp();
|
||||
B.line(`${item} = load ptr, ptr ${itemPtr}`);
|
||||
const itemEnd = B.currentLabel();
|
||||
const shared = B.newLabel("ick.shared");
|
||||
B.br(shared);
|
||||
B.startBlock(probe);
|
||||
let keyCell: string;
|
||||
if (numeric === null) {
|
||||
keyCell = key!.name;
|
||||
} else {
|
||||
keyCell = numeric.box();
|
||||
B.line(`store ptr ${keyCell}, ptr ${keySlot}`);
|
||||
}
|
||||
host.declare(`declare ptr @scr_js_keyed_probe(ptr, ptr, ptr)`);
|
||||
const probed = B.tmp();
|
||||
B.line(
|
||||
`${probed} = call ptr @scr_js_keyed_probe(ptr ${receiver.name}, ptr ${keyCell}, ptr ${ic})`,
|
||||
);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${probed}, null`);
|
||||
const probeEnd = B.currentLabel();
|
||||
B.condBr(found, shared, slow);
|
||||
B.startBlock(shared);
|
||||
{
|
||||
const cell = B.tmp();
|
||||
const owned = B.tmp();
|
||||
B.line(`${cell} = phi ptr [ ${item}, %${itemEnd} ], [ ${probed}, %${probeEnd} ]`);
|
||||
B.line(`${owned} = phi i1 [ false, %${itemEnd} ], [ true, %${probeEnd} ]`);
|
||||
const out = asResult(host, cell, e, owned);
|
||||
incoming.push(`[ ${out}, %${B.currentLabel()} ]`);
|
||||
}
|
||||
B.br(join);
|
||||
B.startBlock(slow);
|
||||
const result = helperCall(host, e, callee, [receiver, { name: keyCell, type: DYN }]);
|
||||
incoming.push(`[ ${result}, %${B.currentLabel()} ]`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi ptr ${incoming.join(", ")}`);
|
||||
return host.own({ name: out, type: e.type });
|
||||
}
|
||||
|
||||
/** dynInvoke's method read on an ordinary object through the site's
|
||||
* cache, else scr_dyn_prepare_method. Returns the owned method (or NULL,
|
||||
* which scr_dyn_invoke_prepared answers with the full dispatch). */
|
||||
export function emitJsIcMethod(host: Host, e: ExprOf<"dynInvoke">, receiver: string): string {
|
||||
const B = host.B;
|
||||
const method = e.method;
|
||||
const ic = host.jsIcSite();
|
||||
const name = keyString(host, method, e);
|
||||
host.declare(`declare ptr @scr_js_load_probe(ptr, ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_prepare_method(ptr, ptr)`);
|
||||
const object = B.newLabel("icm.obj");
|
||||
const probe = B.newLabel("icm.probe");
|
||||
const slow = B.newLabel("icm.slow");
|
||||
const join = B.newLabel("icm.j");
|
||||
const incoming: string[] = [];
|
||||
kindIs(host, receiver, DYN_KIND.OBJ, object, probe);
|
||||
B.startBlock(object);
|
||||
inlineLoad(
|
||||
host,
|
||||
receiver,
|
||||
ic,
|
||||
INLINE_WAYS,
|
||||
INLINE_PROTO,
|
||||
(cell) => host.retainFrame(cell, DYN),
|
||||
incoming,
|
||||
join,
|
||||
probe,
|
||||
);
|
||||
B.startBlock(probe);
|
||||
const probed = B.tmp();
|
||||
B.line(`${probed} = call ptr @scr_js_load_probe(ptr ${receiver}, ptr ${name}, ptr ${ic})`);
|
||||
const found = B.tmp();
|
||||
B.line(`${found} = icmp ne ptr ${probed}, null`);
|
||||
incoming.push(`[ ${probed}, %${B.currentLabel()} ]`);
|
||||
B.condBr(found, join, slow);
|
||||
B.startBlock(slow);
|
||||
const prepared = B.tmp();
|
||||
B.line(
|
||||
`${prepared} = call ptr @scr_dyn_prepare_method(ptr ${receiver}, ptr ${host.cstr(method)}) cold`,
|
||||
);
|
||||
incoming.push(`[ ${prepared}, %${B.currentLabel()} ]`);
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi ptr ${incoming.join(", ")}`);
|
||||
return out;
|
||||
}
|
||||
@@ -205,6 +205,105 @@ function call(host: Host, ret: string, fn: string, args: [string, string][]): st
|
||||
return r;
|
||||
}
|
||||
|
||||
/** `x % y` on two doubles: an integer remainder when both are integers of
|
||||
* magnitude below 2^31 and y is nonzero (the result keeps the dividend's
|
||||
* sign, so a zero result of a negative dividend, including -0, is -0),
|
||||
* else frem (libm's fmod). */
|
||||
function remainder(host: Host, x: string, y: string): string {
|
||||
const B = host.B;
|
||||
host.declare(`declare double @llvm.fabs.f64(double)`);
|
||||
const ax = B.tmp();
|
||||
const ay = B.tmp();
|
||||
const smallX = B.tmp();
|
||||
const smallY = B.tmp();
|
||||
const small = B.tmp();
|
||||
B.line(`${ax} = call double @llvm.fabs.f64(double ${x})`);
|
||||
B.line(`${ay} = call double @llvm.fabs.f64(double ${y})`);
|
||||
B.line(`${smallX} = fcmp olt double ${ax}, 2147483648.0`);
|
||||
B.line(`${smallY} = fcmp olt double ${ay}, 2147483648.0`);
|
||||
B.line(`${small} = and i1 ${smallX}, ${smallY}`);
|
||||
const ints = B.newLabel("js.rem.i");
|
||||
const exact = B.newLabel("js.rem.x");
|
||||
const float = B.newLabel("js.rem.f");
|
||||
const join = B.newLabel("js.rem.j");
|
||||
B.condBr(small, ints, float);
|
||||
B.startBlock(ints);
|
||||
const xi = B.tmp();
|
||||
const yi = B.tmp();
|
||||
const xb = B.tmp();
|
||||
const yb = B.tmp();
|
||||
const xInt = B.tmp();
|
||||
const yInt = B.tmp();
|
||||
const nonzero = B.tmp();
|
||||
const both = B.tmp();
|
||||
const ok = B.tmp();
|
||||
B.line(`${xi} = fptosi double ${x} to i64`);
|
||||
B.line(`${yi} = fptosi double ${y} to i64`);
|
||||
B.line(`${xb} = sitofp i64 ${xi} to double`);
|
||||
B.line(`${yb} = sitofp i64 ${yi} to double`);
|
||||
B.line(`${xInt} = fcmp oeq double ${xb}, ${x}`);
|
||||
B.line(`${yInt} = fcmp oeq double ${yb}, ${y}`);
|
||||
B.line(`${nonzero} = icmp ne i64 ${yi}, 0`);
|
||||
B.line(`${both} = and i1 ${xInt}, ${yInt}`);
|
||||
B.line(`${ok} = and i1 ${both}, ${nonzero}`);
|
||||
B.condBr(ok, exact, float);
|
||||
B.startBlock(exact);
|
||||
const r = B.tmp();
|
||||
const rd = B.tmp();
|
||||
const xBits = B.tmp();
|
||||
const negative = B.tmp();
|
||||
const zero = B.tmp();
|
||||
const negZero = B.tmp();
|
||||
const intResult = B.tmp();
|
||||
B.line(`${r} = srem i64 ${xi}, ${yi}`);
|
||||
B.line(`${rd} = sitofp i64 ${r} to double`);
|
||||
B.line(`${xBits} = bitcast double ${x} to i64`);
|
||||
B.line(`${negative} = icmp slt i64 ${xBits}, 0`);
|
||||
B.line(`${zero} = icmp eq i64 ${r}, 0`);
|
||||
B.line(`${negZero} = and i1 ${negative}, ${zero}`);
|
||||
B.line(`${intResult} = select i1 ${negZero}, double -0.0, double ${rd}`);
|
||||
const exactEnd = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(float);
|
||||
const fr = B.tmp();
|
||||
B.line(`${fr} = frem double ${x}, ${y}`);
|
||||
const floatEnd = B.currentLabel();
|
||||
B.br(join);
|
||||
B.startBlock(join);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi double [ ${intResult}, %${exactEnd} ], [ ${fr}, %${floatEnd} ]`);
|
||||
return out;
|
||||
}
|
||||
|
||||
const BITWISE_OPS: Readonly<Record<string, string>> = {
|
||||
"&": "and",
|
||||
"|": "or",
|
||||
"^": "xor",
|
||||
"<<": "shl",
|
||||
">>": "ashr",
|
||||
">>>": "lshr",
|
||||
};
|
||||
|
||||
/** A double's ToInt32 when its magnitude is below 2^63: truncation toward
|
||||
* zero, then the low 32 bits; branches to `slow` otherwise (NaN and the
|
||||
* infinities included). */
|
||||
function int32Operand(host: Host, d: string, slow: string): string {
|
||||
const B = host.B;
|
||||
host.declare(`declare double @llvm.fabs.f64(double)`);
|
||||
const magnitude = B.tmp();
|
||||
const small = B.tmp();
|
||||
B.line(`${magnitude} = call double @llvm.fabs.f64(double ${d})`);
|
||||
B.line(`${small} = fcmp olt double ${magnitude}, 9223372036854775808.0`);
|
||||
const ok = B.newLabel("js.int");
|
||||
B.condBr(small, ok, slow);
|
||||
B.startBlock(ok);
|
||||
const wide = B.tmp();
|
||||
const i = B.tmp();
|
||||
B.line(`${wide} = fptosi double ${d} to i64`);
|
||||
B.line(`${i} = trunc i64 ${wide} to i32`);
|
||||
return i;
|
||||
}
|
||||
|
||||
const ARITH_OPS: Readonly<Record<string, string>> = {
|
||||
"-": "fsub",
|
||||
"*": "fmul",
|
||||
@@ -334,8 +433,8 @@ export function emitJsLibCall(host: Host, e: LibCallExpr): LlValue {
|
||||
(slow) => {
|
||||
const x = a.number(slow);
|
||||
const y = b.number(slow);
|
||||
const r = B.tmp();
|
||||
B.line(`${r} = ${inst} double ${x}, ${y}`);
|
||||
const r = op === "%" ? remainder(host, x, y) : B.tmp();
|
||||
if (op !== "%") B.line(`${r} = ${inst} double ${x}, ${y}`);
|
||||
return boxDouble(host, r);
|
||||
},
|
||||
runtime,
|
||||
@@ -410,17 +509,54 @@ export function emitJsLibCall(host: Host, e: LibCallExpr): LlValue {
|
||||
return { name: out, type: e.type };
|
||||
}
|
||||
case "js.bitwise": {
|
||||
const a = host.emitExpr(e.args[0]!).name;
|
||||
const b = host.emitExpr(e.args[1]!).name;
|
||||
const op = literalOp(e.args[2]!);
|
||||
const inst = op !== null ? BITWISE_OPS[op] : undefined;
|
||||
if (inst === undefined) {
|
||||
const a = host.emitExpr(e.args[0]!).name;
|
||||
const b = host.emitExpr(e.args[1]!).name;
|
||||
const opArg = host.emitExpr(e.args[2]!).name;
|
||||
const r = call(host, "ptr", "scr_js_bitwise", [
|
||||
["ptr", a],
|
||||
["ptr", b],
|
||||
["ptr", opArg],
|
||||
]);
|
||||
const out = jsval(r);
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
// Two numbers of magnitude below 2^63: ToInt32 is truncation and
|
||||
// wrapping, so the operation runs on i32; anything else (NaN, the
|
||||
// infinities, larger magnitudes) takes the runtime.
|
||||
const a = operand(host, e.args[0]!);
|
||||
const b = operand(host, e.args[1]!);
|
||||
const opArg = host.emitExpr(e.args[2]!).name;
|
||||
const r = call(host, "ptr", "scr_js_bitwise", [
|
||||
["ptr", a],
|
||||
["ptr", b],
|
||||
["ptr", opArg],
|
||||
]);
|
||||
const out = jsval(r);
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
const out = withSlowPath(
|
||||
host,
|
||||
"ptr",
|
||||
(slow) => {
|
||||
const x = int32Operand(host, a.number(slow), slow);
|
||||
const y = int32Operand(host, b.number(slow), slow);
|
||||
const r = B.tmp();
|
||||
const d = B.tmp();
|
||||
if (op === "<<" || op === ">>" || op === ">>>") {
|
||||
const count = B.tmp();
|
||||
B.line(`${count} = and i32 ${y}, 31`);
|
||||
B.line(`${r} = ${inst} i32 ${x}, ${count}`);
|
||||
} else {
|
||||
B.line(`${r} = ${inst} i32 ${x}, ${y}`);
|
||||
}
|
||||
B.line(`${d} = ${op === ">>>" ? "uitofp" : "sitofp"} i32 ${r} to double`);
|
||||
return boxDouble(host, d);
|
||||
},
|
||||
() =>
|
||||
call(host, "ptr", "scr_js_bitwise", [
|
||||
["ptr", a.word()],
|
||||
["ptr", b.word()],
|
||||
["ptr", opArg],
|
||||
]),
|
||||
true,
|
||||
);
|
||||
return jsval(out);
|
||||
}
|
||||
default:
|
||||
throw new InternalCompilerError(`llvm emitter bug: unknown jsval builtin ${e.fn}`);
|
||||
|
||||
@@ -82,6 +82,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"util.inherits": "scr_util_inherits",
|
||||
"util.deprecate": "scr_util_deprecate",
|
||||
"util.callbackify": "scr_util_callbackify",
|
||||
"util.promisifyDyn": "scr_util_promisify",
|
||||
"util.debuglog": "scr_util_debuglog",
|
||||
"util.typeIs": "scr_dyn_util_type_is",
|
||||
"util.typeValue": "scr_dyn_util_type_value",
|
||||
@@ -951,6 +952,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.iterPack": "scr_dyn_iter_pack",
|
||||
"dyn.destructureRest": "scr_dyn_destructure_rest",
|
||||
"dyn.iterator": "scr_dyn_iterator",
|
||||
"dyn.asyncIteratorOpen": "scr_dyn_async_iterator_open",
|
||||
"dyn.asyncIteratorWasSync": "scr_dyn_async_iterator_was_sync",
|
||||
"dyn.arrayFromIterator": "scr_dyn_array_from_iterator",
|
||||
"dyn.iteratorResult": "scr_dyn_iterator_result",
|
||||
"dyn.iteratorCanStep": "scr_dyn_iterator_can_step",
|
||||
@@ -1572,6 +1575,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
// The wrappers schedule warnings and callbacks on the tick queue.
|
||||
"util.deprecate",
|
||||
"util.callbackify",
|
||||
"util.promisifyDyn",
|
||||
"util.debuglog",
|
||||
"worker.new",
|
||||
"worker.parentPort",
|
||||
|
||||
@@ -2,6 +2,7 @@ import { expect, test } from "vitest";
|
||||
import { F64, type IrExpr, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
isSpeculableValue,
|
||||
planProvenIntegerLoop,
|
||||
planSpeculativeLoop,
|
||||
speculativeReads,
|
||||
} from "./speculative-integers.js";
|
||||
@@ -194,3 +195,74 @@ test("loops that only count and compare keep one copy", () => {
|
||||
]);
|
||||
expect(planSpeculativeLoop(scaled as never, all)?.locals.has("n")).toBe(true);
|
||||
});
|
||||
|
||||
test("bounded cursors with proven writes are shadowed in a proven integer loop", () => {
|
||||
// for (i = 0; i < n; i++) { if (a < b) run = run + 1; else run = 0;
|
||||
// if (run > longest) longest = run; }
|
||||
const sum = bin("+", ref("run"), num(1));
|
||||
const step = assign("run", sum);
|
||||
const reset = assign("run", num(0));
|
||||
const copy = assign("longest", ref("run"));
|
||||
const forLoop = (body: IrStmt[]): IrStmt => ({
|
||||
kind: "for",
|
||||
init: null,
|
||||
cond: bin("<", ref("i"), ref("n")),
|
||||
update: assign("i", bin("+", ref("i"), num(1))),
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
const branch = (then: IrStmt[], else_: IrStmt[] | null): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: bin("<", ref("a"), ref("b")),
|
||||
then,
|
||||
else_,
|
||||
loc,
|
||||
});
|
||||
const body = [branch([step], [reset]), branch([copy], null)];
|
||||
const range = { min: 0, max: 100 };
|
||||
const cursors = new Map([
|
||||
["run", range],
|
||||
["longest", range],
|
||||
]);
|
||||
const plan = planProvenIntegerLoop(forLoop(body) as never, cursors, all, () => false, all)!;
|
||||
expect([...plan.locals]).toEqual([
|
||||
["run", range],
|
||||
["longest", range],
|
||||
]);
|
||||
expect(plan.writes).toEqual(new Set([step, reset, copy]));
|
||||
// A binding the loop does not bound keeps its double.
|
||||
const only = planProvenIntegerLoop(
|
||||
forLoop(body) as never,
|
||||
new Map([["run", range]]),
|
||||
all,
|
||||
() => false,
|
||||
all,
|
||||
)!;
|
||||
expect([...only.locals.keys()]).toEqual(["run"]);
|
||||
// An unproven sum leaves `run` unshadowed, and its copy reads `run`
|
||||
// where nothing proves it integral.
|
||||
const unproven = (e: IrExpr): boolean => e !== sum && e.kind !== "varRef";
|
||||
expect(
|
||||
planProvenIntegerLoop(forLoop(body) as never, cursors, all, () => false, unproven),
|
||||
).toBeNull();
|
||||
// An expression-position write refuses the binding.
|
||||
const inner: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "incDec", op: "+", prefix: false, localId: "longest", type: F64, loc },
|
||||
loc,
|
||||
};
|
||||
expect([
|
||||
...planProvenIntegerLoop(
|
||||
forLoop([...body, inner]) as never,
|
||||
cursors,
|
||||
all,
|
||||
() => false,
|
||||
all,
|
||||
)!.locals.keys(),
|
||||
]).toEqual(["run"]);
|
||||
// A labeled jump may leave past the write-back.
|
||||
const jump: IrStmt = { kind: "break", label: "outer", loc };
|
||||
expect(
|
||||
planProvenIntegerLoop(forLoop([...body, jump]) as never, cursors, all, () => false, all),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { withinInt32 } from "../../ir/integer-ranges.js";
|
||||
import { type IntegerRange, withinInt32 } from "../../ir/integer-ranges.js";
|
||||
|
||||
/** Speculative integer loops.
|
||||
*
|
||||
@@ -410,6 +410,97 @@ export function planSpeculativeLoop(
|
||||
return { locals, writes: speculated, observerFree };
|
||||
}
|
||||
|
||||
/** A proven integer loop. The bindings integer-ranges.ts bounds over a for
|
||||
* loop (`loopRanges`) hold an integer on entry and at every write, so the
|
||||
* loop keeps them in i64 shadow slots without a second copy or checks, and
|
||||
* writes their doubles once, when it is left. */
|
||||
export interface ProvenLoopPlan {
|
||||
/** Shadowed bindings and their loop ranges. */
|
||||
locals: Map<string, IntegerRange>;
|
||||
/** Statement-level writes computed as integers. */
|
||||
writes: Set<IrStmt>;
|
||||
}
|
||||
|
||||
/** Plan a proven integer loop. `cursors` are the loop's bounded bindings,
|
||||
* `eligible` the locals a shadow may stand for, `integral` those the emitter
|
||||
* already holds as integers and `proven` the expressions the range analysis
|
||||
* proves integral. A shadowed binding's double is stale until the loop is
|
||||
* left, so every exit has to pass the write-back: the caller plans only
|
||||
* loops where an exception leaves the function, and a labeled jump (which
|
||||
* may leave past the write-back) keeps the loop ordinary. */
|
||||
export function planProvenIntegerLoop(
|
||||
loop: SpeculativeLoop,
|
||||
cursors: ReadonlyMap<string, IntegerRange>,
|
||||
eligible: (localId: string) => boolean,
|
||||
integral: (localId: string) => boolean,
|
||||
proven: (e: IrExpr) => boolean,
|
||||
): ProvenLoopPlan | null {
|
||||
if (cursors.size === 0) return null;
|
||||
const writes = new Map<string, IrStmt[]>();
|
||||
const refused = new Set<string>();
|
||||
let rejected = false;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") refused.add(e.localId);
|
||||
return everyExprChild(e, expr, nested);
|
||||
};
|
||||
const nested = (s: IrStmt): boolean => {
|
||||
if (s.kind === "assign" || s.kind === "varDecl") refused.add(s.localId);
|
||||
return everyStmtChild(s, expr, nested);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (rejected) return false;
|
||||
if (
|
||||
!SIMPLE_STATEMENTS.has(s.kind) ||
|
||||
((s.kind === "break" || s.kind === "continue") && s.label !== undefined)
|
||||
) {
|
||||
rejected = true;
|
||||
return false;
|
||||
}
|
||||
const target = s.kind === "varDecl" ? null : speculativeWriteTarget(s);
|
||||
if (target) {
|
||||
if (target.value !== null && cursors.has(target.localId)) {
|
||||
const list = writes.get(target.localId) ?? [];
|
||||
list.push(s);
|
||||
writes.set(target.localId, list);
|
||||
} else refused.add(target.localId);
|
||||
return target.value === null || expr(target.value);
|
||||
}
|
||||
if (s.kind === "varDecl") refused.add(s.localId);
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
if (loop.cond) expr(loop.cond);
|
||||
if (loop.kind === "for" && loop.update) nested(loop.update);
|
||||
for (const s of loop.body) stmt(s);
|
||||
if (rejected) return null;
|
||||
const locals = new Map<string, IntegerRange>();
|
||||
for (const [id, list] of writes) {
|
||||
if (refused.has(id) || !eligible(id)) continue;
|
||||
const exact = list.every((s) => {
|
||||
const value = speculativeWriteTarget(s)!.value!;
|
||||
return isSpeculableValue(value, false) && proven(value);
|
||||
});
|
||||
if (exact) locals.set(id, cursors.get(id)!);
|
||||
}
|
||||
// Every binding a shadowed write reads is shadowed, already an integer,
|
||||
// or proven integral where it is read.
|
||||
const grounded = (e: IrExpr): boolean =>
|
||||
e.kind === "varRef"
|
||||
? locals.has(e.localId) || integral(e.localId) || proven(e)
|
||||
: everyExprChild(e, grounded, () => false);
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const id of [...locals.keys()])
|
||||
if (!writes.get(id)!.every((s) => grounded(speculativeWriteTarget(s)!.value!))) {
|
||||
locals.delete(id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (locals.size === 0) return null;
|
||||
const planned = new Set<IrStmt>();
|
||||
for (const id of locals.keys()) for (const s of writes.get(id)!) planned.add(s);
|
||||
return { locals, writes: planned };
|
||||
}
|
||||
|
||||
const SAFE = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
/** The range every speculative shadow holds. */
|
||||
@@ -501,11 +592,14 @@ export function emitSpeculativeInteger(
|
||||
e: IrExpr,
|
||||
bail: (condition: string) => void,
|
||||
reads: ReadonlyMap<IrExpr, LlValue> = new Map(),
|
||||
integral = false,
|
||||
): string {
|
||||
const B = host.B;
|
||||
// A value the range analysis already proves takes the ordinary integer
|
||||
// emission, which needs no checks.
|
||||
const proven = e.kind === "bin" ? host.integerRanges.get(e) : undefined;
|
||||
// emission, which needs no checks. A proven integer loop's write
|
||||
// (`integral`) is proven throughout and computes its operands as integers
|
||||
// instead: the ordinary emission of a sum past int32 is a double.
|
||||
const proven = e.kind === "bin" && !integral ? host.integerRanges.get(e) : undefined;
|
||||
if (proven && proven.min >= -SAFE && proven.max <= SAFE && speculativeReads(e).length === 0)
|
||||
return speculativeLeaf(host, host.emitExpr(e), e, bail);
|
||||
switch (e.kind) {
|
||||
@@ -533,8 +627,8 @@ export function emitSpeculativeInteger(
|
||||
}
|
||||
case "bin": {
|
||||
if (e.op !== "+" && e.op !== "-" && e.op !== "*" && e.op !== "%") break;
|
||||
const a = emitSpeculativeInteger(host, e.left, bail, reads);
|
||||
const b = emitSpeculativeInteger(host, e.right, bail, reads);
|
||||
const a = emitSpeculativeInteger(host, e.left, bail, reads, integral);
|
||||
const b = emitSpeculativeInteger(host, e.right, bail, reads, integral);
|
||||
const result = B.tmp();
|
||||
if (e.op === "+" || e.op === "-") {
|
||||
// With one operand's range known, the other is checked against the
|
||||
@@ -590,7 +684,7 @@ export function emitSpeculativeInteger(
|
||||
}
|
||||
case "unary": {
|
||||
if (e.op !== "-") break;
|
||||
const a = emitSpeculativeInteger(host, e.operand, bail, reads);
|
||||
const a = emitSpeculativeInteger(host, e.operand, bail, reads, integral);
|
||||
const zero = B.tmp(),
|
||||
result = B.tmp();
|
||||
// -0 is not an integer result.
|
||||
|
||||
@@ -49,6 +49,9 @@ export interface NativeLinkFeatures {
|
||||
/** Own properties JavaScript adds to children: scr_dyn_handle_props.c.
|
||||
* Absent (false) for programs without them. */
|
||||
handleProps?: boolean;
|
||||
/** for await over checked-dynamic values: scr_dyn_for_await.c. Absent
|
||||
* (false) for programs without it. */
|
||||
dynForAwait?: boolean;
|
||||
/** events.once with options: scr_events_once_signal.c. Absent (not
|
||||
* false) for programs without it. */
|
||||
eventsOnceSignal?: boolean;
|
||||
@@ -64,6 +67,7 @@ export interface NativeLinkFeatures {
|
||||
bigint: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
utilPromisify: boolean;
|
||||
dynBuiltinClass: boolean;
|
||||
dynBuiltinStream: boolean;
|
||||
parseArgs: boolean;
|
||||
@@ -74,6 +78,8 @@ export interface NativeLinkFeatures {
|
||||
dgram: boolean;
|
||||
/** node:dns/promises: scr_dns_promises.c. */
|
||||
dnsPromises: boolean;
|
||||
/** dgram sockets as untyped values: scr_dgram_dyn.c. */
|
||||
dgramDyn: boolean;
|
||||
watch: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
|
||||
@@ -118,6 +118,8 @@ export interface CcOptions {
|
||||
streamDyn?: boolean;
|
||||
/** Own properties JavaScript adds to children: compiles scr_dyn_handle_props.c. */
|
||||
handleProps?: boolean;
|
||||
/** for await over checked-dynamic values: compiles scr_dyn_for_await.c. */
|
||||
dynForAwait?: boolean;
|
||||
/** events.once with options (events.onceSignal): compiles
|
||||
* scr_events_once_signal.c. */
|
||||
eventsOnceSignal?: boolean;
|
||||
@@ -165,6 +167,10 @@ export interface CcOptions {
|
||||
* line (escape-only programs ride the always-linked component encoder
|
||||
* and never flip this). */
|
||||
qs?: boolean;
|
||||
/** The program promisifies an untyped function (util.promisify over
|
||||
* `any`): compiles scr_util_promisify.c, which also references the
|
||||
* dyn-async and dyn-invoke units. */
|
||||
utilPromisify?: boolean;
|
||||
/** The program uses EventEmitter or a stream class as an untyped
|
||||
* constructor value (a constructor function's base): compiles
|
||||
* scr_dyn_builtin_class.c, which also references the inspection,
|
||||
@@ -205,6 +211,10 @@ export interface CcOptions {
|
||||
* compiles scr_dns_promises.c over the dgram unit, so callback-only
|
||||
* binaries keep their exact link line. */
|
||||
dnsPromises?: boolean;
|
||||
/** A dgram socket converts to or from an untyped value: compiles
|
||||
* scr_dgram_dyn.c over the dgram unit, so typed dgram binaries keep
|
||||
* their exact link line. */
|
||||
dgramDyn?: boolean;
|
||||
|
||||
/** The program uses fs.watch (moduleUsesFsWatch on the IR): compiles
|
||||
* scr_watch.c into the binary — the net gating precedent, so watch-free
|
||||
|
||||
@@ -185,7 +185,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
// Builtin functions read as values in JavaScript dependencies.
|
||||
unit((u) => u.opts.builtinValues === true, "scr_builtin_values.c"),
|
||||
unit((u) => u.opts.dc, "scr_dc.c"),
|
||||
unit((u) => u.opts.dynTier, "scr_js_value.c"),
|
||||
unit((u) => u.opts.dynTier, "scr_js_value.c", "scr_js_object.c"),
|
||||
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
|
||||
unit((u) => u.opts.wasm, "scr_wasm.c"),
|
||||
unit(
|
||||
@@ -246,6 +246,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
(u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex,
|
||||
"scr_util_callable.c",
|
||||
),
|
||||
// util.promisify over an untyped function: dyn promises and calls.
|
||||
unit((u) => u.opts.utilPromisify && u.opts.dynAsync && u.opts.dynInvoke, "scr_util_promisify.c"),
|
||||
unit(
|
||||
(u) =>
|
||||
u.opts.dynBuiltinClass &&
|
||||
@@ -275,6 +277,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.streamDyn === true, "scr_stream_dyn.c"),
|
||||
// Own properties JavaScript adds to children.
|
||||
unit((u) => u.opts.handleProps === true, "scr_dyn_handle_props.c"),
|
||||
// for await over checked-dynamic values.
|
||||
unit((u) => u.opts.dynForAwait === true, "scr_dyn_for_await.c"),
|
||||
// FileHandle streams reference both the stream unit and FileHandle.
|
||||
unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"),
|
||||
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
|
||||
@@ -288,6 +292,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit((u) => u.opts.http2, "scr_http2.c"),
|
||||
unit((u) => u.opts.dgram, "scr_dgram.c"),
|
||||
unit((u) => u.opts.dgram && u.opts.dnsPromises, "scr_dns_promises.c"),
|
||||
unit((u) => u.opts.dgram && u.opts.dgramDyn, "scr_dgram_dyn.c"),
|
||||
unit((u) => u.opts.watch, "scr_watch.c"),
|
||||
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
|
||||
unit((u) => u.opts.nodeTest, "scr_test.c"),
|
||||
@@ -338,6 +343,9 @@ const RUNTIME_DEFINES: readonly {
|
||||
when: (opts: CcOptions) => boolean | undefined;
|
||||
}[] = [
|
||||
{ define: "SCR_TEXT_DECODER_LEGACY", when: (opts) => opts.textDecoderLegacy },
|
||||
// Checked objects carry the dynamic tier's shapes (scr_json.c's SCR_JS_TIER build); only
|
||||
// programs with dynamic-tier code select it.
|
||||
{ define: "SCR_JS_TIER", when: (opts) => opts.dynTier === true },
|
||||
{ define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace },
|
||||
{ define: "SCR_STDIO_EXTENDED", when: (opts) => opts.stdioExtended },
|
||||
{ define: "SCR_YIN", when: (opts) => opts.youngNursery },
|
||||
|
||||
@@ -112,7 +112,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
...(opts.assert && opts.bigint ? ["scr_bigint_assert.c"] : []),
|
||||
...(opts.emitter ? ["scr_events_emitter.c", "scr_dyn_handle.c"] : []),
|
||||
...(opts.dynInvoke ? ["scr_dyn_invoke.c"] : []),
|
||||
...(opts.dynTier === true ? ["scr_js_value.c"] : []),
|
||||
...(opts.dynTier === true ? ["scr_js_value.c", "scr_js_object.c"] : []),
|
||||
...(opts.zlib ? ["scr_zlib.c"] : []),
|
||||
...(opts.copying ? ["scr_copying.c"] : []),
|
||||
];
|
||||
@@ -129,6 +129,7 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
"-DSCR_LIB",
|
||||
...(opts.threadInstances ? ["-DSCR_THREAD_INSTANCES"] : []),
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
...(opts.dynTier === true ? ["-DSCR_JS_TIER"] : []),
|
||||
"-I",
|
||||
rtDir,
|
||||
...(regex ? ["-I", vendorEngineDir()] : []),
|
||||
|
||||
@@ -86,6 +86,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
dynTier: boolean;
|
||||
streamDyn: boolean;
|
||||
handleProps: boolean;
|
||||
dynForAwait: boolean;
|
||||
eventsOnceSignal: boolean;
|
||||
dynBufferNum: boolean;
|
||||
builtinValues: boolean;
|
||||
@@ -282,6 +283,7 @@ export function effectiveRuntimeFeatures(
|
||||
dynTier: features.dynTier === true,
|
||||
streamDyn: features.streamDyn === true,
|
||||
handleProps: features.handleProps === true,
|
||||
dynForAwait: features.dynForAwait === true,
|
||||
eventsOnceSignal: features.eventsOnceSignal === true,
|
||||
dynBufferNum: features.dynBufferNum === true,
|
||||
builtinValues: features.builtinValues === true,
|
||||
|
||||
@@ -107,6 +107,7 @@ const features = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
@@ -116,6 +117,7 @@ const features = {
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -44,6 +44,7 @@ const BASE: NativeLinkFeatures = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
@@ -53,6 +54,7 @@ const BASE: NativeLinkFeatures = {
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function warmNativeCaches(
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
@@ -59,6 +60,7 @@ export async function warmNativeCaches(
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -889,10 +889,7 @@ class Int32Contracts {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
if (note && value && !int32(value))
|
||||
fail(note, value, value.loc, "the value stored into");
|
||||
} else if (
|
||||
s.kind === "forOf" ||
|
||||
(s.kind === "tryCatch" && s.catchLocalId !== undefined)
|
||||
) {
|
||||
} else if (s.kind === "forOf" || (s.kind === "tryCatch" && s.catchLocalId !== null)) {
|
||||
const id = s.kind === "forOf" ? s.localId : s.catchLocalId!;
|
||||
const note = localNote(id);
|
||||
if (note)
|
||||
|
||||
@@ -45,6 +45,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
@@ -54,6 +55,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
dynTier?: boolean;
|
||||
streamDyn?: boolean;
|
||||
handleProps?: boolean;
|
||||
dynForAwait?: boolean;
|
||||
eventsOnceSignal?: boolean;
|
||||
dynBufferNum?: boolean;
|
||||
builtinValues?: boolean;
|
||||
@@ -77,6 +78,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
bigint: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
utilPromisify: boolean;
|
||||
dynBuiltinClass: boolean;
|
||||
dynBuiltinStream: boolean;
|
||||
parseArgs: boolean;
|
||||
@@ -86,6 +88,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
http2: boolean;
|
||||
dgram: boolean;
|
||||
dnsPromises: boolean;
|
||||
dgramDyn: boolean;
|
||||
watch: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
@@ -199,6 +202,7 @@ const BOOLEAN_NATIVE_KEYS = [
|
||||
"bigint",
|
||||
"searchParams",
|
||||
"qs",
|
||||
"utilPromisify",
|
||||
"dynBuiltinClass",
|
||||
"dynBuiltinStream",
|
||||
"parseArgs",
|
||||
@@ -208,6 +212,7 @@ const BOOLEAN_NATIVE_KEYS = [
|
||||
"http2",
|
||||
"dgram",
|
||||
"dnsPromises",
|
||||
"dgramDyn",
|
||||
"watch",
|
||||
"foreignFfi",
|
||||
"nodeTest",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { argumentsAliasingUnobservable } from "./arguments-usage.js";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
import { closeSourceParser, parseSourceFile } from "./ts7/source-parser-node.js";
|
||||
|
||||
afterAll(closeSourceParser);
|
||||
|
||||
function unobservable(source: string): boolean {
|
||||
const file = parseSourceFile("scan.js", source, "js");
|
||||
const fn = file.statements.nodes.find(ts.isFunctionDeclaration);
|
||||
if (fn === undefined) throw new Error("no function declaration");
|
||||
return argumentsAliasingUnobservable(fn);
|
||||
}
|
||||
|
||||
test("element reads stay unobservable while nothing writes a parameter", () => {
|
||||
expect(unobservable("function f(a, b) { return arguments[0] + arguments.length; }")).toBe(true);
|
||||
expect(unobservable("function f(a) { var a; return arguments[0]; }")).toBe(true);
|
||||
expect(unobservable("function f(a) { var b = 1; return arguments[0] + b; }")).toBe(true);
|
||||
});
|
||||
|
||||
test("a parameter written by assignment or by a declaration makes element reads observable", () => {
|
||||
for (const body of [
|
||||
"a = 5;",
|
||||
"a++;",
|
||||
"var a = 5;",
|
||||
"for (var a in o);",
|
||||
"for (var a of o);",
|
||||
"var { a } = o;",
|
||||
"var [a] = o;",
|
||||
"function a() {}",
|
||||
]) {
|
||||
expect(unobservable(`function f(a, o) { ${body} return arguments[0]; }`), body).toBe(false);
|
||||
}
|
||||
});
|
||||
@@ -33,3 +33,106 @@ export function bodyReadsArguments(fn: ts.Node): boolean {
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** Whether an expression is assigned, updated or deleted. */
|
||||
function isWritten(expr: ts.Node): boolean {
|
||||
let node = expr;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (ts.isBinaryExpression(parent))
|
||||
return (
|
||||
parent.left === node &&
|
||||
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
||||
);
|
||||
if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent))
|
||||
return (
|
||||
parent.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
parent.operator === ts.SyntaxKind.MinusMinusToken
|
||||
);
|
||||
if (ts.isForInStatement(parent) || ts.isForOfStatement(parent))
|
||||
return parent.initializer === node;
|
||||
if (
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
ts.isSpreadElement(parent) ||
|
||||
ts.isShorthandPropertyAssignment(parent) ||
|
||||
(ts.isPropertyAssignment(parent) && parent.initializer === node)
|
||||
)
|
||||
return isWritten(parent);
|
||||
if (ts.isObjectLiteralExpression(parent) || ts.isSpreadAssignment(parent))
|
||||
return isWritten(parent);
|
||||
return ts.isDeleteExpression(parent);
|
||||
}
|
||||
|
||||
/** Whether an identifier assigns its binding where it declares it: a
|
||||
* `var` with an initializer or in a for-in/for-of head, a destructured
|
||||
* element, or a function declaration's name. A redeclared parameter keeps
|
||||
* its binding, so each of these writes the parameter. */
|
||||
function isDeclarationWrite(id: ts.Identifier): boolean {
|
||||
const parent = id.parent;
|
||||
if (ts.isVariableDeclaration(parent) && parent.name === id) {
|
||||
if (parent.initializer !== undefined) return true;
|
||||
const head = parent.parent?.parent;
|
||||
return (
|
||||
(ts.isForInStatement(head) || ts.isForOfStatement(head)) && head.initializer === parent.parent
|
||||
);
|
||||
}
|
||||
if (ts.isBindingElement(parent)) return parent.name === id;
|
||||
return ts.isFunctionDeclaration(parent) && parent.name === id;
|
||||
}
|
||||
|
||||
/** Whether sloppy-mode `arguments` in `fn`'s own body behaves exactly like
|
||||
* an unmapped copy of the call's arguments. A mapped arguments object
|
||||
* aliases the named parameters, so it differs from a copy only when one
|
||||
* side is written after the other is read: reading `arguments.length`
|
||||
* never observes the aliasing, and element reads cannot observe it while
|
||||
* nothing writes an element or a parameter. The object itself must not
|
||||
* escape, since another holder could write through it. */
|
||||
export function argumentsAliasingUnobservable(fn: ts.SignatureDeclaration): boolean {
|
||||
if (fn.body === undefined) return false;
|
||||
let elementReads = false;
|
||||
let escapes = false;
|
||||
ts.walkPreorder(fn.body, (node) => {
|
||||
if (ts.isTypeNode(node)) return "skip";
|
||||
if (ts.isFunctionLike(node) && !ts.isArrowFunction(node) && node !== fn) return "skip";
|
||||
if (!ts.isIdentifier(node) || node.text !== "arguments" || !isArgumentsRead(node))
|
||||
return undefined;
|
||||
const parent = node.parent;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(parent) &&
|
||||
parent.expression === node &&
|
||||
parent.name.text === "length" &&
|
||||
!isWritten(parent)
|
||||
)
|
||||
return undefined;
|
||||
if (ts.isElementAccessExpression(parent) && parent.expression === node && !isWritten(parent)) {
|
||||
elementReads = true;
|
||||
return undefined;
|
||||
}
|
||||
escapes = true;
|
||||
return "stop";
|
||||
});
|
||||
if (escapes) return false;
|
||||
if (!elementReads) return true;
|
||||
const names = new Set<string>();
|
||||
for (const p of fn.parameters.nodes) {
|
||||
if (!ts.isIdentifier(p.name)) return false;
|
||||
names.add(p.name.text);
|
||||
}
|
||||
// Any write to a parameter's name, including from nested closures and
|
||||
// shadowing bindings, conservatively counts as a parameter write.
|
||||
let paramWritten = false;
|
||||
ts.walkPreorder(fn.body, (node) => {
|
||||
if (ts.isTypeNode(node)) return "skip";
|
||||
if (
|
||||
ts.isIdentifier(node) &&
|
||||
names.has(node.text) &&
|
||||
(isWritten(node) || isDeclarationWrite(node))
|
||||
) {
|
||||
paramWritten = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return !paramWritten;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { numLit } from "../../../ir/build.js";
|
||||
import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
RUNTIME_EMITTER_CLASS,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type SrcLoc,
|
||||
} from "../../../ir/ir.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import type { ClassInfo } from "../lower-classes.js";
|
||||
import { classInstanceOf } from "../class-dynamic-dispatch.js";
|
||||
import { isSafeToDiscard, isSafeToRepeat } from "../expressions/evaluation-safety.js";
|
||||
|
||||
/* EventEmitter and the stream classes as untyped constructor values, the
|
||||
* bases of JavaScript constructor functions:
|
||||
@@ -157,3 +167,57 @@ export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc)
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `value instanceof Base` for an untyped value that may also hold a
|
||||
* compiled instance: an object built by a constructor function inherits
|
||||
* the untyped constructor's prototype, and an instance of a program class
|
||||
* extending Base boxes as a typed reference the class test recognizes.
|
||||
* With `callee`, the right-hand side is an untyped value and the class
|
||||
* test applies only when it is Base's untyped constructor. */
|
||||
export function builtinClassInstanceOfAny(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
kind: number,
|
||||
info: ClassInfo,
|
||||
loc: SrcLoc,
|
||||
callee?: IrExpr,
|
||||
): IrExpr {
|
||||
const stmts: IrStmt[] = [];
|
||||
const once = (expr: IrExpr, name: string, snapshot = false): IrExpr => {
|
||||
if (!snapshot && isSafeToRepeat(expr)) return expr;
|
||||
const local = lowerer.declareHiddenLocal(name, DYN);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: expr, loc });
|
||||
return { kind: "varRef", localId: local.id, type: DYN, loc };
|
||||
};
|
||||
// The left operand evaluates first: a variable read keeps its value in
|
||||
// a local when the right-hand side can assign the variable.
|
||||
const tested = once(value, "%instanceValue", callee !== undefined && !isSafeToDiscard(callee));
|
||||
const ctor = callee ? once(callee, "%instanceCallee") : null;
|
||||
const compiled = classInstanceOf(lowerer, tested, info, loc);
|
||||
const result: IrExpr = {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: ctor
|
||||
? { kind: "libCall", fn: "dyn.functionInstanceOf", args: [tested, ctor], type: BOOL, loc }
|
||||
: builtinClassInstanceOf(tested, kind, loc),
|
||||
right: ctor
|
||||
? {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: {
|
||||
kind: "dynScalarEq",
|
||||
left: ctor,
|
||||
right: builtinClassValue(kind, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
right: compiled,
|
||||
type: BOOL,
|
||||
loc,
|
||||
}
|
||||
: compiled,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
return stmts.length ? { kind: "seqExpr", stmts, result, type: BOOL, loc } : result;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { registerHttpClientFnBinding } from "../lower-server.js";
|
||||
import { isPromisifiedZlibMember } from "./compression.js";
|
||||
import { isPromisifiedCryptoMember } from "./crypto.js";
|
||||
import { isPromisifiedDnsMember } from "../lower-dgram.js";
|
||||
import { callbackPromisifyShape } from "./promisify-callback.js";
|
||||
import { callbackPromisifyShape, untypedPromisifyArgument } from "./promisify-callback.js";
|
||||
|
||||
/** Compile-time util.promisify projections. The immutable result binding
|
||||
* owns no runtime slot: calls rewrite to the target's promise lowering,
|
||||
@@ -167,7 +167,11 @@ export function registerPromisifiedBuiltinDecl(
|
||||
: null;
|
||||
// A typed callback-last function value: the call lowers to a native
|
||||
// promisified function, and the binding holds it like any closure.
|
||||
if (!projection && callbackPromisifyShape(lowerer, e, locOf(e)) !== null) return false;
|
||||
if (
|
||||
!projection &&
|
||||
(callbackPromisifyShape(lowerer, e, locOf(e)) !== null || untypedPromisifyArgument(lowerer, e))
|
||||
)
|
||||
return false;
|
||||
if (!projection) {
|
||||
lowerer.noLowering(
|
||||
"util.promisify of this target",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { type Lowerer } from "../lowerer.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
type IrExpr,
|
||||
type IrLocal,
|
||||
type IrParam,
|
||||
@@ -90,6 +91,14 @@ function callbackShape(
|
||||
return { fnT, resultT, args, callbackT, callbackParamT: callbackParamT!, inner, error };
|
||||
}
|
||||
|
||||
/** Whether `promisify(fn)`'s one argument is typed `any`: the function is
|
||||
* only known at run time, so the promisified function is built there. */
|
||||
export function untypedPromisifyArgument(lowerer: Lowerer, call: ts.CallExpression): boolean {
|
||||
if (call.questionDotToken || call.arguments.nodes.length !== 1) return false;
|
||||
const argNode = call.arguments.nodes[0]!;
|
||||
return !ts.isSpreadElement(argNode) && (lowerer.typeOf(argNode).flags & ts.TypeFlags.Any) !== 0;
|
||||
}
|
||||
|
||||
/** The callback shape of a `promisify(fn)` call, or null. */
|
||||
export function callbackPromisifyShape(
|
||||
lowerer: Lowerer,
|
||||
@@ -131,7 +140,17 @@ export function lowerCallbackPromisify(
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const shape = callbackPromisifyShape(lowerer, call, loc);
|
||||
if (shape === null) return null;
|
||||
if (shape === null) {
|
||||
// An untyped (`any`) function: Node's promisify at run time.
|
||||
if (!untypedPromisifyArgument(lowerer, call)) return null;
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "util.promisifyDyn",
|
||||
args: [lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const argNode = call.arguments.nodes[0]!;
|
||||
let fn = lowerer.lowerExpr(argNode);
|
||||
if (!typeEquals(fn.type, shape.fnT)) fn = lowerer.coerceToExpected(fn, shape.fnT);
|
||||
|
||||
@@ -5,9 +5,10 @@ import {
|
||||
} from "./typed-arguments.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { bodyReadsArguments } from "../arguments-usage.js";
|
||||
import { argumentsAliasingUnobservable, bodyReadsArguments } from "../arguments-usage.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
canBoxFuncIntoDyn,
|
||||
DYN,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
@@ -143,6 +144,26 @@ export function generatorMeta(
|
||||
return { yieldT: type.yieldT, nextT: type.nextT, resultType };
|
||||
}
|
||||
|
||||
/** A function expression stored as a prototype method: `C.prototype.name =
|
||||
* function (...) {}` or `C.prototype[key] = function (...) {}`
|
||||
* (parentheses allowed around the function). */
|
||||
export function prototypeAssignedFunction(fn: ts.Node | undefined): boolean {
|
||||
if (fn === undefined || !ts.isFunctionExpression(fn)) return false;
|
||||
let node: ts.Node = fn;
|
||||
while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (
|
||||
parent === undefined ||
|
||||
!ts.isBinaryExpression(parent) ||
|
||||
parent.right !== node ||
|
||||
parent.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!(ts.isPropertyAccessExpression(parent.left) || ts.isElementAccessExpression(parent.left))
|
||||
)
|
||||
return false;
|
||||
const owner = parent.left.expression;
|
||||
return ts.isPropertyAccessExpression(owner) && owner.name.text === "prototype";
|
||||
}
|
||||
|
||||
/** A parameter with a type annotation or a JSDoc `@param`/`@type` tag. */
|
||||
export function documentedParameter(param: ts.ParameterDeclaration): boolean {
|
||||
return (
|
||||
@@ -199,6 +220,20 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
|
||||
? { type: DYN, mode: "omittable", bodyType: DYN }
|
||||
: { type: DYN, mode: param.questionToken ? "omittable" : "required" };
|
||||
}
|
||||
if (
|
||||
isJsSourceFile(ts.getSourceFileOfNode(param)) &&
|
||||
!param.dotDotDotToken &&
|
||||
prototypeAssignedFunction(param.parent) &&
|
||||
!checkedJsDocumentedParameter(lowerer, param)
|
||||
) {
|
||||
// A prototype method (`C.prototype.m = function (a) {}`) is called
|
||||
// through dynamic dispatch, where callers pass whatever they hold. As
|
||||
// for constructors, JSDoc in a file TypeScript does not check
|
||||
// documents the usual types without constraining callers.
|
||||
return param.initializer
|
||||
? { type: DYN, mode: "omittable", bodyType: DYN }
|
||||
: { type: DYN, mode: param.questionToken ? "omittable" : "required" };
|
||||
}
|
||||
const executor = param.parent;
|
||||
const construction = executor?.parent;
|
||||
const nativeExecutor =
|
||||
@@ -640,6 +675,21 @@ export function requireExactArityValue(
|
||||
? lowerer.mapTypeOf(lowerer.typeOf(contextual))
|
||||
: null;
|
||||
if (mapped && typeEquals(mapped, funcType)) return;
|
||||
// An 'any' slot (`C.prototype.m = function (a, b) {…}` through an
|
||||
// untyped receiver) boxes the value into a checked-dynamic function:
|
||||
// the box completes omitted arguments with undefined and checks every
|
||||
// argument into the completed ABI, so any boxable signature passes.
|
||||
if (
|
||||
target !== undefined &&
|
||||
(target.flags & ts.TypeFlags.Any) !== 0 &&
|
||||
canBoxFuncIntoDyn(
|
||||
funcType,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// An object-literal METHOD has no contextual type of its own: the
|
||||
// literal's contextual type names the member slot it fills. An
|
||||
// overloaded member's slot holds its erased function, which spells the
|
||||
@@ -808,7 +858,19 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara
|
||||
decl.parameters.nodes.length > 0 ? "arguments" : "dynRest",
|
||||
);
|
||||
const nameBlame: ts.Node = decl.name ?? decl;
|
||||
const returnType = lowerer.declaredReturnType(decl, nameBlame);
|
||||
let returnType = lowerer.declaredReturnType(decl, nameBlame);
|
||||
// A JavaScript generator whose inferred return the checker leaves `any`
|
||||
// (it delegates to itself: `yield* walkTree(sub)`) is still a generator:
|
||||
// its channels are checked-dynamic values.
|
||||
if (isGenerator && returnType.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(decl))) {
|
||||
returnType = {
|
||||
kind: "generator",
|
||||
...(isAsync ? { async: true as const } : {}),
|
||||
yieldT: DYN,
|
||||
retT: DYN,
|
||||
nextT: DYN,
|
||||
};
|
||||
}
|
||||
if (isAsync && !isGenerator && returnType.kind !== "promise") {
|
||||
lowerer.badType(nameBlame, lowerer.typeOf(nameBlame));
|
||||
}
|
||||
@@ -839,7 +901,8 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara
|
||||
/** Arrows inherit arguments. Ordinary JS functions receive a hidden
|
||||
* array unless a declared rest parameter already owns that contract.
|
||||
* Class methods are strict; other parameterized forms require ES modules
|
||||
* because sloppy-mode arguments alias named parameters. */
|
||||
* because sloppy-mode arguments alias named parameters, unless the body
|
||||
* cannot observe that aliasing (argumentsAliasingUnobservable). */
|
||||
function appendArgumentsParameter(
|
||||
lowerer: Lowerer,
|
||||
node: ts.SignatureDeclaration,
|
||||
@@ -864,7 +927,8 @@ function appendArgumentsParameter(
|
||||
if (
|
||||
!classMethod &&
|
||||
!isNodeEsmFile(ts.getSourceFileOfNode(node), lowerer.program) &&
|
||||
!unmappedArguments(node)
|
||||
!unmappedArguments(node) &&
|
||||
!argumentsAliasingUnobservable(node)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1058,9 +1122,21 @@ export function lambdaSignature(
|
||||
// Union-inferred returns adopt too (a mixed-return body inferring a
|
||||
// SUB-union of the slot's union — adopting is a no-op when the two
|
||||
|
||||
// A synchronous object-literal method adopts its slot's return the same
|
||||
// way: its slot is the property of that name on the literal's
|
||||
// contextual type. A literal under `satisfies` keeps its own type (the
|
||||
// constraint is only checked), so its methods keep their inferred
|
||||
// returns: callers of the value see those, not the constraint's.
|
||||
const literalMethod =
|
||||
ts.isMethodDeclaration(node) &&
|
||||
ts.isObjectLiteralExpression(node.parent) &&
|
||||
!node.asteriskToken &&
|
||||
node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) !== true &&
|
||||
!isSatisfiesOperand(node.parent);
|
||||
if (
|
||||
(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
|
||||
(!isAsyncLike || ret.kind === "promise")
|
||||
((ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
|
||||
(!isAsyncLike || ret.kind === "promise")) ||
|
||||
literalMethod
|
||||
) {
|
||||
// The slot's signature: the contextual type stripped of its nullish
|
||||
// parts (an OPTIONAL callback field's context is the whole
|
||||
@@ -1068,7 +1144,10 @@ export function lambdaSignature(
|
||||
// declared in USER code: stdlib callback slots (flatMap's
|
||||
// `U | readonly U[]`, sort comparators) have intrinsic lowerings
|
||||
// that inspect the INFERRED type, so they must not widen.
|
||||
const ctxType = lowerer.checker.getContextualType(node);
|
||||
const ctxType =
|
||||
literalMethod && ts.isMethodDeclaration(node) && node.parent
|
||||
? objectLiteralMethodSlot(lowerer, node, node.parent)
|
||||
: lowerer.checker.getContextualType(node);
|
||||
const ctxSigs = ctxType
|
||||
? lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType))
|
||||
: [];
|
||||
@@ -1136,6 +1215,7 @@ export function lambdaSignature(
|
||||
const inferredLiteral =
|
||||
literalSlot !== undefined && (literalType === null || literalType.kind === "dyn");
|
||||
if (
|
||||
!literalMethod &&
|
||||
!isAsyncLike &&
|
||||
!node.type &&
|
||||
!inferredLiteral &&
|
||||
@@ -1151,3 +1231,35 @@ export function lambdaSignature(
|
||||
const funcType = funcTypeFromParamShapes(shapes, ret);
|
||||
return { shapes, funcType };
|
||||
}
|
||||
|
||||
/** Whether the object literal `literal` is the operand of a `satisfies`
|
||||
* expression, directly or as a property value of literals that are. */
|
||||
function isSatisfiesOperand(literal: ts.Node): boolean {
|
||||
let n: ts.Node | undefined = literal;
|
||||
while (n !== undefined) {
|
||||
const parent: ts.Node | undefined = n.parent;
|
||||
if (parent === undefined) return false;
|
||||
if (ts.isParenthesizedExpression(parent)) n = parent;
|
||||
else if (ts.isPropertyAssignment(parent) && parent.initializer === n) n = parent.parent;
|
||||
else return ts.isSatisfiesExpression(parent) && parent.expression === n;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The type of the property an object-literal method fills on the
|
||||
* literal's contextual type; undefined when the literal has no contextual
|
||||
* type or the method has a computed name. */
|
||||
function objectLiteralMethodSlot(
|
||||
lowerer: Lowerer,
|
||||
node: ts.MethodDeclaration,
|
||||
literal: ts.Node,
|
||||
): ts.Type | undefined {
|
||||
if (!ts.isIdentifier(node.name) && !ts.isStringLiteral(node.name)) return undefined;
|
||||
const literalType = lowerer.checker.getContextualType(literal);
|
||||
if (!literalType) return undefined;
|
||||
const property = lowerer.checker.getPropertyOfType(
|
||||
lowerer.checker.getNonNullableType(literalType),
|
||||
node.name.text,
|
||||
);
|
||||
return property ? lowerer.checker.getTypeOfSymbol(property) : undefined;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { own } from "../lowerer.js";
|
||||
import { isRequireMainFilename } from "../expressions/optional-chains.js";
|
||||
import { isSafeToDiscard } from "../expressions/evaluation-safety.js";
|
||||
import { STRING_INDEX_METHODS, STRING_REPLACE_METHODS, STR_METHODS } from "../surfaces.js";
|
||||
import { execResultCaptureMask } from "./exec-result-uses.js";
|
||||
import { lowerStringReplacement } from "./string-replacement.js";
|
||||
@@ -1504,6 +1505,7 @@ function lowerLocaleCompareCall(
|
||||
/** String.prototype members (Annex B included) whose typed-receiver
|
||||
* lowering lives here. */
|
||||
export const EXTRA_STRING_METHODS: ReadonlySet<string> = new Set([
|
||||
"concat",
|
||||
"substr",
|
||||
"toLocaleLowerCase",
|
||||
"toLocaleUpperCase",
|
||||
@@ -1565,10 +1567,17 @@ export function isDynPrimitiveKindMethod(name: string): boolean {
|
||||
return DYN_STRING_KIND_METHODS.has(name) || DYN_NUMBER_KIND_METHODS.has(name);
|
||||
}
|
||||
|
||||
/** Evaluate `value` once into a hidden local (effect-free reads stay). */
|
||||
function hoist(lowerer: Lowerer, value: IrExpr, stmts: IrStmt[], name: string): IrExpr {
|
||||
/** Evaluate `value` once into a hidden local (effect-free reads stay,
|
||||
* unless `snapshot` asks for the value a variable holds now). */
|
||||
function hoist(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
stmts: IrStmt[],
|
||||
name: string,
|
||||
snapshot = false,
|
||||
): IrExpr {
|
||||
if (
|
||||
value.kind === "varRef" ||
|
||||
(value.kind === "varRef" && !snapshot) ||
|
||||
value.kind === "numLit" ||
|
||||
value.kind === "strLit" ||
|
||||
value.kind === "boolLit" ||
|
||||
@@ -1647,6 +1656,13 @@ function buildStringMethod(
|
||||
): IrExpr | null {
|
||||
const loc = locOf(call);
|
||||
const node = (i: number): ts.Expression => call.arguments.nodes[i] ?? call;
|
||||
// concat(...values): each argument's ToString, in order, after all of
|
||||
// them evaluated.
|
||||
if (name === "concat") {
|
||||
return args
|
||||
.map((arg, i) => stringArg(lowerer, arg, node(i)))
|
||||
.reduce((left, right) => ({ kind: "strConcat", left, right, type: STRING, loc }), s);
|
||||
}
|
||||
if (name === "substr") {
|
||||
const start = args[0]
|
||||
? positionNumber(lowerer, args[0], numLit(0, loc), node(0), "substr start")
|
||||
@@ -1767,21 +1783,39 @@ function buildNumberMethod(
|
||||
return { kind: "ternary", cond: digits.has, then: fixed, else_: shortest, type: STRING, loc };
|
||||
}
|
||||
|
||||
/** Evaluate the call's arguments, in order, into hidden locals. */
|
||||
function evaluatedArgs(
|
||||
/** Operand kinds whose string and number conversions run no program code. */
|
||||
const PLAIN_CONVERSION_KINDS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"f64",
|
||||
"bigint",
|
||||
"string",
|
||||
"bool",
|
||||
"symbol",
|
||||
"nullT",
|
||||
"undefinedT",
|
||||
]);
|
||||
|
||||
/** Evaluate the receiver (when given) and the call's arguments, in order,
|
||||
* into hidden locals. A variable read stays a read unless a later operand
|
||||
* can assign the variable, or an argument's conversion can run program
|
||||
* code: the methods convert their arguments after evaluating all of them
|
||||
* (an object's toString or valueOf). */
|
||||
function evaluatedOperands(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
stmts: IrStmt[],
|
||||
expected: IrType | undefined,
|
||||
): IrExpr[] {
|
||||
return call.arguments.nodes.map((arg) =>
|
||||
hoist(
|
||||
lowerer,
|
||||
expected ? lowerer.lowerExprExpecting(arg, expected) : lowerer.lowerExpr(arg),
|
||||
stmts,
|
||||
"%methodArg",
|
||||
),
|
||||
receiver?: { value: IrExpr; name: string },
|
||||
): { receiver: IrExpr | undefined; args: IrExpr[] } {
|
||||
const args = call.arguments.nodes.map((arg) =>
|
||||
expected ? lowerer.lowerExprExpecting(arg, expected) : lowerer.lowerExpr(arg),
|
||||
);
|
||||
const conversionsRunCode = args.some((arg) => !PLAIN_CONVERSION_KINDS.has(arg.type.kind));
|
||||
const mayChange = (from: number): boolean =>
|
||||
conversionsRunCode || !args.slice(from).every(isSafeToDiscard);
|
||||
return {
|
||||
receiver: receiver && hoist(lowerer, receiver.value, stmts, receiver.name, mayChange(0)),
|
||||
args: args.map((arg, i) => hoist(lowerer, arg, stmts, "%methodArg", mayChange(i + 1))),
|
||||
};
|
||||
}
|
||||
|
||||
/** A typed STRING receiver calling one of EXTRA_STRING_METHODS: receiver,
|
||||
@@ -1795,9 +1829,12 @@ export function lowerExtraStringMethodCall(
|
||||
): IrExpr | null {
|
||||
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
|
||||
const stmts: IrStmt[] = [];
|
||||
const s = hoist(lowerer, receiver, stmts, "%stringReceiver");
|
||||
const args = evaluatedArgs(lowerer, call, stmts, undefined);
|
||||
const result = buildStringMethod(lowerer, call, name, s, args, receiverNode, stmts);
|
||||
const operands = evaluatedOperands(lowerer, call, stmts, undefined, {
|
||||
value: receiver,
|
||||
name: "%stringReceiver",
|
||||
});
|
||||
const s = operands.receiver!;
|
||||
const result = buildStringMethod(lowerer, call, name, s, operands.args, receiverNode, stmts);
|
||||
return result ? seq(stmts, result) : null;
|
||||
}
|
||||
|
||||
@@ -1812,9 +1849,12 @@ export function lowerExtraNumberMethodCall(
|
||||
if (name !== "toPrecision" && !(name === "toExponential" && call.arguments.nodes.length === 1))
|
||||
return null;
|
||||
const stmts: IrStmt[] = [];
|
||||
const x = hoist(lowerer, receiver, stmts, "%numberReceiver");
|
||||
const args = evaluatedArgs(lowerer, call, stmts, undefined);
|
||||
const result = buildNumberMethod(lowerer, call, name, x, args, stmts);
|
||||
const operands = evaluatedOperands(lowerer, call, stmts, undefined, {
|
||||
value: receiver,
|
||||
name: "%numberReceiver",
|
||||
});
|
||||
const x = operands.receiver!;
|
||||
const result = buildNumberMethod(lowerer, call, name, x, operands.args, stmts);
|
||||
return result ? seq(stmts, result) : null;
|
||||
}
|
||||
|
||||
@@ -1838,7 +1878,7 @@ export function lowerDynPrimitiveMethodCall(
|
||||
const local = lowerer.declareHiddenLocal("%primitiveReceiver", DYN);
|
||||
stmts.push({ kind: "varDecl", localId: local.id, init: recv, loc });
|
||||
const r = varRef(local.id, DYN, loc);
|
||||
const args = evaluatedArgs(lowerer, call, stmts, DYN);
|
||||
const { args } = evaluatedOperands(lowerer, call, stmts, DYN);
|
||||
let result: IrExpr = {
|
||||
kind: "dynInvoke",
|
||||
recv: r,
|
||||
|
||||
@@ -45,6 +45,7 @@ import { lowerElementCompoundParts } from "./element-access.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { storedClassValueType } from "../lower-classes.js";
|
||||
import { builtinClassKind } from "../builtins/builtin-class-dyn.js";
|
||||
import { hasRuntimeStatics } from "../class-runtime-statics.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { fenceNodeModuleMutation, lowerRequireCacheHas } from "../lower-node-module.js";
|
||||
@@ -1486,6 +1487,25 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// EventEmitter or a stream class against an untyped value in
|
||||
// JavaScript: the untyped value can hold the class's untyped
|
||||
// constructor (coerceToExpected), which compares by identity.
|
||||
if (
|
||||
dynSide.type.kind === "dyn" &&
|
||||
isJsSourceFile(expr.getSourceFile()) &&
|
||||
((scalarSide.type.kind === "moduleNs" && scalarSide.type.moduleId === "builtin:events") ||
|
||||
(scalarSide.kind === "classRef" && builtinClassKind(scalarSide.className) !== null))
|
||||
) {
|
||||
const boxed = lowerer.coerceToExpected(scalarSide, DYN);
|
||||
return {
|
||||
kind: "dynScalarEq",
|
||||
left: scalarSide === left ? boxed : left,
|
||||
right: scalarSide === right ? boxed : right,
|
||||
...(negated ? { negated: true as const } : {}),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (
|
||||
dynSide.type.kind === "dyn" &&
|
||||
(scalarSide.type.kind === "f64" ||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { hasRuntimeStatics } from "../class-runtime-statics.js";
|
||||
import { jsBindingHasOpenWrites } from "../lower-stmts.js";
|
||||
import { documentedParameter, prototypeAssignedFunction } from "../call-signatures.js";
|
||||
import { narrowClassUnion, narrowStoredClassValue, structuralNarrowing } from "../class-unions.js";
|
||||
import { checkedClassAssertion, checkedClassUnionAssertion } from "../class-assertions.js";
|
||||
import {
|
||||
@@ -487,6 +488,20 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
)
|
||||
)
|
||||
return expr;
|
||||
// A prototype method in a file TypeScript does not check takes its
|
||||
// documented parameters as checked-dynamic values whatever their
|
||||
// JSDoc says (paramShape); reads keep the actual value.
|
||||
if (
|
||||
symbol &&
|
||||
!lowerer.checkedJsFile(ts.getSourceFileOfNode(node)) &&
|
||||
symbol.declarations.some(
|
||||
(decl) =>
|
||||
ts.isParameter(decl) &&
|
||||
prototypeAssignedFunction(decl.parent) &&
|
||||
documentedParameter(decl),
|
||||
)
|
||||
)
|
||||
return expr;
|
||||
if (
|
||||
symbol &&
|
||||
symbol.declarations.some(
|
||||
|
||||
@@ -255,6 +255,25 @@ function isStatFsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression
|
||||
);
|
||||
}
|
||||
|
||||
/** The dgram.Socket methods its checked-dynamic handle answers as
|
||||
* function values (scr_dgram.c's scr_dgram_dyn_method_names). */
|
||||
const DGRAM_METHOD_VALUES = new Set([
|
||||
"on",
|
||||
"once",
|
||||
"addListener",
|
||||
"removeListener",
|
||||
"off",
|
||||
"emit",
|
||||
"send",
|
||||
"close",
|
||||
"bind",
|
||||
"connect",
|
||||
"disconnect",
|
||||
"address",
|
||||
"ref",
|
||||
"unref",
|
||||
]);
|
||||
|
||||
export function lowerPropertyAccessExpr(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
@@ -264,6 +283,26 @@ export function lowerPropertyAccessExpr(
|
||||
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
|
||||
if (navigator) return navigator;
|
||||
}
|
||||
// A dgram socket's method read as a value in JavaScript (`emit:
|
||||
// socket.emit.bind(socket)`): the socket crosses as its checked-dynamic
|
||||
// handle, whose methods are function values that dispatch on their
|
||||
// receiver, as the prototype methods do. The receiver is a native
|
||||
// socket whatever its declaration spells (the shipped types declare no
|
||||
// EventEmitter members), so the name decides.
|
||||
if (
|
||||
!expr.questionDotToken &&
|
||||
DGRAM_METHOD_VALUES.has(expr.name.text) &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(expr.expression))?.kind === "dgramSocket"
|
||||
) {
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(lowerer.lowerExpr(expr.expression), DYN),
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// `fn.name` on a function parameter of a generic instance keyed by
|
||||
// its argument's static name (call-site specialization).
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* instanceof and in, including private-name and runtime-key in. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js";
|
||||
import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js";
|
||||
import {
|
||||
checkedPrivateIn,
|
||||
localPrivateBrandClass,
|
||||
@@ -401,6 +401,16 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
}
|
||||
const left = lowerer.lowerExpr(expr.left);
|
||||
if (left.type.kind === "dyn") {
|
||||
// An untyped binding the checker narrowed to EventEmitter or a
|
||||
// stream class (`ReleaseEmitter = EventEmitter` in JavaScript):
|
||||
// the binding holds an untyped constructor, so the test is
|
||||
// Function.prototype[Symbol.hasInstance] over it.
|
||||
const builtinKind = targetInfo.def.runtime ? builtinClassKind(targetInfo.def.name) : null;
|
||||
if (builtinKind !== null) {
|
||||
const right = lowerer.lowerExpr(expr.right);
|
||||
if (right.type.kind === "dyn")
|
||||
return builtinClassInstanceOfAny(lowerer, left, builtinKind, targetInfo, loc, right);
|
||||
}
|
||||
if (targetInfo.def.runtime) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -600,11 +610,11 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
}
|
||||
// `u instanceof EventEmitter` (or a stream class): an untyped object
|
||||
// built by a constructor function inherits the untyped constructor's
|
||||
// prototype (builtin-class-dyn.ts). Compiled emitters and streams have
|
||||
// no untyped form, so the prototype chain answers for every value.
|
||||
// prototype (builtin-class-dyn.ts), and a compiled emitter or stream
|
||||
// boxes as a typed reference the class test answers for.
|
||||
const builtinKind = builtinClassKind(target.def.name);
|
||||
if (left.type.kind === "dyn" && builtinKind !== null) {
|
||||
return builtinClassInstanceOf(left, builtinKind, loc);
|
||||
return builtinClassInstanceOfAny(lowerer, left, builtinKind, target, loc);
|
||||
}
|
||||
if (left.type.kind === "dyn") {
|
||||
lowerer.unsupported(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
@@ -247,6 +248,202 @@ export function lowerCheckedDelegation(
|
||||
return { kind: "seqExpr", stmts, result, generatorDelegate: true, type: result.type, loc };
|
||||
}
|
||||
|
||||
/** `yield* e` in an async generator: the async iterator protocol (a compiled
|
||||
* async generator, Symbol.asyncIterator's result, or a synchronous iterator
|
||||
* whose values are awaited). Each step's result is awaited, its values are
|
||||
* yielded, and the expression completes with the final result's value. An
|
||||
* abrupt completion at the yield (a consumer's return() or throw()) closes
|
||||
* the delegate through its return(), as for-await does; next() arguments
|
||||
* are not forwarded to the delegate. */
|
||||
export function lowerAsyncDelegation(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.YieldExpression,
|
||||
source: IrExpr,
|
||||
): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const gen = lowerer.ctx.generator!;
|
||||
const slot = (name: string, type: IrType, mutable = false) => {
|
||||
const local = lowerer.declareHiddenLocal(name, type);
|
||||
local.mutable = mutable;
|
||||
return { local, ref: varRef(local.id, type, loc) };
|
||||
};
|
||||
const iterator = slot("%adelegate", DYN);
|
||||
const sync = slot("%adelegateSync", BOOL);
|
||||
const next = slot("%adelegateNext", DYN);
|
||||
const step = slot("%adelegateStep", DYN, true);
|
||||
const needsClose = slot("%adelegateClose", BOOL, true);
|
||||
const returned = slot("%adelegateReturn", DYN);
|
||||
const error = slot("%adelegateError", CAUGHT);
|
||||
const get = (value: IrExpr, key: string): IrExpr => ({
|
||||
kind: "dynKeyGet",
|
||||
value,
|
||||
key: { kind: "strLit", value: key, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const awaitDyn = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "async.awaitDyn",
|
||||
args: [value],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
// Each step's awaited result, which must be an object (TypeError
|
||||
// otherwise), for next() and return() alike.
|
||||
const invoke = (callee: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.iteratorResult",
|
||||
args: [
|
||||
awaitDyn({
|
||||
kind: "dynCall",
|
||||
callee,
|
||||
receiver: iterator.ref,
|
||||
calleeName: "iterator method",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const stepNext = invoke(next.ref);
|
||||
const setClose = (value: boolean): IrStmt => ({
|
||||
kind: "assign",
|
||||
localId: needsClose.local.id,
|
||||
value: { kind: "boolLit", value, type: BOOL, loc },
|
||||
loc,
|
||||
});
|
||||
const close = (): IrStmt[] => [
|
||||
{ kind: "varDecl", localId: returned.local.id, init: get(iterator.ref, "return"), loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: { kind: "dynTest", test: "nullish", value: returned.ref, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [{ kind: "exprStmt", expr: invoke(returned.ref), loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
// A synchronous delegate's values are awaited, its final value too.
|
||||
const stepValue = (): IrExpr => ({
|
||||
kind: "ternary",
|
||||
cond: sync.ref,
|
||||
then: awaitDyn(get(step.ref, "value")),
|
||||
else_: get(step.ref, "value"),
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const yielded = stepValue();
|
||||
const stmts: IrStmt[] = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: iterator.local.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.iteratorResult",
|
||||
args: [
|
||||
{
|
||||
kind: "libCall",
|
||||
fn: "dyn.asyncIteratorOpen",
|
||||
args: [source, { kind: "strLit", value: "", type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: sync.local.id,
|
||||
init: { kind: "libCall", fn: "dyn.asyncIteratorWasSync", args: [], type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "varDecl", localId: next.local.id, init: get(iterator.ref, "next"), loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: needsClose.local.id,
|
||||
init: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "varDecl", localId: step.local.id, init: stepNext, loc },
|
||||
{
|
||||
kind: "tryCatch",
|
||||
tryBody: [
|
||||
{
|
||||
kind: "while",
|
||||
cond: {
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: {
|
||||
kind: "dynTest",
|
||||
test: "truthy",
|
||||
value: get(step.ref, "done"),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
body: [
|
||||
setClose(true),
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "yieldExpr",
|
||||
value: lowerer.coerceToExpected(yielded, gen.yieldT),
|
||||
type: gen.nextT.kind === "undefinedT" ? VOID : gen.nextT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
setClose(false),
|
||||
{ kind: "assign", localId: step.local.id, value: stepNext, loc },
|
||||
],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
catchBody: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: needsClose.ref,
|
||||
then: [
|
||||
setClose(false),
|
||||
{
|
||||
kind: "tryCatch",
|
||||
tryBody: close(),
|
||||
catchBody: [],
|
||||
catchLocalId: null,
|
||||
finallyBody: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "rethrow", localId: error.local.id, loc },
|
||||
],
|
||||
catchLocalId: error.local.id,
|
||||
finallyBody: [{ kind: "if", cond: needsClose.ref, then: close(), else_: null, loc }],
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const expected = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
const result =
|
||||
expected && expected.kind !== "void"
|
||||
? lowerer.coerceToExpected(stepValue(), expected)
|
||||
: stepValue();
|
||||
return { kind: "seqExpr", stmts, result, generatorDelegate: true, type: result.type, loc };
|
||||
}
|
||||
|
||||
/** `yield* xs` over an array, a string, a Set or a Map: the built-in
|
||||
* iterator walked in place, with the same live view as for-of (an array
|
||||
* re-reads its length every step, a string steps by code point, a Map
|
||||
|
||||
@@ -29,11 +29,12 @@ function iterableHeadText(expr: ts.Expression): string | null {
|
||||
/** V8's TypeError message for iterating a value that is not iterable:
|
||||
* `x is not iterable` for an identifier or property path, `f is not a
|
||||
* function or its return value is not iterable` for a call of one, and ""
|
||||
* for other expressions, which the runtime words by the value's kind. */
|
||||
export function notIterableSpelling(expr: ts.Expression): string {
|
||||
* for other expressions, which the runtime words by the value's kind.
|
||||
* for await words it with "async iterable". */
|
||||
export function notIterableSpelling(expr: ts.Expression, word = "iterable"): string {
|
||||
const name = iterableHeadText(expr);
|
||||
if (name !== null) return `${name} is not iterable`;
|
||||
if (name !== null) return `${name} is not ${word}`;
|
||||
const e = unwrapped(expr);
|
||||
const callee = ts.isCallExpression(e) ? iterableHeadText(e.expression) : null;
|
||||
return callee !== null ? `${callee} is not a function or its return value is not iterable` : "";
|
||||
return callee !== null ? `${callee} is not a function or its return value is not ${word}` : "";
|
||||
}
|
||||
|
||||
@@ -3803,8 +3803,20 @@ function lowerDynReceiverMethodCall(
|
||||
)
|
||||
)
|
||||
return true;
|
||||
// A standard-library member spelled as a function-typed property
|
||||
// (undici-types declares Headers.append/forEach/... and the body mixin
|
||||
// as `readonly append: (...) => void`; scriptc's own declarations use
|
||||
// methods) is still the handle's native method: it takes the shared
|
||||
// receiver dispatch below either way (which also calls a plain object's
|
||||
// own member), so the lowering does not depend on which declaration
|
||||
// file supplies the type.
|
||||
const libMethod =
|
||||
prop !== undefined &&
|
||||
prop.declarations.length > 0 &&
|
||||
prop.declarations.every((decl) => lowerer.isStdlibFile(decl.getSourceFile()));
|
||||
return (
|
||||
prop !== undefined &&
|
||||
!libMethod &&
|
||||
prop.declarations.some(
|
||||
(decl) => ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl),
|
||||
)
|
||||
@@ -9405,7 +9417,9 @@ function lowerObjLitGenericMethodCall(
|
||||
) {
|
||||
const value = tryLowerExpression(lowerer, access.expression);
|
||||
// Builtin function values held as checked-dynamic functions
|
||||
// (String.fromCharCode, JSON.stringify) bind the same way.
|
||||
// (String.fromCharCode, JSON.stringify) bind the same way, and so
|
||||
// does a member that lowers to a checked-dynamic value (a native
|
||||
// handle's method read in JavaScript).
|
||||
if (
|
||||
value &&
|
||||
(value.type.kind === "dyn" ||
|
||||
@@ -10300,7 +10314,33 @@ export function lowerStaticNumberGlobal(
|
||||
const radix: IrExpr = args[1]
|
||||
? lowerer.lowerExprExpecting(args[1], F64)
|
||||
: { kind: "numLit", value: 0, type: F64, loc };
|
||||
const optional = optionalCallValue(lowerer, args[0]!);
|
||||
let optional = optionalCallValue(lowerer, args[0]!);
|
||||
let lowered: IrExpr | null = null;
|
||||
// A possibly-missing or untyped text in JavaScript (`parseInt(process.
|
||||
// env.PORT, 10)`): TypeScript rejects the call, Node parses the text
|
||||
// of whatever arrives ("undefined").
|
||||
if (optional === null && isJsSourceFile(ts.getSourceFileOfNode(call))) {
|
||||
const argT = lowerer.typeOf(args[0]!);
|
||||
const parts = argT.isUnionType() ? ts.constituentTypes(argT) : [argT];
|
||||
if (
|
||||
parts.some(
|
||||
(part) =>
|
||||
(part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null | ts.TypeFlags.Any)) !== 0,
|
||||
)
|
||||
) {
|
||||
const raw = lowerer.lowerExpr(args[0]!);
|
||||
if (raw.type.kind === "union") {
|
||||
optional = raw;
|
||||
lowered = raw;
|
||||
} else {
|
||||
const text: IrExpr =
|
||||
raw.type.kind === "dyn"
|
||||
? { kind: "libCall", fn: "dyn.toStringCoerce", args: [raw], type: STRING, loc }
|
||||
: lowerer.coerceInto(args[0]!, raw, STRING);
|
||||
return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (optional?.type.kind === "union") {
|
||||
const def = lowerer.unions.get(optional.type.unionId);
|
||||
const stringTag = def?.arms.findIndex((arm) => arm.kind === "string") ?? -1;
|
||||
@@ -10399,7 +10439,10 @@ export function lowerStaticNumberGlobal(
|
||||
return { kind: "call", callee: helper, args: [optional, radix], type: F64, loc };
|
||||
}
|
||||
}
|
||||
const s = lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
const s =
|
||||
lowered !== null
|
||||
? lowerer.coerceInto(args[0]!, lowered, STRING)
|
||||
: lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
return { kind: "libCall", fn: "num.parseInt", args: [s, radix], type: F64, loc };
|
||||
}
|
||||
// STATIC parseFloat/isFinite over exactly-typed arguments —
|
||||
|
||||
@@ -90,14 +90,17 @@ import {
|
||||
withUnitArm,
|
||||
} from "../type-mapper.js";
|
||||
import {
|
||||
canonicalBuiltinModule,
|
||||
cjsClassExprWholeExportOf,
|
||||
isCjsJsFile,
|
||||
isJsSourceFile,
|
||||
isModuleExportsAccess,
|
||||
locOf,
|
||||
requireSpecOf,
|
||||
} from "../program.js";
|
||||
import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js";
|
||||
import { isAssertClassNew } from "./assert-instances.js";
|
||||
import { isCjsRequireGlobal } from "./lower-node-module.js";
|
||||
import { lowerArrayConstructor } from "./containers/array-construction.js";
|
||||
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
|
||||
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
|
||||
@@ -979,6 +982,19 @@ export function registerBuiltinEmitterClass(lowerer: Lowerer): void {
|
||||
lowerer.classes.set(RUNTIME_EMITTER_CLASS, info);
|
||||
}
|
||||
|
||||
/** `class Bus extends require("events") {}` in a CommonJS module: the
|
||||
* events module is the EventEmitter class itself, so the base is the
|
||||
* runtime emitter, exactly as through a `require("events")` binding. */
|
||||
export function builtinRequireBaseOf(lowerer: Lowerer, node: ts.Expression): ClassInfo | null {
|
||||
let expr = node;
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (!ts.isCallExpression(expr) || !isJsSourceFile(expr.getSourceFile())) return null;
|
||||
const spec = requireSpecOf(expr);
|
||||
if (spec === null || canonicalBuiltinModule(spec) !== "events") return null;
|
||||
if (!isCjsRequireGlobal(lowerer, expr.expression)) return null;
|
||||
return lowerer.classes.get(RUNTIME_EMITTER_CLASS) ?? null;
|
||||
}
|
||||
|
||||
/** The runtime-provided node:stream classes as eagerly-registered
|
||||
* ClassInfos (the emitter story): mapType names `%Readable` et al the
|
||||
* moment a stream type appears, so the infos must exist before any
|
||||
@@ -2099,6 +2115,11 @@ export function collectClassShapeInner(
|
||||
// callee is NOT a mixin function keeps the computed-expression
|
||||
// fence below.
|
||||
if (t && ts.isCallExpression(t.expression) && !t.typeArguments?.nodes) {
|
||||
const requiredBase = builtinRequireBaseOf(lowerer, t.expression);
|
||||
if (requiredBase) {
|
||||
base = requiredBase;
|
||||
continue;
|
||||
}
|
||||
const mixinBase = lowerer.mixinCallClassInfoOf(t.expression);
|
||||
if (mixinBase) {
|
||||
base = mixinBase;
|
||||
@@ -5765,7 +5786,7 @@ export function lowerClassExpressionInfo(
|
||||
)?.types?.nodes[0]?.expression;
|
||||
if (
|
||||
heritage &&
|
||||
(ts.isCallExpression(heritage) ||
|
||||
((ts.isCallExpression(heritage) && !builtinRequireBaseOf(lowerer, heritage)) ||
|
||||
(ts.isIdentifier(heritage) &&
|
||||
(lowerer.peekLocal(heritage) ?? lowerer.globalOf(heritage)) != null))
|
||||
) {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { dynUndefinedExpr, boolLit } from "../../ir/build.js";
|
||||
* generic rejection, never silence. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { ladderFenceExpr } from "./lowerer.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -28,6 +27,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { resultIsDiscarded } from "./call-position.js";
|
||||
import { lowerCallbackArg as lowerCallbackArgShared } from "./callback-arg.js";
|
||||
import { documentedParameter } from "./call-signatures.js";
|
||||
|
||||
const DGRAM_SURFACE_HINT =
|
||||
"bind, connect, disconnect, send, address, close, unref/ref, the socket " +
|
||||
@@ -521,6 +521,17 @@ function lowerDnsModuleCall(
|
||||
);
|
||||
}
|
||||
|
||||
/** A listener JavaScript leaves untyped: a function literal with a
|
||||
* parameter that has neither an annotation nor JSDoc (paramShape makes it
|
||||
* checked-dynamic), or any other untyped value. */
|
||||
function untypedJsListener(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e))
|
||||
return e.parameters.nodes.some((param) => !documentedParameter(param));
|
||||
return (lowerer.typeOf(e).flags & ts.TypeFlags.Any) !== 0;
|
||||
}
|
||||
|
||||
/** Method calls on dgram.Socket receivers — one entry in lower-calls.ts's
|
||||
* intrinsic chain (after lowerServerMethodCall). Null for other
|
||||
* receivers. */
|
||||
@@ -576,9 +587,9 @@ export function lowerDgramMethodCall(
|
||||
// send(msg, port, address) — one datagram to an explicit destination
|
||||
// (the static fast path). Every OTHER shape in a JS source rides the
|
||||
// checked-dynamic ladder (dgram.sendChk): Node's signature shuffle,
|
||||
// slice bounds, list/type contracts, port/address validation, and
|
||||
// the connected-state errors — with the compiler-rendered fence as
|
||||
// the post-validation tail for the callback/list/connected forms.
|
||||
// slice bounds, list/type contracts, port/address validation, the
|
||||
// connected-state errors, then the send itself — lists, connected
|
||||
// sends and completion callbacks included.
|
||||
const staticShape =
|
||||
args.length === 3 &&
|
||||
!args.some(ts.isSpreadElement) &&
|
||||
@@ -602,13 +613,13 @@ export function lowerDgramMethodCall(
|
||||
}
|
||||
if (
|
||||
isJsSourceFile(ts.getSourceFileOfNode(call)) &&
|
||||
args.length <= 5 &&
|
||||
args.length <= 6 &&
|
||||
!args.some(ts.isSpreadElement)
|
||||
) {
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const slots: IrExpr[] = [];
|
||||
let ok = true;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const n = args[i];
|
||||
if (!n) {
|
||||
slots.push(dynUndefinedExpr(loc));
|
||||
@@ -636,16 +647,7 @@ export function lowerDgramMethodCall(
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dgram.sendChk",
|
||||
args: [
|
||||
receiver,
|
||||
...slots,
|
||||
ladderFenceExpr(
|
||||
lowerer,
|
||||
`send in this form`,
|
||||
call,
|
||||
"send(msg, port, address) — one string or Buffer datagram — is the lowered form; callback, list, and connected sends have no lowering yet",
|
||||
),
|
||||
],
|
||||
args: [receiver, ...slots],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
@@ -709,6 +711,22 @@ export function lowerDgramMethodCall(
|
||||
}
|
||||
if ((name === "on" || name === "once") && args.length === 2) {
|
||||
requireStatementPosition(lowerer, call, `socket.${name}(...)`);
|
||||
// A JavaScript listener whose parameters are untyped registers through
|
||||
// the socket's checked-dynamic handle, which hands it untyped values.
|
||||
if (isJsSourceFile(ts.getSourceFileOfNode(call)) && untypedJsListener(lowerer, args[1]!)) {
|
||||
return {
|
||||
kind: "dynInvoke",
|
||||
recv: lowerer.coerceToExpected(lowerer.lowerExpr(access.expression), DYN),
|
||||
method: name,
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: [
|
||||
lowerer.lowerExprExpecting(args[0]!, DYN),
|
||||
lowerer.lowerExprExpecting(args[1]!, DYN),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const once = boolLit(name === "once", loc);
|
||||
const evT = lowerer.typeOf(args[0]!);
|
||||
const event = evT.isStringLiteralType() ? evT.value : null;
|
||||
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
import { locOf } from "../program.js";
|
||||
import { genResultRecord } from "../type-mapper.js";
|
||||
import { forOfVarTarget, lowerDestructuringAssign } from "./lower-stmts.js";
|
||||
import { lowerBuiltinDelegation, lowerCheckedDelegation } from "./generator-delegation.js";
|
||||
import {
|
||||
lowerAsyncDelegation,
|
||||
lowerBuiltinDelegation,
|
||||
lowerCheckedDelegation,
|
||||
} from "./generator-delegation.js";
|
||||
|
||||
export type GenType = IrType & { kind: "generator" };
|
||||
|
||||
@@ -818,14 +822,21 @@ export function lowerYieldStarStatement(lowerer: Lowerer, expr: ts.Expression):
|
||||
function lowerYieldStar(lowerer: Lowerer, expr: ts.YieldExpression): IrExpr {
|
||||
const gen = lowerer.ctx.generator;
|
||||
if (!gen) lowerer.unsupported("SC1071", expr);
|
||||
if (!expr.expression) lowerer.unsupported("SC1071", expr, "'yield*' with no operand");
|
||||
// An async generator delegates through the async iterator protocol: a
|
||||
// compiled async generator operand crosses as its checked capsule.
|
||||
if (lowerer.ctx.isAsync) {
|
||||
const source = lowerer.lowerExpr(expr.expression);
|
||||
const delegate =
|
||||
source.kind === "dynCheck" && source.value.type.kind === "dyn" ? source.value : source;
|
||||
if (delegate.type.kind === "dyn" || lowerer.dynConvertible(delegate.type))
|
||||
return lowerAsyncDelegation(lowerer, expr, lowerer.coerceToExpected(delegate, DYN));
|
||||
lowerer.unsupported(
|
||||
"SC1071",
|
||||
expr,
|
||||
"'yield*' in async generators (use 'for await' and yield each value explicitly)",
|
||||
"'yield*' in async generators over this operand (use 'for await' and yield each value explicitly)",
|
||||
);
|
||||
}
|
||||
if (!expr.expression) lowerer.unsupported("SC1071", expr, "'yield*' with no operand");
|
||||
const loc = locOf(expr);
|
||||
// An array literal operand builds as its own array type: the yield*
|
||||
// context would otherwise type it by the generator.
|
||||
|
||||
@@ -134,6 +134,7 @@ import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
import { isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import {
|
||||
builtinRequireBaseOf,
|
||||
collectInheritedJsFieldWrites,
|
||||
collectVirtualJsMethods,
|
||||
decoratorNodesOf,
|
||||
@@ -432,7 +433,8 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void {
|
||||
const heritage = decl.heritageClauses?.nodes.find(
|
||||
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
|
||||
)?.types?.nodes[0]?.expression;
|
||||
if (heritage && ts.isCallExpression(heritage)) computed.add(decl);
|
||||
if (heritage && ts.isCallExpression(heritage) && !builtinRequireBaseOf(lowerer, heritage))
|
||||
computed.add(decl);
|
||||
}
|
||||
// Descendants need the computed ancestor's completed layout before
|
||||
// their own fields and inherited methods can be collected.
|
||||
|
||||
@@ -622,6 +622,20 @@ export function lowerFetchCall(lowerer: Lowerer, call: ts.CallExpression): IrExp
|
||||
: lowerer.lowerExpr(argument);
|
||||
stmts.push({ kind: "exprStmt", expr: discarded, loc: discarded.loc });
|
||||
}
|
||||
// A union input holding a URL arm (`string | URL`) converts through the
|
||||
// checked-dynamic input path, which reads the URL's href at the call
|
||||
// like the bare-URL arm below; narrowing it to a string would throw on
|
||||
// the URL arm.
|
||||
const unionWithUrl =
|
||||
input.type.kind === "union" &&
|
||||
(lowerer.unions.get(input.type.unionId)?.arms.some((arm) => arm.kind === "url") ?? false) &&
|
||||
lowerer.dynConvertible(input.type);
|
||||
const dynInput: IrExpr | null =
|
||||
input.type.kind === "dyn"
|
||||
? inputRef
|
||||
: unionWithUrl
|
||||
? { kind: "dynFrom", value: inputRef, type: DYN, loc: locOf(inputNode) }
|
||||
: null;
|
||||
const url: IrExpr =
|
||||
input.type.kind === "url"
|
||||
? {
|
||||
@@ -631,13 +645,13 @@ export function lowerFetchCall(lowerer: Lowerer, call: ts.CallExpression): IrExp
|
||||
type: STRING,
|
||||
loc: locOf(inputNode),
|
||||
}
|
||||
: input.type.kind === "dyn"
|
||||
? inputRef
|
||||
: dynInput !== null
|
||||
? dynInput
|
||||
: lowerer.coerceInto(inputNode, inputRef, STRING);
|
||||
const answer: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: input.type.kind === "dyn" ? "fetch.input" : "fetch.start",
|
||||
args: [input.type.kind === "dyn" ? inputRef : url, initRef],
|
||||
fn: dynInput !== null ? "fetch.input" : "fetch.start",
|
||||
args: [url, initRef],
|
||||
type: { kind: "promise", inner: DYN },
|
||||
loc,
|
||||
};
|
||||
|
||||
@@ -9222,6 +9222,14 @@ function lowerArrayAssignInto(
|
||||
* specific diagnostics; other iterables fail on their types. The loop
|
||||
* variable is a fresh const binding per iteration (its own scope, like a
|
||||
* loop body). */
|
||||
/** Whether a `for await` head is untyped: the checker answers any or
|
||||
* unknown, or a type that maps to a checked-dynamic value. */
|
||||
function isUntypedIterable(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
const type = lowerer.typeOf(node);
|
||||
if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) return true;
|
||||
return lowerer.mapTypeOf(type)?.kind === "dyn";
|
||||
}
|
||||
|
||||
export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
// Labels consume HERE (nested statements must never see them). The
|
||||
// plain array/string paths carry them; the container/matchAll/stdin
|
||||
@@ -9283,6 +9291,32 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
if (dynStream) return dynStream;
|
||||
const builtin = lowerForAwaitBuiltin(lowerer, stmt);
|
||||
if (builtin) return builtin;
|
||||
// An untyped value (a JavaScript async generator whose recursive
|
||||
// return the checker types `any`): a compiled async generator steps
|
||||
// natively; any other value takes the checked-dynamic async protocol.
|
||||
// A synchronous generator, and in JavaScript any other head that
|
||||
// reaches here, takes the same protocol: values of a synchronous
|
||||
// iterator are awaited as the async-from-sync iterator awaits them, and
|
||||
// a value that is not iterable throws Node's TypeError.
|
||||
const headT = lowerer.mapTypeOf(lowerer.typeOf(stmt.expression));
|
||||
const syncGenerator =
|
||||
(headT?.kind === "generator" && headT.async !== true) || isJsSourceFile(stmt.getSourceFile());
|
||||
if (lowerer.ctx.isAsync && (syncGenerator || isUntypedIterable(lowerer, stmt.expression))) {
|
||||
const iterable = lowerer.lowerExpr(stmt.expression);
|
||||
if (iterable.type.kind === "generator" && iterable.type.async)
|
||||
return lowerForAwaitGenerator(
|
||||
lowerer,
|
||||
stmt,
|
||||
iterable as IrExpr & { type: GenType },
|
||||
labels,
|
||||
);
|
||||
if (iterable.type.kind === "dyn") return lowerForOfDyn(lowerer, stmt, iterable, labels, true);
|
||||
if (
|
||||
(iterable.type.kind === "generator" || isJsSourceFile(stmt.getSourceFile())) &&
|
||||
lowerer.dynConvertible(iterable.type)
|
||||
)
|
||||
return lowerForOfDyn(lowerer, stmt, lowerer.coerceToExpected(iterable, DYN), labels, true);
|
||||
}
|
||||
// A NodeJS.ReadableStream iterates as the general Readable.
|
||||
requestGeneralStreamSurface(lowerer, stmt.expression);
|
||||
lowerer.unsupported(
|
||||
@@ -9582,9 +9616,11 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
|
||||
// A CHECKED-DYNAMIC iterable (`unknown[]`, the collapsed
|
||||
// `(string | object)[]`, JSON-parsed values): a checked IteratorRecord
|
||||
// binds each yielded dyn value.
|
||||
// binds each yielded dyn value. Under `for await` (a JavaScript array
|
||||
// literal mixing promises and plain values) the async-from-sync rules
|
||||
// await each value.
|
||||
if (iterable.type.kind === "dyn") {
|
||||
return lowerForOfDyn(lowerer, stmt, iterable, labels);
|
||||
return lowerForOfDyn(lowerer, stmt, iterable, labels, stmt.awaitModifier !== undefined);
|
||||
}
|
||||
if (tupleSource === null) {
|
||||
lowerer.badType(stmt.expression, lowerer.typeOf(stmt.expression));
|
||||
@@ -10578,9 +10614,10 @@ function lowerForOfDyn(
|
||||
stmt: ts.ForOfStatement,
|
||||
iterable: IrExpr,
|
||||
labels?: string[],
|
||||
asyncLoop = false,
|
||||
): IrStmt {
|
||||
const loc = locOf(stmt);
|
||||
const spell = notIterableSpelling(stmt.expression);
|
||||
const spell = notIterableSpelling(stmt.expression, asyncLoop ? "async iterable" : "iterable");
|
||||
const iterator = lowerer.declareHiddenLocal("%dofIterator", DYN);
|
||||
const next = lowerer.declareHiddenLocal("%dofNext", DYN);
|
||||
const fast = lowerer.declareHiddenLocal("%dofNative", BOOL);
|
||||
@@ -10591,6 +10628,14 @@ function lowerForOfDyn(
|
||||
const needsClose = lowerer.declareHiddenLocal("%dofNeedsClose", BOOL);
|
||||
needsClose.mutable = true;
|
||||
const error = lowerer.declareHiddenLocal("%dofError", CAUGHT);
|
||||
const syncIterator = asyncLoop ? lowerer.declareHiddenLocal("%dofSync", BOOL) : null;
|
||||
const awaitDyn = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "async.awaitDyn",
|
||||
args: [value],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const get = (source: IrExpr, key: string): IrExpr => ({
|
||||
kind: "dynKeyGet",
|
||||
value: source,
|
||||
@@ -10643,7 +10688,26 @@ function lowerForOfDyn(
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [{ kind: "exprStmt", expr: call(varRef(returned.id, DYN, loc)), loc }],
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
// return()'s (awaited) result must be an object, as for next().
|
||||
expr: asyncLoop
|
||||
? check(
|
||||
awaitDyn({
|
||||
kind: "dynCall",
|
||||
callee: varRef(returned.id, DYN, loc),
|
||||
receiver: varRef(iterator.id, DYN, loc),
|
||||
calleeName: "iterator method",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
)
|
||||
: call(varRef(returned.id, DYN, loc)),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
@@ -10736,15 +10800,24 @@ function lowerForOfDyn(
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: value.id,
|
||||
init: iteratorValue(
|
||||
lowerer,
|
||||
varRef(iterator.id, DYN, loc),
|
||||
varRef(next.id, DYN, loc),
|
||||
varRef(fast.id, BOOL, loc),
|
||||
done,
|
||||
loc,
|
||||
"iterator method",
|
||||
),
|
||||
init: syncIterator
|
||||
? asyncIteratorValue(
|
||||
lowerer,
|
||||
varRef(iterator.id, DYN, loc),
|
||||
varRef(next.id, DYN, loc),
|
||||
varRef(syncIterator.id, BOOL, loc),
|
||||
done,
|
||||
loc,
|
||||
)
|
||||
: iteratorValue(
|
||||
lowerer,
|
||||
varRef(iterator.id, DYN, loc),
|
||||
varRef(next.id, DYN, loc),
|
||||
varRef(fast.id, BOOL, loc),
|
||||
done,
|
||||
loc,
|
||||
"iterator method",
|
||||
),
|
||||
loc,
|
||||
},
|
||||
{
|
||||
@@ -10769,13 +10842,29 @@ function lowerForOfDyn(
|
||||
localId: iterator.id,
|
||||
init: check({
|
||||
kind: "libCall",
|
||||
fn: "dyn.iterator",
|
||||
fn: asyncLoop ? "dyn.asyncIteratorOpen" : "dyn.iterator",
|
||||
args: [iterable, { kind: "strLit", value: spell, type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
loc,
|
||||
},
|
||||
...(syncIterator
|
||||
? [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: syncIterator.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.asyncIteratorWasSync",
|
||||
args: [],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
} satisfies IrStmt,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: next.id,
|
||||
@@ -10785,7 +10874,9 @@ function lowerForOfDyn(
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: fast.id,
|
||||
init: iteratorCanStep(varRef(iterator.id, DYN, loc), varRef(next.id, DYN, loc), loc),
|
||||
init: asyncLoop
|
||||
? { kind: "boolLit", value: false, type: BOOL, loc }
|
||||
: iteratorCanStep(varRef(iterator.id, DYN, loc), varRef(next.id, DYN, loc), loc),
|
||||
loc,
|
||||
},
|
||||
{
|
||||
@@ -10837,6 +10928,99 @@ function lowerForOfDyn(
|
||||
}
|
||||
}
|
||||
|
||||
/** One step of `for await` over a checked-dynamic iterator: await next()'s
|
||||
* result, read done before value, and await each value of a synchronous
|
||||
* iterator, including its final one, as the async-from-sync iterator does. */
|
||||
function asyncIteratorValue(
|
||||
lowerer: Lowerer,
|
||||
iterator: IrExpr,
|
||||
next: IrExpr,
|
||||
sync: IrExpr,
|
||||
done: IrLocal,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const result = lowerer.declareHiddenLocal("%asyncIteratorResult", DYN);
|
||||
const get = (key: string): IrExpr => ({
|
||||
kind: "dynKeyGet",
|
||||
value: varRef(result.id, DYN, loc),
|
||||
key: { kind: "strLit", value: key, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const awaitDyn = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "async.awaitDyn",
|
||||
args: [value],
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
type: DYN,
|
||||
loc,
|
||||
stmts: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: result.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.iteratorResult",
|
||||
args: [
|
||||
awaitDyn({
|
||||
kind: "dynCall",
|
||||
callee: next,
|
||||
receiver: iterator,
|
||||
calleeName: "iterator method",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "assign",
|
||||
localId: done.id,
|
||||
value: { kind: "dynTest", test: "truthy", value: get("done"), type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
// A synchronous iterator's final value is still read and awaited, so a
|
||||
// throwing getter or a rejected promise there rejects the loop.
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: sync,
|
||||
then: {
|
||||
kind: "ternary",
|
||||
cond: varRef(done.id, BOOL, loc),
|
||||
then: {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: awaitDyn(get("value")), loc }],
|
||||
result: dynUndefinedExpr(loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
else_: awaitDyn(get("value")),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
else_: {
|
||||
kind: "ternary",
|
||||
cond: varRef(done.id, BOOL, loc),
|
||||
then: dynUndefinedExpr(loc),
|
||||
else_: get("value"),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function lowerForOfString(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.ForOfStatement,
|
||||
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
} from "./general-streams.js";
|
||||
import { extendAssertedRecordMembers } from "./assertion-members.js";
|
||||
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
|
||||
import { builtinClassValue, builtinMemberClassKind } from "./builtins/builtin-class-dyn.js";
|
||||
import { isJsSourceFileName } from "../tsc-codes.js";
|
||||
import {
|
||||
builtinClassKind,
|
||||
builtinClassValue,
|
||||
builtinMemberClassKind,
|
||||
} from "./builtins/builtin-class-dyn.js";
|
||||
import {
|
||||
lifetimeRegionType,
|
||||
lowerLifetimeRegionCall,
|
||||
@@ -9010,7 +9015,33 @@ export class Lowerer {
|
||||
}
|
||||
// A runtime-provided class value has no checked-dynamic form that
|
||||
// inspects as Node's does (`[Function: EventEmitter] { ... }`).
|
||||
// JavaScript sources already read EventEmitter and the stream
|
||||
// classes as the runtime's untyped constructors (builtin-class-dyn.ts),
|
||||
// so one of those classes turning untyped there becomes the same
|
||||
// constructor. `require("events")` is the EventEmitter class itself,
|
||||
// so its module binding is too (`ReleaseEmitter = EventEmitter` over
|
||||
// `const EventEmitter = require("events")`).
|
||||
if (
|
||||
expr.type.kind === "moduleNs" &&
|
||||
expr.type.moduleId === "builtin:events" &&
|
||||
isJsSourceFileName(expr.loc.file)
|
||||
) {
|
||||
const value = builtinClassValue(0, expr.loc);
|
||||
if (expr.kind === "moduleNsRef") return value;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr, loc: expr.loc }],
|
||||
result: value,
|
||||
type: DYN,
|
||||
loc: expr.loc,
|
||||
};
|
||||
}
|
||||
if (expr.type.kind === "classval" && this.classes.get(expr.type.className)?.def.runtime) {
|
||||
// Only the class itself: a slot of the class's type may hold a
|
||||
// program subclass, which this constructor would misidentify.
|
||||
const kind = builtinClassKind(expr.type.className);
|
||||
if (kind !== null && expr.kind === "classRef" && isJsSourceFileName(expr.loc.file))
|
||||
return builtinClassValue(kind, expr.loc);
|
||||
this.pushDiag(
|
||||
unsupportedDiag(
|
||||
"SC1090",
|
||||
@@ -9090,6 +9121,24 @@ export class Lowerer {
|
||||
);
|
||||
const handleOk = DYN_HANDLE_KINDS.has(expected.kind);
|
||||
noteUntypedStreamFlow(this, expected);
|
||||
// `[EventEmitter]` in JavaScript builds an untyped array of the
|
||||
// untyped constructor, which a typed array of the class cannot
|
||||
// hold: no class check admits that constructor.
|
||||
if (
|
||||
expr.kind === "dynArrLit" &&
|
||||
expected.kind === "array" &&
|
||||
expected.elem.kind === "classval" &&
|
||||
expr.elems.some((elem) => elem.kind === "libCall" && elem.fn === "dyn.builtinClass")
|
||||
) {
|
||||
this.pushDiag(
|
||||
unsupportedDiag(
|
||||
"SC1090",
|
||||
expr.loc,
|
||||
`holding runtime-provided class values ('${expected.elem.className.slice(1)}') in a typed array`,
|
||||
),
|
||||
);
|
||||
throw new PoisonError();
|
||||
}
|
||||
if (
|
||||
canDynCheckTo(
|
||||
expected,
|
||||
|
||||
@@ -243,7 +243,8 @@ export class StringIndexBounds {
|
||||
/** Every write of a block-scoped local or parameter, or null for any
|
||||
* other binding (module imports, var, class members, ...). */
|
||||
private binding(symbol: ts.Symbol): Binding | null {
|
||||
if (this.bindings.has(symbol)) return this.bindings.get(symbol)!;
|
||||
const cached = this.bindings.get(symbol);
|
||||
if (cached !== undefined) return cached;
|
||||
const result = this.scanBinding(symbol);
|
||||
this.bindings.set(symbol, result);
|
||||
return result;
|
||||
|
||||
@@ -2121,7 +2121,8 @@ function stableCreateRequireBindingReason7(
|
||||
decl: ts.VariableDeclaration,
|
||||
): string | null {
|
||||
const cache = program.analysis.createRequireReasons;
|
||||
if (cache.has(decl)) return cache.get(decl)!;
|
||||
const cached = cache.get(decl);
|
||||
if (cached !== undefined) return cached;
|
||||
const check = (): string | null => {
|
||||
const stmt = decl.parent?.parent;
|
||||
if (
|
||||
|
||||
@@ -21,9 +21,13 @@ import { dirname, join, resolve } from "node:path";
|
||||
import { Ts7Api } from "./api.js";
|
||||
import type { Ts7TimingInfo } from "./session-timing.js";
|
||||
import type { Ts7FileSystem } from "./rpc-filesystem.js";
|
||||
import type { CompilerOptions, Diagnostic, SourceFileMetadata } from "./semantic-types.js";
|
||||
import type { Diagnostic, SourceFileMetadata } from "./semantic-types.js";
|
||||
import type { SourceFile } from "./ast-types.js";
|
||||
import type { Ts7SessionProject as Project, Ts7SessionSnapshot as Snapshot } from "./session.js";
|
||||
import type {
|
||||
Ts7ProjectCompilerOptions,
|
||||
Ts7SessionProject as Project,
|
||||
Ts7SessionSnapshot as Snapshot,
|
||||
} from "./session.js";
|
||||
import { CheckerFacade } from "./checker.js";
|
||||
import {
|
||||
moduleDetectionKindName,
|
||||
@@ -45,8 +49,9 @@ import { packageShimDeclarationText } from "../package-shims.js";
|
||||
/** The compiler options our createProgram accepts: TS7's CompilerOptions
|
||||
* shape (numeric enums for target/module/moduleResolution — the enums module
|
||||
* generates them from the pinned SDK) with the 5.9.3 "lib.es2025.d.ts" lib
|
||||
* spelling also accepted. */
|
||||
export type Ts7CompilerOptions = CompilerOptions;
|
||||
* spelling also accepted, plus the project options scriptc adopts that the
|
||||
* TS7 schema no longer declares (Ts7ProjectCompilerOptions). */
|
||||
export type Ts7CompilerOptions = Ts7ProjectCompilerOptions;
|
||||
|
||||
/* tsconfig JSON wants enum NAMES; the options object carries TS7's numeric
|
||||
* enum values. Generated name lookups preserve the SDK's reverse mappings
|
||||
@@ -288,7 +293,7 @@ export class Ts7Program {
|
||||
private readonly configPath: string,
|
||||
) {}
|
||||
|
||||
getCompilerOptions(): CompilerOptions {
|
||||
getCompilerOptions(): Ts7CompilerOptions {
|
||||
return this.project.program.getCompilerOptions();
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,24 @@ import type {
|
||||
Ts7SourceMetadata,
|
||||
} from "./session-schema.generated.js";
|
||||
|
||||
/** Project options tsgo still reports for a tsconfig but the TS 7 SDK
|
||||
* schema no longer declares. scriptc adopts them (ADOPTED_OPTIONS), so the
|
||||
* options a project reports are parsed with them declared: a native
|
||||
* compiler keeps only declared fields when it parses the response. */
|
||||
export interface Ts7ProjectCompilerOptions extends Ts7CompilerOptionsData {
|
||||
alwaysStrict?: boolean;
|
||||
esModuleInterop?: boolean;
|
||||
allowSyntheticDefaultImports?: boolean;
|
||||
}
|
||||
|
||||
interface Ts7ReportedProjectData extends Ts7ProjectData {
|
||||
compilerOptions: Ts7ProjectCompilerOptions;
|
||||
}
|
||||
|
||||
interface Ts7ReportedSnapshotData extends Ts7SnapshotData {
|
||||
projects: Ts7ReportedProjectData[];
|
||||
}
|
||||
|
||||
export interface Ts7FileChanges {
|
||||
changed?: SemanticDocument[];
|
||||
created?: SemanticDocument[];
|
||||
@@ -104,7 +122,7 @@ export class Ts7Session {
|
||||
|
||||
updateSnapshot(params: Ts7Update = {}): Ts7SessionSnapshot {
|
||||
const paths = this.initialize();
|
||||
const data = parseSemanticJson<Ts7SnapshotData>(
|
||||
const data = parseSemanticJson<Ts7ReportedSnapshotData>(
|
||||
this.text("updateSnapshot", JSON.stringify(params)),
|
||||
);
|
||||
const snapshot = new Ts7SessionSnapshot(data, this, paths);
|
||||
@@ -160,7 +178,7 @@ export class Ts7SessionSnapshot {
|
||||
private disposed = false;
|
||||
|
||||
constructor(
|
||||
data: Ts7SnapshotData,
|
||||
data: Ts7ReportedSnapshotData,
|
||||
private readonly session: Ts7Session,
|
||||
readonly paths: Ts7Paths,
|
||||
) {
|
||||
@@ -189,7 +207,7 @@ export class Ts7SessionSnapshot {
|
||||
}
|
||||
getDefaultProjectForFile(file: SemanticDocument): Ts7SessionProject | undefined {
|
||||
this.ensureActive();
|
||||
const data = this.session.request<Ts7ProjectData | null>("getDefaultProjectForFile", {
|
||||
const data = this.session.request<Ts7ReportedProjectData | null>("getDefaultProjectForFile", {
|
||||
snapshot: this.id,
|
||||
file,
|
||||
});
|
||||
@@ -215,12 +233,12 @@ export class Ts7SessionSnapshot {
|
||||
export class Ts7SessionProject {
|
||||
readonly id: string;
|
||||
readonly configFileName: string;
|
||||
readonly compilerOptions: Ts7CompilerOptionsData;
|
||||
readonly compilerOptions: Ts7ProjectCompilerOptions;
|
||||
readonly rootFiles: string[];
|
||||
readonly program: Ts7SessionProgram;
|
||||
readonly checker: SemanticChecker;
|
||||
|
||||
constructor(data: Ts7ProjectData, snapshot: Ts7SessionSnapshot, session: Ts7Session) {
|
||||
constructor(data: Ts7ReportedProjectData, snapshot: Ts7SessionSnapshot, session: Ts7Session) {
|
||||
this.id = data.id;
|
||||
this.configFileName = data.configFileName;
|
||||
this.compilerOptions = data.compilerOptions;
|
||||
@@ -261,7 +279,7 @@ export class Ts7SessionProgram {
|
||||
...(file === undefined ? {} : { file }),
|
||||
};
|
||||
}
|
||||
getCompilerOptions(): Ts7CompilerOptionsData {
|
||||
getCompilerOptions(): Ts7ProjectCompilerOptions {
|
||||
this.ensureActive();
|
||||
return this.project.compilerOptions;
|
||||
}
|
||||
|
||||
@@ -4718,7 +4718,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// semantics would DEGRADE under the collapse — class instances,
|
||||
// Maps/Sets, functions (closure identity, `x === String`
|
||||
// narrowing), promises, regexes, generators, handles — keep their
|
||||
// existing union homes and fences below. A pending recursive
|
||||
// existing union homes and fences below. URL is the
|
||||
// exception: its handle boxes by reference and keeps every member,
|
||||
// so it collapses too (dynSubsumableUnionArm). A pending recursive
|
||||
// placeholder cannot collapse (back-references already hold the
|
||||
// union id); the degenerate recursive spelling keeps its fence.
|
||||
if (
|
||||
@@ -7704,6 +7706,13 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
|
||||
case "undefinedT":
|
||||
case "nullT":
|
||||
return true;
|
||||
// URL boxes BY REFERENCE (its checked-dynamic handle keeps identity,
|
||||
// reads and writes its members through the native accessors, answers
|
||||
// `instanceof URL`, and checks back out to the typed value), so it
|
||||
// rides the collapse beside fetch's Request/Response handles:
|
||||
// `string | URL | Request`, fetch's own input type under @types/node.
|
||||
case "url":
|
||||
return true;
|
||||
case "record":
|
||||
case "array":
|
||||
// `T | Promise<T>` with T bound to `unknown`: the checked-dynamic value
|
||||
|
||||
@@ -547,6 +547,7 @@ async function compileExecutableNative(
|
||||
...(features.dynTier ? { dynTier: true } : {}),
|
||||
...(features.streamDyn ? { streamDyn: true } : {}),
|
||||
...(features.handleProps ? { handleProps: true } : {}),
|
||||
...(features.dynForAwait ? { dynForAwait: true } : {}),
|
||||
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
|
||||
...(features.dynBufferNum ? { dynBufferNum: true } : {}),
|
||||
...(features.builtinValues ? { builtinValues: true } : {}),
|
||||
@@ -556,6 +557,7 @@ async function compileExecutableNative(
|
||||
bigint: features.bigint,
|
||||
searchParams: features.searchParams,
|
||||
qs: features.qs,
|
||||
utilPromisify: features.utilPromisify,
|
||||
dynBuiltinClass: features.dynBuiltinClass,
|
||||
dynBuiltinStream: features.dynBuiltinStream,
|
||||
parseArgs: features.parseArgs,
|
||||
@@ -565,6 +567,7 @@ async function compileExecutableNative(
|
||||
http2: features.http2,
|
||||
dgram: features.dgram,
|
||||
dnsPromises: features.dnsPromises,
|
||||
dgramDyn: features.dgramDyn,
|
||||
watch: features.watch,
|
||||
foreignFfi: features.foreignFfi,
|
||||
...(features.workers ? { workers: true } : {}),
|
||||
|
||||
@@ -542,6 +542,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"util.inherits",
|
||||
"util.deprecate",
|
||||
"util.callbackify",
|
||||
"util.promisifyDyn",
|
||||
"util.debuglog",
|
||||
// The URI codecs throw the spec's URIError: decoding on bad hex/invalid
|
||||
// UTF-8 octets, encoding (and querystring.escape) on a lone surrogate.
|
||||
@@ -634,6 +635,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dyn.destructureRest",
|
||||
"dyn.arrayFromIterator",
|
||||
"dyn.iterator",
|
||||
"dyn.asyncIteratorOpen",
|
||||
"dyn.iteratorResult",
|
||||
"dyn.iteratorStep",
|
||||
"dyn.mapSeedEntries",
|
||||
|
||||
@@ -219,6 +219,11 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"dyn.destructureRest": { argTypes: [DYN, STRING], result: DYN },
|
||||
"dyn.arrayFromIterator": { argTypes: [DYN], result: DYN },
|
||||
"dyn.iterator": { argTypes: [DYN, STRING], result: DYN },
|
||||
/** for await over a checked-dynamic value: the loop's iterator (a
|
||||
* compiled async generator, Symbol.asyncIterator's result, or the sync
|
||||
* iterator), and whether that iterator is synchronous. */
|
||||
"dyn.asyncIteratorOpen": { argTypes: [DYN, STRING], result: DYN },
|
||||
"dyn.asyncIteratorWasSync": { argTypes: [], result: BOOL },
|
||||
"dyn.iteratorResult": { argTypes: [DYN], result: DYN },
|
||||
"dyn.iteratorCanStep": { argTypes: [DYN, DYN], result: BOOL },
|
||||
"dyn.iteratorStep": { argTypes: [DYN], result: DYN },
|
||||
@@ -1100,6 +1105,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"util.inherits": { argTypes: [DYN, DYN], result: VOID },
|
||||
"util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN },
|
||||
"util.callbackify": { argTypes: [DYN], result: DYN },
|
||||
/** util.promisify over an untyped function value: a native dyn function
|
||||
* whose calls answer a dyn promise the appended callback settles. */
|
||||
"util.promisifyDyn": { argTypes: [DYN], result: DYN },
|
||||
"util.debuglog": { argTypes: [DYN, DYN], result: DYN },
|
||||
"util.typeIs": { argTypes: [DYN, STRING], result: BOOL },
|
||||
"util.typeValue": { argTypes: [DYN, STRING], result: DYN },
|
||||
@@ -1516,10 +1524,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"dgram.sendBytes": { argTypes: [DGRAMSOCK_T, BYTES_U8, F64, STRING], result: VOID },
|
||||
/** The send argument-validation ladder over dyn arguments (Node's
|
||||
* signature shuffle: slice bounds, list/type contracts, port/address
|
||||
* validation, connected-state errors) — a fully-validated unconnected
|
||||
* single-payload send RUNS; callback/list/connected forms meet the
|
||||
* trailing fence. May-throw. */
|
||||
"dgram.sendChk": { argTypes: [DGRAMSOCK_T, DYN, DYN, DYN, DYN, DYN, STRING], result: VOID },
|
||||
* validation, connected-state errors), then the send: lists, connected
|
||||
* sends and completion callbacks included. Up to six arguments, the
|
||||
* missing ones undefined. May-throw. */
|
||||
"dgram.sendChk": { argTypes: [DGRAMSOCK_T, DYN, DYN, DYN, DYN, DYN, DYN], result: VOID },
|
||||
"dgram.address": { argTypes: [DGRAMSOCK_T], result: VOID },
|
||||
"dgram.close": { argTypes: [DGRAMSOCK_T], result: VOID },
|
||||
"dgram.closeCb": {
|
||||
|
||||
@@ -36,6 +36,11 @@ function variableIncrement(id: string, value: IrExpr): { localId: string; sign:
|
||||
* filtering, breaks and continues can only reduce the budget. A nested
|
||||
* loop or opaque control region invalidates the bindings it writes.
|
||||
*
|
||||
* A cursor may also be reset to an integer literal (`run = 0`) or set to
|
||||
* another bounded binding (`longest = run`): every value it then holds is
|
||||
* its entry value, a reset value or a value the source held, plus at most
|
||||
* the increments of the whole loop.
|
||||
*
|
||||
* The counter's step is a positive integer literal or a binding the loop
|
||||
* never writes whose entry fact is a positive integer range (the sieve's
|
||||
* `j += i`): every update then adds an exact integer in that range, so the
|
||||
@@ -68,6 +73,8 @@ export function boundedIntegerLoopFacts(
|
||||
refused = new Set<string>();
|
||||
const lower = new Map<string, number>(),
|
||||
upper = new Map<string, number>();
|
||||
const resets = new Map<string, IntegerRange>(),
|
||||
copies = new Map<string, Set<string>>();
|
||||
let opaque = 0;
|
||||
const write = (id: string, delta: number | null): void => {
|
||||
written.add(id);
|
||||
@@ -81,8 +88,29 @@ export function boundedIntegerLoopFacts(
|
||||
lower.set(id, lo);
|
||||
upper.set(id, hi);
|
||||
};
|
||||
const assignment = (id: string, value: IrExpr): void => {
|
||||
const delta = increment(id, value);
|
||||
if (opaque || delta !== null) write(id, delta);
|
||||
else if (
|
||||
value.kind === "numLit" &&
|
||||
Number.isSafeInteger(value.value) &&
|
||||
!Object.is(value.value, -0)
|
||||
) {
|
||||
written.add(id);
|
||||
const range = resets.get(id);
|
||||
resets.set(id, {
|
||||
min: Math.min(range?.min ?? value.value, value.value),
|
||||
max: Math.max(range?.max ?? value.value, value.value),
|
||||
});
|
||||
} else if (value.kind === "varRef" && value.localId !== id) {
|
||||
written.add(id);
|
||||
const sources = copies.get(id) ?? new Set<string>();
|
||||
sources.add(value.localId);
|
||||
copies.set(id, sources);
|
||||
} else write(id, null);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "assignExpr") write(e.localId, increment(e.localId, e.value));
|
||||
if (e.kind === "assignExpr") assignment(e.localId, e.value);
|
||||
if (e.kind === "incDec") write(e.localId, e.op === "+" ? 1 : -1);
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
@@ -95,12 +123,19 @@ export function boundedIntegerLoopFacts(
|
||||
s.kind === "tryCatch" ||
|
||||
s.kind === "switch";
|
||||
if (region) opaque++;
|
||||
if (s.kind === "assign") write(s.localId, increment(s.localId, s.value));
|
||||
if (s.kind === "assign") assignment(s.localId, s.value);
|
||||
if (s.kind === "varDecl" || s.kind === "forOf") write(s.localId, null);
|
||||
const completed = everyStmtChild(s, expr, stmt);
|
||||
if (region) opaque--;
|
||||
return completed;
|
||||
};
|
||||
// The condition and the update run on every pass too. A binding either
|
||||
// one writes (`i < (src = next(), n)`) changes in ways the budget does
|
||||
// not count, so it is neither a stable bound nor a copy source.
|
||||
opaque++;
|
||||
expr(loop.cond);
|
||||
expr(loop.update.value);
|
||||
opaque--;
|
||||
loop.body.every(stmt);
|
||||
if (written.has(counter)) return result;
|
||||
const stable = (id: string): boolean => {
|
||||
@@ -147,15 +182,45 @@ export function boundedIntegerLoopFacts(
|
||||
// Header facts also cover the failing final condition and zero iterations.
|
||||
const maximum = Math.max(start.max, last + step.max);
|
||||
if (Number.isSafeInteger(maximum)) result.set(counter, { min: start.min, max: maximum });
|
||||
for (const [id, hi] of upper) {
|
||||
const range = initial.get(id);
|
||||
if (!range || refused.has(id) || id === counter) continue;
|
||||
const decrement = iterations * lower.get(id)!;
|
||||
const increment = iterations * hi;
|
||||
if (!Number.isSafeInteger(decrement) || !Number.isSafeInteger(increment)) continue;
|
||||
const min = range.min + decrement;
|
||||
const max = range.max + increment;
|
||||
if (Number.isSafeInteger(min) && Number.isSafeInteger(max)) result.set(id, { min, max });
|
||||
// Every value a copy reads lies in its source's range: a cursor bounded
|
||||
// here, or a binding the loop never writes.
|
||||
const source = (id: string): IntegerRange | undefined =>
|
||||
result.get(id) ?? (stable(id) ? initial.get(id) : undefined);
|
||||
// Copies wait for their sources; a cycle of copies stays unbounded.
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const id of written) {
|
||||
const range = initial.get(id);
|
||||
if (!range || refused.has(id) || id === counter || result.has(id)) continue;
|
||||
let min = range.min,
|
||||
max = range.max;
|
||||
const reset = resets.get(id);
|
||||
if (reset) {
|
||||
min = Math.min(min, reset.min);
|
||||
max = Math.max(max, reset.max);
|
||||
}
|
||||
let bounded = true;
|
||||
const sources = copies.get(id);
|
||||
if (sources)
|
||||
for (const from of sources) {
|
||||
const copied = source(from);
|
||||
if (!copied) bounded = false;
|
||||
else {
|
||||
min = Math.min(min, copied.min);
|
||||
max = Math.max(max, copied.max);
|
||||
}
|
||||
}
|
||||
if (!bounded) continue;
|
||||
const decrement = iterations * (lower.get(id) ?? 0);
|
||||
const increment = iterations * (upper.get(id) ?? 0);
|
||||
if (!Number.isSafeInteger(decrement) || !Number.isSafeInteger(increment)) continue;
|
||||
min += decrement;
|
||||
max += increment;
|
||||
if (Number.isSafeInteger(min) && Number.isSafeInteger(max)) {
|
||||
result.set(id, { min, max });
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
analyzeIntegerRangeFacts,
|
||||
analyzeIntegerRanges,
|
||||
bitwiseIdentityOperand,
|
||||
bitwiseRange,
|
||||
@@ -867,6 +868,99 @@ test("an array length bounds while and for counters even when the body resizes t
|
||||
expect(analyzeIntegerRanges(g).get(cursor)).toEqual({ min: 0, max: 4294967295 });
|
||||
});
|
||||
|
||||
test("a counter plus or minus a literal compared with a bound narrows the counter", () => {
|
||||
// for (let i = 0; i + 1 < items.length; i++) { pair(items, i, i + 1); }
|
||||
const ARR = { kind: "array", elem: F64 } as const;
|
||||
const items: IrExpr = { loc, kind: "varRef", localId: "items", type: ARR };
|
||||
const length: IrExpr = {
|
||||
loc,
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: items,
|
||||
args: [],
|
||||
type: F64,
|
||||
};
|
||||
const inside = ref("i"),
|
||||
next = bin("+", ref("i"), num(1)),
|
||||
step = bin("+", ref("i"), num(1));
|
||||
const call: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "call", callee: "pair", args: [items, inside, next], type: F64 },
|
||||
};
|
||||
const f = fn([
|
||||
{
|
||||
loc,
|
||||
kind: "for",
|
||||
init: { loc, kind: "varDecl", localId: "i", init: num(0) },
|
||||
cond: condition("<", bin("+", ref("i"), num(1)), length),
|
||||
update: { loc, kind: "assign", localId: "i", value: step },
|
||||
body: [call],
|
||||
},
|
||||
]);
|
||||
f.locals.push(
|
||||
{ id: "i", name: "i", type: F64, mutable: true },
|
||||
{ id: "items", name: "items", type: ARR, mutable: false },
|
||||
);
|
||||
const ranges = analyzeIntegerRanges(f);
|
||||
expect(ranges.get(inside)).toEqual({ min: 0, max: 4294967293 });
|
||||
expect(ranges.get(next)).toEqual({ min: 1, max: 4294967294 });
|
||||
expect(ranges.get(step)).toEqual({ min: 1, max: 4294967294 });
|
||||
|
||||
// if (1 + x < 10) observe(x); if (x - 2 >= 0) observe(x); with x in [0, 100]
|
||||
const below = ref(),
|
||||
above = ref(),
|
||||
wide = ref();
|
||||
const g = fn([
|
||||
assign(bin("&", ref("unknown"), num(100))),
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: condition("<", bin("+", num(1), ref()), num(10)),
|
||||
then: [observe(below)],
|
||||
else_: null,
|
||||
},
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: condition(">=", bin("-", ref(), num(2)), num(0)),
|
||||
then: [observe(above)],
|
||||
else_: null,
|
||||
},
|
||||
// A bound beyond 2^52 narrows nothing through an offset.
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: condition("<", bin("+", ref(), num(1)), num(2 ** 53)),
|
||||
then: [observe(wide)],
|
||||
else_: null,
|
||||
},
|
||||
]);
|
||||
const gRanges = analyzeIntegerRanges(g);
|
||||
expect(gRanges.get(below)).toEqual({ min: 0, max: 8 });
|
||||
expect(gRanges.get(above)).toEqual({ min: 2, max: 100 });
|
||||
expect(gRanges.get(wide)).toEqual({ min: 0, max: 100 });
|
||||
});
|
||||
|
||||
test("an offset comes off a fractional bound after the bound rounds to an integer", () => {
|
||||
// x = unknown | 0; if (x + 2147483648 > -(2 ** -60)) observe(x): the sum is
|
||||
// an integer, so the condition holds exactly when x + 2^31 >= 0. Subtracting
|
||||
// the offset from the fractional bound first would round to -2^31 and drop
|
||||
// x = -2^31 from the range.
|
||||
const kept = ref();
|
||||
const f = fn([
|
||||
assign(bin("|", ref("unknown"), num(0))),
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: condition(">", bin("+", ref(), num(2147483648)), num(-(2 ** -60))),
|
||||
then: [observe(kept)],
|
||||
else_: null,
|
||||
},
|
||||
]);
|
||||
expect(analyzeIntegerRanges(f).get(kept)).toEqual({ min: -2147483648, max: 2147483647 });
|
||||
});
|
||||
|
||||
test("whole-program slot facts seed parameters, field reads and call results", () => {
|
||||
const field: IrExpr = {
|
||||
loc,
|
||||
@@ -1041,3 +1135,119 @@ test("bitwise identity operands follow ToInt32 and the shift-count mask", () =>
|
||||
expect(identity("<<", num(0), x)).toBeNull();
|
||||
expect(identity(">>>", num(0), x)).toBeNull();
|
||||
});
|
||||
|
||||
test("a for loop bounds cursors it resets and copies, and their writes keep the loop range", () => {
|
||||
// for (x = 0; x < 100; x++) { if (flag === 0) run = run + 1; else run = 0;
|
||||
// if (run > longest) longest = run; changes = changes + 1; }
|
||||
const sum = bin("+", ref("run"), num(1)),
|
||||
copied = ref("run"),
|
||||
counted = bin("+", ref("changes"), num(1));
|
||||
const write = (localId: string, value: IrExpr): IrStmt => ({
|
||||
loc,
|
||||
kind: "assign",
|
||||
localId,
|
||||
value,
|
||||
});
|
||||
const loop: IrStmt & { kind: "for" } = {
|
||||
loc,
|
||||
kind: "for",
|
||||
init: { loc, kind: "varDecl", localId: "x", init: num(0) },
|
||||
cond: condition("<", ref(), num(100)),
|
||||
update: assign(bin("+", ref(), num(1))),
|
||||
body: [
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: condition("===", ref("flag"), num(0)),
|
||||
then: [write("run", sum)],
|
||||
else_: [write("run", num(0))],
|
||||
},
|
||||
{
|
||||
loc,
|
||||
kind: "if",
|
||||
cond: condition(">", ref("run"), ref("longest")),
|
||||
then: [write("longest", copied)],
|
||||
else_: null,
|
||||
},
|
||||
write("changes", counted),
|
||||
],
|
||||
};
|
||||
const declare = (localId: string): IrStmt => ({ loc, kind: "varDecl", localId, init: num(0) });
|
||||
const f = fn([declare("run"), declare("longest"), declare("changes"), loop]);
|
||||
f.locals.push(
|
||||
...["run", "longest", "changes", "flag"].map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
type: F64,
|
||||
mutable: true,
|
||||
})),
|
||||
);
|
||||
const ranges = analyzeIntegerRanges(f);
|
||||
// Each of the 100 passes adds at most one, so `run + 1` and
|
||||
// `changes + 1` stay within the loop range though their operands reach it.
|
||||
const range = { min: 0, max: 100 };
|
||||
expect(ranges.get(sum)).toEqual({ min: 1, max: 100 });
|
||||
expect(ranges.get(counted)).toEqual({ min: 1, max: 100 });
|
||||
expect(ranges.get(copied)).toEqual({ min: 1, max: 100 });
|
||||
expect(analyzeIntegerRangeFacts(f).loopRanges.get(loop)).toEqual(
|
||||
new Map([
|
||||
["run", range],
|
||||
["longest", range],
|
||||
["changes", range],
|
||||
]),
|
||||
);
|
||||
for (const mutation of ["fraction", "cycle", "nested"] as const) {
|
||||
const changed = structuredClone(f);
|
||||
const body = (changed.body[3] as IrStmt & { kind: "for" }).body;
|
||||
if (mutation === "fraction") body.push(write("run", num(0.5)));
|
||||
// `run = longest` and `longest = run` bound neither.
|
||||
if (mutation === "cycle") body.push(write("run", ref("longest")));
|
||||
if (mutation === "nested")
|
||||
body.push({
|
||||
loc,
|
||||
kind: "while",
|
||||
cond: condition("<", ref("unknown"), num(1)),
|
||||
body: [write("run", num(0))],
|
||||
});
|
||||
const cursors = analyzeIntegerRangeFacts(changed).loopRanges.get(changed.body[3]!);
|
||||
expect(cursors?.has("run"), mutation).toBe(false);
|
||||
expect(cursors?.get("changes"), mutation).toEqual(range);
|
||||
}
|
||||
});
|
||||
|
||||
test("a cursor of a loop over a typed array keeps its writes exact up to 2^53 - 1", () => {
|
||||
// for (x = 0; x < bytes.length; x++) changes = changes + 1;
|
||||
const bytes: IrExpr = {
|
||||
loc,
|
||||
kind: "varRef",
|
||||
localId: "bytes",
|
||||
type: { kind: "bytes", elem: "u8" },
|
||||
};
|
||||
const length: IrExpr = {
|
||||
loc,
|
||||
kind: "bytesIntrinsic",
|
||||
method: "length",
|
||||
receiver: bytes,
|
||||
args: [],
|
||||
type: F64,
|
||||
};
|
||||
const counted = bin("+", ref("changes"), num(1));
|
||||
const f = fn([
|
||||
{ loc, kind: "varDecl", localId: "changes", init: num(0) },
|
||||
{
|
||||
loc,
|
||||
kind: "for",
|
||||
init: { loc, kind: "varDecl", localId: "x", init: num(0) },
|
||||
cond: condition("<", ref(), length),
|
||||
update: assign(bin("+", ref(), num(1))),
|
||||
body: [{ loc, kind: "assign", localId: "changes", value: counted }],
|
||||
},
|
||||
]);
|
||||
f.locals.push(
|
||||
{ id: "changes", name: "changes", type: F64, mutable: true },
|
||||
{ id: "bytes", name: "bytes", type: { kind: "bytes", elem: "u8" }, mutable: false },
|
||||
);
|
||||
// The sum of the header range passes 2^53 - 1; the write takes the loop
|
||||
// range instead.
|
||||
expect(analyzeIntegerRanges(f).get(counted)).toEqual({ min: 0, max: Number.MAX_SAFE_INTEGER });
|
||||
});
|
||||
|
||||
@@ -15,6 +15,13 @@ export interface IntegerRange {
|
||||
max: number;
|
||||
}
|
||||
export type IntegerRanges = ReadonlyMap<IrExpr, IntegerRange | null>;
|
||||
/** Facts per expression, and per for loop the bindings its body writes only
|
||||
* as bounded cursors (boundedIntegerLoopFacts): each holds an integer in its
|
||||
* loop range on entry and at every write in the loop. */
|
||||
export interface IntegerRangeFacts {
|
||||
ranges: IntegerRanges;
|
||||
loopRanges: ReadonlyMap<IrStmt, ReadonlyMap<string, IntegerRange>>;
|
||||
}
|
||||
const SIGNED: IntegerRange = { min: -2147483648, max: 2147483647 };
|
||||
const UNSIGNED: IntegerRange = { min: 0, max: 4294967295 };
|
||||
/** Int32 and uint32 at once: the bound under which `%` (modeled only for
|
||||
@@ -215,6 +222,24 @@ export interface IntegerSlotFacts {
|
||||
elements?: (arr: IrExpr) => boolean;
|
||||
}
|
||||
|
||||
/** A local read, or a numeric local plus or minus an integer literal
|
||||
* (`i + 1`, `1 + i`, `i - 2`): the local and its offset. */
|
||||
function offsetRef(e: IrExpr): { localId: string; offset: number } | null {
|
||||
if (e.kind === "varRef") return { localId: e.localId, offset: 0 };
|
||||
if (e.kind !== "bin" || (e.op !== "+" && e.op !== "-") || e.type.kind !== "f64") return null;
|
||||
const literal = (x: IrExpr): number | null =>
|
||||
x.kind === "numLit" && Number.isSafeInteger(x.value) && !Object.is(x.value, -0)
|
||||
? x.value
|
||||
: null;
|
||||
const right = literal(e.right);
|
||||
if (e.left.kind === "varRef" && e.left.type.kind === "f64" && right !== null)
|
||||
return { localId: e.left.localId, offset: e.op === "+" ? right : -right };
|
||||
const left = literal(e.left);
|
||||
if (e.op === "+" && e.right.kind === "varRef" && e.right.type.kind === "f64" && left !== null)
|
||||
return { localId: e.right.localId, offset: left };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Structured local dataflow. Calls cannot write uncaptured local slots,
|
||||
* but argument expressions can; unknown evaluation forms invalidate all
|
||||
* syntactically written slots before their children are inspected. Loops
|
||||
@@ -227,8 +252,17 @@ export interface IntegerSlotFacts {
|
||||
* range those writes and its entry value span across the region.
|
||||
* Suspensions and captured bindings deliberately remain outside the proof. */
|
||||
export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {}): IntegerRanges {
|
||||
return analyzeIntegerRangeFacts(fn, slots).ranges;
|
||||
}
|
||||
|
||||
/** analyzeIntegerRanges with the loop ranges of the function's for loops. */
|
||||
export function analyzeIntegerRangeFacts(
|
||||
fn: IrFunction,
|
||||
slots: IntegerSlotFacts = {},
|
||||
): IntegerRangeFacts {
|
||||
const ranges = new Map<IrExpr, IntegerRange | null>();
|
||||
if (fn.async || fn.generator) return ranges;
|
||||
const loopRanges = new Map<IrStmt, Map<string, IntegerRange>>();
|
||||
if (fn.async || fn.generator) return { ranges, loopRanges };
|
||||
const captures = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
@@ -436,6 +470,26 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Loop ranges of the bindings the enclosing for loops bound
|
||||
// (boundedIntegerLoopFacts) while their bodies are analyzed.
|
||||
let counterRanges: ReadonlyMap<string, IntegerRange> = new Map();
|
||||
/** The value a write stores in a binding a for loop bounds. Every value
|
||||
* the binding holds in the loop lies in its loop range, so the written
|
||||
* value takes that range even when its operands' ranges add past it
|
||||
* (`changes = changes + 1` with `changes` at the loop's limit). */
|
||||
function boundedWrite(id: string, value: IrExpr, facts: Facts): IntegerRange | null {
|
||||
const bound = counterRanges.get(id);
|
||||
const before = ranges.get(value);
|
||||
const computed = expr(value, facts);
|
||||
if (!bound) return computed;
|
||||
const range = computed
|
||||
? { min: Math.max(computed.min, bound.min), max: Math.min(computed.max, bound.max) }
|
||||
: bound;
|
||||
if (range.min > range.max) return computed;
|
||||
ranges.set(value, before === undefined ? range : before ? union(before, range) : null);
|
||||
return range;
|
||||
}
|
||||
|
||||
function remember(e: IrExpr, range: IntegerRange | null): IntegerRange | null {
|
||||
// Shared IR objects have to satisfy the proof at every occurrence.
|
||||
const previous = ranges.get(e);
|
||||
@@ -508,7 +562,7 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
let ref = condition.left;
|
||||
let bound = condition.right;
|
||||
let op: string = condition.op;
|
||||
if (ref.kind !== "varRef" && bound.kind === "varRef") {
|
||||
if (offsetRef(ref) === null && offsetRef(bound) !== null) {
|
||||
[ref, bound] = [bound, ref];
|
||||
op =
|
||||
(
|
||||
@@ -518,18 +572,30 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
>
|
||||
)[op] ?? "";
|
||||
}
|
||||
if (ref.kind !== "varRef") return;
|
||||
const boundRange =
|
||||
const target = offsetRef(ref);
|
||||
if (target === null) return;
|
||||
const literalBound =
|
||||
bound.kind === "numLit" && Number.isFinite(bound.value)
|
||||
? { min: bound.value, max: bound.value }
|
||||
: ranges.get(bound);
|
||||
if (!boundRange) return;
|
||||
if (!literalBound) return;
|
||||
// An Int32 contract local holds an int32 even where the dataflow has
|
||||
// no fact for it (loop headers, after unknown regions).
|
||||
const previous =
|
||||
facts.get(ref.localId) ??
|
||||
(eligible.has(ref.localId) ? (slots.locals?.(ref.localId) ?? undefined) : undefined);
|
||||
facts.get(target.localId) ??
|
||||
(eligible.has(target.localId) ? (slots.locals?.(target.localId) ?? undefined) : undefined);
|
||||
if (!previous) return;
|
||||
// `x + c < b` bounds x as `x < b - c` does. A sum that rounds has a
|
||||
// magnitude of at least 2^53, so against a bound within 2^52 every
|
||||
// comparison answers as the exact sum would.
|
||||
const offset = target.offset;
|
||||
if (
|
||||
offset !== 0 &&
|
||||
(Math.abs(offset) > 2 ** 31 ||
|
||||
Math.abs(literalBound.min) > 2 ** 52 ||
|
||||
Math.abs(literalBound.max) > 2 ** 52)
|
||||
)
|
||||
return;
|
||||
if (!truth)
|
||||
op =
|
||||
(
|
||||
@@ -538,21 +604,30 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
string
|
||||
>
|
||||
)[op] ?? "";
|
||||
// The sum `x + c` is an integer, so the bound rounds to an integer first
|
||||
// and the offset comes off after: integer arithmetic below 2^53 is exact,
|
||||
// where subtracting the offset from a fractional bound could round
|
||||
// across an integer.
|
||||
let { min, max } = previous;
|
||||
if (op === "<") max = Math.min(max, Math.ceil(boundRange.max) - 1);
|
||||
else if (op === "<=") max = Math.min(max, Math.floor(boundRange.max));
|
||||
else if (op === ">") min = Math.max(min, Math.floor(boundRange.min) + 1);
|
||||
else if (op === ">=") min = Math.max(min, Math.ceil(boundRange.min));
|
||||
if (op === "<") max = Math.min(max, Math.ceil(literalBound.max) - 1 - offset);
|
||||
else if (op === "<=") max = Math.min(max, Math.floor(literalBound.max) - offset);
|
||||
else if (op === ">") min = Math.max(min, Math.floor(literalBound.min) + 1 - offset);
|
||||
else if (op === ">=") min = Math.max(min, Math.ceil(literalBound.min) - offset);
|
||||
else if (op === "===") {
|
||||
min = Math.max(min, boundRange.min);
|
||||
max = Math.min(max, boundRange.max);
|
||||
} else if (op === "!==" && boundRange.min === boundRange.max) {
|
||||
min = Math.max(min, Math.ceil(literalBound.min) - offset);
|
||||
max = Math.min(max, Math.floor(literalBound.max) - offset);
|
||||
} else if (
|
||||
op === "!==" &&
|
||||
literalBound.min === literalBound.max &&
|
||||
Number.isInteger(literalBound.min)
|
||||
) {
|
||||
// Excluding one value narrows a range that ends at it.
|
||||
if (min === boundRange.min) min += 1;
|
||||
else if (max === boundRange.max) max -= 1;
|
||||
const excluded = literalBound.min - offset;
|
||||
if (min === excluded) min += 1;
|
||||
else if (max === excluded) max -= 1;
|
||||
}
|
||||
if (min <= max && Number.isInteger(min) && Number.isInteger(max))
|
||||
facts.set(ref.localId, { min, max });
|
||||
facts.set(target.localId, { min, max });
|
||||
}
|
||||
function expr(e: IrExpr, facts: Facts): IntegerRange | null {
|
||||
let range: IntegerRange | null = null;
|
||||
@@ -565,12 +640,17 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
range = facts.get(e.localId) ?? boxedRange(e.localId);
|
||||
break;
|
||||
case "assignExpr":
|
||||
range = expr(e.value, facts);
|
||||
range = boundedWrite(e.localId, e.value, facts);
|
||||
set(facts, e.localId, range);
|
||||
break;
|
||||
case "incDec": {
|
||||
const before = facts.get(e.localId) ?? null;
|
||||
const after = before ? integerArithmeticRange(e.op, before, { min: 1, max: 1 }) : null;
|
||||
const bound = counterRanges.get(e.localId);
|
||||
const sum = before ? integerArithmeticRange(e.op, before, { min: 1, max: 1 }) : null;
|
||||
const after =
|
||||
bound && sum
|
||||
? { min: Math.max(sum.min, bound.min), max: Math.min(sum.max, bound.max) }
|
||||
: sum;
|
||||
set(facts, e.localId, after);
|
||||
range = e.prefix ? after : before;
|
||||
break;
|
||||
@@ -756,11 +836,11 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
for (const s of stmts) {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
case "assign": {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
set(facts, s.localId, value ? expr(value, facts) : null);
|
||||
set(facts, s.localId, s.init ? expr(s.init, facts) : null);
|
||||
break;
|
||||
case "assign":
|
||||
set(facts, s.localId, boundedWrite(s.localId, s.value, facts));
|
||||
break;
|
||||
}
|
||||
case "exprStmt":
|
||||
expr(s.expr, facts);
|
||||
break;
|
||||
@@ -831,7 +911,31 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
if (s.cond) expr(s.cond, loop);
|
||||
const inside = new Map(loop);
|
||||
if (s.cond) refine(s.cond, true, inside);
|
||||
body(s.body, inside);
|
||||
// Only the body's writes are the ones the bound counted.
|
||||
const outer = counterRanges;
|
||||
const headWrites = s.cond ? writes(s.cond) : new Set<string>();
|
||||
if (s.update) for (const id of writes(s.update)) headWrites.add(id);
|
||||
const cursors = new Map<string, IntegerRange>();
|
||||
for (const [id, range] of bounded)
|
||||
if (id !== counter?.id && !headWrites.has(id)) cursors.set(id, range);
|
||||
// A loop analyzed again (inside another loop) keeps the bindings
|
||||
// every analysis bounds, with the union of their ranges.
|
||||
const previous = loopRanges.get(s);
|
||||
if (!previous) loopRanges.set(s, cursors);
|
||||
else
|
||||
for (const [id, range] of previous) {
|
||||
const other = cursors.get(id);
|
||||
if (other) previous.set(id, union(range, other));
|
||||
else previous.delete(id);
|
||||
}
|
||||
const active = new Map(outer);
|
||||
for (const [id, range] of cursors) active.set(id, range);
|
||||
counterRanges = active;
|
||||
try {
|
||||
body(s.body, inside);
|
||||
} finally {
|
||||
counterRanges = outer;
|
||||
}
|
||||
// Continue can bypass any body assignment. The update therefore
|
||||
// starts from header invariants rather than the body's exit facts,
|
||||
// plus what the condition proved about bindings the body never
|
||||
@@ -919,5 +1023,5 @@ export function analyzeIntegerRanges(fn: IrFunction, slots: IntegerSlotFacts = {
|
||||
const entry: Facts = new Map();
|
||||
if (slots.params) for (const [id, range] of slots.params) set(entry, id, range);
|
||||
body(fn.body, entry);
|
||||
return ranges;
|
||||
return { ranges, loopRanges };
|
||||
}
|
||||
|
||||
@@ -4100,6 +4100,7 @@ export const DYN_HANDLE_KINDS: ReadonlyMap<string, { tag: string; cls: string }>
|
||||
["http2Session", { tag: "SCR_DYNH_H2_SESSION", cls: "Http2Session" }],
|
||||
["http2Stream", { tag: "SCR_DYNH_H2_STREAM", cls: "Http2Stream" }],
|
||||
["httpClientReq", { tag: "SCR_DYNH_HTTP_CLIENT", cls: "ClientRequest" }],
|
||||
["dgramSocket", { tag: "SCR_DYNH_DGRAM_SOCKET", cls: "Socket" }],
|
||||
["dir", { tag: "SCR_DYNH_DIR", cls: "Dir" }],
|
||||
["cryptoHash", { tag: "SCR_DYNH_HASH", cls: "Hash" }],
|
||||
["cryptoHmac", { tag: "SCR_DYNH_HMAC", cls: "Hmac" }],
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, DYN, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "./ir.js";
|
||||
import { JsIcSites, hasJsIcSite } from "./js-ic.js";
|
||||
|
||||
const loc = { file: "js-ic.ts", start: 0, end: 0 };
|
||||
const ref = (id: string, type = DYN): IrExpr => ({ kind: "varRef", localId: id, type, loc });
|
||||
const str = (value: string): IrExpr => ({ kind: "strLit", value, type: STRING, loc });
|
||||
const call = (callee: string, ...args: IrExpr[]): IrExpr => ({
|
||||
kind: "call",
|
||||
callee,
|
||||
args,
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
const ret = (value: IrExpr | null): IrStmt => ({ kind: "return", value, loc });
|
||||
|
||||
function helper(name: string, params: string[], body: IrStmt[], locals: string[] = []): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: params.map((id) => ({ localId: id, name: id, type: DYN })),
|
||||
returnType: DYN,
|
||||
locals: [...params, ...locals].map((id) => ({ id, name: id, type: DYN, mutable: false })),
|
||||
body,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `if (dyn.typedRefIs(p, "C")) return undefined` — a capsule branch. */
|
||||
function capsuleBranch(param: string, fn = "dyn.typedRefIs"): IrStmt {
|
||||
return {
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "libCall",
|
||||
fn: fn as "dyn.typedRefIs",
|
||||
args: [ref(param), str("C")],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [ret(ref(param))],
|
||||
else_: null,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
const identityBag = helper(
|
||||
"%dyn.class.readPropertiesOrEmpty",
|
||||
["v"],
|
||||
[capsuleBranch("v"), ret(ref("v"))],
|
||||
);
|
||||
|
||||
function getHelper(name: string, key: string, prefix: IrStmt[] = [], bag?: IrExpr): IrFunction {
|
||||
const bagRef = ref("bag.0");
|
||||
return helper(
|
||||
name,
|
||||
["p.0"],
|
||||
[
|
||||
...prefix,
|
||||
{ kind: "varDecl", localId: "bag.0", init: bag ?? call(identityBag.name, ref("p.0")), loc },
|
||||
ret({
|
||||
kind: "libCall",
|
||||
fn: "js.fromDyn",
|
||||
args: [
|
||||
{
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynScalarEq", left: bagRef, right: ref("p.0"), type: BOOL, loc },
|
||||
then: { kind: "dynKeyGet", key: str(key), value: bagRef, type: DYN, loc },
|
||||
else_: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bagGet",
|
||||
args: [bagRef, str(key), ref("p.0")],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: { kind: "jsval" },
|
||||
loc,
|
||||
}),
|
||||
],
|
||||
["bag.0"],
|
||||
);
|
||||
}
|
||||
|
||||
function setHelper(name: string, key: string, prefix: IrStmt[] = []): IrFunction {
|
||||
return {
|
||||
...helper(
|
||||
name,
|
||||
["p.0", "p.1"],
|
||||
[
|
||||
...prefix,
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [call("%dyn.class.properties", ref("p.0")), str(key), ref("p.1")],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
ret(null),
|
||||
],
|
||||
),
|
||||
returnType: VOID,
|
||||
};
|
||||
}
|
||||
|
||||
const properties = helper("%dyn.class.properties", ["v"], [capsuleBranch("v"), ret(ref("v"))]);
|
||||
|
||||
function sitesOf(...functions: IrFunction[]): JsIcSites {
|
||||
return new JsIcSites({ functions: [identityBag, properties, ...functions] });
|
||||
}
|
||||
|
||||
test("property helpers whose class branches only test capsules are cache sites", () => {
|
||||
const sites = sitesOf(
|
||||
getHelper("%dyn.class.property.0", "x", [
|
||||
capsuleBranch("p.0"),
|
||||
capsuleBranch("p.0", "dyn.classIs"),
|
||||
]),
|
||||
setHelper("%dyn.class.property.1", "y", [capsuleBranch("p.0", "dyn.typedRefKeyIs")]),
|
||||
);
|
||||
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
|
||||
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
|
||||
});
|
||||
|
||||
test("any other prefix or tail keeps the helper call", () => {
|
||||
const errorBranch: IrStmt = {
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "error", value: ref("p.0"), type: BOOL, loc },
|
||||
then: [ret(ref("p.0"))],
|
||||
else_: null,
|
||||
loc,
|
||||
};
|
||||
// A branch an ordinary object can take (toString of an error object).
|
||||
const withError = getHelper("%dyn.class.property.0", "toString", [errorBranch]);
|
||||
// A bag that is not the receiver for ordinary objects.
|
||||
const otherBag = helper("%dyn.class.other", ["v"], [ret({ kind: "dynObjLit", type: DYN, loc })]);
|
||||
const foreignBag = getHelper("%dyn.class.property.1", "x", [], call(otherBag.name, ref("p.0")));
|
||||
// A function outside the generated helper family.
|
||||
const userFn = { ...getHelper("%dyn.class.property.2", "x"), name: "user" };
|
||||
const sites = sitesOf(withError, otherBag, foreignBag, userFn);
|
||||
expect(sites.site("%dyn.class.property.0")).toBeNull();
|
||||
expect(sites.site("%dyn.class.property.1")).toBeNull();
|
||||
expect(sites.site("user")).toBeNull();
|
||||
expect(sites.site("%dyn.class.missing")).toBeNull();
|
||||
});
|
||||
|
||||
test("a function holds a site when it calls a classified helper or dispatches a method", () => {
|
||||
const getter = getHelper("%dyn.class.property.0", "x");
|
||||
const sites = sitesOf(getter);
|
||||
const caller = helper("f", ["o"], [ret(call(getter.name, ref("o")))]);
|
||||
const invoker = helper(
|
||||
"g",
|
||||
["o"],
|
||||
[
|
||||
ret({
|
||||
kind: "dynInvoke",
|
||||
recv: ref("o"),
|
||||
method: "m",
|
||||
calleeName: "o.m",
|
||||
args: [],
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
],
|
||||
);
|
||||
const plain = helper("h", ["o"], [ret(ref("o"))]);
|
||||
expect(hasJsIcSite(caller, sites)).toBe(true);
|
||||
expect(hasJsIcSite(invoker, sites)).toBe(true);
|
||||
expect(hasJsIcSite(plain, sites)).toBe(false);
|
||||
// Module initializers keep plain method dispatch (top-level handle
|
||||
// wiring), while their property sites still cache.
|
||||
expect(hasJsIcSite({ ...invoker, name: "%init.0" }, sites)).toBe(false);
|
||||
expect(hasJsIcSite({ ...caller, name: "%init.0" }, sites)).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,236 @@
|
||||
/* Inline-cache sites of the dynamic tier (two-tier design stage S4).
|
||||
*
|
||||
* Lowering builds every dynamic property access on a constant key as a
|
||||
* call to a generated helper (class-dynamic-dispatch.ts): a read
|
||||
* `%dyn.class.property.N(receiver)`, a write
|
||||
* `%dyn.class.property.N(receiver, value)` and a keyed read
|
||||
* `%dyn.class.computed.N(receiver, key)`. A helper starts with one branch
|
||||
* per compiled class whose capsules may reach it and ends with the generic
|
||||
* checked-dynamic operation on the receiver itself. The capsule branches
|
||||
* test capsule and class-object kinds only, so for an ordinary checked
|
||||
* object or array receiver a helper is exactly its generic tail: [[Get]]
|
||||
* (`dynKeyGet`) or [[Set]] (`dyn.keySet`) of the key.
|
||||
*
|
||||
* A call to such a helper from a dynamic-tier function is an inline-cache
|
||||
* site: the backend (backend/llvm/js-ic.ts) answers plain data properties
|
||||
* of shaped objects through the site's cache and calls the helper for
|
||||
* everything else, so the helper stays the single definition of the
|
||||
* operation's semantics. This module recognizes the helpers by structure,
|
||||
* never by name alone: a helper whose body is not exactly capsule branches
|
||||
* plus the generic tail is not a site. `dynInvoke` is a site too: its
|
||||
* method read on an ordinary object is a [[Get]] (outside module
|
||||
* initializers, see jsIcMethodSites).
|
||||
*
|
||||
* Static functions never contain sites (invariant I1): the backend only
|
||||
* consults this module for calls in dynamic-tier functions. */
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js";
|
||||
import { everyStmtList } from "./traverse.js";
|
||||
|
||||
export type JsIcSite =
|
||||
| { kind: "get"; key: string }
|
||||
| { kind: "set"; key: string }
|
||||
| { kind: "keyed" };
|
||||
|
||||
const CAPSULE_TESTS = new Set(["dyn.typedRefIs", "dyn.typedRefKeyIs", "dyn.classIs"]);
|
||||
|
||||
/** Classifies the helpers of a module once. */
|
||||
export class JsIcSites {
|
||||
private readonly fns: ReadonlyMap<string, IrFunction>;
|
||||
private readonly sites = new Map<string, JsIcSite | null>();
|
||||
private readonly identities = new Map<string, boolean>();
|
||||
|
||||
constructor(mod: Pick<IrModule, "functions">) {
|
||||
this.fns = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
}
|
||||
|
||||
/** The cache site a direct call to `callee` is, or null. */
|
||||
site(callee: string): JsIcSite | null {
|
||||
let site = this.sites.get(callee);
|
||||
if (site === undefined) {
|
||||
const fn = this.fns.get(callee);
|
||||
site = fn ? this.classify(fn) : null;
|
||||
this.sites.set(callee, site);
|
||||
}
|
||||
return site;
|
||||
}
|
||||
|
||||
private classify(fn: IrFunction): JsIcSite | null {
|
||||
if (!fn.name.startsWith("%dyn.class.")) return null;
|
||||
if (fn.async !== undefined || fn.generator !== undefined || fn.captures !== undefined)
|
||||
return null;
|
||||
if (fn.params.some((p) => p.type.kind !== "dyn")) return null;
|
||||
const receiver = fn.params[0]?.localId;
|
||||
if (receiver === undefined) return null;
|
||||
let i = 0;
|
||||
while (i < fn.body.length && this.capsuleBranch(fn.body[i]!, receiver)) i++;
|
||||
const tail = fn.body.slice(i);
|
||||
if (fn.params.length === 1) return this.getTail(tail, receiver);
|
||||
if (fn.params.length !== 2) return null;
|
||||
const second = fn.params[1]!.localId;
|
||||
return this.setTail(tail, receiver, second) ?? this.keyedTail(tail, receiver, second);
|
||||
}
|
||||
|
||||
/** `varDecl bag = BAG(receiver); return [js.fromDyn](bag === receiver ?
|
||||
* dynKeyGet(bag, K) : dyn.bagGet(bag, K, receiver))`. */
|
||||
private getTail(tail: IrStmt[], receiver: string): JsIcSite | null {
|
||||
const [decl, ret] = tail;
|
||||
if (tail.length !== 2 || decl?.kind !== "varDecl" || ret?.kind !== "return") return null;
|
||||
if (decl.init === null || !this.bagOf(decl.init, receiver)) return null;
|
||||
const bag = decl.localId;
|
||||
const value = unwrapFromDyn(ret.value);
|
||||
if (value?.kind !== "ternary") return null;
|
||||
const { cond, then } = value;
|
||||
if (
|
||||
cond.kind !== "dynScalarEq" ||
|
||||
cond.negated === true ||
|
||||
!isVar(cond.left, bag) ||
|
||||
!isVar(cond.right, receiver) ||
|
||||
then.kind !== "dynKeyGet" ||
|
||||
!isVar(then.value, bag) ||
|
||||
then.key.kind !== "strLit"
|
||||
)
|
||||
return null;
|
||||
return { kind: "get", key: then.key.value };
|
||||
}
|
||||
|
||||
/** `dyn.keySet(BAG(receiver), K, value); return`. */
|
||||
private setTail(tail: IrStmt[], receiver: string, value: string): JsIcSite | null {
|
||||
const [stmt, ret] = tail;
|
||||
if (tail.length !== 2 || stmt?.kind !== "exprStmt" || ret?.kind !== "return") return null;
|
||||
if (ret.value !== null) return null;
|
||||
const call = stmt.expr;
|
||||
if (call.kind !== "libCall" || call.fn !== "dyn.keySet" || call.args.length !== 3) return null;
|
||||
const [bag, key, stored] = call.args;
|
||||
if (!bag || !this.bagOf(bag, receiver) || key?.kind !== "strLit" || !isVar(stored, value))
|
||||
return null;
|
||||
return { kind: "set", key: key.value };
|
||||
}
|
||||
|
||||
/** `if (typeof key === "symbol") return dynKeyGet(BAG(receiver), key);
|
||||
* if (receiver == null) return dynKeyGet(receiver, key);
|
||||
* const s = String(key); return dynKeyGet(BAG(receiver), s)`, each read
|
||||
* possibly under js.fromDyn. */
|
||||
private keyedTail(tail: IrStmt[], receiver: string, key: string): JsIcSite | null {
|
||||
const [symbol, nullish, decl, ret] = tail;
|
||||
if (tail.length !== 4) return null;
|
||||
if (!this.guard(symbol, "symbol", key, (read) => this.bagOf(read, receiver), key)) return null;
|
||||
if (!this.guard(nullish, "nullish", receiver, (read) => isVar(read, receiver), key))
|
||||
return null;
|
||||
if (decl?.kind !== "varDecl" || decl.init?.kind !== "libCall") return null;
|
||||
if (decl.init.fn !== "dyn.toStringCoerce" || !isVar(decl.init.args[0], key)) return null;
|
||||
if (ret?.kind !== "return") return null;
|
||||
const read = unwrapFromDyn(ret.value);
|
||||
if (read?.kind !== "dynKeyGet" || !isVar(read.key, decl.localId)) return null;
|
||||
if (!this.bagOf(read.value, receiver)) return null;
|
||||
return { kind: "keyed" };
|
||||
}
|
||||
|
||||
private guard(
|
||||
s: IrStmt | undefined,
|
||||
test: "symbol" | "nullish",
|
||||
tested: string,
|
||||
object: (e: IrExpr) => boolean,
|
||||
key: string,
|
||||
): boolean {
|
||||
if (s?.kind !== "if" || s.else_ !== null || s.then.length !== 1) return false;
|
||||
const { cond } = s;
|
||||
if (cond.kind !== "dynTest" || cond.test !== test || cond.negated === true) return false;
|
||||
if (!isVar(cond.value, tested)) return false;
|
||||
const ret = s.then[0]!;
|
||||
if (ret.kind !== "return") return false;
|
||||
const read = unwrapFromDyn(ret.value);
|
||||
return read?.kind === "dynKeyGet" && isVar(read.key, key) && object(read.value);
|
||||
}
|
||||
|
||||
/** A branch that can only be taken for a capsule or class-object
|
||||
* receiver: `if (<capsule test>) { ... }` with no else. */
|
||||
private capsuleBranch(s: IrStmt, receiver: string): boolean {
|
||||
return s.kind === "if" && s.else_ === null && this.capsuleTest(s.cond, receiver);
|
||||
}
|
||||
|
||||
private capsuleTest(e: IrExpr, receiver: string): boolean {
|
||||
if (e.kind === "libCall")
|
||||
return CAPSULE_TESTS.has(e.fn) && e.args[0] !== undefined && this.bagOf(e.args[0], receiver);
|
||||
if (e.kind === "ternary")
|
||||
return (
|
||||
this.capsuleTest(e.cond, receiver) && e.else_.kind === "boolLit" && e.else_.value === false
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether `e` is the receiver itself for every ordinary object or array:
|
||||
* the receiver, or a chain of property-bag helpers applied to it. */
|
||||
private bagOf(e: IrExpr, receiver: string): boolean {
|
||||
if (isVar(e, receiver)) return true;
|
||||
return (
|
||||
e.kind === "call" &&
|
||||
e.args.length === 1 &&
|
||||
this.bagOf(e.args[0]!, receiver) &&
|
||||
this.identityHelper(e.callee)
|
||||
);
|
||||
}
|
||||
|
||||
/** A property-bag helper that returns its argument unless a capsule
|
||||
* branch is taken. */
|
||||
private identityHelper(name: string): boolean {
|
||||
const known = this.identities.get(name);
|
||||
if (known !== undefined) return known;
|
||||
this.identities.set(name, false); // a recursive chain is not an identity
|
||||
const fn = this.fns.get(name);
|
||||
let identity = false;
|
||||
if (
|
||||
fn &&
|
||||
name.startsWith("%dyn.class.") &&
|
||||
fn.params.length === 1 &&
|
||||
fn.params[0]!.type.kind === "dyn" &&
|
||||
fn.async === undefined &&
|
||||
fn.generator === undefined &&
|
||||
fn.captures === undefined
|
||||
) {
|
||||
const param = fn.params[0]!.localId;
|
||||
const last = fn.body[fn.body.length - 1];
|
||||
identity =
|
||||
fn.body.slice(0, -1).every((s) => this.capsuleBranch(s, param)) &&
|
||||
last?.kind === "return" &&
|
||||
last.value !== null &&
|
||||
this.bagOf(last.value, param);
|
||||
}
|
||||
this.identities.set(name, identity);
|
||||
return identity;
|
||||
}
|
||||
}
|
||||
|
||||
function isVar(e: IrExpr | undefined | null, localId: string): boolean {
|
||||
return e?.kind === "varRef" && e.localId === localId;
|
||||
}
|
||||
|
||||
function unwrapFromDyn(e: IrExpr | null | undefined): IrExpr | null {
|
||||
if (!e) return null;
|
||||
if (e.kind === "libCall" && e.fn === "js.fromDyn" && e.args.length === 1) return e.args[0]!;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** Whether a function holds a cache site: a call to a classified helper or
|
||||
* a method dispatch. */
|
||||
export function hasJsIcSite(fn: IrFunction, sites: JsIcSites): boolean {
|
||||
const methods = jsIcMethodSites(fn.name);
|
||||
return !everyStmtList(fn.body, {
|
||||
expr: (e) =>
|
||||
!(
|
||||
(methods && e.kind === "dynInvoke") ||
|
||||
(e.kind === "call" && sites.site(e.callee) !== null)
|
||||
),
|
||||
stmt: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether method dispatch in a function goes through caches. Module
|
||||
* initializers (`%init.N`) run once, and the methods they call at top level
|
||||
* are overwhelmingly registrations on native handles (`worker.on(...)`,
|
||||
* `server.listen(...)`), which a cache never answers: they keep the plain
|
||||
* dispatch, so a typed program whose only dynamic code is such top-level
|
||||
* wiring links no dynamic-tier runtime. Property sites still cache there
|
||||
* (top-level loops over objects). */
|
||||
export function jsIcMethodSites(fnName: string): boolean {
|
||||
return !fnName.startsWith("%init.");
|
||||
}
|
||||
@@ -42,6 +42,7 @@
|
||||
* suspensions keep jsvals like any pointer) but keep the dyn result ABI. */
|
||||
import { exprFunctionReference, moduleFunctionReferences } from "./function-references.js";
|
||||
import { DYN, JSVAL, type IrExpr, type IrFunction, type IrModule, type IrStmt } from "./ir.js";
|
||||
import { JsIcSites } from "./js-ic.js";
|
||||
import {
|
||||
everyExprChild,
|
||||
everyStmtChild,
|
||||
@@ -104,8 +105,12 @@ export function jsvalEligible(fn: IrFunction): boolean {
|
||||
* Returns the number of functions that now use jsval. */
|
||||
export function promoteJsvals(mod: IrModule): number {
|
||||
const results = jsvalResults(mod);
|
||||
const sites = new JsIcSites(mod);
|
||||
const storesJs = (e: IrExpr): boolean =>
|
||||
e.kind === "call" && sites.site(e.callee)?.kind === "set";
|
||||
let count = 0;
|
||||
for (const fn of mod.functions) if (jsvalEligible(fn) && promoteFunction(fn, results)) count++;
|
||||
for (const fn of mod.functions)
|
||||
if (jsvalEligible(fn) && promoteFunction(fn, results, storesJs)) count++;
|
||||
for (const fn of mod.functions) if (results.has(fn.name)) fn.returnType = JSVAL;
|
||||
return count;
|
||||
}
|
||||
@@ -165,6 +170,7 @@ function promotableLocals(
|
||||
fn: IrFunction,
|
||||
producer: (e: IrExpr) => boolean,
|
||||
jsResult: boolean,
|
||||
storesJs: (e: IrExpr) => boolean,
|
||||
): Set<string> {
|
||||
const candidates = new Map<string, LocalCost>();
|
||||
for (const local of fn.locals) {
|
||||
@@ -218,6 +224,14 @@ function promotableLocals(
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case "call":
|
||||
// An inline-cache store takes its value as a jsval (S4).
|
||||
if (storesJs(e)) {
|
||||
expr(e.args[0]!, false, weight);
|
||||
expr(e.args[1]!, true, weight);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -247,6 +261,17 @@ function promotableLocals(
|
||||
};
|
||||
const stmt = (s: IrStmt, weight: number): void => {
|
||||
switch (s.kind) {
|
||||
case "exprStmt":
|
||||
// A discarded sequence result (a compound assignment statement)
|
||||
// is never boxed.
|
||||
if (discardedSequence(s.expr)) {
|
||||
for (const inner of s.expr.stmts) stmt(inner, weight);
|
||||
expr(s.expr.result, true, weight);
|
||||
return;
|
||||
}
|
||||
blockStmtReferences(s);
|
||||
expr(s.expr, false, weight);
|
||||
return;
|
||||
case "return":
|
||||
if (s.value !== null) expr(s.value, jsResult && s.value.type.kind === "dyn", weight);
|
||||
return;
|
||||
@@ -300,15 +325,25 @@ function promotableLocals(
|
||||
return promoted;
|
||||
}
|
||||
|
||||
/** A statement-level sequence whose result nothing reads. */
|
||||
function discardedSequence(e: IrExpr): e is Extract<IrExpr, { kind: "seqExpr" }> {
|
||||
return e.kind === "seqExpr" && e.generatorDelegate === undefined && e.diverges === undefined;
|
||||
}
|
||||
|
||||
/* ── the rewrite ─────────────────────────────────────────────────────── */
|
||||
|
||||
function promoteFunction(fn: IrFunction, results: ReadonlySet<string>): boolean {
|
||||
function promoteFunction(
|
||||
fn: IrFunction,
|
||||
results: ReadonlySet<string>,
|
||||
storesJs: (e: IrExpr) => boolean,
|
||||
): boolean {
|
||||
const resultCall = (e: IrExpr): boolean => e.kind === "call" && results.has(e.callee);
|
||||
const operatorCall = (e: IrExpr): boolean => isProducerCall(e);
|
||||
const promoted = promotableLocals(
|
||||
fn,
|
||||
(e) => operatorCall(e) || resultCall(e),
|
||||
results.has(fn.name),
|
||||
storesJs,
|
||||
);
|
||||
const ownResult = results.has(fn.name);
|
||||
let changed = false;
|
||||
@@ -371,6 +406,13 @@ function promoteFunction(fn: IrFunction, results: ReadonlySet<string>): boolean
|
||||
if (s.kind === "return" && ownResult && s.value !== null) return { ...s, value: toJs(s.value) };
|
||||
// A discarded result stays a jsval: nothing boxes it.
|
||||
if (s.kind === "exprStmt" && producesJs(s.expr)) return { ...s, expr: toJs(s.expr) };
|
||||
if (s.kind === "exprStmt" && discardedSequence(s.expr) && producesJs(s.expr.result)) {
|
||||
const result = toJs(s.expr.result);
|
||||
return {
|
||||
...s,
|
||||
expr: { ...s.expr, stmts: s.expr.stmts.map(rewriteStmt), result, type: result.type },
|
||||
};
|
||||
}
|
||||
if (s.kind === "varDecl" && promoted.has(s.localId) && s.init !== null)
|
||||
return { ...s, init: toJs(s.init) };
|
||||
if (s.kind === "assign" && promoted.has(s.localId)) return { ...s, value: toJs(s.value) };
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
RUNTIME_STREAM_CLASSES,
|
||||
} from "./ir.js";
|
||||
import { everyModuleNode } from "./traverse.js";
|
||||
import { JsIcSites, hasJsIcSite } from "./js-ic.js";
|
||||
|
||||
/** The fixed-width Buffer read/write method names (both "UInt" and "Uint"
|
||||
* spellings) a checked-dynamic method call routes to scr_bytes_dyn.c. */
|
||||
@@ -92,6 +93,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dc.chanRunStores",
|
||||
"dc.chanUnbindStore",
|
||||
"util.callbackify",
|
||||
"util.promisifyDyn",
|
||||
"util.debuglog",
|
||||
"util.deprecate",
|
||||
"util.inherits",
|
||||
@@ -346,6 +348,8 @@ interface TypeTables {
|
||||
records: Map<string, IrRecordShape>;
|
||||
unions: Map<string, IrUnionDef>;
|
||||
classes: Map<string, IrClassDef>;
|
||||
/** mentionsDgramSocket's carriers, computed on first use. */
|
||||
dgramCarriers?: DgramCarriers;
|
||||
}
|
||||
|
||||
/** Whether boxing a value of type `t` as dyn boxes a promise, which goes
|
||||
@@ -409,6 +413,78 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set<string>): boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** The record shapes, unions and classes whose values carry a dgram socket
|
||||
* (directly or through other members), computed once per module. */
|
||||
interface DgramCarriers {
|
||||
records: Set<string>;
|
||||
unions: Set<string>;
|
||||
anyClass: boolean;
|
||||
}
|
||||
|
||||
/** Whether `t` carries a dgram socket, given the carrier sets so far:
|
||||
* structural through functions (parameters and result), promises and
|
||||
* collections; named records, unions and class instances by the sets. */
|
||||
function carriesDgram(t: IrType, carriers: DgramCarriers): boolean {
|
||||
switch (t.kind) {
|
||||
case "dgramSocket":
|
||||
return true;
|
||||
case "func":
|
||||
return t.params.some((p) => carriesDgram(p, carriers)) || carriesDgram(t.ret, carriers);
|
||||
case "promise":
|
||||
return carriesDgram(t.inner, carriers);
|
||||
case "array":
|
||||
case "set":
|
||||
return carriesDgram(t.elem, carriers);
|
||||
case "map":
|
||||
return carriesDgram(t.key, carriers) || carriesDgram(t.value, carriers);
|
||||
case "record":
|
||||
return carriers.records.has(t.shapeId);
|
||||
case "union":
|
||||
return carriers.unions.has(t.unionId);
|
||||
case "object":
|
||||
// Any class may be the instance's runtime class or one of its bases.
|
||||
return carriers.anyClass;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** The module's dgram carriers: a fixpoint over its records, unions and
|
||||
* class fields. */
|
||||
function dgramCarriers(tables: TypeTables): DgramCarriers {
|
||||
const carriers: DgramCarriers = { records: new Set(), unions: new Set(), anyClass: false };
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const [id, shape] of tables.records)
|
||||
if (!carriers.records.has(id) && shape.fields.some((f) => carriesDgram(f.type, carriers))) {
|
||||
carriers.records.add(id);
|
||||
changed = true;
|
||||
}
|
||||
for (const [id, def] of tables.unions)
|
||||
if (!carriers.unions.has(id) && def.arms.some((a) => carriesDgram(a, carriers))) {
|
||||
carriers.unions.add(id);
|
||||
changed = true;
|
||||
}
|
||||
if (
|
||||
!carriers.anyClass &&
|
||||
[...tables.classes.values()].some((c) => c.fields.some((f) => carriesDgram(f.type, carriers)))
|
||||
) {
|
||||
carriers.anyClass = true;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return carriers;
|
||||
}
|
||||
|
||||
/** Whether converting a value of type `t` to or from dyn converts a dgram
|
||||
* socket, which goes through scr_dgram_dyn.c: the socket itself, or a
|
||||
* function, collection, record, union, promise or class instance that
|
||||
* carries one (boxed functions convert their parameters too). */
|
||||
function mentionsDgramSocket(t: IrType, tables: TypeTables): boolean {
|
||||
tables.dgramCarriers ??= dgramCarriers(tables);
|
||||
return carriesDgram(t, tables.dgramCarriers);
|
||||
}
|
||||
|
||||
/** The libCall fns served by the OPTIONAL events unit (scr_events.c):
|
||||
* process signal/exit listeners and the piped-stdin surface. */
|
||||
const PROCESS_EVENT_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
@@ -544,6 +620,14 @@ const RUNTIME_FEATURE_RULES = {
|
||||
dc: rule({ prefixes: ["dc."] }),
|
||||
/** dns.lookup lives in the dgram unit. */
|
||||
dgram: rule({ prefixes: ["dgram.", "dns."], types: ["dgramSocket"] }),
|
||||
/** A dgram socket converts to or from an untyped value (its handle box,
|
||||
* a boxed function or record that carries one): scr_dgram_dyn.c over
|
||||
* the dgram unit. */
|
||||
dgramDyn: rule({
|
||||
expr: (node, tables) =>
|
||||
(node.kind === "dynFrom" && mentionsDgramSocket(node.value.type, tables())) ||
|
||||
(node.kind === "dynCheck" && mentionsDgramSocket(node.type, tables())),
|
||||
}),
|
||||
/** node:dns/promises: scr_dns_promises.c over the dgram unit. */
|
||||
dnsPromises: rule({ fns: ["dns.promisesLookup", "dns.promisesLookupService"] }),
|
||||
dynAsync: rule({
|
||||
@@ -584,7 +668,12 @@ const RUNTIME_FEATURE_RULES = {
|
||||
dynBuiltinStream: rule({ fns: ["dyn.builtinStreamClass"] }),
|
||||
dynInvoke: rule({
|
||||
initial: (mod) => mod.workers === true,
|
||||
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
|
||||
fns: [
|
||||
...DYN_INVOKE_LIB_FNS,
|
||||
...UTIL_CALLABLE_LIB_FNS,
|
||||
...BUILTIN_CLASS_LIB_FNS,
|
||||
"util.promisifyDyn",
|
||||
],
|
||||
// Constant-key reads of Object.prototype member names answer the
|
||||
// inherited member through scr_dyn_invoke.c.
|
||||
expr: (node, _tables) =>
|
||||
@@ -603,6 +692,8 @@ const RUNTIME_FEATURE_RULES = {
|
||||
}),
|
||||
/** Own properties JavaScript adds to children (scr_dyn_handle_props.c). */
|
||||
handleProps: rule({ fns: ["child.propsInstall"] }),
|
||||
/** for await over checked-dynamic values (scr_dyn_for_await.c). */
|
||||
dynForAwait: rule({ fns: ["dyn.asyncIteratorOpen", "dyn.asyncIteratorWasSync"] }),
|
||||
/** Fixed-width Buffer read/write methods called on checked-dynamic
|
||||
* values (scr_bytes_dyn.c). */
|
||||
dynBufferNum: rule({
|
||||
@@ -741,6 +832,10 @@ const RUNTIME_FEATURE_RULES = {
|
||||
}),
|
||||
tls: rule({ prefixes: ["tls.", "https.", "http2."], types: ["secureCtx"] }),
|
||||
tlsCa: rule({ prefixes: ["tlsca."] }),
|
||||
/** util.promisify over an untyped function (scr_util_promisify.c): its
|
||||
* promises go through the dyn-async unit and its calls through the
|
||||
* dyn-invoke unit. */
|
||||
utilPromisify: rule({ fns: ["util.promisifyDyn"] }),
|
||||
/** process.emitWarning is assigned (process.setEmitWarning). */
|
||||
warningReplace: rule({ fns: ["process.setEmitWarning"] }),
|
||||
/** The WebAssembly API (scr_wasm.c). */
|
||||
@@ -770,6 +865,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
copying: false,
|
||||
dc: false,
|
||||
dgram: false,
|
||||
dgramDyn: false,
|
||||
dnsPromises: false,
|
||||
dynAsync: false,
|
||||
dynBuiltinClass: false,
|
||||
@@ -801,11 +897,13 @@ function noRuntimeFeatures(): RuntimeFeatures {
|
||||
stream: false,
|
||||
streamDyn: false,
|
||||
handleProps: false,
|
||||
dynForAwait: false,
|
||||
dynBufferNum: false,
|
||||
builtinValues: false,
|
||||
symbol: false,
|
||||
tls: false,
|
||||
tlsCa: false,
|
||||
utilPromisify: false,
|
||||
warningReplace: false,
|
||||
wasm: false,
|
||||
webcrypto: false,
|
||||
@@ -872,6 +970,12 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): Runtim
|
||||
return keepGoing();
|
||||
};
|
||||
if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type });
|
||||
// Inline-cache sites of dynamic-tier functions use the tier's shapes
|
||||
// (scr_js_object.c, with the SCR_JS_TIER build of scr_json.c).
|
||||
if (!features.dynTier && mod.functions.some((fn) => fn.tier === "dynamic")) {
|
||||
const sites = new JsIcSites(mod);
|
||||
features.dynTier = mod.functions.some((fn) => fn.tier === "dynamic" && hasJsIcSite(fn, sites));
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
@@ -1136,9 +1240,9 @@ export function moduleUsesTlsCa(mod: IrModule): boolean {
|
||||
return scanRuntimeFeatures(mod, "tlsCa").tlsCa;
|
||||
}
|
||||
|
||||
/** True when the module has dynamic-tier jsval operations: the link switch
|
||||
* for the scr_js_* runtime units (two-tier design I3). Programs without
|
||||
* them never link a scr_js_ symbol. */
|
||||
/** True when the module has dynamic-tier jsval operations or inline-cache
|
||||
* sites: the link switch for the scr_js_* runtime units (two-tier design
|
||||
* I3). Programs without them never link a scr_js_ symbol. */
|
||||
export function moduleUsesDynTier(mod: IrModule): boolean {
|
||||
return scanRuntimeFeatures(mod, "dynTier").dynTier;
|
||||
}
|
||||
|
||||
@@ -1622,10 +1622,10 @@ function validateFunction(
|
||||
"while",
|
||||
]);
|
||||
if (e.generatorDelegate) {
|
||||
if (!fn.generator || fn.async)
|
||||
err("delegation sequence outside a synchronous generator", e.loc);
|
||||
if (!fn.generator) err("delegation sequence outside a generator", e.loc);
|
||||
allowed.add("return");
|
||||
allowed.add("throw");
|
||||
allowed.add("rethrow");
|
||||
allowed.add("runtimeFence");
|
||||
allowed.add("tryCatch");
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ async function main(): Promise<number> {
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
@@ -98,6 +99,7 @@ async function main(): Promise<number> {
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -143,6 +143,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
|
||||
"bigint",
|
||||
"searchParams",
|
||||
"qs",
|
||||
"utilPromisify",
|
||||
"dynBuiltinClass",
|
||||
"dynBuiltinStream",
|
||||
"parseArgs",
|
||||
@@ -152,6 +153,7 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
|
||||
"http2",
|
||||
"dgram",
|
||||
"dnsPromises",
|
||||
"dgramDyn",
|
||||
"watch",
|
||||
"foreignFfi",
|
||||
"nodeTest",
|
||||
|
||||
@@ -79,6 +79,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
|
||||
dynTier: false,
|
||||
streamDyn: false,
|
||||
handleProps: false,
|
||||
dynForAwait: false,
|
||||
eventsOnceSignal: false,
|
||||
dynBufferNum: false,
|
||||
builtinValues: false,
|
||||
@@ -101,6 +102,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
utilPromisify: false,
|
||||
dynBuiltinClass: false,
|
||||
dynBuiltinStream: false,
|
||||
parseArgs: false,
|
||||
@@ -110,6 +112,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
dgramDyn: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule, validateModule } from "../src/index.js";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
import { moduleUsesDynTier } from "../src/ir/runtime-features.js";
|
||||
|
||||
/** The LLVM body of a function by source name. */
|
||||
function body(llvm: string, name: string): string {
|
||||
const match = llvm.match(
|
||||
new RegExp(
|
||||
`define internal [^\\n]+ @sc_(?:b)?f_${name}(?:\\.i32)?\\([^\\n]*\\) #0 \\{([\\s\\S]*?)\\n\\}`,
|
||||
),
|
||||
);
|
||||
expect(match, `${name} LLVM function`).not.toBeNull();
|
||||
return match![1]!;
|
||||
}
|
||||
|
||||
test("dynamic-tier property sites read and write through inline caches", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-"));
|
||||
try {
|
||||
const entry = join(dir, "main.js");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(
|
||||
entry,
|
||||
`
|
||||
function dyn(v) { return v; }
|
||||
function read(o) { return o.x; }
|
||||
function write(o, v) { o.x = v; }
|
||||
function bump(o) { o.n += 1; }
|
||||
function at(a, i) { return a[i]; }
|
||||
function call(o) { return o.m(); }
|
||||
var o = dyn({ x: 1, n: 0, m: function () { return 2; } });
|
||||
write(o, 3);
|
||||
bump(o);
|
||||
console.log(read(o), at(dyn([5, 6]), 1), call(o), o.n);
|
||||
`,
|
||||
);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(validateModule(mod)).toEqual([]);
|
||||
expect(moduleUsesDynTier(mod)).toBe(true);
|
||||
const llvm = emitLlvmModule(mod);
|
||||
// Each site owns a zeroed thread-local cache record.
|
||||
expect(llvm).toMatch(
|
||||
/@sc_js_ic_0 = internal (thread_local )?global \[21 x i64\] zeroinitializer/,
|
||||
);
|
||||
expect(body(llvm, "read")).toContain("@scr_js_load_probe(");
|
||||
expect(body(llvm, "write")).toContain("@scr_js_store_probe(");
|
||||
expect(body(llvm, "at")).toContain("@scr_js_keyed_probe(");
|
||||
expect(body(llvm, "call")).toContain("@scr_js_load_probe(");
|
||||
// A compound assignment stores its number result from the jsval: an
|
||||
// unshared number cell is updated in place.
|
||||
expect(body(llvm, "bump")).toMatch(/store double %t\d+, ptr %t\d+/);
|
||||
// The helper stays the out-of-line slow path of every site.
|
||||
expect(body(llvm, "read")).toMatch(
|
||||
/@sc_bf__x25_dyn_class_property_\d+\(ptr %t\d+\) noinline cold/,
|
||||
);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("typed programs have no cache sites and keep the ordinary runtime", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-js-ic-typed-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(
|
||||
entry,
|
||||
`
|
||||
interface P { x: number; y: number }
|
||||
function len2(p: P): number { return p.x * p.x + p.y * p.y; }
|
||||
const parsed: unknown = JSON.parse('{"x":3,"y":4}');
|
||||
console.log(len2({ x: 3, y: 4 }), typeof parsed);
|
||||
`,
|
||||
);
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
expect(moduleUsesDynTier(mod)).toBe(false);
|
||||
const llvm = emitLlvmModule(mod);
|
||||
expect(llvm).not.toContain("@sc_js_ic_");
|
||||
expect(llvm).not.toContain("@scr_js_");
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5685-loop-counter-offset-bounds.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5686-loop-cursor-resets-and-copies.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6022-width-lift-discriminant.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6024-object-literal-method-slot-return.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6148-shape-caches.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6149-jsval-integer-paths.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6251-net-connect-poll-phase-order.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6252-fetch-input-url-unions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6452-promisify-untyped-functions.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6571-events-module-class-value.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6572-extends-require-events.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user