mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Merge main
This commit is contained in:
@@ -48,6 +48,12 @@ Corpus programs are differential tests against Node: every program runs under No
|
||||
|
||||
Before opening a PR that adds or changes corpus, diagnostic, or frontend fixtures, run `pnpm test:ts7 --baselines-only`. This fast completeness check uses the same fixture discovery as the parity suite; it does not type-check or replace behavioral parity. Generate missing preflight/order baselines, review their diagnostics and module order, and run the affected `order-parity.test.ts` batches before pushing. Keep existing baseline entries unchanged unless the corresponding behavior intentionally changes. The full `pnpm test:ts7` command also checks completeness before starting its sweep.
|
||||
|
||||
A change that must not cost good code anything, such as a compatibility fix that replaces a refusal, proves it with the zero-cost gate. The gate compiles the benchmark workloads, the self-hosted compiler, and the TypeScript corpus with two built checkouts. It requires byte-identical IR, LLVM, and release executables, and compile time within tolerance. `--allow=<regex>` names the change's pattern signature: programs whose sources match may differ, and the PR lists them. Any other difference fails the gate, which names the first differing function. The gate also enforces the two-tier runtime separation: no executable may gain symbols from `DYNAMIC_TIER_PREFIXES` in `scripts/zero-cost-compare.mjs`, and fully typed benchmarks may link none; extend that list as dynamic-tier units land. `--quick` checks only the benchmarks; `--cachegrind` adds per-function instruction counts for executables that differ. Run it in a Sandbox with both checkouts prepared:
|
||||
|
||||
```bash
|
||||
node scripts/zero-cost-gate.mjs --baseline=/vercel/sandbox/prev --candidate=. --allow='\barguments\b'
|
||||
```
|
||||
|
||||
Test location follows scope:
|
||||
|
||||
- Co-locate white-box unit tests with implementation files under
|
||||
|
||||
@@ -127,6 +127,12 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
<dt><code>--external-types <specifier=file.d.ts></code></dt>
|
||||
<dd>Coverage only. Map an exact bare module specifier to a local declaration file supplied by an embedder. Repeat the option for multiple modules. The declaration supplies checker types so application coverage can continue; runtime imports and values remain explicit <code>SC1010</code> blockers. Relative paths resolve from the current working directory. Accepted files end in <code>.d.ts</code>, <code>.d.mts</code>, or <code>.d.cts</code>.</dd>
|
||||
|
||||
<dt><code>--tier <auto|static-only></code></dt>
|
||||
<dd>The tier policy for code that has no static lowering. <code>auto</code> is the default. <code>static-only</code> guarantees that the program contains no dynamic-tier demotions: every such construct stays a compile error in TypeScript and a deferred runtime error in JavaScript, which is how both modes behave today. Use it in CI to keep a program fully static. Library mode is always static-only. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
|
||||
<dt><code>--explain-dynamic[=<name>]</code></dt>
|
||||
<dd>Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
|
||||
|
||||
<dt><code>--sanitize</code></dt>
|
||||
<dd>Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with <code>--emit=asm</code> or <code>--emit=obj</code>.</dd>
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ When a program has unsupported operations, the report adds a group for each root
|
||||
<tbody>
|
||||
<tr><td><code>statements analyzed</code></td><td>Executable statements in application code, excluding dependency implementation code.</td></tr>
|
||||
<tr><td><code>compile statically</code></td><td>Statements that compile to native instructions.</td></tr>
|
||||
<tr><td><code>in dynamic tier</code></td><td>Of those, the statements in functions that operate on values typed only at runtime, such as <code>any</code> values and untyped JavaScript. Shown when there are any. See <a href="#dynamic-tier">Dynamic tier</a>.</td></tr>
|
||||
<tr><td><code>blockers</code></td><td>Unsupported operations with counts, diagnostic codes, reasons, locations, and hints.</td></tr>
|
||||
<tr><td><code>in unreached code</code></td><td>Blockers in code the entry never reaches. A build never compiles that code, so these cannot fail it.</td></tr>
|
||||
<tr><td><code>differs from Node</code></td><td>Sites that compile but can behave differently from Node.js. See <a href="#node-divergences">Node divergences</a>.</td></tr>
|
||||
@@ -51,6 +52,16 @@ If a function's signature can't compile, the statements in its body count as not
|
||||
<span id="analyze-with-dynamic" />
|
||||
<span id="embedded-nodejs-builtins" />
|
||||
|
||||
<span id="dynamic-tier" />
|
||||
|
||||
## Dynamic tier
|
||||
|
||||
Code that operates on values whose types are known only at runtime, such as `any` values, the result of an untyped `JSON.parse`, or untyped JavaScript, compiles into dynamic-tier functions. They are native code too, but they check types and look up properties at runtime, so they are slower than code with static types. The report's `in dynamic tier` line counts their statements.
|
||||
|
||||
`--explain-dynamic` lists those functions and the operation that makes each one dynamic. It also lists the units an unsupported construct would move to the dynamic tier: the binding, field, class, function, or module that has no static representation for the construct, with the diagnostic behind each one. These are the constructs the dynamic tier is planned to compile; today they remain blockers in TypeScript and deferred runtime errors in JavaScript. Add a name, such as `--explain-dynamic=Point`, to show only the matching units. With `--print=diagnostics`, the document gains a `tiers` object with the same information.
|
||||
|
||||
`--tier=static-only` keeps every unsupported construct a blocker instead of moving it to the dynamic tier; code that already operates on runtime-typed values compiles as before. Today `auto`, the default, behaves the same way.
|
||||
|
||||
## Diagnostic codes
|
||||
|
||||
Coverage and `scriptc build` report the same diagnostic codes. Common ones include:
|
||||
@@ -160,7 +171,7 @@ For editors, CI annotations, and other tools, `scriptc coverage --print=diagnost
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>stats</code></td>
|
||||
<td>Coverage only, absent after a preflight failure: the report's statement counts. <code>statementsDynamic</code> is always 0 and remains for schema compatibility.</td>
|
||||
<td>Coverage only, absent after a preflight failure: the report's statement counts. <code>statementsStatic</code> counts every statement that compiles to native code; <code>statementsDynamic</code> is the part of those in dynamic-tier functions.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>category</code></td>
|
||||
|
||||
@@ -2,6 +2,8 @@ Typed-array reads at an index that is not an integer in the array's range return
|
||||
|
||||
Ordinary arrays track holes separately from present `undefined` values. Missing reads and empty `pop()`/`shift()` return `undefined`, indexed writes can grow the array, and length growth creates holes.
|
||||
|
||||
Keyed reads of a `Record` or an index signature return `undefined` for a missing key, as in Node.js, with or without `noUncheckedIndexedAccess`. A missing array element or key that reaches storage that cannot hold `undefined`, such as a class field typed `number`, throws a `TypeError`.
|
||||
|
||||
Runtime traps cannot be caught. User-thrown values and supported exceptions, including JSON parse, checked-cast, filesystem, regex, and `String.prototype.repeat` count errors, can be caught; hard traps such as out-of-range reads of statically typed ordinary arrays terminate the process.
|
||||
|
||||
Deep recursion in Linux and macOS executables throws Node's catchable `RangeError: Maximum call stack size exceeded`, on the main thread, in worker threads, and inside async functions and generators. The depth at which it is thrown differs from Node: the main thread uses the process stack limit (usually 8 MiB), worker threads get an 8 MiB stack, and async functions and generators run on 512 KiB stacks, so native code usually recurses deeper than Node on threads and somewhat less deeply inside async bodies. Recursion inside runtime library operations, such as serializing deeply nested data, is not checked. Windows executables, WASI builds and library builds do not check the stack, and exhausting it there terminates the process.
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`, and the flag getters (`global`, `unicodeSets`, and the rest) compile on statically typed regexes. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `v` (unicode sets) flag supports class set operations (`--`, `&&`), nested classes, `\q{...}` string literals, and properties of strings such as `\p{RGI_Emoji}`. `new RegExp` with an invalid pattern throws a `SyntaxError` whose message comes from the native engine and can differ from Node's. The `d` (match indices) flag is unsupported: regex literals and `new RegExp` calls with a literal flag string containing it fail to compile with SC1120, and `new RegExp` with a computed flag string containing it throws an error naming the flag.
|
||||
- **Regular expressions:** Native checked storage preserves identity, `source`, `flags`, flag properties, and stateful `test`, and the flag getters (`global`, `unicodeSets`, and the rest) compile on statically typed regexes. Static `exec`, global `match`, and numeric `lastIndex` access use the native matcher. Runtime constructors accept checked strings and native regex copies; `replace` and `replaceAll` accept native regex values with string templates or callbacks, including captures, named groups, and UTF-16 offsets. Named groups (`groups`) read on matches of a statically known regex: from `exec` and `match`, through optional chains (`m?.groups?.name`), in a `for...of` over `matchAll`, and on the element parameter of an array-method callback over `matchAll` rows. The groups object has a null prototype, as in Node. Non-numeric `lastIndex`, detached methods, and `exec` through an untyped receiver are unsupported. The `v` (unicode sets) flag supports class set operations (`--`, `&&`), nested classes, `\q{...}` string literals, and properties of strings such as `\p{RGI_Emoji}`. `new RegExp` with an invalid pattern throws a `SyntaxError` whose message comes from the native engine and can differ from Node's. The `d` (match indices) flag is unsupported: regex literals and `new RegExp` calls with a literal flag string containing it fail to compile with SC1120, and `new RegExp` with a computed flag string containing it throws an error naming the flag.
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
- **Concurrent tests:** with `{ concurrency }` above 1, subtests run on their own fibers, and results are reported in declaration order, as in Node. Concurrent bodies resume in Node's order around `beforeEach` hooks and nested suites. Other runner steps, such as `afterEach` and `after` hooks, take no microtask turns between bodies, so code that observes the interleaving of concurrent bodies across those steps can see a different order than Node.
|
||||
- **Snapshots:** `t.assert.snapshot` and `t.assert.fileSnapshot` compile, with Node's default serializer, `JSON.stringify(value, null, 2)`, or an `options.serializers` array literal. A compiled test reads the snapshot file Node writes beside the entry file, `<entry file>.snapshot`, at the entry file's path when the program was compiled. Running the binary with Node's flag `--test-update-snapshots` as its first argument writes the snapshot files instead; as in Node, the flag is kept out of `process.argv` and appears in `process.execArgv`. Snapshot files are read in the form Node writes them, `` exports[`key`] = `value`; `` statements. The `snapshot.setDefaultSnapshotSerializers` and `snapshot.setResolveSnapshotPath` module functions throw `SC2020`.
|
||||
- **TestContext values:** `t.filePath` is the entry file's path when the program was compiled. `t.attempt` is `0`, since a test runs once. `t.passed` is `false` while the body runs and the test's outcome in its `afterEach` and `t.after` hooks. `t.workerId` is `Number(process.env.NODE_TEST_WORKER_ID) || undefined`, as in Node. `t.tags` is `[]`, since the `tags` test option throws `SC2020`; the array is not frozen as Node's is.
|
||||
- **Suite and hook contexts:** a `describe` body may take its `SuiteContext`, which has `name`, `fullName`, `filePath`, `attempt`, `passed` and `diagnostic`. Hooks may take their context, as in Node: the suite's for `before` and `after`, the test's for `beforeEach`, `afterEach`, `t.before` and `t.after`. The `SuiteContext` `signal` and `log` members, and hook `done` callbacks, throw `SC2020`.
|
||||
- **Module-level hooks:** `before` hooks outside any suite run at their `before()` call, and the first top-level test waits for the async ones; `after` hooks outside any suite run after the last top-level test. Both get the root's context, named `<root>`. As in Node, a throwing root `before` fails each top-level test with its error, without running its body, and cancels the tests and suites inside each top-level suite. A throwing root `after` reports the entry file as a failed result, and the exit code is unchanged.
|
||||
|
||||
@@ -5,7 +5,7 @@ Some builtins can be stored and passed as functions:
|
||||
- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`.
|
||||
- `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`.
|
||||
|
||||
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), 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.
|
||||
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.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.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
- **Error causes:** Accessor constructor options and explicit subclass redeclarations of `cause` are unsupported. `Error`, `TypeError`, `RangeError`, and `SyntaxError` otherwise accept `{ cause }`, including through inherited constructors. Reads, writes, deletion, and `"cause" in error` distinguish an absent cause from an explicitly undefined cause. Constructor causes are non-enumerable; assignment to an absent cause creates an enumerable property. Property descriptors and checked-value aliases share the cause, including getters and setters installed after construction.
|
||||
- **Error causes:** Accessor constructor options are unsupported. Subclasses may redeclare `cause` as `unknown` or `object` (for example `cause?: unknown`), which defines the own property after `super()` returns as Node does; redeclarations with other types, parameter properties, and accessors named `cause` are unsupported. `Error`, `TypeError`, `RangeError`, and `SyntaxError` otherwise accept `{ cause }`, including through inherited constructors. Reads, writes, deletion, and `"cause" in error` distinguish an absent cause from an explicitly undefined cause. Constructor causes are non-enumerable; assignment to an absent cause creates an enumerable property. Property descriptors and checked-value aliases share the cause, including getters and setters installed after construction.
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"artifactVersion": "f88d62ab68019e678e6b",
|
||||
"artifactVersion": "d9da17b113d39b0cf1b5",
|
||||
"rowCount": 3880
|
||||
}
|
||||
|
||||
@@ -689,9 +689,9 @@
|
||||
"apiEntries": 117,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 53,
|
||||
"partial": 64,
|
||||
"refused": 0,
|
||||
"not-implemented": 64,
|
||||
"not-implemented": 53,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -66447,9 +66447,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66516,9 +66516,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66562,9 +66562,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66585,9 +66585,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66608,9 +66608,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66677,9 +66677,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66723,9 +66723,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66746,9 +66746,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66769,9 +66769,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66815,9 +66815,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -66838,9 +66838,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"tierItemCount": 3457,
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1155,
|
||||
"verify-gap": 2175,
|
||||
"audit-partial": 1166,
|
||||
"verify-gap": 2164,
|
||||
"replace-refusal": 40,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
@@ -67775,12 +67775,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.context.filePath",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -67856,12 +67859,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.passed",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -67906,12 +67912,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.attempt",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -67931,12 +67940,15 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.tags",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -67956,12 +67968,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.workerId",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68031,12 +68046,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.context.diagnostic",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68081,12 +68099,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.context.filePath",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68106,12 +68127,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.context.fullName",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68131,12 +68155,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.context.name",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68181,12 +68208,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.passed",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68206,12 +68236,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:test",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:test.attempt",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -76126,6 +76159,10 @@
|
||||
"tests/corpus/5629-promisify-stream.ts",
|
||||
"tests/corpus/5630-promisify-zlib.ts",
|
||||
"tests/fixtures/node-types/promisify-zlib.ts",
|
||||
"tests/corpus/5631-promisify-crypto.ts",
|
||||
"tests/fixtures/node-types/promisify-crypto.ts",
|
||||
"tests/corpus/5632-promisify-dns.ts",
|
||||
"tests/fixtures/node-types/promisify-dns.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -746,9 +746,9 @@
|
||||
"apiEntries": 117,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 53,
|
||||
"partial": 64,
|
||||
"refused": 0,
|
||||
"not-implemented": 64,
|
||||
"not-implemented": 53,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -91021,6 +91021,7 @@
|
||||
"tests/corpus/5813-mock-timers-module.ts",
|
||||
"tests/corpus/5814-mock-timers-date.ts",
|
||||
"tests/fixtures/node-test/cases/ctx-hooks.ts",
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/fixtures/node-test/cases/ctx-plan.ts",
|
||||
"tests/fixtures/node-test/cases/mock-fn.ts",
|
||||
"tests/fixtures/node-test/cases/mock-method.ts",
|
||||
@@ -91028,6 +91029,7 @@
|
||||
"tests/fixtures/node-test/cases/mock-timers.ts",
|
||||
"tests/fixtures/node-test/cases/only-nested.ts",
|
||||
"tests/fixtures/node-test/cases/snapshot.ts",
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/fixtures/node-test/cases/suite-variants.ts",
|
||||
"tests/fixtures/node-test/cases/todo-suites.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
@@ -93733,8 +93735,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.context.filePath",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -93810,8 +93816,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.passed",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -93856,8 +93866,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.attempt",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -93879,8 +93893,12 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.tags",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -93902,8 +93920,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.workerId",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/ctx-members.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -93971,8 +93993,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.context.diagnostic",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -94017,8 +94043,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.context.filePath",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -94040,8 +94070,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.context.fullName",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -94063,8 +94097,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.context.name",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -94109,8 +94147,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.passed",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -94132,8 +94174,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:test"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:test.attempt",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/suite-context.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "ancestor"
|
||||
},
|
||||
@@ -101058,6 +101104,8 @@
|
||||
"tests/corpus/5628-promisify-timers.ts",
|
||||
"tests/corpus/5629-promisify-stream.ts",
|
||||
"tests/corpus/5630-promisify-zlib.ts",
|
||||
"tests/corpus/5631-promisify-crypto.ts",
|
||||
"tests/corpus/5632-promisify-dns.ts",
|
||||
"tests/corpus/5831-util-types-brands.ts",
|
||||
"tests/corpus/5832-util-types-brands-dynamic.js",
|
||||
"tests/corpus/5835-util-format-integer-specifiers.ts",
|
||||
@@ -101088,6 +101136,8 @@
|
||||
"tests/corpus/text-codec-values/main.ts",
|
||||
"tests/fixtures/node-types/deep-equality.ts",
|
||||
"tests/fixtures/node-types/env-files.ts",
|
||||
"tests/fixtures/node-types/promisify-crypto.ts",
|
||||
"tests/fixtures/node-types/promisify-dns.ts",
|
||||
"tests/fixtures/node-types/promisify-fs.ts",
|
||||
"tests/fixtures/node-types/promisify-timers.ts",
|
||||
"tests/fixtures/node-types/promisify-zlib.ts",
|
||||
@@ -102740,6 +102790,10 @@
|
||||
"tests/corpus/5629-promisify-stream.ts",
|
||||
"tests/corpus/5630-promisify-zlib.ts",
|
||||
"tests/fixtures/node-types/promisify-zlib.ts",
|
||||
"tests/corpus/5631-promisify-crypto.ts",
|
||||
"tests/fixtures/node-types/promisify-crypto.ts",
|
||||
"tests/corpus/5632-promisify-dns.ts",
|
||||
"tests/fixtures/node-types/promisify-dns.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -360,6 +360,8 @@
|
||||
{ "chapter": "url", "signatures": ["`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/2946-url-port-hash.ts"] },
|
||||
{ "chapter": "url", "signatures": ["Legacy `urlObject`", "`urlObject.auth`", "`urlObject.hash`", "`urlObject.host`", "`urlObject.hostname`", "`urlObject.href`", "`urlObject.path`", "`urlObject.pathname`", "`urlObject.port`", "`urlObject.protocol`", "`urlObject.query`", "`urlObject.search`", "`urlObject.slashes`"], "status": "partial", "evidence": ["tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5224-url-format-objects.ts", "tests/fixtures/node-types/url-legacy.ts"] },
|
||||
{ "chapter": "test", "parent": "`context.assert`", "symbols": ["context.assert.snapshot", "context.assert.fileSnapshot"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/snapshot.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.filePath", "passed", "attempt", "tags", "workerId"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-members.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `SuiteContext`", "symbols": ["context.name", "context.fullName", "context.filePath", "context.diagnostic", "passed", "attempt"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-context.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.diagnostic", "context.name"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `MockTracker`", "symbols": ["mock.method", "mock.reset", "mock.restoreAll"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-method.ts", "tests/corpus/5810-mock-method-records.ts", "tests/corpus/5811-mock-tracker-restore.ts", "tests/harness/node-test.test.ts"] },
|
||||
@@ -368,7 +370,7 @@
|
||||
{ "chapter": "test", "parent": "Class: `MockFunctionContext`", "symbols": ["calls", "ctx.callCount", "ctx.mockImplementation", "ctx.mockImplementationOnce", "ctx.resetCalls", "ctx.restore"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-method.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `TestContext`", "symbols": ["context.before", "context.after", "context.beforeEach", "context.afterEach", "context.plan", "context.fullName"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/ctx-hooks.ts", "tests/fixtures/node-test/cases/ctx-plan.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "symbols": ["describe.skip", "describe.todo", "describe.only", "suite.skip", "suite.todo", "suite.only", "it.skip", "it.todo", "it.only"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/suite-variants.ts", "tests/fixtures/node-test/cases/only-nested.ts", "tests/fixtures/node-test/cases/todo-suites.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/harness/project-config.test.ts"] },
|
||||
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
|
||||
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
|
||||
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] },
|
||||
|
||||
+81
-2
@@ -3181,6 +3181,69 @@ declare module "util" {
|
||||
export function promisify(
|
||||
fn: typeof import("node:stream").finished,
|
||||
): typeof import("node:stream/promises").finished;
|
||||
/* The crypto callback functions: the promise settles with the callback's
|
||||
* value. */
|
||||
export function promisify(
|
||||
fn: typeof import("node:crypto").randomBytes,
|
||||
): (size: number) => Promise<Buffer>;
|
||||
export function promisify(
|
||||
fn: typeof import("node:crypto").pbkdf2,
|
||||
): (
|
||||
password: string | Uint8Array,
|
||||
salt: string | Uint8Array,
|
||||
iterations: number,
|
||||
keylen: number,
|
||||
digest: string,
|
||||
) => Promise<Buffer>;
|
||||
export function promisify(
|
||||
fn: typeof import("node:crypto").scrypt,
|
||||
): (
|
||||
password: import("node:crypto").BinaryLike,
|
||||
salt: import("node:crypto").BinaryLike,
|
||||
keylen: number,
|
||||
options?: import("node:crypto").ScryptOptions,
|
||||
) => Promise<Buffer>;
|
||||
export function promisify(
|
||||
fn: typeof import("node:crypto").hkdf,
|
||||
): (
|
||||
digest: string,
|
||||
ikm: import("node:crypto").BinaryLike | import("node:crypto").KeyObject,
|
||||
salt: import("node:crypto").BinaryLike,
|
||||
info: import("node:crypto").BinaryLike,
|
||||
keylen: number,
|
||||
) => Promise<ArrayBuffer>;
|
||||
export function promisify(
|
||||
fn: typeof import("node:crypto").randomFill,
|
||||
): <T extends ArrayBufferView | ArrayBufferLike>(
|
||||
buffer: T,
|
||||
offset?: number,
|
||||
size?: number,
|
||||
) => Promise<T>;
|
||||
export function promisify(fn: typeof import("node:crypto").randomInt): {
|
||||
(max: number): Promise<number>;
|
||||
(min: number, max: number): Promise<number>;
|
||||
};
|
||||
export function promisify(
|
||||
fn: typeof import("node:crypto").generateKey,
|
||||
): (
|
||||
type: "hmac" | "aes",
|
||||
options: { length: number },
|
||||
) => Promise<import("node:crypto").KeyObject>;
|
||||
/* dns.lookup's promise settles with { address, family } (the address
|
||||
* array with options.all), and lookupService's with { hostname, service }. */
|
||||
export function promisify(fn: typeof import("node:dns").lookup): {
|
||||
(
|
||||
hostname: string,
|
||||
options: import("node:dns").LookupAllOptions,
|
||||
): Promise<import("node:dns").LookupAddress[]>;
|
||||
(
|
||||
hostname: string,
|
||||
options?: number | import("node:dns").LookupOneOptions,
|
||||
): Promise<import("node:dns").LookupAddress>;
|
||||
};
|
||||
export function promisify(
|
||||
fn: typeof import("node:dns").lookupService,
|
||||
): (address: string, port: number) => Promise<{ hostname: string; service: string }>;
|
||||
/* Declared surface without full lowering (the type-level tolerance
|
||||
* story: guarded/diagnostic-path code must TYPECHECK; a reached use
|
||||
* without a lowering fences at its site). inspect's options carry the
|
||||
@@ -3319,8 +3382,18 @@ declare module "node:util/types" {
|
||||
* suites typecheck but fence at their use sites (no lowering). */
|
||||
declare module "node:test" {
|
||||
type TestFn = (t: TestContext) => void | Promise<void>;
|
||||
type SuiteFn = () => void;
|
||||
type HookFn = () => void | Promise<void>;
|
||||
type SuiteFn = (s: SuiteContext) => void;
|
||||
type HookFn = (c: TestContext | SuiteContext) => void | Promise<void>;
|
||||
/* The describe/suite callback's context, and the before/after hook
|
||||
* context of a suite. */
|
||||
interface SuiteContext {
|
||||
readonly name: string;
|
||||
readonly fullName: string;
|
||||
readonly filePath: string | undefined;
|
||||
readonly attempt: number;
|
||||
readonly passed: boolean;
|
||||
diagnostic(message: string): void;
|
||||
}
|
||||
interface TestOptions {
|
||||
skip?: boolean | string;
|
||||
todo?: boolean | string;
|
||||
@@ -3332,6 +3405,11 @@ declare module "node:test" {
|
||||
interface TestContext {
|
||||
readonly name: string;
|
||||
readonly fullName: string;
|
||||
readonly filePath: string | undefined;
|
||||
readonly attempt: number;
|
||||
readonly passed: boolean;
|
||||
readonly workerId: number | undefined;
|
||||
readonly tags: readonly string[];
|
||||
readonly assert: TestContextAssert;
|
||||
readonly mock: MockTracker;
|
||||
test(name: string, fn?: TestFn): Promise<void>;
|
||||
@@ -3476,6 +3554,7 @@ declare module "node:test" {
|
||||
run,
|
||||
snapshot,
|
||||
TestContext,
|
||||
SuiteContext,
|
||||
TestOptions,
|
||||
Mock,
|
||||
MockFunctionContext,
|
||||
|
||||
@@ -47,7 +47,8 @@ interface DenseGuard {
|
||||
* then extends the length over hole states exactly the same way). An index
|
||||
* below the hole-free prefix bound branches to `filledLabel` first; both
|
||||
* that block and the returned (state-checked) dense block may use the
|
||||
* guard's values. */
|
||||
* guard's values. A `proven` index is known to lie below the bound (a dense
|
||||
* array loop's fast copy, dense-array-loops.ts) and always takes it. */
|
||||
function emitDenseGuard(
|
||||
host: LlvmEmitterContext,
|
||||
array: string,
|
||||
@@ -56,6 +57,7 @@ function emitDenseGuard(
|
||||
slow: string,
|
||||
requireBelowLen: boolean,
|
||||
filledLabel: string,
|
||||
proven = false,
|
||||
): DenseGuard {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
@@ -137,8 +139,10 @@ function emitDenseGuard(
|
||||
B.line(`${offset} = trunc i64 ${wide} to i32`);
|
||||
}
|
||||
// The bound never exceeds the length or the dense capacity.
|
||||
let inFilled = B.tmp();
|
||||
B.line(`${inFilled} = icmp ult i64 ${wide}, ${filledWide}`);
|
||||
// The proof covers the counter's integer value only.
|
||||
const below = proven && integral === null;
|
||||
let inFilled = below ? "true" : B.tmp();
|
||||
if (!below) B.line(`${inFilled} = icmp ult i64 ${wide}, ${filledWide}`);
|
||||
if (integral) {
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = and i1 ${integral}, ${inFilled}`);
|
||||
@@ -272,6 +276,7 @@ export function emitDenseArrayGet(
|
||||
element: IrType,
|
||||
getter: string,
|
||||
retain: boolean,
|
||||
proven = false,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
@@ -285,7 +290,7 @@ export function emitDenseArrayGet(
|
||||
const slow = B.newLabel("arr.get.slow"),
|
||||
join = B.newLabel("arr.get.join"),
|
||||
valueLabel = B.newLabel("arr.get.value");
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel);
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel, proven);
|
||||
const state = loadState(host, guard);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = icmp eq i8 ${state}, ${SCR_ARR_VALUE}`);
|
||||
@@ -331,6 +336,7 @@ export function emitDenseArrayNumber(
|
||||
array: string,
|
||||
index: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
proven = false,
|
||||
): string {
|
||||
return emitDenseArrayGet(
|
||||
host,
|
||||
@@ -341,6 +347,7 @@ export function emitDenseArrayNumber(
|
||||
{ kind: "f64" },
|
||||
"scr_arr_get_number",
|
||||
false,
|
||||
proven,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,6 +406,7 @@ export function emitDenseArraySet(
|
||||
acc: ArrayAccess,
|
||||
element: IrType,
|
||||
value: string,
|
||||
proven = false,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
@@ -413,7 +421,7 @@ export function emitDenseArraySet(
|
||||
const slow = B.newLabel("arr.set.slow"),
|
||||
join = B.newLabel("arr.set.join"),
|
||||
filledLabel = B.newLabel("arr.set.filled");
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel);
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel, proven);
|
||||
const { offset, len } = guard;
|
||||
const storeValue = (valuePtr: string): void => {
|
||||
if (acc === "f64") {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
countedDenseShape,
|
||||
matchDenseArrayLoop,
|
||||
type DenseLoopShape,
|
||||
} from "./dense-array-loops.js";
|
||||
|
||||
const loc = { file: "dense-array-loops.ts", start: 0, end: 0 };
|
||||
const ARR = arrayOf(F64);
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });
|
||||
const arr = (localId: string): IrExpr & { kind: "varRef" } => ({
|
||||
kind: "varRef",
|
||||
localId,
|
||||
type: ARR,
|
||||
loc,
|
||||
});
|
||||
const method = (
|
||||
receiver: string,
|
||||
name: Extract<IrExpr, { kind: "arrIntrinsic" }>["method"],
|
||||
args: IrExpr[] = [],
|
||||
): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: name,
|
||||
receiver: arr(receiver),
|
||||
args,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const read = (receiver: string, index = ref("i")): IrExpr => method(receiver, "getNumber", [index]);
|
||||
const write = (receiver: string, value: IrExpr, index = ref("i")): IrStmt => ({
|
||||
kind: "arraySet",
|
||||
arr: arr(receiver),
|
||||
index,
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
const locals = new Map<string, IrLocal>([
|
||||
["i", { id: "i", name: "i", type: F64, mutable: true }],
|
||||
["a", { id: "a", name: "a", type: ARR, mutable: false }],
|
||||
["b", { id: "b", name: "b", type: ARR, mutable: true }],
|
||||
["c", { id: "c", name: "c", type: ARR, mutable: false }],
|
||||
]);
|
||||
|
||||
/** for (let i = 0; i < a.length; i++) body */
|
||||
function loop(body: IrStmt[]): IrStmt & { kind: "for" } {
|
||||
return {
|
||||
kind: "for",
|
||||
loc,
|
||||
init: { kind: "varDecl", localId: "i", init: num(0), loc },
|
||||
cond: { kind: "bin", op: "<", left: ref("i"), right: method("a", "length"), type: BOOL, loc },
|
||||
update: {
|
||||
kind: "assign",
|
||||
localId: "i",
|
||||
value: { kind: "bin", op: "+", left: ref("i"), right: num(1), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
body,
|
||||
};
|
||||
}
|
||||
const shape: DenseLoopShape = {
|
||||
counter: "i",
|
||||
length: arr("a"),
|
||||
range: { min: 0, max: 4294967294 },
|
||||
};
|
||||
const match = (body: IrStmt[], initialized = new Set(["a", "b"])) =>
|
||||
matchDenseArrayLoop(loop(body), shape, locals, new Set(), initialized);
|
||||
|
||||
test("arrays indexed at the counter of a length-preserving loop get an int32 fast copy", () => {
|
||||
const scale = write("b", {
|
||||
kind: "bin",
|
||||
op: "*",
|
||||
left: read("a"),
|
||||
right: num(2),
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const dense = match([scale]);
|
||||
expect([...dense!.arrays.keys()].sort()).toEqual(["a", "b"]);
|
||||
expect(dense!.range).toEqual({ min: 0, max: 2147483646 });
|
||||
// A binding not definitely assigned before the loop is not read by the guard.
|
||||
expect([...match([scale], new Set())!.arrays.keys()]).toEqual(["a"]);
|
||||
});
|
||||
|
||||
test("loops that may resize, rebind or move storage keep the ordinary copy only", () => {
|
||||
expect(match([{ kind: "exprStmt", expr: method("a", "push", [num(1)]), loc }])).toBeNull();
|
||||
expect(match([write("a", num(0), ref("unknown"))])).toBeNull();
|
||||
expect(
|
||||
match([
|
||||
{ kind: "exprStmt", expr: { kind: "call", callee: "f", args: [], type: F64, loc }, loc },
|
||||
]),
|
||||
).toBeNull();
|
||||
// A nested loop would be duplicated too.
|
||||
expect(match([{ kind: "exprStmt", expr: read("a"), loc }, loop([])])).toBeNull();
|
||||
// No access at the counter: nothing to prove.
|
||||
expect(match([{ kind: "exprStmt", expr: read("a", num(0)), loc }])).toBeNull();
|
||||
// Copying the bounding array into another binding lets it escape.
|
||||
expect(
|
||||
match([
|
||||
{ kind: "exprStmt", expr: read("a"), loc },
|
||||
{ kind: "assign", localId: "b", value: arr("a"), loc },
|
||||
]),
|
||||
).toBeNull();
|
||||
// A rebound array is not covered, but the bounding array still is.
|
||||
const rebound = match([
|
||||
{ kind: "exprStmt", expr: read("b"), loc },
|
||||
{ kind: "exprStmt", expr: read("a"), loc },
|
||||
{ kind: "assign", localId: "b", value: arr("c"), loc },
|
||||
]);
|
||||
expect([...rebound!.arrays.keys()]).toEqual(["a"]);
|
||||
});
|
||||
|
||||
test("counted shapes need a non-negative counter strictly below the length", () => {
|
||||
const ascending = (limit: IrExpr, inclusive: boolean, min = 0) =>
|
||||
countedDenseShape(loop([]), {
|
||||
localId: "i",
|
||||
start: num(min),
|
||||
limit,
|
||||
step: 1,
|
||||
inclusive,
|
||||
range: { min, max: 100 },
|
||||
});
|
||||
const minus = (offset: number): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "-",
|
||||
left: method("a", "length"),
|
||||
right: num(offset),
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
expect(ascending(method("a", "length"), false)?.length.localId).toBe("a");
|
||||
expect(ascending(minus(1), true)?.length.localId).toBe("a");
|
||||
expect(ascending(method("a", "length"), true)).toBeNull();
|
||||
expect(ascending(minus(1), false, -1)).toBeNull();
|
||||
const descending = (offset: number) =>
|
||||
countedDenseShape(loop([]), {
|
||||
localId: "i",
|
||||
start: minus(offset),
|
||||
limit: num(0),
|
||||
step: -1,
|
||||
inclusive: true,
|
||||
range: { min: 0, max: 100 },
|
||||
});
|
||||
expect(descending(1)?.length.localId).toBe("a");
|
||||
expect(descending(0)).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
/* Dense array loops: a versioned copy of an index loop over an array whose
|
||||
* length the loop never changes.
|
||||
*
|
||||
* `for (let i = 0; i < a.length; i++) b[i] = a[i] * 2` reads its bound from
|
||||
* `a`, and every element access at the counter normally tests the index
|
||||
* against the array's hole-free prefix bound (`filled`, see
|
||||
* dense-array-access.ts) before it touches the slot. When the loop provably
|
||||
* never changes `a.length` (ir/array-length-stability.ts) and never moves or
|
||||
* cuts the storage of the arrays it indexes, one test before the loop covers
|
||||
* every pass: if `a.length` is at most each indexed array's `filled` bound,
|
||||
* every counter value the loop can reach (at least 0, below `a.length`)
|
||||
* names a present value in dense storage. The fast copy then reads and
|
||||
* replaces those slots directly, which leaves LLVM a loop without
|
||||
* per-element branches (vectorizable where the body allows it). The same
|
||||
* test bounds `a.length` by 2^31 - 1, so the fast copy's counter is an int32.
|
||||
* Arrays longer than that are sparse in practice (a dense one would need
|
||||
* 16 GB of slots) and take the ordinary copy, as does every array with a
|
||||
* hole, present undefined or sparse storage below `a.length`. */
|
||||
|
||||
import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js";
|
||||
import type { IntegerRange } from "../../ir/integer-ranges.js";
|
||||
import { preservesArrayLength } from "../../ir/array-length-stability.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { SCR_ARR_FIELD } from "./common.js";
|
||||
|
||||
export const INT32_MAX = 2147483647;
|
||||
|
||||
export interface DenseArrayLoop {
|
||||
/** The loop's counter binding. */
|
||||
counter: string;
|
||||
/** The array whose length bounds the counter. */
|
||||
length: IrExpr & { kind: "varRef" };
|
||||
/** Arrays whose accesses at the counter are proven to be inside the
|
||||
* hole-free prefix in the fast copy, with a read of each binding. */
|
||||
arrays: ReadonlyMap<string, IrExpr & { kind: "varRef" }>;
|
||||
/** The counter's range on every pass of the fast copy. */
|
||||
range: IntegerRange;
|
||||
}
|
||||
|
||||
/** The fast copy duplicates the loop; keep the duplicated code small. */
|
||||
const MAX_LOOP_NODES = 120;
|
||||
|
||||
/** How a matched integer loop bounds its counter below `length.length`. */
|
||||
export interface DenseLoopShape {
|
||||
counter: string;
|
||||
length: IrExpr & { kind: "varRef" };
|
||||
/** The counter's proven range on every pass of the ordinary loop; the
|
||||
* fast copy keeps its minimum, which must be at least 0. */
|
||||
range: IntegerRange;
|
||||
}
|
||||
|
||||
/** `length.length - c` (c an integer literal, 0 when absent). */
|
||||
function lengthOffset(e: IrExpr): { receiver: IrExpr & { kind: "varRef" }; offset: number } | null {
|
||||
let offset = 0;
|
||||
if (
|
||||
e.kind === "bin" &&
|
||||
e.op === "-" &&
|
||||
e.right.kind === "numLit" &&
|
||||
Number.isSafeInteger(e.right.value)
|
||||
) {
|
||||
offset = e.right.value;
|
||||
e = e.left;
|
||||
}
|
||||
return e.kind === "arrIntrinsic" &&
|
||||
e.method === "length" &&
|
||||
e.args.length === 0 &&
|
||||
e.receiver.kind === "varRef" &&
|
||||
e.receiver.type.kind === "array"
|
||||
? { receiver: e.receiver, offset }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** The array bounding a counted loop's counter below its length: an
|
||||
* ascending loop compared below `a.length - c`, or a descending one starting
|
||||
* at `a.length - c` (c >= 1). */
|
||||
export function countedDenseShape(
|
||||
loop: IrStmt & { kind: "for" },
|
||||
counted: {
|
||||
localId: string;
|
||||
start: IrExpr;
|
||||
limit: IrExpr;
|
||||
step: number;
|
||||
inclusive: boolean;
|
||||
range: IntegerRange;
|
||||
},
|
||||
): DenseLoopShape | null {
|
||||
if (counted.range.min < 0) return null;
|
||||
if (counted.step > 0) {
|
||||
const bound = lengthOffset(counted.limit);
|
||||
if (!bound || bound.offset < (counted.inclusive ? 1 : 0)) return null;
|
||||
return { counter: counted.localId, length: bound.receiver, range: counted.range };
|
||||
}
|
||||
// The start is evaluated once, before the loop; the counter only falls.
|
||||
const start = lengthOffset(counted.start);
|
||||
if (!start || start.offset < 1 || loop.init?.kind !== "varDecl") return null;
|
||||
return { counter: counted.localId, length: start.receiver, range: counted.range };
|
||||
}
|
||||
|
||||
/** Match the fast copy of a loop of the given shape. `initialized` holds the
|
||||
* bindings definitely assigned when the loop starts; `unaliased` the local
|
||||
* arrays no other binding holds. */
|
||||
export function matchDenseArrayLoop(
|
||||
loop: IrStmt & { kind: "for" },
|
||||
shape: DenseLoopShape,
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
unaliased: ReadonlySet<string>,
|
||||
initialized: ReadonlySet<string>,
|
||||
): DenseArrayLoop | null {
|
||||
const { counter, length } = shape;
|
||||
if (shape.range.min < 0 || !loop.cond) return null;
|
||||
let nodes = 0,
|
||||
nested = false;
|
||||
const candidates = new Map<string, IrExpr & { kind: "varRef" }>();
|
||||
// Arrays written, resized, or indexed other than at the counter by an
|
||||
// element write.
|
||||
const disturbed = new Set<string>();
|
||||
const access = (arr: IrExpr, index: IrExpr): void => {
|
||||
if (arr.kind === "varRef" && index.kind === "varRef" && index.localId === counter)
|
||||
candidates.set(arr.localId, arr);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (++nodes > MAX_LOOP_NODES) return false;
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") disturbed.add(e.localId);
|
||||
if (e.kind === "arrayGet") access(e.arr, e.index);
|
||||
if (e.kind === "arrIntrinsic") {
|
||||
if (e.method === "getNumber" && e.args[0]) access(e.receiver, e.args[0]);
|
||||
else if (e.method !== "length" && e.receiver.kind === "varRef")
|
||||
disturbed.add(e.receiver.localId);
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (++nodes > MAX_LOOP_NODES) return false;
|
||||
if (s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf")
|
||||
nested = true;
|
||||
if (s.kind === "assign" || s.kind === "varDecl") disturbed.add(s.localId);
|
||||
if (s.kind === "arraySet") {
|
||||
access(s.arr, s.index);
|
||||
if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || s.index.localId !== counter))
|
||||
disturbed.add(s.arr.localId);
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null;
|
||||
if (
|
||||
!preservesArrayLength(
|
||||
[loop.cond, ...loop.body],
|
||||
{ receiver: length.localId, inRange: counter },
|
||||
locals,
|
||||
unaliased,
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const arrays = new Map<string, IrExpr & { kind: "varRef" }>();
|
||||
for (const [id, ref] of candidates) {
|
||||
const local = locals.get(id);
|
||||
if (
|
||||
!local ||
|
||||
local.type.kind !== "array" ||
|
||||
local.boxed ||
|
||||
local.tdz ||
|
||||
disturbed.has(id) ||
|
||||
(id !== length.localId && !initialized.has(id))
|
||||
)
|
||||
continue;
|
||||
arrays.set(id, ref);
|
||||
}
|
||||
if (arrays.size === 0) return null;
|
||||
return {
|
||||
counter,
|
||||
length,
|
||||
arrays,
|
||||
range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) },
|
||||
};
|
||||
}
|
||||
|
||||
/** The fast copy's entry test: the bounding length is at most 2^31 - 1 and
|
||||
* at most every indexed array's hole-free prefix bound. */
|
||||
export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoop): string {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
const header = (array: string, field: number): string => {
|
||||
const ptr = B.tmp();
|
||||
B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${field}`);
|
||||
host.markMemoryPointer(ptr, "array:header");
|
||||
return ptr;
|
||||
};
|
||||
const receiver = host.emitStableReceiver(loop.length, []);
|
||||
const lenPtr = header(receiver.name, 1),
|
||||
len = B.tmp(),
|
||||
small = B.tmp();
|
||||
B.line(
|
||||
`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`,
|
||||
);
|
||||
B.line(`${small} = icmp ule ${size} ${len}, ${INT32_MAX}`);
|
||||
let valid = small;
|
||||
for (const [id, ref] of loop.arrays) {
|
||||
const array = id === loop.length.localId ? receiver : host.emitStableReceiver(ref, []);
|
||||
const filledPtr = header(array.name, SCR_ARR_FIELD.filled),
|
||||
filled = B.tmp(),
|
||||
covered = B.tmp(),
|
||||
both = B.tmp();
|
||||
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
|
||||
let bound = filled;
|
||||
if (size === "i64") {
|
||||
bound = B.tmp();
|
||||
B.line(`${bound} = zext i32 ${filled} to i64`);
|
||||
}
|
||||
B.line(`${covered} = icmp ule ${size} ${len}, ${bound}`);
|
||||
B.line(`${both} = and i1 ${valid}, ${covered}`);
|
||||
valid = both;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
@@ -1217,12 +1217,13 @@ export class LlDyn {
|
||||
`llvm emitter bug: JSON schema of unknown shape ${t.shapeId}`,
|
||||
);
|
||||
const struct = mangleRecordStruct(t.shapeId);
|
||||
const index = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const index = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
// The runtime names kept-source keys by this index (record-sources.ts).
|
||||
const order = recordSourceFieldOrder(shape);
|
||||
const fieldTy = `{ ptr, ${S}, ${S}, ptr }`;
|
||||
const entries = order.map((fieldName) => {
|
||||
const field = shape.fields[index.get(fieldName)! - 1]!;
|
||||
const field = shape.fields[index.get(fieldName)! - base]!;
|
||||
const sub = this.jsonSchema(field.type);
|
||||
const offset = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${index.get(fieldName)}) to ${S})`;
|
||||
return `${fieldTy} { ptr ${host.cstr(fieldName)}, ${S} ${Buffer.byteLength(fieldName, "utf8")}, ${S} ${offset}, ptr @${sub} }`;
|
||||
@@ -1240,7 +1241,7 @@ export class LlDyn {
|
||||
const overflow = shape.indexValue
|
||||
? {
|
||||
elem: `@${this.jsonSchema(shape.indexValue)}`,
|
||||
offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + 1}) to ${S})`,
|
||||
offset: `ptrtoint (ptr getelementptr (%${struct}, ptr null, i64 0, i32 ${shape.fields.length + base}) to ${S})`,
|
||||
}
|
||||
: { elem: "null", offset: "0" };
|
||||
body = `${S} 3, ${S} ${entries.length}, ptr ${fields}, ptr ${overflow.elem}, ptr @${mangleRecordNew(t.shapeId)}, ptr null, ptr ${vAdapters(host, t).release}, ptr null, ${S} ${overflow.offset}`;
|
||||
@@ -1547,18 +1548,19 @@ export class LlDyn {
|
||||
B.line(`call void @scr_arr_outline(ptr ${cached})`);
|
||||
B.line(`call void @scr_arr_outline(ptr ${checked})`);
|
||||
}
|
||||
const base = t.kind === "record" ? host.nullableUnions.fieldBase(t.shapeId) : 1;
|
||||
const members =
|
||||
t.kind === "record"
|
||||
? [
|
||||
...shape!.fields.map((field, index) => ({
|
||||
index: index + 1,
|
||||
index: index + base,
|
||||
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
|
||||
name: field.name,
|
||||
})),
|
||||
...(shape!.indexValue
|
||||
? [
|
||||
{
|
||||
index: shape!.fields.length + 1,
|
||||
index: shape!.fields.length + base,
|
||||
type: "ptr" as const,
|
||||
name: "[key: string] overflow",
|
||||
},
|
||||
@@ -1819,7 +1821,8 @@ export class LlDyn {
|
||||
`llvm emitter bug: dynCheck of unknown shape ${t.shapeId}`,
|
||||
);
|
||||
const struct = mangleRecordStruct(t.shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
const releaseR = (): void => {
|
||||
B.line(`call void ${releaseSym(host, t)}(ptr %r0)`);
|
||||
};
|
||||
@@ -1996,7 +1999,7 @@ export class LlDyn {
|
||||
const ovfp = B.tmp();
|
||||
const ovf = B.tmp();
|
||||
B.line(
|
||||
`${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
`${ovfp} = getelementptr inbounds %${struct}, ptr %r0, i64 0, i32 ${shape.fields.length + base}`,
|
||||
);
|
||||
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
|
||||
if (shape.fields.length === 0) {
|
||||
@@ -2578,7 +2581,8 @@ export class LlDyn {
|
||||
if (!shape)
|
||||
throw new InternalCompilerError(`llvm emitter bug: to-dyn of unknown shape ${t.shapeId}`);
|
||||
const struct = mangleRecordStruct(t.shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + base]));
|
||||
const loadFieldOf = (fname: string, ft: IrType): string => {
|
||||
const p = B.tmp();
|
||||
B.line(
|
||||
@@ -2702,7 +2706,7 @@ export class LlDyn {
|
||||
const ovfp = B.tmp();
|
||||
const ovf = B.tmp();
|
||||
B.line(
|
||||
`${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
`${ovfp} = getelementptr inbounds %${struct}, ptr %v, i64 0, i32 ${shape.fields.length + base}`,
|
||||
);
|
||||
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
|
||||
const ks = B.tmp();
|
||||
|
||||
@@ -55,6 +55,13 @@ import {
|
||||
matchIntegerBytesForLoop,
|
||||
matchIntegerCountedForLoop,
|
||||
} from "../../ir/integer-loops.js";
|
||||
import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js";
|
||||
import {
|
||||
countedDenseShape,
|
||||
emitDenseLoopGuard,
|
||||
matchDenseArrayLoop,
|
||||
type DenseArrayLoop,
|
||||
} from "./dense-array-loops.js";
|
||||
import { emitCountedLoopLimit } from "./counted-loops.js";
|
||||
import {
|
||||
emitSafeIntegerCheck,
|
||||
@@ -229,7 +236,10 @@ import { findAppendedArrayLiterals } from "./appended-array-literals.js";
|
||||
import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
|
||||
import {
|
||||
findScalarStringSlices,
|
||||
findScratchStringSlices,
|
||||
findLentStringParameters,
|
||||
emitStringSliceSnapshot,
|
||||
type LentStringParameters,
|
||||
type StringSliceSnapshot,
|
||||
} from "./string-slices.js";
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
@@ -383,6 +393,7 @@ import {
|
||||
elemAccess,
|
||||
emitInlineRcHelpers,
|
||||
emitNullableRetainWrappers,
|
||||
emitShapeUnionHelpers,
|
||||
FN_ATTRS,
|
||||
inlineRcDecls,
|
||||
type InlineRcMode,
|
||||
@@ -398,6 +409,7 @@ import {
|
||||
} from "./shapes.js";
|
||||
import type { ExprOf, LibCallExpr, LlStreamTypedRefAdapter, LlValue } from "./expr-context.js";
|
||||
import { INLINE_LITERAL_BYTES, literalEqualityDefinition } from "./string-literal-equality.js";
|
||||
import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js";
|
||||
|
||||
export { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
@@ -622,6 +634,10 @@ export class LlEmitter {
|
||||
private loopArrayBorrows: ReadonlySet<IrStmt> = new Set();
|
||||
private scalarStringSlices = new Map<string, IrExpr & { kind: "strIntrinsic" }>();
|
||||
readonly stringSlices = new Map<string, StringSliceSnapshot>();
|
||||
/** Slices written into stack buffers (findScratchStringSlices). */
|
||||
scratchStringSlices: ReadonlySet<IrExpr> = new Set();
|
||||
/** String parameters a caller may lend scratch slices to. */
|
||||
private lentStringParameters: LentStringParameters = new Map();
|
||||
private localStackUnions = new Map<string, IrExpr & { kind: "unionWrap" }>();
|
||||
private localStackObjects = new Map<string, StackObjectSource>();
|
||||
private reassignedStackObjects = new Set<string>();
|
||||
@@ -673,6 +689,21 @@ export class LlEmitter {
|
||||
return this.sizeType === "i64" ? ", !range !{i64 0, i64 4294967296}" : "";
|
||||
}
|
||||
|
||||
/** Whether an element access at `index` of `arr` lies in the hole-free
|
||||
* prefix: the counter of a dense array loop's fast copy indexing one of
|
||||
* the arrays its entry test covered. */
|
||||
provenDenseIndex(arr: IrExpr, index: IrExpr): boolean {
|
||||
const loop = this.denseLoop;
|
||||
return (
|
||||
loop !== null &&
|
||||
arr.kind === "varRef" &&
|
||||
loop.arrays.has(arr.localId) &&
|
||||
index.kind === "varRef" &&
|
||||
index.localId === loop.counter &&
|
||||
this.integerLoopBindings.has(loop.counter)
|
||||
);
|
||||
}
|
||||
|
||||
fieldAliasAttachment(ptr: string): string {
|
||||
const tag = this.fieldPointerTags.get(ptr);
|
||||
return tag === undefined ? "" : `, !tbaa !${tag}`;
|
||||
@@ -1029,6 +1060,14 @@ export class LlEmitter {
|
||||
* immortal from construction (classes.ts programLifetimeClass). */
|
||||
readonly programLifetime: ReadonlySet<string>;
|
||||
private numericLocals = new Map<string, IrLocal>();
|
||||
/** Local arrays of the current function that no other binding can hold
|
||||
* (ir/array-length-stability.ts). */
|
||||
private unaliasedArrays: ReadonlySet<string> = new Set();
|
||||
/** Dense array loops (dense-array-loops.ts) run in this function. */
|
||||
private denseLoopsEnabled = false;
|
||||
private denseLoopEntries: ReturnType<typeof findInitializedByteLoopBindings> = new Map();
|
||||
/** The fast copy of a dense array loop being emitted. */
|
||||
private denseLoop: DenseArrayLoop | null = null;
|
||||
private captureIds = new Set<string>();
|
||||
/** Active induction bindings retain their width independently of size_t. */
|
||||
integerLoopBindings = new Map<
|
||||
@@ -1145,7 +1184,7 @@ export class LlEmitter {
|
||||
expr: (expr) => !(expr.kind === "libCall" && expr.fn === "error.stack"),
|
||||
}),
|
||||
);
|
||||
this.nullableUnions = new NullableUnions(mod);
|
||||
this.nullableUnions = new NullableUnions(mod, options.debugSources === undefined);
|
||||
this.mockTimers = moduleUsesMockTimers(mod);
|
||||
this.debug =
|
||||
options.debugSources === undefined
|
||||
@@ -1244,8 +1283,10 @@ export class LlEmitter {
|
||||
// Optional array reads also cover nullable unions: emitLocalArrayRead
|
||||
// then yields the (borrowed or owned) element pointer, not a stack box.
|
||||
// Arrays of such a union are read the same way.
|
||||
this.optionalArrayReads = new OptionalArrayReads(this.fnByName, this.unionsById, (id) =>
|
||||
this.nullableUnions.has(id),
|
||||
this.optionalArrayReads = new OptionalArrayReads(
|
||||
this.fnByName,
|
||||
this.unionsById,
|
||||
(id) => this.nullableUnions.single(id) !== null,
|
||||
);
|
||||
this.unionSources = {
|
||||
unions: this.boxedUnionsById,
|
||||
@@ -1298,7 +1339,8 @@ export class LlEmitter {
|
||||
(type) =>
|
||||
this.plainReference(type) &&
|
||||
type.kind !== "string" &&
|
||||
(type.kind !== "union" || this.nullableUnions.get(type.unionId)?.arm.kind !== "string"),
|
||||
(type.kind !== "union" ||
|
||||
this.nullableUnions.single(type.unionId)?.arm.kind !== "string"),
|
||||
this.walkBorrowsByFunction.get(fn.name) ?? new Set(),
|
||||
);
|
||||
if (rebound.size === 0) continue;
|
||||
@@ -1324,6 +1366,8 @@ export class LlEmitter {
|
||||
return shape !== undefined && !recordDynKeepsIdentity(shape);
|
||||
},
|
||||
);
|
||||
this.lentStringParameters =
|
||||
this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map();
|
||||
this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes);
|
||||
// Integer clones signal a stack overflow through their result, which
|
||||
// needs the single-threaded guard; worker programs also poll for
|
||||
@@ -1349,6 +1393,27 @@ export class LlEmitter {
|
||||
const traced = computeTraced(mod);
|
||||
this.tracedShapes = traced.shapes;
|
||||
this.tracedUnions = traced.unions;
|
||||
// A shape union's value is whichever record it holds, and the collector
|
||||
// visits it as that record: when one arm carries a collector header,
|
||||
// every arm must (nullable-unions.ts). Unions that share an arm pass a
|
||||
// header on to each other's arms, so repeat until no union adds one.
|
||||
const shapeUnions = this.nullableUnions.allShapeUnions();
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const union of shapeUnions) {
|
||||
if (
|
||||
!this.tracedUnions.has(union.unionId) &&
|
||||
!union.arms.some((arm) => this.tracedShapes.has(`record:${arm.shapeId}`))
|
||||
)
|
||||
continue;
|
||||
for (const arm of union.arms) {
|
||||
const key = `record:${arm.shapeId}`;
|
||||
if (this.tracedShapes.has(key)) continue;
|
||||
this.tracedShapes.add(key);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.shapeHost = {
|
||||
sizeType: this.sizeType,
|
||||
cycleBufOffset: this.cycleBufOffset,
|
||||
@@ -3689,6 +3754,7 @@ export class LlEmitter {
|
||||
return [
|
||||
...late,
|
||||
...emitNullableRetainWrappers(this.shapeHost),
|
||||
...emitShapeUnionHelpers(this.shapeHost),
|
||||
...emitInlineRcHelpers(this.shapeHost),
|
||||
];
|
||||
}
|
||||
@@ -5856,7 +5922,11 @@ export class LlEmitter {
|
||||
const nullable = this.nullableUnions.get(unionId);
|
||||
if (nullable) {
|
||||
// The owned payload IS the nullable union's value.
|
||||
if (tag !== nullable.refTag)
|
||||
if (
|
||||
nullable.repr === "single"
|
||||
? tag !== nullable.refTag
|
||||
: !nullable.arms.some((arm) => arm.tag === tag)
|
||||
)
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: payload wrap into unit arm ${tag} of ${unionId}`,
|
||||
);
|
||||
@@ -5961,7 +6031,7 @@ export class LlEmitter {
|
||||
const index = shape.fields.findIndex((entry) => entry.name === field);
|
||||
if (index < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: missing union field ${field}`);
|
||||
layout = "record:";
|
||||
layout = this.nullableUnions.fieldBase(arm.shapeId) === 2 ? "record:tagged;" : "record:";
|
||||
for (let i = 0; i <= index; i++) {
|
||||
const type = shape.fields[i]!.type;
|
||||
layout += `${llFieldType(type, this.inlineUnions.record(type))};`;
|
||||
@@ -5997,7 +6067,7 @@ export class LlEmitter {
|
||||
);
|
||||
const p = this.B.tmp();
|
||||
this.B.line(
|
||||
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + 1}`,
|
||||
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${idx + this.nullableUnions.fieldBase(shapeId)}`,
|
||||
);
|
||||
const type = shape.fields[idx]!.type;
|
||||
return { ptr: p, type, inline: this.inlineUnions.record(type) };
|
||||
@@ -6011,7 +6081,7 @@ export class LlEmitter {
|
||||
const p = this.B.tmp();
|
||||
const v = this.B.tmp();
|
||||
this.B.line(
|
||||
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
`${p} = getelementptr inbounds %${mangleRecordStruct(shapeId)}, ptr ${objName}, i64 0, i32 ${shape.fields.length + this.nullableUnions.fieldBase(shapeId)}`,
|
||||
);
|
||||
this.B.line(`${v} = load ptr, ptr ${p}`);
|
||||
return v;
|
||||
@@ -6674,6 +6744,7 @@ export class LlEmitter {
|
||||
const initializerBindings = this.initializerBindings.get(fn.name) ?? [];
|
||||
const numericFn = withInitializerBindings(fn, initializerBindings);
|
||||
this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
|
||||
this.unaliasedArrays = findUnaliasedLocalArrays(fn);
|
||||
this.borrowedParameters.clear();
|
||||
this.referenceWrites?.enter(fn);
|
||||
this.reboundOwnerSlots.clear();
|
||||
@@ -6712,6 +6783,12 @@ export class LlEmitter {
|
||||
this.finalFieldBorrows =
|
||||
!fn.async && !fn.generator && constructorClass(fn.name) === null && this.finalFields.size > 0;
|
||||
this.byteWindowsEnabled = this.countedLoopsEnabled;
|
||||
this.denseLoopsEnabled = this.countedLoopsEnabled;
|
||||
this.denseLoop = null;
|
||||
this.denseLoopEntries =
|
||||
this.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
: new Map();
|
||||
this.byteWindowEntries =
|
||||
this.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
@@ -6749,6 +6826,15 @@ export class LlEmitter {
|
||||
);
|
||||
this.scalarStringSlices =
|
||||
this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
|
||||
this.scratchStringSlices =
|
||||
this.debug === null
|
||||
? findScratchStringSlices(
|
||||
fn,
|
||||
this.callLifetimes,
|
||||
this.scalarStringSlices,
|
||||
this.lentStringParameters,
|
||||
)
|
||||
: new Set();
|
||||
this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
|
||||
this.readNumberMapWrites = findReadNumberMapWrites(fn);
|
||||
this.mapEntryFusion = this.debug === null ? findMapEntryFusions(fn) : null;
|
||||
@@ -8184,7 +8270,16 @@ export class LlEmitter {
|
||||
const acc = elemAccess(s.arr.type.elem);
|
||||
this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
|
||||
if (acc === "ref") this.moveTemp(v);
|
||||
emitDenseArraySet(this, arr.name, idx, s.index, acc, s.arr.type.elem, v.name);
|
||||
emitDenseArraySet(
|
||||
this,
|
||||
arr.name,
|
||||
idx,
|
||||
s.index,
|
||||
acc,
|
||||
s.arr.type.elem,
|
||||
v.name,
|
||||
this.provenDenseIndex(s.arr, s.index),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "arraySetLength": {
|
||||
@@ -8631,7 +8726,12 @@ export class LlEmitter {
|
||||
(this.debug === null ? matchIntegerBytesForLoop(s, this.numericLocals) : null);
|
||||
const countedLoop =
|
||||
!integerLoop && this.countedLoopsEnabled
|
||||
? matchIntegerCountedForLoop(s, this.numericLocals, this.integerRanges)
|
||||
? matchIntegerCountedForLoop(
|
||||
s,
|
||||
this.numericLocals,
|
||||
this.integerRanges,
|
||||
this.unaliasedArrays,
|
||||
)
|
||||
: null;
|
||||
const window =
|
||||
countedLoop && this.byteWindowsEnabled
|
||||
@@ -8644,6 +8744,49 @@ export class LlEmitter {
|
||||
this.byteWindowEntries.get(s) ?? new Set(),
|
||||
)
|
||||
: null;
|
||||
const denseShape = !this.denseLoopsEnabled
|
||||
? null
|
||||
: integerArrayLoop
|
||||
? {
|
||||
counter: integerArrayLoop.localId,
|
||||
length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" },
|
||||
range: { min: 0, max: 4294967294 },
|
||||
}
|
||||
: countedLoop && !countedLoop.guarded
|
||||
? countedDenseShape(s, countedLoop)
|
||||
: null;
|
||||
const dense =
|
||||
denseShape && !window
|
||||
? matchDenseArrayLoop(
|
||||
s,
|
||||
denseShape,
|
||||
this.numericLocals,
|
||||
this.unaliasedArrays,
|
||||
this.denseLoopEntries.get(s) ?? new Set(),
|
||||
)
|
||||
: null;
|
||||
if (dense) {
|
||||
const valid = emitDenseLoopGuard(this, dense);
|
||||
const fast = B.newLabel("dense.loop.fast"),
|
||||
slow = B.newLabel("dense.loop.slow"),
|
||||
done = B.newLabel("dense.loop.done");
|
||||
B.condBr(valid, fast, slow);
|
||||
this.denseLoopsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
this.emitStmt(s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
this.denseLoop = dense;
|
||||
this.emitStmt(s);
|
||||
B.br(done);
|
||||
} finally {
|
||||
this.denseLoop = null;
|
||||
this.denseLoopsEnabled = true;
|
||||
}
|
||||
B.startBlock(done);
|
||||
break;
|
||||
}
|
||||
if (window) {
|
||||
const valid = emitByteWindowGuard(this, window);
|
||||
const fast = B.newLabel("bytes.window.fast"),
|
||||
@@ -8746,7 +8889,10 @@ export class LlEmitter {
|
||||
this.integerLoopBindings.set(integerLoop.localId, {
|
||||
slot: integerSlot,
|
||||
type: this.sizeType,
|
||||
range: { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 },
|
||||
range:
|
||||
this.denseLoop?.counter === integerLoop.localId
|
||||
? this.denseLoop.range
|
||||
: { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 },
|
||||
});
|
||||
} else if (countedLoop) {
|
||||
integerSlot = B.slot();
|
||||
@@ -8761,7 +8907,10 @@ export class LlEmitter {
|
||||
slot: integerSlot,
|
||||
type: "i64",
|
||||
signed: true,
|
||||
range: countedLoop.range,
|
||||
range:
|
||||
this.denseLoop?.counter === countedLoop.localId
|
||||
? this.denseLoop.range
|
||||
: countedLoop.range,
|
||||
});
|
||||
countedLimit = emitCountedLoopLimit(this, countedLoop);
|
||||
} else if (s.init) {
|
||||
@@ -10104,7 +10253,12 @@ export class LlEmitter {
|
||||
unionsById: this.unionsById,
|
||||
recordsById: this.recordsById,
|
||||
inlineUnions: this.inlineUnions,
|
||||
nullableArm: (t) => this.nullableUnions.of(t)?.arm ?? null,
|
||||
nullableArm: (t) => {
|
||||
const nullable = this.nullableUnions.of(t);
|
||||
return nullable?.repr === "single" ? nullable.arm : null;
|
||||
},
|
||||
shapeUnion: (t) => this.nullableUnions.of(t)?.repr === "shapes",
|
||||
fieldBase: (shapeId) => this.nullableUnions.fieldBase(shapeId),
|
||||
classInfo: (className) => {
|
||||
const meta = this.classMetaOf(className);
|
||||
return {
|
||||
@@ -10356,11 +10510,16 @@ export class LlEmitter {
|
||||
if (canStackUnion(e, this.boxedUnionsById)) return emitStackUnion(this, e).value;
|
||||
if (e.kind === "unionNarrow") {
|
||||
const union = this.emitUnionProjection(e.value);
|
||||
emitUnionNarrowGuard(this, union.name, e.unionId, e.tag);
|
||||
return { name: this.unionPeek(union.name, e.unionId), type: e.type };
|
||||
}
|
||||
if ((e.kind === "downcast" || e.kind === "upcast") && e.value.type.kind === "object") {
|
||||
// Prefix layouts: a class cast reads the operand's own pointer.
|
||||
return { name: this.emitReadReceiver(e.value).name, type: e.type };
|
||||
// Prefix layouts: a class cast reads the operand's own pointer; a
|
||||
// downcast verifies its target class (narrow-guards.ts).
|
||||
const receiver = this.emitReadReceiver(e.value);
|
||||
if (e.kind === "downcast" && e.type.kind === "object")
|
||||
emitDowncastGuard(this, receiver.name, e.value.type.className, e.type.className);
|
||||
return { name: receiver.name, type: e.type };
|
||||
}
|
||||
// A nullable-pointer field has no box to borrow. Projections use
|
||||
// emitUnionProjection; other borrowers get an owned heap box.
|
||||
@@ -10820,7 +10979,11 @@ export class LlEmitter {
|
||||
const nullable = this.nullableUnions.get(e.unionId);
|
||||
if (!nullable) return null;
|
||||
if (isUnitType(e.value.type)) return e.value.kind === "unitLit" ? "unit" : null;
|
||||
return e.tag === nullable.refTag && isRefCounted(e.value.type) ? "ref" : null;
|
||||
const refArm =
|
||||
nullable.repr === "single"
|
||||
? e.tag === nullable.refTag
|
||||
: nullable.arms.some((arm) => arm.tag === e.tag);
|
||||
return refArm && isRefCounted(e.value.type) ? "ref" : null;
|
||||
}
|
||||
|
||||
materializeSplitLocal(localId: string): void {
|
||||
@@ -11105,6 +11268,7 @@ export class LlEmitter {
|
||||
resultType: IrType,
|
||||
overflowOnly: boolean,
|
||||
loc?: SrcLoc,
|
||||
missNaN = false,
|
||||
): void {
|
||||
return keyedRecordReadInto(
|
||||
this,
|
||||
@@ -11116,6 +11280,7 @@ export class LlEmitter {
|
||||
resultType,
|
||||
overflowOnly,
|
||||
loc,
|
||||
missNaN,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,7 @@ export function genResultThunkFor(
|
||||
sym = mangleGenResThunk(host.resolveThunks.size);
|
||||
host.resolveThunks.set(key, sym);
|
||||
const shape = host.recordsById.get(recT.shapeId);
|
||||
const base = host.nullableUnions.fieldBase(recT.shapeId);
|
||||
const valueT = shape?.fields.find((f) => f.name === "value")?.type;
|
||||
if (!shape || !valueT)
|
||||
throw new InternalCompilerError("llvm emitter bug: genResume record lacks its value field");
|
||||
@@ -242,7 +243,7 @@ export function genResultThunkFor(
|
||||
` %r = call ptr @${mangleRecordNew(recT.shapeId)}()`,
|
||||
` %d = call zeroext i1 @scr_gen_done(ptr %g)`,
|
||||
` %dz = zext i1 %d to i8`,
|
||||
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdxD + 1}`,
|
||||
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdxD + base}`,
|
||||
` store i8 %dz, ptr %dp`,
|
||||
` %has = call zeroext i1 @scr_gen_out_has(ptr %g)`,
|
||||
` br i1 %has, label %take, label %undefv`,
|
||||
@@ -259,7 +260,7 @@ export function genResultThunkFor(
|
||||
` br label %join`,
|
||||
`join:`,
|
||||
` %v = load ptr, ptr %vslot`,
|
||||
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdxD + 1}`,
|
||||
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdxD + base}`,
|
||||
` store ptr %v, ptr %vp`,
|
||||
` ret ptr %r`,
|
||||
`}`,
|
||||
@@ -293,7 +294,7 @@ export function genResultThunkFor(
|
||||
` %r = call ptr @${mangleRecordNew(recT.shapeId)}()`,
|
||||
` %d = call zeroext i1 @scr_gen_done(ptr %g)`,
|
||||
` %dz = zext i1 %d to i8`,
|
||||
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdx + 1}`,
|
||||
` %dp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${doneIdx + base}`,
|
||||
` store i8 %dz, ptr %dp`,
|
||||
` br i1 %d, label %doneb, label %susp`,
|
||||
];
|
||||
@@ -410,7 +411,7 @@ export function genResultThunkFor(
|
||||
` br label %join`,
|
||||
`join:`,
|
||||
` %v = load ptr, ptr %vslot`,
|
||||
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdx + 1}`,
|
||||
` %vp = getelementptr inbounds %${mangleRecordStruct(recT.shapeId)}, ptr %r, i64 0, i32 ${valueIdx + base}`,
|
||||
// An inline value field copies the box's bits and drops the box.
|
||||
...(host.inlineUnions.record(valueT)
|
||||
? [
|
||||
@@ -1092,7 +1093,8 @@ export function settledStoreFor(
|
||||
return { tag, arm };
|
||||
};
|
||||
const fieldIndex = (arm: IrType & { kind: "record" }, field: string): number =>
|
||||
host.recordsById.get(arm.shapeId)!.fields.findIndex((f) => f.name === field) + 1;
|
||||
host.recordsById.get(arm.shapeId)!.fields.findIndex((f) => f.name === field) +
|
||||
host.nullableUnions.fieldBase(arm.shapeId);
|
||||
const fulfilled = armOf("value");
|
||||
const rejected = armOf("reason");
|
||||
const valueT = host.recordsById
|
||||
|
||||
@@ -32,7 +32,6 @@ import {
|
||||
mangleVtStruct,
|
||||
} from "../mangle.js";
|
||||
import {
|
||||
classMembershipIntervals,
|
||||
classEnvironmentIndex,
|
||||
classStructSym,
|
||||
vtEntriesFor,
|
||||
@@ -52,6 +51,7 @@ import {
|
||||
inputSuffixWrites,
|
||||
} from "./borrowed-inputs.js";
|
||||
import type { Writes } from "./reference-writes.js";
|
||||
import { emitClassMembership, emitDowncastGuard } from "./narrow-guards.js";
|
||||
|
||||
export function emitCallExpr(
|
||||
host: LlvmEmitterContext,
|
||||
@@ -1129,8 +1129,11 @@ export function emitCallExpr(
|
||||
// Prefix layout: both directions are reinterprets of the SAME
|
||||
// pointer — no RC traffic, ownership transfers from the operand
|
||||
// temp to the result temp (struck so the one +1 releases exactly
|
||||
// once, under the RESULT type's release).
|
||||
// once, under the RESULT type's release). A downcast verifies the
|
||||
// class tsc's narrowing proved (narrow-guards.ts).
|
||||
const v = host.emitExpr(e.value);
|
||||
if (e.kind === "downcast" && e.type.kind === "object" && v.type.kind === "object")
|
||||
emitDowncastGuard(host, v.name, v.type.className, e.type.className);
|
||||
if (isRefCounted(v.type)) host.moveTemp(v);
|
||||
return host.own({ name: v.name, type: e.type });
|
||||
}
|
||||
@@ -1142,24 +1145,14 @@ export function emitCallExpr(
|
||||
// The test only reads the vtable word: a borrowable operand needs no
|
||||
// retain/release pair around it.
|
||||
const v = emitBorrowedInputs(host, [e.value])[0]!;
|
||||
const target = host.classMetaOf(e.className);
|
||||
const pre = host.loadVtPre(v.name, e.value.type.className);
|
||||
let result: string | undefined;
|
||||
for (const interval of classMembershipIntervals(host.classMeta, target.def.name)) {
|
||||
const ge = B.tmp(),
|
||||
le = B.tmp(),
|
||||
test = B.tmp();
|
||||
B.line(`${ge} = icmp uge ${host.sizeType} ${pre}, ${interval.pre}`);
|
||||
B.line(`${le} = icmp ule ${host.sizeType} ${pre}, ${interval.post}`);
|
||||
B.line(`${test} = and i1 ${ge}, ${le} ; instanceof ${e.className}`);
|
||||
if (result === undefined) result = test;
|
||||
else {
|
||||
const combined = B.tmp();
|
||||
B.line(`${combined} = or i1 ${result}, ${test}`);
|
||||
result = combined;
|
||||
}
|
||||
}
|
||||
return { name: result!, type: e.type };
|
||||
const result = emitClassMembership(
|
||||
host,
|
||||
v.name,
|
||||
e.value.type.className,
|
||||
e.className,
|
||||
`instanceof ${e.className}`,
|
||||
);
|
||||
return { name: result, type: e.type };
|
||||
}
|
||||
case "virtualCall": {
|
||||
// Dispatch through the receiver's vtable: the slot lives on the
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
} from "./map-number-lookup.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js";
|
||||
import { emitStringSliceRead } from "./string-slices.js";
|
||||
import { emitScratchStringSlice, emitStringSliceRead } from "./string-slices.js";
|
||||
import {
|
||||
STRING_CHAR_CODE_AT_DECL,
|
||||
STRING_CHAR_CODE_AT_INT_MARKER,
|
||||
@@ -395,6 +395,7 @@ export function emitStrIntrinsic(
|
||||
false,
|
||||
);
|
||||
case "slice":
|
||||
if (host.scratchStringSlices.has(e)) return emitScratchStringSlice(host, e, r, args);
|
||||
return call(
|
||||
"scr_str_slice",
|
||||
"ptr (ptr, double, double)",
|
||||
@@ -403,6 +404,7 @@ export function emitStrIntrinsic(
|
||||
true,
|
||||
);
|
||||
case "substring":
|
||||
if (host.scratchStringSlices.has(e)) return emitScratchStringSlice(host, e, r, args);
|
||||
return call(
|
||||
"scr_str_substring",
|
||||
"ptr (ptr, double, double)",
|
||||
@@ -575,7 +577,16 @@ export function emitArrIntrinsic(
|
||||
? host.constantNumericTables.get(e.receiver.localId)
|
||||
: undefined;
|
||||
if (!table && elem.kind === "f64")
|
||||
return { name: emitDenseArrayNumber(host, r.name, index, e.args[0]), type: e.type };
|
||||
return {
|
||||
name: emitDenseArrayNumber(
|
||||
host,
|
||||
r.name,
|
||||
index,
|
||||
e.args[0],
|
||||
host.provenDenseIndex(e.receiver, e.args[0]!),
|
||||
),
|
||||
type: e.type,
|
||||
};
|
||||
host.declare(`declare double @scr_arr_get_number(ptr, double)`);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
|
||||
@@ -330,7 +330,7 @@ export function emitControlExpr(
|
||||
);
|
||||
B.line(`store ${ty} ${t}, ptr ${slot}`);
|
||||
if (host.mayThrow.has(e.retag)) host.emitPendingCheck();
|
||||
} else if (host.nullableUnions.has(e.left.type.unionId)) {
|
||||
} else if (host.nullableUnions.single(e.left.type.unionId)) {
|
||||
// The owned nullable pointer IS the owned arm value.
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
} else {
|
||||
@@ -440,7 +440,7 @@ export function emitControlExpr(
|
||||
host.emitBranchInto(slot, e.right);
|
||||
B.br(lj);
|
||||
B.startBlock(lv);
|
||||
if (typeEquals(e.type, e.left.type) || host.nullableUnions.has(def.id)) {
|
||||
if (typeEquals(e.type, e.left.type) || host.nullableUnions.single(def.id)) {
|
||||
// Pass-through, or a nullable pointer that IS its owned arm value.
|
||||
B.line(`store ${ty} ${l.name}, ptr ${slot}`);
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { classMembershipIntervals } from "./classes.js";
|
||||
import {
|
||||
emitCaughtInstanceof,
|
||||
emitCaughtNarrowGuard,
|
||||
emitUnionNarrowGuard,
|
||||
} from "./narrow-guards.js";
|
||||
import { borrowableInputs, emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import { preservesDynTest } from "./checked-value-lifetimes.js";
|
||||
import { typedRefConstructor } from "./shapes.js";
|
||||
@@ -295,13 +300,14 @@ export function emitDynamicExpr(
|
||||
return host.own({ name: host.unionNewOwned(e.unionId, e.tag, v), type: e.type });
|
||||
}
|
||||
case "unionNarrow": {
|
||||
// Tag-UNCHECKED payload extraction: the frontend emits this only
|
||||
// where tsc's control-flow narrowing proved the tag. Ref payloads
|
||||
// come out +1. The receiver is consumed before any later expression.
|
||||
// Payload extraction where tsc's control-flow narrowing proved the
|
||||
// tag; the guard verifies it (narrow-guards.ts). Ref payloads come
|
||||
// out +1. The receiver is consumed before any later expression.
|
||||
const u = host.emitUnionProjection(e.value);
|
||||
const arm = e.type;
|
||||
if (isUnitType(arm))
|
||||
throw new InternalCompilerError(`llvm emitter bug: unionNarrow to unit arm ${arm.kind}`);
|
||||
emitUnionNarrowGuard(host, u.name, e.unionId, e.tag);
|
||||
const v = host.unionExtract(u.name, e.unionId, arm);
|
||||
return host.own({ name: v, type: arm });
|
||||
}
|
||||
@@ -974,56 +980,7 @@ export function emitDynamicExpr(
|
||||
// borrowed. SCR_EXC_STR = 3, SCR_EXC_F64 = 1, SCR_EXC_BOOL = 2.
|
||||
const c = host.emitReadReceiver(e.value);
|
||||
if (e.test === "instanceof") {
|
||||
const target = host.classMetaOf(e.className!);
|
||||
if (!target.hierarchy) {
|
||||
// Standalone class payloads have no vtable word. Their retain
|
||||
// adapter identifies the exact native layout in the snapshot.
|
||||
const adapterSlot = B.tmp();
|
||||
const adapter = B.tmp();
|
||||
B.line(`${adapterSlot} = getelementptr inbounds %ScrCaught, ptr ${c.name}, i64 0, i32 5`);
|
||||
B.line(`${adapter} = load ptr, ptr ${adapterSlot}`);
|
||||
let result: string | undefined;
|
||||
const intervals = classMembershipIntervals(host.classMeta, target.def.name);
|
||||
for (const candidate of host.classMeta.values()) {
|
||||
if (
|
||||
candidate.hierarchy ||
|
||||
!intervals.some((range) => range.pre <= candidate.pre && candidate.pre <= range.post)
|
||||
)
|
||||
continue;
|
||||
const rc = vAdapters(host.shapeHost, { kind: "object", className: candidate.def.name });
|
||||
const matches = B.tmp();
|
||||
B.line(`${matches} = icmp eq ptr ${adapter}, ${rc.retain}`);
|
||||
if (result === undefined) result = matches;
|
||||
else {
|
||||
const joined = B.tmp();
|
||||
B.line(`${joined} = or i1 ${result}, ${matches}`);
|
||||
result = joined;
|
||||
}
|
||||
}
|
||||
if (result === undefined)
|
||||
throw new InternalCompilerError("empty standalone class membership");
|
||||
if (e.negated !== true) return { name: result, type: e.type };
|
||||
const negated = B.tmp();
|
||||
B.line(`${negated} = xor i1 ${result}, true`);
|
||||
return { name: negated, type: e.type };
|
||||
}
|
||||
host.declare(
|
||||
`declare zeroext i1 @scr_caught_instanceof(ptr, ${host.sizeType}, ${host.sizeType})`,
|
||||
);
|
||||
let t: string | undefined;
|
||||
for (const interval of classMembershipIntervals(host.classMeta, target.def.name)) {
|
||||
const test = B.tmp();
|
||||
B.line(
|
||||
`${test} = call zeroext i1 @scr_caught_instanceof(ptr ${c.name}, ${host.sizeType} ${interval.pre}, ${host.sizeType} ${interval.post})`,
|
||||
);
|
||||
if (t === undefined) t = test;
|
||||
else {
|
||||
const combined = B.tmp();
|
||||
B.line(`${combined} = or i1 ${t}, ${test}`);
|
||||
t = combined;
|
||||
}
|
||||
}
|
||||
if (t === undefined) throw new InternalCompilerError("empty class membership");
|
||||
const t = emitCaughtInstanceof(host, c.name, e.className!);
|
||||
if (e.negated !== true) return { name: t, type: e.type };
|
||||
const n = B.tmp();
|
||||
B.line(`${n} = xor i1 ${t}, true`);
|
||||
@@ -1097,10 +1054,18 @@ export function emitDynamicExpr(
|
||||
return out;
|
||||
}
|
||||
case "caughtNarrow": {
|
||||
// Checker-trusted extraction (the matching caughtTest was proven
|
||||
// by tsc's narrowing): scalars read the snapshot's slots,
|
||||
// refcounted payloads come out retained (+1). Box borrowed.
|
||||
// Extraction where tsc's narrowing proved the matching caughtTest;
|
||||
// the guard verifies it (narrow-guards.ts). Scalars read the
|
||||
// snapshot's slots, refcounted payloads come out retained (+1). Box
|
||||
// borrowed.
|
||||
const c = host.emitReadReceiver(e.value);
|
||||
if (
|
||||
e.type.kind === "f64" ||
|
||||
e.type.kind === "bool" ||
|
||||
e.type.kind === "string" ||
|
||||
e.type.kind === "object"
|
||||
)
|
||||
emitCaughtNarrowGuard(host, c.name, e.type);
|
||||
if (e.type.kind === "f64") {
|
||||
const p = B.tmp();
|
||||
const v = B.tmp();
|
||||
|
||||
@@ -64,16 +64,25 @@ export function emitLiteralExpr(
|
||||
const integer = B.tmp();
|
||||
const number = B.tmp();
|
||||
B.line(`${integer} = load ${integerSlot.type}, ptr ${integerSlot.slot}`);
|
||||
// A 64-bit induction value is a safe integer, so its sign bit is
|
||||
// clear and the signed conversion is exact: one cvtsi2sd instead of
|
||||
// x86-64's multi-instruction unsigned 64-bit conversion.
|
||||
const signedConversion = integerSlot.signed === true || integerSlot.type === "i64";
|
||||
B.line(
|
||||
`${number} = ${signedConversion ? "sitofp" : "uitofp"} ${integerSlot.type} ${integer} to double`,
|
||||
);
|
||||
const uint32 = integerSlot.type === "i32" ? integer : B.tmp();
|
||||
if (integerSlot.type !== "i32")
|
||||
B.line(`${uint32} = trunc ${integerSlot.type} ${integer} to i32`);
|
||||
// An int32 counter (a dense array loop's) converts from its low half,
|
||||
// so LLVM folds a later `fptosi` of the double (an inlined callee's
|
||||
// index) back to the integer. Otherwise a 64-bit induction value is
|
||||
// a safe integer, so its sign bit is clear and the signed conversion
|
||||
// is exact: one cvtsi2sd instead of x86-64's multi-instruction
|
||||
// unsigned 64-bit conversion. (A uint32 counter would fold through
|
||||
// `uitofp i32` too, but the zero extension it adds on x86-64 cost
|
||||
// more in loops that never convert the double than it saved.)
|
||||
const range = integerSlot.range;
|
||||
const signedConversion = integerSlot.signed === true || integerSlot.type === "i64";
|
||||
if (range && range.min >= -2147483648 && range.max <= 2147483647)
|
||||
B.line(`${number} = sitofp i32 ${uint32} to double`);
|
||||
else
|
||||
B.line(
|
||||
`${number} = ${signedConversion ? "sitofp" : "uitofp"} ${integerSlot.type} ${integer} to double`,
|
||||
);
|
||||
return {
|
||||
name: number,
|
||||
type: e.type,
|
||||
@@ -851,6 +860,7 @@ export function emitContainerExpr(
|
||||
elem,
|
||||
`scr_arr_get_${acc}`,
|
||||
true,
|
||||
host.provenDenseIndex(e.arr, e.index),
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
@@ -62,9 +62,17 @@ export function emitUnionRetype(
|
||||
const from = host.nullableUnions.get(e.fromId);
|
||||
const to = host.nullableUnions.get(e.unionId);
|
||||
if (from && to) {
|
||||
// Equal encodings make the pointer itself the target value.
|
||||
if (from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag)
|
||||
return host.own({ name: value.name, type: e.type });
|
||||
// Equal encodings make the pointer itself the target value. A shape
|
||||
// union derives its record arms from the record, so only the unit
|
||||
// encodings must agree.
|
||||
const same =
|
||||
from.repr === "single" && to.repr === "single"
|
||||
? from.refTag === to.refTag && from.unitTag === to.unitTag && from.nullTag === to.nullTag
|
||||
: from.repr === "shapes" &&
|
||||
to.repr === "shapes" &&
|
||||
from.unitTag === to.unitTag &&
|
||||
from.nullTag === to.nullTag;
|
||||
if (same) return host.own({ name: value.name, type: e.type });
|
||||
return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
|
||||
}
|
||||
if (from || to) return host.own({ name: rebuildRetype(host, value.name, e), type: e.type });
|
||||
@@ -339,6 +347,7 @@ export function emitRecordKeyGet(
|
||||
e.type,
|
||||
e.overflowOnly === true,
|
||||
e.loc,
|
||||
e.missNaN === true,
|
||||
);
|
||||
B.startBlock(join);
|
||||
const t = B.tmp();
|
||||
@@ -356,6 +365,7 @@ export function keyedRecordReadInto(
|
||||
resultType: IrType,
|
||||
overflowOnly: boolean,
|
||||
loc?: SrcLoc,
|
||||
missNaN = false,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const shape = host.recordShape(shapeId);
|
||||
@@ -563,6 +573,12 @@ export function keyedRecordReadInto(
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
if (missNaN && resultType.kind === "f64") {
|
||||
// A NaN-coded read: every consumer treats undefined like NaN.
|
||||
B.line(`store double ${f64Lit(NaN)}, ptr ${slot}`);
|
||||
B.br(join);
|
||||
return;
|
||||
}
|
||||
// A direct result cannot represent a miss. This is unreachable for
|
||||
// programs whose behavior matches Node, but preserve the missing-key trap.
|
||||
host.needsBadKey = true;
|
||||
|
||||
@@ -500,16 +500,17 @@ export function streamTypedRefCommitAdapter(
|
||||
` br i1 %missing, label %done, label %swap`,
|
||||
`swap:`,
|
||||
];
|
||||
const base = host.nullableUnions.fieldBase(t.shapeId);
|
||||
const members = [
|
||||
...shape.fields.map((field, index) => ({
|
||||
index: index + 1,
|
||||
index: index + base,
|
||||
type: llFieldType(field.type, host.inlineUnions.record(field.type)),
|
||||
name: field.name,
|
||||
})),
|
||||
...(shape.indexValue
|
||||
? [
|
||||
{
|
||||
index: shape.fields.length + 1,
|
||||
index: shape.fields.length + base,
|
||||
type: "ptr" as const,
|
||||
name: "[key: string] overflow",
|
||||
},
|
||||
@@ -943,7 +944,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
B.line(`${out} = call ptr @scr_dyn_new_arr()`);
|
||||
const fields = [...shape.fields].sort((a, b) => Number(a.name) - Number(b.name));
|
||||
for (const field of fields) {
|
||||
const index = shape.fields.indexOf(field) + 1;
|
||||
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldPtr = B.tmp();
|
||||
B.line(
|
||||
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
|
||||
@@ -980,7 +981,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
liveRecordOps(host, t, snapshot, fields);
|
||||
}
|
||||
for (const field of fields) {
|
||||
const index = shape.fields.indexOf(field) + 1;
|
||||
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldPtr = B.tmp();
|
||||
B.line(
|
||||
`${fieldPtr} = getelementptr inbounds %${mangleRecordStruct(t.shapeId)}, ptr %p, i64 0, i32 ${index}`,
|
||||
@@ -1202,7 +1203,7 @@ function liveRecordOps(
|
||||
const names = `${snapshot}_names`;
|
||||
const ops = `${snapshot}_rops`;
|
||||
const loadField = (B: BlockBuilder, field: IrRecordShape["fields"][number]): string => {
|
||||
const index = shape.fields.indexOf(field) + 1;
|
||||
const index = shape.fields.indexOf(field) + host.nullableUnions.fieldBase(t.shapeId);
|
||||
const fieldPtr = B.tmp();
|
||||
B.line(`${fieldPtr} = getelementptr inbounds %${struct}, ptr %p, i64 0, i32 ${index}`);
|
||||
return loadStructField(
|
||||
|
||||
@@ -29,7 +29,7 @@ function messageAdapter(host: LlvmEmitterContext, type: IrType): string {
|
||||
const isString = field === "address" || field === "family";
|
||||
if (isString) lines.push(` %${field}.owned = call ptr @scr_str_retain_v(ptr %${field})`);
|
||||
lines.push(
|
||||
` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + 1}`,
|
||||
` %${field}.slot = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr %record, i64 0, i32 ${index + host.nullableUnions.fieldBase(type.shapeId)}`,
|
||||
` store ${isString ? "ptr" : "double"} %${field}${isString ? ".owned" : ""}, ptr %${field}.slot`,
|
||||
);
|
||||
}
|
||||
@@ -121,6 +121,8 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
"dgram.getSendQueueSize": "scr_dgram_get_send_queue_size",
|
||||
"dgram.getSendQueueCount": "scr_dgram_get_send_queue_count",
|
||||
"dgram.disconnect": "scr_dgram_disconnect",
|
||||
"dns.lookupPromise": "scr_dns_lookup_promise",
|
||||
"dns.lookupServicePromise": "scr_dns_lookup_service_promise",
|
||||
};
|
||||
const symbol = direct[e.fn];
|
||||
if (symbol !== undefined) return finishRuntimeCall(host, e, symbol, a);
|
||||
@@ -167,7 +169,7 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
);
|
||||
const slot = host.B.tmp();
|
||||
host.B.line(
|
||||
`${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + 1}`,
|
||||
`${slot} = getelementptr inbounds %${mangleRecordStruct(type.shapeId)}, ptr ${record}, i64 0, i32 ${index + host.nullableUnions.fieldBase(type.shapeId)}`,
|
||||
);
|
||||
host.B.line(`store ${ty} ${value}, ptr ${slot}`);
|
||||
}
|
||||
@@ -234,10 +236,10 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
return finishRuntimeCall(host, e, "scr_dns_lookup_dyn", a);
|
||||
case "dns.lookupService":
|
||||
return finishRuntimeCall(host, e, "scr_dns_lookup_service", a);
|
||||
case "dns.lookupPromise":
|
||||
return finishRuntimeCall(host, e, "scr_dns_lookup_promise", a);
|
||||
case "dns.lookupServicePromise":
|
||||
return finishRuntimeCall(host, e, "scr_dns_lookup_service_promise", a);
|
||||
case "dns.promisesLookup":
|
||||
return finishRuntimeCall(host, e, "scr_dns_promises_lookup", a);
|
||||
case "dns.promisesLookupService":
|
||||
return finishRuntimeCall(host, e, "scr_dns_promises_lookup_service", a);
|
||||
case "dns.getServers":
|
||||
return finishRuntimeCall(host, e, "scr_dns_get_servers", a);
|
||||
case "dns.getDefaultResultOrder":
|
||||
|
||||
@@ -327,6 +327,94 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn.startsWith("crypto.") && e.fn.includes("Promise")) {
|
||||
// util.promisify over the callback forms: the callback form runs with
|
||||
// a runtime callback that owns the +1 promise and settles it in the
|
||||
// promise's value representation; an argument error the callback form
|
||||
// threw moves into the promise as its rejection.
|
||||
host.usesTimers = true;
|
||||
if (e.type.kind !== "promise")
|
||||
throw new InternalCompilerError("llvm emitter bug: crypto promise result not a promise");
|
||||
const inner = e.type.inner;
|
||||
const dyn = inner.kind === "dyn";
|
||||
const args = e.args.map((arg) => host.emitExpr(arg));
|
||||
const settle =
|
||||
e.fn === "crypto.randomIntPromise"
|
||||
? dyn
|
||||
? "scr_crypto_promise_settle_number_dyn"
|
||||
: "scr_crypto_promise_settle_number"
|
||||
: e.fn === "crypto.hkdfPromise"
|
||||
? "scr_crypto_promise_settle_array_buffer"
|
||||
: e.fn === "crypto.generateKeyPromise" || e.fn === "crypto.randomFillPromiseDyn"
|
||||
? "scr_crypto_promise_settle_dyn"
|
||||
: !dyn
|
||||
? "scr_crypto_promise_settle_bytes"
|
||||
: e.fn === "crypto.randomFillPromise"
|
||||
? "scr_crypto_promise_settle_view_dyn"
|
||||
: "scr_crypto_promise_settle_buffer_dyn";
|
||||
host.declare(`declare ptr @scr_promise_new()`);
|
||||
host.declare(`declare ptr @scr_crypto_promise_callback(ptr)`);
|
||||
host.declare(`declare void @scr_crypto_promise_reject_thrown(ptr)`);
|
||||
host.declare(
|
||||
e.fn === "crypto.randomIntPromise"
|
||||
? `declare void @${settle}(ptr, double)`
|
||||
: `declare void @${settle}(ptr, ptr, ptr)`,
|
||||
);
|
||||
const promise = B.tmp();
|
||||
const cb = B.tmp();
|
||||
B.line(`${promise} = call ptr @scr_promise_new()`);
|
||||
B.line(`${cb} = call ptr @scr_crypto_promise_callback(ptr ${promise})`);
|
||||
const a = (i: number, type = "ptr") => `${type} ${args[i]!.name}`;
|
||||
const call = (symbol: string, params: string[], values: string[]) => {
|
||||
host.declare(`declare void @${symbol}(${[...params, "ptr", "ptr"].join(", ")})`);
|
||||
B.line(`call void @${symbol}(${[...values, `ptr ${cb}`, `ptr @${settle}`].join(", ")})`);
|
||||
};
|
||||
if (e.fn === "crypto.randomBytesPromise") {
|
||||
call("scr_crypto_random_bytes_async", ["double"], [a(0, "double")]);
|
||||
} else if (e.fn === "crypto.pbkdf2Promise") {
|
||||
call(
|
||||
"scr_crypto_pbkdf2_async",
|
||||
["ptr", "ptr", "double", "double", "ptr"],
|
||||
[a(0), a(1), a(2, "double"), a(3, "double"), a(4)],
|
||||
);
|
||||
} else if (e.fn === "crypto.scryptPromise") {
|
||||
call(
|
||||
"scr_crypto_scrypt_async",
|
||||
["ptr", "ptr", "double", "ptr"],
|
||||
[a(0), a(1), a(2, "double"), a(3)],
|
||||
);
|
||||
} else if (e.fn === "crypto.hkdfPromise") {
|
||||
call(
|
||||
"scr_crypto_hkdf_async",
|
||||
["ptr", "ptr", "ptr", "ptr", "double"],
|
||||
[a(0), a(1), a(2), a(3), a(4, "double")],
|
||||
);
|
||||
} else if (e.fn === "crypto.randomFillPromise") {
|
||||
call(
|
||||
"scr_crypto_random_fill_async",
|
||||
["ptr", "double", "double", "double"],
|
||||
[a(0), a(1, "double"), a(2, "double"), a(3, "double")],
|
||||
);
|
||||
} else if (e.fn === "crypto.randomFillPromiseDyn") {
|
||||
call(
|
||||
"scr_crypto_random_fill_async_dyn",
|
||||
["ptr", "ptr", "ptr", "double"],
|
||||
[a(0), a(1), a(2), a(3, "double")],
|
||||
);
|
||||
} else if (e.fn === "crypto.randomIntPromise") {
|
||||
call(
|
||||
"scr_crypto_random_int_async",
|
||||
["double", "double", "double"],
|
||||
[a(0, "double"), a(1, "double"), a(2, "double")],
|
||||
);
|
||||
} else if (e.fn === "crypto.generateKeyPromise") {
|
||||
call("scr_crypto_generate_key_async", ["ptr", "ptr"], [a(0), a(1)]);
|
||||
} else {
|
||||
throw new InternalCompilerError(`llvm emitter bug: unknown crypto promise ${e.fn}`);
|
||||
}
|
||||
B.line(`call void @scr_crypto_promise_reject_thrown(ptr ${promise})`);
|
||||
return host.own({ name: promise, type: e.type });
|
||||
}
|
||||
if (
|
||||
e.fn === "crypto.randomBytesCb" ||
|
||||
e.fn === "crypto.pbkdf2Cb" ||
|
||||
|
||||
@@ -912,11 +912,8 @@ export function emitPathUrlLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
|
||||
scopeid.inline,
|
||||
);
|
||||
}
|
||||
const rc = vAdapters(host.shapeHost, t);
|
||||
const rowU = B.tmp();
|
||||
B.line(
|
||||
`${rowU} = call ptr @scr_union_new_ref(i32 ${tag}, ptr ${r}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${traceArg(host.shapeHost, t)})`,
|
||||
);
|
||||
// The row (+1) moves into its union arm (a shape union is the row).
|
||||
const rowU = host.unionNewOwned(infoT.unionId, tag, { name: r, type: t });
|
||||
B.line(`store ptr ${rowU}, ptr ${rowSlot}`);
|
||||
B.br(lRow);
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ export function emitLookupConnect(host: LlvmEmitterContext, e: LibCallExpr): LlV
|
||||
" br i1 %more, label %body, label %finish",
|
||||
"body:",
|
||||
" %record = call ptr @scr_arr_get_ref(ptr %addresses, double %index)",
|
||||
` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + 1}`,
|
||||
` %addressp = getelementptr inbounds %${struct}, ptr %record, i64 0, i32 ${field + host.nullableUnions.fieldBase(addresses.elem.shapeId)}`,
|
||||
" %address = load ptr, ptr %addressp",
|
||||
" %owned.address = call ptr @scr_str_retain_v(ptr %address)",
|
||||
" call double @scr_arr_push_ref(ptr %ips, ptr %owned.address)",
|
||||
|
||||
@@ -168,7 +168,7 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
|
||||
const i = shape.fields.findIndex((f) => f.name === name);
|
||||
if (i < 0)
|
||||
throw new InternalCompilerError(`llvm emitter bug: net.serverAddress record lacks ${name}`);
|
||||
return i + 1;
|
||||
return i + host.nullableUnions.fieldBase(recT.shapeId);
|
||||
};
|
||||
const store = (name: string, ty: string, v: string): void => {
|
||||
const p = B.tmp();
|
||||
|
||||
@@ -848,6 +848,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"fetch.abortAny": "scr_fetch_abort_any",
|
||||
"fetch.webIs": "scr_fetch_web_is",
|
||||
"fetch.streamIs": "scr_fetch_stream_is",
|
||||
"zlib.streamIs": "scr_zlib_stream_is",
|
||||
"fetch.streamNew": "scr_fetch_stream_new",
|
||||
"fetch.streamFrom": "scr_fetch_stream_from",
|
||||
"fetch.readerRead": "scr_fetch_reader_read",
|
||||
|
||||
@@ -45,6 +45,10 @@ export function emitTestLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlVal
|
||||
return finishRuntimeCall(host, e, "scr_test_ctx_name", a);
|
||||
case "test.ctxFullName":
|
||||
return finishRuntimeCall(host, e, "scr_test_ctx_full_name", a);
|
||||
case "test.ctxPassed":
|
||||
return finishRuntimeCall(host, e, "scr_test_ctx_passed", a);
|
||||
case "test.workerId":
|
||||
return finishRuntimeCall(host, e, "scr_test_worker_id", a);
|
||||
case "test.ctxHook":
|
||||
host.moveTemp(args[2]!);
|
||||
return finishRuntimeCall(host, e, "scr_test_ctx_hook", a);
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
/* Runtime guards for checker-proven narrowings.
|
||||
*
|
||||
* `unionNarrow`, `downcast` and `caughtNarrow` extract a payload whose arm,
|
||||
* class or kind TypeScript's control-flow narrowing proved. Emitted code
|
||||
* verifies that proof once at the narrowing site: the tag, class interval or
|
||||
* caught kind is compared and a mismatch stops the program through the
|
||||
* runtime trap funnel instead of reading the payload as the wrong type. The
|
||||
* frontend lowers every narrowing it knows it cannot prove through checked
|
||||
* extraction, which throws the catchable TypeError, so a failing guard means
|
||||
* either a compiler bug or a program whose hand-written type predicate or
|
||||
* assertion function (`x is T`, `asserts x is T`) lied about a value's type:
|
||||
* TypeScript trusts those claims without checking them. The guard aborts
|
||||
* rather than unwinding through code that assumed the extraction could not
|
||||
* throw. The guards are cheap: one load and compare per site, which LLVM
|
||||
* folds where an identical test already dominates. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { classMembershipIntervals } from "./classes.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { vAdapters } from "./shapes.js";
|
||||
|
||||
const NARROW_GUARD_HINT = " (a type predicate or assertion may be wrong)\n";
|
||||
|
||||
export const NARROW_GUARD_MESSAGES = {
|
||||
union: `scriptc: a narrowed value does not have the type its narrowing proved${NARROW_GUARD_HINT}`,
|
||||
downcast: `scriptc: a narrowed object is not an instance of the class its narrowing proved${NARROW_GUARD_HINT}`,
|
||||
caught: `scriptc: a narrowed catch binding does not have the type its narrowing proved${NARROW_GUARD_HINT}`,
|
||||
} as const;
|
||||
|
||||
/** Continues in a fresh block where `ok` holds; otherwise traps with the
|
||||
* message for `kind`. */
|
||||
export function emitNarrowGuard(
|
||||
host: LlvmEmitterContext,
|
||||
ok: string,
|
||||
kind: keyof typeof NARROW_GUARD_MESSAGES,
|
||||
): void {
|
||||
const B = host.B;
|
||||
host.declare(`declare i1 @llvm.expect.i1(i1, i1)`);
|
||||
host.declare(`declare void @scr_trap(ptr)`);
|
||||
const expected = B.tmp();
|
||||
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${ok}, i1 true)`);
|
||||
const pass = B.newLabel("narrow.ok");
|
||||
const fail = B.newLabel("narrow.bad");
|
||||
B.condBr(expected, pass, fail);
|
||||
B.startBlock(fail);
|
||||
B.line(`call void @scr_trap(ptr ${host.cstr(NARROW_GUARD_MESSAGES[kind])})`);
|
||||
B.terminate("unreachable");
|
||||
B.startBlock(pass);
|
||||
}
|
||||
|
||||
/** Verifies that the union value `unionName` holds arm `tag`. A two-arm
|
||||
* union holds it exactly when it does not hold the other arm; testing the
|
||||
* other arm lets LLVM fold the guard under a dominating `!== other` test as
|
||||
* well as under `=== tag` (a tag outside the arms is not a union value). */
|
||||
export function emitUnionNarrowGuard(
|
||||
host: LlvmEmitterContext,
|
||||
unionName: string,
|
||||
unionId: string,
|
||||
tag: number,
|
||||
): void {
|
||||
const ok = host.B.tmp();
|
||||
const actual = host.unionTag(unionName, unionId);
|
||||
if (host.unionsById.get(unionId)?.arms.length === 2)
|
||||
host.B.line(`${ok} = icmp ne i32 ${actual}, ${1 - tag}`);
|
||||
else host.B.line(`${ok} = icmp eq i32 ${actual}, ${tag}`);
|
||||
emitNarrowGuard(host, ok, "union");
|
||||
}
|
||||
|
||||
/** The i1 `instanceof` test of the borrowed object `objName`, statically a
|
||||
* `staticClassName`, against `targetClassName`'s preorder intervals. */
|
||||
export function emitClassMembership(
|
||||
host: LlvmEmitterContext,
|
||||
objName: string,
|
||||
staticClassName: string,
|
||||
targetClassName: string,
|
||||
comment: string,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const target = host.classMetaOf(targetClassName);
|
||||
const pre = host.loadVtPre(objName, staticClassName);
|
||||
let result: string | undefined;
|
||||
for (const interval of classMembershipIntervals(host.classMeta, target.def.name)) {
|
||||
const ge = B.tmp(),
|
||||
le = B.tmp(),
|
||||
test = B.tmp();
|
||||
B.line(`${ge} = icmp uge ${host.sizeType} ${pre}, ${interval.pre}`);
|
||||
B.line(`${le} = icmp ule ${host.sizeType} ${pre}, ${interval.post}`);
|
||||
B.line(`${test} = and i1 ${ge}, ${le} ; ${comment}`);
|
||||
if (result === undefined) result = test;
|
||||
else {
|
||||
const combined = B.tmp();
|
||||
B.line(`${combined} = or i1 ${result}, ${test}`);
|
||||
result = combined;
|
||||
}
|
||||
}
|
||||
return result!;
|
||||
}
|
||||
|
||||
/** Verifies that the borrowed object `objName` is a `targetClassName`. */
|
||||
export function emitDowncastGuard(
|
||||
host: LlvmEmitterContext,
|
||||
objName: string,
|
||||
staticClassName: string,
|
||||
targetClassName: string,
|
||||
): void {
|
||||
const ok = emitClassMembership(
|
||||
host,
|
||||
objName,
|
||||
staticClassName,
|
||||
targetClassName,
|
||||
`downcast ${targetClassName}`,
|
||||
);
|
||||
emitNarrowGuard(host, ok, "downcast");
|
||||
}
|
||||
|
||||
/** The i1 `e instanceof C` test of the borrowed catch snapshot `caught`.
|
||||
* Hierarchy classes compare the OBJ payload's vtable preorder; standalone
|
||||
* class payloads have no vtable word, so their retain adapter identifies
|
||||
* the exact native layout. */
|
||||
export function emitCaughtInstanceof(
|
||||
host: LlvmEmitterContext,
|
||||
caught: string,
|
||||
className: string,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const target = host.classMetaOf(className);
|
||||
const intervals = classMembershipIntervals(host.classMeta, target.def.name);
|
||||
if (!target.hierarchy) {
|
||||
const adapterSlot = B.tmp();
|
||||
const adapter = B.tmp();
|
||||
B.line(`${adapterSlot} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 5`);
|
||||
B.line(`${adapter} = load ptr, ptr ${adapterSlot}`);
|
||||
let result: string | undefined;
|
||||
for (const candidate of host.classMeta.values()) {
|
||||
if (
|
||||
candidate.hierarchy ||
|
||||
!intervals.some((range) => range.pre <= candidate.pre && candidate.pre <= range.post)
|
||||
)
|
||||
continue;
|
||||
const rc = vAdapters(host.shapeHost, { kind: "object", className: candidate.def.name });
|
||||
const matches = B.tmp();
|
||||
B.line(`${matches} = icmp eq ptr ${adapter}, ${rc.retain}`);
|
||||
if (result === undefined) result = matches;
|
||||
else {
|
||||
const joined = B.tmp();
|
||||
B.line(`${joined} = or i1 ${result}, ${matches}`);
|
||||
result = joined;
|
||||
}
|
||||
}
|
||||
if (result === undefined) throw new InternalCompilerError("empty standalone class membership");
|
||||
return result;
|
||||
}
|
||||
host.declare(
|
||||
`declare zeroext i1 @scr_caught_instanceof(ptr, ${host.sizeType}, ${host.sizeType})`,
|
||||
);
|
||||
let result: string | undefined;
|
||||
for (const interval of intervals) {
|
||||
const test = B.tmp();
|
||||
B.line(
|
||||
`${test} = call zeroext i1 @scr_caught_instanceof(ptr ${caught}, ${host.sizeType} ${interval.pre}, ${host.sizeType} ${interval.post})`,
|
||||
);
|
||||
if (result === undefined) result = test;
|
||||
else {
|
||||
const combined = B.tmp();
|
||||
B.line(`${combined} = or i1 ${result}, ${test}`);
|
||||
result = combined;
|
||||
}
|
||||
}
|
||||
if (result === undefined) throw new InternalCompilerError("empty class membership");
|
||||
return result;
|
||||
}
|
||||
|
||||
/** SCR_EXC_F64 = 1, SCR_EXC_BOOL = 2, SCR_EXC_STR = 3 (scr_exception.h). */
|
||||
export const CAUGHT_SCALAR_KINDS = { f64: 1, bool: 2, string: 3 } as const;
|
||||
|
||||
/** Verifies that the borrowed catch snapshot `caught` holds a value of the
|
||||
* narrowed type: a scalar's kind tag, or an object's class. */
|
||||
export function emitCaughtNarrowGuard(
|
||||
host: LlvmEmitterContext,
|
||||
caught: string,
|
||||
type: { kind: "f64" | "bool" | "string" } | { kind: "object"; className: string },
|
||||
): void {
|
||||
const B = host.B;
|
||||
let ok: string;
|
||||
if (type.kind === "object") {
|
||||
ok = emitCaughtInstanceof(host, caught, type.className);
|
||||
} else {
|
||||
const kp = B.tmp();
|
||||
const k = B.tmp();
|
||||
ok = B.tmp();
|
||||
B.line(`${kp} = getelementptr inbounds %ScrCaught, ptr ${caught}, i64 0, i32 1`);
|
||||
B.line(`${k} = load i32, ptr ${kp}`);
|
||||
B.line(`${ok} = icmp eq i32 ${k}, ${CAUGHT_SCALAR_KINDS[type.kind]}`);
|
||||
}
|
||||
emitNarrowGuard(host, ok, "caught");
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
F64,
|
||||
NULL_T,
|
||||
UNDEFINED_T,
|
||||
type IrModule,
|
||||
type IrType,
|
||||
type IrUnionDef,
|
||||
} from "../../ir/ir.js";
|
||||
import { NullableUnions } from "./nullable-unions.js";
|
||||
|
||||
const loc = { file: "nullable-unions.ts", start: 0, end: 0 };
|
||||
const a: IrType = { kind: "record", shapeId: "a" };
|
||||
const b: IrType = { kind: "record", shapeId: "b" };
|
||||
const c: IrType = { kind: "record", shapeId: "c" };
|
||||
|
||||
function module(unions: IrUnionDef[], extra: Partial<IrModule> = {}): IrModule {
|
||||
return {
|
||||
irVersion: 16,
|
||||
sourceFile: loc.file,
|
||||
entry: "main",
|
||||
records: ["a", "b", "c"].map((id) => ({ id, fields: [{ name: id, type: F64 }] })),
|
||||
unions,
|
||||
functions: [],
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
test("unions of two or more records become shape unions", () => {
|
||||
const unions = new NullableUnions(
|
||||
module([
|
||||
{ id: "ab", arms: [a, b] },
|
||||
{ id: "abu", arms: [a, UNDEFINED_T, b] },
|
||||
{ id: "abnu", arms: [a, b, NULL_T, UNDEFINED_T] },
|
||||
{ id: "af", arms: [a, F64] },
|
||||
{ id: "au", arms: [a, UNDEFINED_T] },
|
||||
]),
|
||||
);
|
||||
expect(unions.shapes("ab")).toMatchObject({ unitTag: -1, nullTag: -1 });
|
||||
expect(unions.shapes("abu")).toMatchObject({ unitTag: 1, nullTag: -1 });
|
||||
expect(unions.shapes("abnu")).toMatchObject({ unitTag: 3, nullTag: 2 });
|
||||
expect(unions.shapes("abnu")?.arms).toEqual([
|
||||
{ tag: 0, shapeId: "a" },
|
||||
{ tag: 1, shapeId: "b" },
|
||||
]);
|
||||
// A scalar arm keeps the box; one record arm is a single-arm nullable.
|
||||
expect(unions.get("af")).toBeNull();
|
||||
expect(unions.single("au")).not.toBeNull();
|
||||
expect(unions.shapes("au")).toBeNull();
|
||||
// Tagged shapes number from 2 and move their fields after the word.
|
||||
expect([unions.shapeNumber("a"), unions.shapeNumber("b"), unions.shapeNumber("c")]).toEqual([
|
||||
2, 3, 0,
|
||||
]);
|
||||
expect([unions.fieldBase("a"), unions.fieldBase("c")]).toEqual([2, 1]);
|
||||
expect(unions.boxedUnions(new Map([["af", { id: "af", arms: [a, F64] }]])).size).toBe(1);
|
||||
});
|
||||
|
||||
test("records of runtime-inspected unions keep every union over them boxed", () => {
|
||||
const set: IrType = { kind: "set", elem: { kind: "union", unionId: "ab" } };
|
||||
const unions = new NullableUnions(
|
||||
module(
|
||||
[
|
||||
{ id: "ab", arms: [a, b] },
|
||||
{ id: "abu", arms: [a, b, UNDEFINED_T] },
|
||||
{ id: "bc", arms: [b, c] },
|
||||
{ id: "ac", arms: [a, c] },
|
||||
],
|
||||
{ globals: [{ id: "%g.s", name: "s", type: set, mutable: false }] },
|
||||
),
|
||||
);
|
||||
expect(unions.get("ab")).toBeNull();
|
||||
expect(unions.get("abu")).toBeNull();
|
||||
expect(unions.get("bc")).toBeNull();
|
||||
expect(unions.get("ac")).toBeNull();
|
||||
expect(unions.shapeNumber("c")).toBe(0);
|
||||
});
|
||||
|
||||
test("library, lifetime-region and debug builds keep record unions boxed", () => {
|
||||
const unions = [{ id: "ab", arms: [a, b] }];
|
||||
expect(new NullableUnions(module(unions), false).get("ab")).toBeNull();
|
||||
expect(new NullableUnions(module(unions, { lifetimeRegions: true })).get("ab")).toBeNull();
|
||||
expect(new NullableUnions(module(unions)).shapes("ab")).not.toBeNull();
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import { everyModuleNode } from "../../ir/traverse.js";
|
||||
* every container, field, capture, parameter, and return slot holds the
|
||||
* plain pointer. Every other union keeps the tagged `%ScrUnion` box. */
|
||||
export interface NullableUnion {
|
||||
repr: "single";
|
||||
unionId: string;
|
||||
refTag: number;
|
||||
/** The unit arm NULL encodes. */
|
||||
@@ -24,7 +25,57 @@ export interface NullableUnion {
|
||||
/** The `null` arm of a two-unit-arm union, encoded by NULLABLE_NULL;
|
||||
* -1 when the union has one unit arm. */
|
||||
nullTag: number;
|
||||
arm: NullableRefArm;
|
||||
/** One of NullableRefArm. Typed as the plain IrType so the self-hosted
|
||||
* frontend keeps IrType's structurally identical arms one record shape
|
||||
* (a PointerUnion arm holding the narrowed intersection would split them). */
|
||||
arm: IrType;
|
||||
}
|
||||
|
||||
/** A union of two or more record arms and at most two unit arms (`A | B`,
|
||||
* `A | B | undefined`, discriminated unions of object literals) whose
|
||||
* VALUES are the record pointer itself, like a NullableUnion: NULL is the
|
||||
* unit arm (`undefined` when both exist, NULLABLE_NULL is then `null`).
|
||||
* Every record shape that is an arm of such a union is a TAGGED shape: its
|
||||
* struct carries the shape's module-wide number in a hidden `i32` word
|
||||
* right after the reference count (struct index 1), stored by every
|
||||
* allocation, so the arm of a pointer is that word mapped through the
|
||||
* union's arm table (the per-union `@sc_stag.<id>` helper). Records share
|
||||
* one retain; release and trace dispatch on the arm. */
|
||||
export interface ShapeUnion {
|
||||
repr: "shapes";
|
||||
unionId: string;
|
||||
/** The unit arm NULL encodes; -1 when the union has no unit arm. */
|
||||
unitTag: number;
|
||||
/** The `null` arm of a two-unit-arm union, encoded by NULLABLE_NULL;
|
||||
* -1 otherwise. */
|
||||
nullTag: number;
|
||||
/** The record arms in tag order. */
|
||||
arms: readonly { tag: number; shapeId: string }[];
|
||||
}
|
||||
|
||||
/** A union whose values are plain pointers (no `%ScrUnion` box). */
|
||||
export type PointerUnion = NullableUnion | ShapeUnion;
|
||||
|
||||
/** The struct index of a tagged record shape's hidden shape-number word. */
|
||||
export const SHAPE_WORD_INDEX = 1;
|
||||
|
||||
/** The LLVM type through which any tagged record's shape word is read
|
||||
* (the reference count, then the word). */
|
||||
export function shapeWordHeader(sizeType: "i32" | "i64"): string {
|
||||
return `{ ${sizeType}, i32 }`;
|
||||
}
|
||||
|
||||
/** The per-union helper mapping a shape-union value to its tag. */
|
||||
export function shapeTagFn(unionId: string): string {
|
||||
return `@sc_stag.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
|
||||
}
|
||||
|
||||
/** The per-union release and trace dispatchers of a shape union. */
|
||||
export function shapeReleaseFn(unionId: string): string {
|
||||
return `@sc_srelease.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
|
||||
}
|
||||
export function shapeTraceFn(unionId: string): string {
|
||||
return `@sc_strace.${unionId.replace(/[^A-Za-z0-9_.]/g, "_")}`;
|
||||
}
|
||||
|
||||
/** The module-scope immortal sentinel standing for `null` in a
|
||||
@@ -48,44 +99,98 @@ export type NullableRefArm =
|
||||
| (IrType & { kind: "array" | "map" | "set" });
|
||||
|
||||
/** The arm is a JS object: always truthy, compared by identity. */
|
||||
export function isObjectArm(nullable: NullableUnion): boolean {
|
||||
return nullable.arm.kind !== "string";
|
||||
export function isObjectArm(nullable: PointerUnion): boolean {
|
||||
return nullable.repr === "shapes" || nullable.arm.kind !== "string";
|
||||
}
|
||||
|
||||
/** The representation decision for every union of one module. It is a
|
||||
* pure function of the module, so serialized-IR and in-memory emission
|
||||
* agree. */
|
||||
export class NullableUnions {
|
||||
private readonly byId = new Map<string, NullableUnion>();
|
||||
private readonly byId = new Map<string, PointerUnion>();
|
||||
/** Tagged record shapes (arms of a shape union) by their module-wide
|
||||
* shape number (2 and up: 0 and 1 are the unit encodings' pseudo
|
||||
* numbers, never stored). */
|
||||
private readonly shapeNumbers = new Map<string, number>();
|
||||
/** NULL-skipping retain wrappers requested so far: symbol → inner retain
|
||||
* call target. The emitter defines each once. */
|
||||
readonly retainWrappers = new Map<string, string>();
|
||||
/** Some nullable union encodes `null` with NULLABLE_NULL. */
|
||||
readonly usesNullSentinel: boolean = false;
|
||||
|
||||
constructor(mod: IrModule) {
|
||||
/** `shapes`: allow shape unions (debug builds keep boxes, whose
|
||||
* debug-info layouts describe every arm). */
|
||||
constructor(mod: IrModule, shapes = true) {
|
||||
const emitted = new Set<string>();
|
||||
for (const cls of mod.classes ?? []) if (!cls.runtime) emitted.add(cls.name);
|
||||
const records = new Set<string>();
|
||||
for (const shape of mod.records ?? []) records.add(shape.id);
|
||||
const boxed = boxedUnionIds(mod);
|
||||
// Shape unions stay out of library builds (their host ABI marshals
|
||||
// boxes) and of programs with lifetime regions (whose walks report a
|
||||
// reference slot by its type's release entry point).
|
||||
const allowShapes = shapes && mod.lib === undefined && mod.lifetimeRegions !== true;
|
||||
// A record that is an arm of a boxed union never becomes a shape
|
||||
// union's arm: conversions between unions over the same records then
|
||||
// keep one representation.
|
||||
const boxedRecords = new Set<string>();
|
||||
for (const def of mod.unions ?? [])
|
||||
if (boxed.has(def.id))
|
||||
for (const arm of def.arms) if (arm.kind === "record") boxedRecords.add(arm.shapeId);
|
||||
for (const def of mod.unions ?? []) {
|
||||
if (boxed.has(def.id)) continue;
|
||||
const nullable = nullableShape(def, emitted, records);
|
||||
const nullable =
|
||||
nullableShape(def, emitted, records) ??
|
||||
(allowShapes ? shapeUnion(def, records, boxedRecords) : null);
|
||||
if (!nullable) continue;
|
||||
this.byId.set(def.id, nullable);
|
||||
if (nullable.nullTag >= 0) this.usesNullSentinel = true;
|
||||
if (nullable.repr === "shapes")
|
||||
for (const arm of nullable.arms)
|
||||
if (!this.shapeNumbers.has(arm.shapeId))
|
||||
this.shapeNumbers.set(arm.shapeId, this.shapeNumbers.size + 2);
|
||||
}
|
||||
}
|
||||
|
||||
get(unionId: string): NullableUnion | null {
|
||||
get(unionId: string): PointerUnion | null {
|
||||
return this.byId.get(unionId) ?? null;
|
||||
}
|
||||
|
||||
of(type: IrType): NullableUnion | null {
|
||||
of(type: IrType): PointerUnion | null {
|
||||
return type.kind === "union" ? this.get(type.unionId) : null;
|
||||
}
|
||||
|
||||
/** The single-reference-arm form only (shape unions answer null). */
|
||||
single(unionId: string): NullableUnion | null {
|
||||
const nullable = this.byId.get(unionId);
|
||||
return nullable?.repr === "single" ? nullable : null;
|
||||
}
|
||||
|
||||
/** The shape-union form only. */
|
||||
shapes(unionId: string): ShapeUnion | null {
|
||||
const nullable = this.byId.get(unionId);
|
||||
return nullable?.repr === "shapes" ? nullable : null;
|
||||
}
|
||||
|
||||
/** Every shape union, in module order. */
|
||||
allShapeUnions(): ShapeUnion[] {
|
||||
const out: ShapeUnion[] = [];
|
||||
for (const nullable of this.byId.values()) if (nullable.repr === "shapes") out.push(nullable);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The module-wide number a tagged record shape stores in its shape
|
||||
* word, or 0 for shapes that carry none. */
|
||||
shapeNumber(shapeId: string): number {
|
||||
return this.shapeNumbers.get(shapeId) ?? 0;
|
||||
}
|
||||
|
||||
/** The struct index of a record shape's first declared field: 2 for
|
||||
* tagged shapes (after the shape word), 1 otherwise. */
|
||||
fieldBase(shapeId: string): number {
|
||||
return this.shapeNumbers.has(shapeId) ? 2 : 1;
|
||||
}
|
||||
|
||||
has(unionId: string): boolean {
|
||||
return this.byId.has(unionId);
|
||||
}
|
||||
@@ -94,10 +199,11 @@ export class NullableUnions {
|
||||
return this.byId.size;
|
||||
}
|
||||
|
||||
/** The reference arm of every nullable union, by union id. */
|
||||
/** The reference arm of every single-arm nullable union, by union id. */
|
||||
armTypes(): Map<string, IrType> {
|
||||
const result = new Map<string, IrType>();
|
||||
for (const [id, nullable] of this.byId) result.set(id, nullable.arm);
|
||||
for (const [id, nullable] of this.byId)
|
||||
if (nullable.repr === "single") result.set(id, nullable.arm);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -129,7 +235,7 @@ function nullableShape(
|
||||
if (unitTag < 0 || nullTag < 0) return null;
|
||||
}
|
||||
const arm = def.arms[refTag]!;
|
||||
const base = { unionId: def.id, refTag, unitTag, nullTag };
|
||||
const base = { repr: "single" as const, unionId: def.id, refTag, unitTag, nullTag };
|
||||
if (arm.kind === "object" && emittedClasses.has(arm.className))
|
||||
return { ...base, arm: { kind: "object", className: arm.className } };
|
||||
if (arm.kind === "record" && records.has(arm.shapeId))
|
||||
@@ -140,6 +246,34 @@ function nullableShape(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Two or more emitted record arms and at most two unit arms (both:
|
||||
* undefined and null). */
|
||||
function shapeUnion(
|
||||
def: IrUnionDef,
|
||||
records: ReadonlySet<string>,
|
||||
boxedRecords: ReadonlySet<string>,
|
||||
): ShapeUnion | null {
|
||||
const units = def.arms.filter(isUnitType).length;
|
||||
if (units > 2 || def.arms.length - units < 2) return null;
|
||||
const arms: { tag: number; shapeId: string }[] = [];
|
||||
for (let tag = 0; tag < def.arms.length; tag++) {
|
||||
const arm = def.arms[tag]!;
|
||||
if (isUnitType(arm)) continue;
|
||||
if (arm.kind !== "record" || !records.has(arm.shapeId) || boxedRecords.has(arm.shapeId))
|
||||
return null;
|
||||
arms.push({ tag, shapeId: arm.shapeId });
|
||||
}
|
||||
let unitTag = -1,
|
||||
nullTag = -1;
|
||||
if (units === 1) unitTag = def.arms.findIndex(isUnitType);
|
||||
else if (units === 2) {
|
||||
unitTag = def.arms.findIndex((arm) => arm.kind === "undefinedT");
|
||||
nullTag = def.arms.findIndex((arm) => arm.kind === "nullT");
|
||||
if (unitTag < 0 || nullTag < 0) return null;
|
||||
}
|
||||
return { repr: "shapes", unionId: def.id, unitTag, nullTag, arms };
|
||||
}
|
||||
|
||||
/** Unions the runtime itself inspects as tagged boxes keep that
|
||||
* representation: Map keys and Set elements hash and compare union keys by
|
||||
* tag and payload inside the C map. */
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { IrClassDef, IrRecordShape, IrType, IrUnionDef, SrcLoc } from "../.
|
||||
import { mapOf, STRING, typeKey } from "../../ir/ir.js";
|
||||
import { mangleClassStruct, mangleRecordStruct } from "../mangle.js";
|
||||
import { FN_ATTRS } from "./shapes.js";
|
||||
import { shapeTagFn } from "./nullable-unions.js";
|
||||
import type { InlineUnionFields } from "./inline-unions.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
|
||||
@@ -44,6 +45,10 @@ export interface PublishHost {
|
||||
readonly inlineUnions: InlineUnionFields;
|
||||
/** The nullable-pointer representation of a union, when it has one. */
|
||||
nullableArm(t: IrType): IrType | null;
|
||||
/** The union's values are tagged record pointers (nullable-unions.ts). */
|
||||
shapeUnion(t: IrType): boolean;
|
||||
/** The struct index of a record shape's first field. */
|
||||
fieldBase(shapeId: string): number;
|
||||
/** Class layout: hierarchy membership, preorder interval, field slots
|
||||
* (with their storage types), runtime roots. */
|
||||
classInfo(className: string): PublishClassInfo;
|
||||
@@ -210,6 +215,19 @@ export class PublishWalkers {
|
||||
if (def.arms.some((arm) => scalarRefusal(arm)))
|
||||
throw new LlvmUnsupportedError(`publish of ${typeKey(t)}`, loc);
|
||||
const live = arms.flatMap((a) => (a.fn === null ? [] : [{ tag: a.tag, fn: a.fn }]));
|
||||
if (this.host.shapeUnion(t)) {
|
||||
// The value is the record itself (or a unit encoding): no box to
|
||||
// mark; the arm's walker marks and walks the record.
|
||||
const lines = [head, `entry:`];
|
||||
lines.push(
|
||||
` %tag = call i32 ${shapeTagFn(t.unionId)}(ptr %o)`,
|
||||
` switch i32 %tag, label %done [ ${live.map((a) => `i32 ${a.tag}, label %arm${a.tag}`).join(" ")} ]`,
|
||||
);
|
||||
for (const a of live)
|
||||
lines.push(`arm${a.tag}:`, ` call void ${a.fn}(ptr %o, ptr %ctx)`, ` br label %done`);
|
||||
lines.push(`done:`, ` ret void`, `}`);
|
||||
return lines;
|
||||
}
|
||||
const lines = [
|
||||
head,
|
||||
`entry:`,
|
||||
@@ -240,17 +258,18 @@ export class PublishWalkers {
|
||||
const shape = this.host.recordsById.get(t.shapeId);
|
||||
if (!shape)
|
||||
throw new LlvmUnsupportedError(`publish of an unknown record ${t.shapeId}`, loc);
|
||||
const base = this.host.fieldBase(shape.id);
|
||||
const members = [
|
||||
...shape.fields.flatMap((f, i) =>
|
||||
this.host.inlineUnions.record(f.type)
|
||||
? []
|
||||
: [{ name: f.name, index: i + 1, type: f.type }],
|
||||
: [{ name: f.name, index: i + base, type: f.type }],
|
||||
),
|
||||
...(shape.indexValue
|
||||
? [
|
||||
{
|
||||
name: "[key: string]",
|
||||
index: shape.fields.length + 1,
|
||||
index: shape.fields.length + base,
|
||||
type: mapOf(STRING, shape.indexValue),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -101,8 +101,8 @@ export class LifetimeRegions {
|
||||
/** Can a value of type `t` hold (or own something that holds) a
|
||||
* reference to a ProgramLifetime instance the walk can explain? */
|
||||
private reaches(t: IrType): boolean {
|
||||
const arm = this.host.nullableUnions.of(t)?.arm;
|
||||
if (arm !== undefined) return this.reaches(arm);
|
||||
const nullable = this.host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "single") return this.reaches(nullable.arm);
|
||||
switch (t.kind) {
|
||||
case "object":
|
||||
return this.isLifetime(t.className) || this.reach.get(`object:${t.className}`) === true;
|
||||
@@ -287,7 +287,8 @@ export class LifetimeRegions {
|
||||
* ProgramLifetime instance). */
|
||||
private reportLines(struct: string, f: Field, i: number): string[] {
|
||||
if (!isRefCounted(f.type) || !this.reaches(f.type)) return [];
|
||||
const arm = this.host.nullableUnions.of(f.type)?.arm ?? f.type;
|
||||
const nullable = this.host.nullableUnions.of(f.type);
|
||||
const arm = nullable?.repr === "single" ? nullable.arm : f.type;
|
||||
const load = [
|
||||
` %f${i} = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${f.index}`,
|
||||
` %v${i} = load ptr, ptr %f${i}`,
|
||||
@@ -411,7 +412,7 @@ export class LifetimeRegions {
|
||||
...this.reportBody(
|
||||
`define internal void ${walk}(ptr %o, ptr %w) ${FN_ATTRS} { ; region walk record ${shape.id}`,
|
||||
mangleRecordStruct(shape.id),
|
||||
rcMembers(shape, this.host.inlineUnions),
|
||||
rcMembers(shape, this.host.inlineUnions, this.host.nullableUnions.fieldBase(shape.id)),
|
||||
),
|
||||
);
|
||||
rows.push(`{ ptr, ptr } { ptr @${mangleRecordRelease(shape.id)}, ptr ${walk} }`);
|
||||
|
||||
@@ -31,7 +31,16 @@ import {
|
||||
import { cycTypeDescriptor, emitObjectAlloc, emitObjectFree } from "./alloc.js";
|
||||
import type { BlockBuilder } from "./blocks.js";
|
||||
import { SCR_ARR_FIELD, llvmCommentText } from "./common.js";
|
||||
import type { NullableUnions } from "./nullable-unions.js";
|
||||
import {
|
||||
NULLABLE_NULL,
|
||||
SHAPE_WORD_INDEX,
|
||||
shapeReleaseFn,
|
||||
shapeTagFn,
|
||||
shapeTraceFn,
|
||||
shapeWordHeader,
|
||||
type NullableUnions,
|
||||
type ShapeUnion,
|
||||
} from "./nullable-unions.js";
|
||||
import {
|
||||
INLINE_UNION_FIELD,
|
||||
loadInlineUnion,
|
||||
@@ -129,6 +138,7 @@ export { computeTraced } from "../cycle-analysis.js";
|
||||
* `_v`-shaped. */
|
||||
export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release: string } {
|
||||
const nullable = host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(nullable);
|
||||
if (nullable) {
|
||||
const arm = vAdapters(host, nullable.arm);
|
||||
return { retain: nullableRetainSym(host, nullable.arm), release: arm.release };
|
||||
@@ -184,6 +194,7 @@ export function vAdapters(host: ShapeHost, t: IrType): { retain: string; release
|
||||
* Families with a mirrored fast path call the inline helper instead. */
|
||||
export function retainSym(host: ShapeHost, t: IrType): string {
|
||||
const nullable = host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(nullable).retain;
|
||||
if (nullable) return nullableRetainSym(host, nullable.arm);
|
||||
const stem = runtimeRcStem(t);
|
||||
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "retain") !== null) {
|
||||
@@ -195,6 +206,99 @@ export function retainSym(host: ShapeHost, t: IrType): string {
|
||||
return vAdapters(host, t).retain;
|
||||
}
|
||||
|
||||
/** A shape union's RC entry points: every record retain is the same
|
||||
* NULL- and immortal-skipping increment (the null sentinel is immortal),
|
||||
* so the first arm's serves; release dispatches on the arm. */
|
||||
function shapeUnionRc(union: ShapeUnion): { retain: string; release: string } {
|
||||
return {
|
||||
retain: `@${mangleRecordRetain(union.arms[0]!.shapeId)}`,
|
||||
release: shapeReleaseFn(union.unionId),
|
||||
};
|
||||
}
|
||||
|
||||
/** The tag, release and trace helpers of every shape union
|
||||
* (nullable-unions.ts). The tag helper maps a value to its arm: NULL and
|
||||
* the null sentinel are the unit arms, anything else a tagged record whose
|
||||
* shape word names its arm (the switch folds to a lookup table). */
|
||||
export function emitShapeUnionHelpers(host: ShapeHost): string[] {
|
||||
const out: string[] = [];
|
||||
const S = host.sizeType;
|
||||
for (const union of host.nullableUnions.allShapeUnions()) {
|
||||
const header = shapeWordHeader(S);
|
||||
const tagFn = shapeTagFn(union.unionId);
|
||||
const cases = union.arms
|
||||
.map((arm) => `i32 ${host.nullableUnions.shapeNumber(arm.shapeId)}, label %a${arm.tag}`)
|
||||
.join(" ");
|
||||
out.push(
|
||||
`define internal i32 ${tagFn}(ptr %u) alwaysinline ${FN_ATTRS} { ; shape union tag`,
|
||||
`entry:`,
|
||||
` %isnull = icmp eq ptr %u, null`,
|
||||
` br i1 %isnull, label %unit, label %ref`,
|
||||
`unit:`,
|
||||
` ret i32 ${union.unitTag}`,
|
||||
`ref:`,
|
||||
);
|
||||
if (union.nullTag >= 0)
|
||||
out.push(
|
||||
` %isnullarm = icmp eq ptr %u, ${NULLABLE_NULL}`,
|
||||
` br i1 %isnullarm, label %nullarm, label %rec`,
|
||||
`nullarm:`,
|
||||
` ret i32 ${union.nullTag}`,
|
||||
`rec:`,
|
||||
);
|
||||
out.push(
|
||||
` %wp = getelementptr inbounds ${header}, ptr %u, i32 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
` %w = load i32, ptr %wp`,
|
||||
` switch i32 %w, label %bad [ ${cases} ]`,
|
||||
...union.arms.flatMap((arm) => [`a${arm.tag}:`, ` ret i32 ${arm.tag}`]),
|
||||
`bad:`,
|
||||
` ret i32 -1`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
// release: unit encodings are NULL or immortal; each arm releases
|
||||
// through its own (NULL- and immortal-tolerant) helper.
|
||||
out.push(
|
||||
`define internal void ${shapeReleaseFn(union.unionId)}(ptr %u) ${FN_ATTRS} { ; shape union release`,
|
||||
`entry:`,
|
||||
` %t = call i32 ${tagFn}(ptr %u)`,
|
||||
` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
|
||||
...union.arms.flatMap((arm) => [
|
||||
`a${arm.tag}:`,
|
||||
` call void @${mangleRecordRelease(arm.shapeId)}(ptr %u)`,
|
||||
` br label %done`,
|
||||
]),
|
||||
`done:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
if (shapeUnionTraced(host, union))
|
||||
out.push(
|
||||
`define internal void ${shapeTraceFn(union.unionId)}(ptr %u, ptr %visit, ptr %ctx) ${FN_ATTRS} { ; shape union trace`,
|
||||
`entry:`,
|
||||
` %t = call i32 ${tagFn}(ptr %u)`,
|
||||
` switch i32 %t, label %done [ ${union.arms.map((arm) => `i32 ${arm.tag}, label %a${arm.tag}`).join(" ")} ]`,
|
||||
...union.arms.flatMap((arm) => [
|
||||
`a${arm.tag}:`,
|
||||
` call void @${mangleRecordTrace(arm.shapeId)}(ptr %u, ptr %visit, ptr %ctx)`,
|
||||
` br label %done`,
|
||||
]),
|
||||
`done:`,
|
||||
` ret void`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** A shape union's arms carry collector headers (all or none: the emitter
|
||||
* traces every arm of a union with one traced arm). */
|
||||
function shapeUnionTraced(host: ShapeHost, union: ShapeUnion): boolean {
|
||||
return union.arms.some((arm) => host.tracedShapes.has(`record:${arm.shapeId}`));
|
||||
}
|
||||
|
||||
/** The retain entry point of a nullable union's value: the arm's own when
|
||||
* it tolerates NULL (emitted class and record helpers), otherwise a
|
||||
* NULL-skipping wrapper around it (the null sentinel and literals are
|
||||
@@ -233,6 +337,7 @@ export function emitNullableRetainWrappers(host: ShapeHost): string[] {
|
||||
* Families with a mirrored fast path call the inline helper instead. */
|
||||
export function releaseSym(host: ShapeHost, t: IrType): string {
|
||||
const nullable = host.nullableUnions.of(t);
|
||||
if (nullable?.repr === "shapes") return shapeUnionRc(nullable).release;
|
||||
if (nullable) return releaseSym(host, nullable.arm);
|
||||
const stem = runtimeRcStem(t);
|
||||
if (host.rcHelpers !== null && stem !== null && inlineRcFamily(host, stem, "release") !== null) {
|
||||
@@ -643,6 +748,8 @@ export function traceAdapter(host: ShapeHost, t: IrType): string | null {
|
||||
return "@scr_promise_trace_v";
|
||||
case "union": {
|
||||
const nullable = host.nullableUnions.get(t.unionId);
|
||||
if (nullable?.repr === "shapes")
|
||||
return shapeUnionTraced(host, nullable) ? shapeTraceFn(nullable.unionId) : null;
|
||||
if (nullable) return traceAdapter(host, nullable.arm);
|
||||
if (!host.tracedUnions.has(t.unionId)) return null;
|
||||
host.declare(`declare void @scr_union_trace_v(ptr, ptr, ptr)`);
|
||||
@@ -945,15 +1052,17 @@ export function mapValKindNum(value: IrType): number {
|
||||
export function rcMembers(
|
||||
shape: IrRecordShape,
|
||||
inline: InlineUnionFields,
|
||||
/** The struct index of the first field (2 on tagged shapes). */
|
||||
base = 1,
|
||||
): { index: number; type: IrType; name: string }[] {
|
||||
return [
|
||||
...shape.fields.flatMap((f, i) =>
|
||||
inline.record(f.type) ? [] : [{ index: i + 1, type: f.type, name: f.name }],
|
||||
inline.record(f.type) ? [] : [{ index: i + base, type: f.type, name: f.name }],
|
||||
),
|
||||
...(shape.indexValue
|
||||
? [
|
||||
{
|
||||
index: shape.fields.length + 1,
|
||||
index: shape.fields.length + base,
|
||||
type: mapOf(STRING, shape.indexValue),
|
||||
name: "[key: string] overflow",
|
||||
},
|
||||
@@ -1107,7 +1216,12 @@ export function directDestroyTarget(
|
||||
}
|
||||
|
||||
function directDestroyKey(host: ShapeHost, type: IrType): string | null {
|
||||
const t = host.nullableUnions.of(type)?.arm ?? type;
|
||||
const nullable = host.nullableUnions.of(type);
|
||||
let t = type;
|
||||
if (nullable !== null) {
|
||||
if (nullable.repr === "shapes") return null;
|
||||
t = nullable.arm;
|
||||
}
|
||||
if (t.kind === "record") return `record:${t.shapeId}`;
|
||||
return t.kind === "object" ? `object:${t.className}` : null;
|
||||
}
|
||||
@@ -1338,8 +1452,12 @@ export function emitRecordShapes(
|
||||
|
||||
for (const shape of records) {
|
||||
const struct = mangleRecordStruct(shape.id);
|
||||
const fieldTys = shape.fields.map((f) => llFieldType(f.type, host.inlineUnions.record(f.type)));
|
||||
const fieldTys: string[] = shape.fields.map((f) =>
|
||||
llFieldType(f.type, host.inlineUnions.record(f.type)),
|
||||
);
|
||||
if (shape.indexValue) fieldTys.push("ptr"); // the overflow ScrMap *
|
||||
// A tagged shape (an arm of a shape union) carries its shape number.
|
||||
if (host.nullableUnions.shapeNumber(shape.id) !== 0) fieldTys.unshift("i32");
|
||||
typeDefs.push(
|
||||
`%${struct} = type { ${host.sizeType}${fieldTys.length ? ", " + fieldTys.join(", ") : ""} } ` +
|
||||
`; record ${shape.id} { ${shape.fields.map((f) => llvmCommentText(f.name)).join("; ")}${shape.indexValue ? "; [key: string]" : ""} }`,
|
||||
@@ -1352,7 +1470,7 @@ export function emitRecordShapes(
|
||||
const candidates: [string, DirectDestroy][] = [];
|
||||
const memberTypes: IrType[] = [];
|
||||
for (const shape of records) {
|
||||
const members = rcMembers(shape, host.inlineUnions);
|
||||
const members = rcMembers(shape, host.inlineUnions, host.nullableUnions.fieldBase(shape.id));
|
||||
for (const m of members) memberTypes.push(m.type);
|
||||
if (!members.some((m) => isRefCounted(m.type) && needsBoundedRelease(m.type))) continue;
|
||||
const release = mangleRecordRelease(shape.id);
|
||||
@@ -1370,7 +1488,9 @@ export function emitRecordShapes(
|
||||
for (const shape of records) {
|
||||
const struct = mangleRecordStruct(shape.id);
|
||||
const traced = host.tracedShapes.has(`record:${shape.id}`);
|
||||
const members = rcMembers(shape, host.inlineUnions);
|
||||
const base = host.nullableUnions.fieldBase(shape.id);
|
||||
const shapeNumber = host.nullableUnions.shapeNumber(shape.id);
|
||||
const members = rcMembers(shape, host.inlineUnions, base);
|
||||
const refMembers = members.filter((m) => isRefCounted(m.type));
|
||||
const sizeOf = `ptrtoint (ptr getelementptr (%${struct}, ptr null, i32 1) to ${host.sizeType})`;
|
||||
|
||||
@@ -1439,6 +1559,11 @@ export function emitRecordShapes(
|
||||
);
|
||||
nw.push(...emitObjectAlloc(host, sizeOf, traced ? { type: cycType } : null));
|
||||
nw.push(` store ${host.sizeType} 1, ptr %o`);
|
||||
if (shapeNumber !== 0)
|
||||
nw.push(
|
||||
` %shp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
` store i32 ${shapeNumber}, ptr %shp ; shape number`,
|
||||
);
|
||||
if (shape.indexValue) {
|
||||
// The overflow map (string-keyed): value handling is type-directed
|
||||
// exactly like shapes.ts's overflowNewC.
|
||||
@@ -1449,7 +1574,7 @@ export function emitRecordShapes(
|
||||
const trace = valKind === 2 ? traceArg(host, v) : "null";
|
||||
nw.push(
|
||||
` %ovf = call ptr @scr_map_new(i32 1, i32 ${valKind}, ptr ${host.mapOps(["null", "null", "null", rc.retain, rc.release, trace, "null"])})`,
|
||||
` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
` %ovfp = getelementptr inbounds %${struct}, ptr %o, i64 0, i32 ${shape.fields.length + base}`,
|
||||
` store ptr %ovf, ptr %ovfp`,
|
||||
);
|
||||
}
|
||||
@@ -1465,7 +1590,7 @@ export function emitRecordShapes(
|
||||
];
|
||||
let i = 0;
|
||||
for (const field of shape.fields) {
|
||||
const index = i + 1;
|
||||
const index = i + base;
|
||||
const inline = host.inlineUnions.record(field.type);
|
||||
const fieldTy = llFieldType(field.type, inline);
|
||||
clone.push(
|
||||
|
||||
@@ -21,6 +21,7 @@ import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
import { emitCallExpr, emitConstruction } from "./expr-calls.js";
|
||||
import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { emitRecordLitFields } from "./expr-primitives.js";
|
||||
import { SHAPE_WORD_INDEX } from "./nullable-unions.js";
|
||||
import { llFieldType } from "./shapes.js";
|
||||
|
||||
export type StackObjectSource = IrExpr & { kind: "recordLit" | "recordClone" | "new" | "call" };
|
||||
@@ -196,12 +197,13 @@ export function stackObjectFields(
|
||||
const cached = host.stackObjectFieldPtrs.get(slot);
|
||||
if (cached) return cached;
|
||||
const struct = stackObjectStruct(type);
|
||||
const base = host.nullableUnions.fieldBase(type.shapeId);
|
||||
const fields: StackObjectField[] = [];
|
||||
host.recordShape(type.shapeId).fields.forEach((field, index) => {
|
||||
if (!isRefCounted(field.type) || host.inlineUnions.record(field.type)) return;
|
||||
const ptr = host.B.slot();
|
||||
host.B.entryAllocas.push(
|
||||
`${ptr} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${index + 1}`,
|
||||
`${ptr} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${index + base}`,
|
||||
);
|
||||
fields.push({ name: field.name, ptr, type: field.type });
|
||||
});
|
||||
@@ -296,6 +298,17 @@ export function emitStackObject(
|
||||
}
|
||||
}
|
||||
B.line(`store ${host.sizeType} -1, ptr ${slot}`);
|
||||
if (type.kind === "record") {
|
||||
// A tagged shape's word names it to every shape union's tag read.
|
||||
const shapeNumber = host.nullableUnions.shapeNumber(type.shapeId);
|
||||
if (shapeNumber !== 0) {
|
||||
const p = B.tmp();
|
||||
B.line(
|
||||
`${p} = getelementptr inbounds ${struct}, ptr ${slot}, i64 0, i32 ${SHAPE_WORD_INDEX}`,
|
||||
);
|
||||
B.line(`store i32 ${shapeNumber}, ptr ${p}`);
|
||||
}
|
||||
}
|
||||
if (e.kind === "call") {
|
||||
// The entry writes the result into the slot it receives last.
|
||||
emitCallExpr(
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import { BOOL, F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import { analyzeCallLifetimes } from "./call-lifetimes.js";
|
||||
import { findScalarStringSlices } from "./string-slices.js";
|
||||
import {
|
||||
findLentStringParameters,
|
||||
findScalarStringSlices,
|
||||
findScratchStringSlices,
|
||||
} from "./string-slices.js";
|
||||
const loc = { file: "slices.ts", start: 0, end: 0 };
|
||||
const ref = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: STRING, loc });
|
||||
function fixture(): IrFunction {
|
||||
@@ -95,3 +99,146 @@ test.each(["escape", "capture", "write", "other-consumer", "suspend"])(
|
||||
expect(facts(fn).size).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
const slice = (receiver: string, start: number, end: number): IrExpr => ({
|
||||
kind: "strIntrinsic",
|
||||
method: "slice",
|
||||
receiver: ref(receiver),
|
||||
args: [
|
||||
{ kind: "numLit", value: start, type: F64, loc },
|
||||
{ kind: "numLit", value: end, type: F64, loc },
|
||||
],
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
const equals = (left: IrExpr): IrExpr => ({
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left,
|
||||
right: { kind: "strLit", value: "GET", type: STRING, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const parse = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "num.fromString",
|
||||
args: [value],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
function lending(body: IrStmt[]): IrFunction {
|
||||
return {
|
||||
name: "scan",
|
||||
params: [{ localId: "source", name: "source", type: STRING }],
|
||||
locals: [
|
||||
{ id: "source", name: "source", type: STRING, mutable: false },
|
||||
{ id: "word", name: "word", type: STRING, mutable: false },
|
||||
],
|
||||
returnType: VOID,
|
||||
loc,
|
||||
body,
|
||||
};
|
||||
}
|
||||
function scratch(fn: IrFunction, extra: IrFunction[] = []): Set<IrExpr> {
|
||||
const functions = new Map([fn, ...extra].map((f) => [f.name, f]));
|
||||
const lifetimes = analyzeCallLifetimes(functions);
|
||||
return findScratchStringSlices(
|
||||
fn,
|
||||
lifetimes,
|
||||
findScalarStringSlices(fn, lifetimes),
|
||||
findLentStringParameters(functions, lifetimes),
|
||||
);
|
||||
}
|
||||
test("slices read only by lending consumers use a stack buffer", () => {
|
||||
const inline = slice("source", 0, 3);
|
||||
const parsed = slice("source", 4, 6);
|
||||
const init = slice("source", 1, 5);
|
||||
const fn = lending([
|
||||
{ kind: "exprStmt", expr: equals(inline), loc },
|
||||
{ kind: "exprStmt", expr: parse(parsed), loc },
|
||||
{ kind: "varDecl", localId: "word", init, loc },
|
||||
{ kind: "exprStmt", expr: equals(ref("word")), loc },
|
||||
{ kind: "exprStmt", expr: parse(ref("word")), loc },
|
||||
]);
|
||||
expect(scratch(fn)).toEqual(new Set([inline, parsed, init]));
|
||||
});
|
||||
test.each(["return", "concat", "slice-of-slice", "capture", "async"])(
|
||||
"a slice local that may be kept (%s) stays owned",
|
||||
(reason) => {
|
||||
const init = slice("source", 1, 5);
|
||||
const fn = lending([
|
||||
{ kind: "varDecl", localId: "word", init, loc },
|
||||
{ kind: "exprStmt", expr: equals(ref("word")), loc },
|
||||
]);
|
||||
if (reason === "return") fn.body.push({ kind: "return", value: ref("word"), loc });
|
||||
if (reason === "concat")
|
||||
fn.body.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "strConcat",
|
||||
left: { kind: "strLit", value: "", type: STRING, loc },
|
||||
right: ref("word"),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
if (reason === "slice-of-slice")
|
||||
fn.body.push({ kind: "exprStmt", expr: equals(slice("word", 0, 2)), loc });
|
||||
if (reason === "capture") fn.locals[1]!.boxed = true;
|
||||
if (reason === "async") fn.async = true;
|
||||
expect(scratch(fn).has(init)).toBe(false);
|
||||
},
|
||||
);
|
||||
function helper(name: string, body: IrStmt[]): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: [{ localId: "text", name: "text", type: STRING }],
|
||||
locals: [{ id: "text", name: "text", type: STRING, mutable: false }],
|
||||
returnType: BOOL,
|
||||
loc,
|
||||
body,
|
||||
};
|
||||
}
|
||||
const callOf = (callee: string, arg: IrExpr): IrExpr => ({
|
||||
kind: "call",
|
||||
callee,
|
||||
args: [arg],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
test("helper parameters read only by lending consumers accept stack slices", () => {
|
||||
const recursive = helper("isGet", [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: equals(ref("text")),
|
||||
right: callOf("isGet", ref("text")),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]);
|
||||
const keeps = helper("keeps", [{ kind: "return", value: callOf("stores", ref("text")), loc }]);
|
||||
const stores = helper("stores", [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "arrayLit", elems: [ref("text")], type: { kind: "array", elem: STRING }, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
|
||||
]);
|
||||
const lent = slice("source", 0, 3);
|
||||
const kept = slice("source", 0, 4);
|
||||
const fn = lending([
|
||||
{ kind: "exprStmt", expr: callOf("isGet", lent), loc },
|
||||
{ kind: "exprStmt", expr: callOf("keeps", kept), loc },
|
||||
]);
|
||||
const functions = [recursive, keeps, stores];
|
||||
expect(scratch(fn, functions)).toEqual(new Set([lent]));
|
||||
const all = new Map([fn, ...functions].map((f) => [f.name, f]));
|
||||
expect([...findLentStringParameters(all, analyzeCallLifetimes(all)).keys()]).toEqual(["isGet"]);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrStrIntrinsicMethod,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import { F64_INF, f64Lit, sizeToDouble } from "./common.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
@@ -6,6 +11,12 @@ import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
type Slice = IrExpr & { kind: "strIntrinsic" };
|
||||
|
||||
function isSlice(value: IrExpr): value is Slice {
|
||||
return (
|
||||
value.kind === "strIntrinsic" && (value.method === "slice" || value.method === "substring")
|
||||
);
|
||||
}
|
||||
|
||||
/** A local used exclusively for scalar indexing needs no copied bytes.
|
||||
* Snapshot the receiver and normalized bounds at its initializer, including
|
||||
* side effects in the bounds. Escapes, captures, writes, suspension, and all
|
||||
@@ -111,3 +122,234 @@ export function emitStringSliceRead(host: LlvmEmitterContext, value: Slice): LlV
|
||||
);
|
||||
return { name: result, type: value.type };
|
||||
}
|
||||
|
||||
/** Bytes of the stack buffer behind one scratch slice, header included. */
|
||||
export const SCRATCH_SLICE_BYTES = 96;
|
||||
|
||||
/** String methods that read a lent receiver or argument only during their
|
||||
* runtime call and never return it. Methods returning a string can return
|
||||
* their unchanged receiver, so they stay on the owned path. */
|
||||
const LENDING_METHODS: ReadonlySet<IrStrIntrinsicMethod> = new Set([
|
||||
"length",
|
||||
"charCodeAt",
|
||||
"indexOf",
|
||||
"includes",
|
||||
"startsWith",
|
||||
"endsWith",
|
||||
]);
|
||||
|
||||
/** Numeric parsers read their text during the call. */
|
||||
const LENDING_LIB_FNS: ReadonlySet<string> = new Set([
|
||||
"num.fromString",
|
||||
"num.parseInt",
|
||||
"num.parseFloat",
|
||||
]);
|
||||
|
||||
/** Typed string-keyed collection operations: reads probe with the key, and
|
||||
* an insertion stores scr_str_scratch_keep's copy (scr_map_append). */
|
||||
const LENDING_KEY_METHODS: ReadonlySet<string> = new Set(["get", "has", "set", "add", "delete"]);
|
||||
|
||||
function stringKeyed(value: IrExpr & { kind: "mapIntrinsic" | "setIntrinsic" }): boolean {
|
||||
const receiver = value.receiver.type;
|
||||
if (receiver.kind === "map")
|
||||
return receiver.key.kind === "string" && receiver.value.kind !== "dyn";
|
||||
if (receiver.kind === "set") return receiver.elem.kind === "string";
|
||||
return false;
|
||||
}
|
||||
|
||||
/** String parameters, by function and index, that the body reads only in
|
||||
* lending positions (findLentStringParameters). */
|
||||
export type LentStringParameters = ReadonlyMap<string, ReadonlySet<number>>;
|
||||
|
||||
/** The operands `value` reads only while it runs, keeping no reference. */
|
||||
function lentOperands(value: IrExpr, parameters: LentStringParameters): readonly IrExpr[] {
|
||||
switch (value.kind) {
|
||||
case "strEq":
|
||||
case "strCmp":
|
||||
return [value.left, value.right];
|
||||
case "strIntrinsic":
|
||||
return LENDING_METHODS.has(value.method) ? [value.receiver, ...value.args] : [];
|
||||
case "libCall":
|
||||
return LENDING_LIB_FNS.has(value.fn) ? value.args.slice(0, 1) : [];
|
||||
case "mapIntrinsic":
|
||||
case "setIntrinsic":
|
||||
return LENDING_KEY_METHODS.has(value.method) && stringKeyed(value)
|
||||
? value.args.slice(0, 1)
|
||||
: [];
|
||||
case "call": {
|
||||
const lent = parameters.get(value.callee);
|
||||
return lent ? value.args.filter((_, index) => lent.has(index)) : [];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
interface LendingScan {
|
||||
/** Slice operands of lending consumers, excluding any node also reached
|
||||
* in an owning position. */
|
||||
scratch: Set<IrExpr>;
|
||||
/** Candidates read anywhere other than a lending position. */
|
||||
invalid: Set<string>;
|
||||
/** Direct callees, for the parameter fixpoint. */
|
||||
callees: Set<string>;
|
||||
}
|
||||
|
||||
/** One pass over `fn`: which candidate bindings only lending consumers read
|
||||
* (a slice initializer of a candidate local is not itself a read), and
|
||||
* which slice operands those consumers lend. A switch discriminant is only
|
||||
* compared with the case tests. */
|
||||
function scanLending(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlySet<string>,
|
||||
initializers: ReadonlyMap<string, Slice>,
|
||||
parameters: LentStringParameters,
|
||||
): LendingScan {
|
||||
const scan: LendingScan = { scratch: new Set(), invalid: new Set(), callees: new Set() };
|
||||
const owned = new Set<IrExpr>();
|
||||
function lent(value: IrExpr): boolean {
|
||||
if (value.kind === "varRef" && candidates.has(value.localId)) return true;
|
||||
if (!isSlice(value)) return expr(value);
|
||||
scan.scratch.add(value);
|
||||
return everyExprChild(value, expr, stmt);
|
||||
}
|
||||
function expr(value: IrExpr): boolean {
|
||||
if (isSlice(value)) owned.add(value);
|
||||
if (value.kind === "varRef" && candidates.has(value.localId)) scan.invalid.add(value.localId);
|
||||
if (value.kind === "closure" || value.kind === "classRef")
|
||||
for (const id of value.captures ?? []) scan.invalid.add(id);
|
||||
if (value.kind === "call") scan.callees.add(value.callee);
|
||||
const operands = lentOperands(value, parameters);
|
||||
if (operands.length === 0) return everyExprChild(value, expr, stmt);
|
||||
return everyExprChild(
|
||||
value,
|
||||
(child) => (operands.includes(child) ? lent(child) : expr(child)),
|
||||
stmt,
|
||||
);
|
||||
}
|
||||
function stmt(value: IrStmt): boolean {
|
||||
// A candidate's single declaration initializes it with its slice.
|
||||
if (value.kind === "varDecl" && value.init !== null && initializers.has(value.localId))
|
||||
return everyExprChild(value.init, expr, stmt);
|
||||
if (value.kind === "switch" && value.disc.type.kind === "string") {
|
||||
lent(value.disc);
|
||||
for (const entry of value.cases) {
|
||||
if (entry.test) expr(entry.test);
|
||||
entry.body.forEach(stmt);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return everyStmtChild(value, expr, stmt);
|
||||
}
|
||||
fn.body.forEach(stmt);
|
||||
for (const node of owned) scan.scratch.delete(node);
|
||||
return scan;
|
||||
}
|
||||
|
||||
/** String parameters that every body reads only in lending positions,
|
||||
* including lent arguments of other such parameters: a caller may pass a
|
||||
* scratch slice to them. Start from every borrowed (unchanged) string
|
||||
* parameter of a synchronous function and drop violations until nothing changes, so
|
||||
* recursion through a lent parameter stays lent. A coroutine can keep its
|
||||
* parameters past the caller's frame, so it never lends. */
|
||||
export function findLentStringParameters(
|
||||
functions: ReadonlyMap<string, IrFunction>,
|
||||
lifetimes: CallLifetimes,
|
||||
): Map<string, Set<number>> {
|
||||
const lent = new Map<string, Set<number>>();
|
||||
for (const fn of functions.values()) {
|
||||
if (fn.async || fn.generator) continue;
|
||||
// Borrowed parameters are the unchanged ones nothing redeclares.
|
||||
const stable = lifetimes.borrowed.get(fn.name);
|
||||
const indexes = new Set<number>();
|
||||
fn.params.forEach((param, index) => {
|
||||
if (param.type.kind === "string" && stable?.has(index)) indexes.add(index);
|
||||
});
|
||||
if (indexes.size > 0) lent.set(fn.name, indexes);
|
||||
}
|
||||
const callers = new Map<string, Set<string>>();
|
||||
const pending = new Set(lent.keys());
|
||||
while (pending.size > 0) {
|
||||
const name = pending.values().next().value as string;
|
||||
pending.delete(name);
|
||||
const indexes = lent.get(name);
|
||||
const fn = functions.get(name);
|
||||
if (!indexes || !fn) continue;
|
||||
const ids = new Set([...indexes].map((index) => fn.params[index]!.localId));
|
||||
const scan = scanLending(fn, ids, new Map(), lent);
|
||||
for (const callee of scan.callees) {
|
||||
let set = callers.get(callee);
|
||||
if (!set) callers.set(callee, (set = new Set()));
|
||||
set.add(name);
|
||||
}
|
||||
let changed = false;
|
||||
for (const index of [...indexes]) {
|
||||
if (!scan.invalid.has(fn.params[index]!.localId)) continue;
|
||||
indexes.delete(index);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) continue;
|
||||
if (indexes.size === 0) lent.delete(name);
|
||||
const affected = callers.get(name);
|
||||
if (affected) for (const caller of affected) if (lent.has(caller)) pending.add(caller);
|
||||
}
|
||||
return lent;
|
||||
}
|
||||
|
||||
/** Slices whose every reader borrows them are written into a stack buffer
|
||||
* instead of a heap allocation (scr_str_slice_scratch): a slice operand of
|
||||
* a lending consumer, and the initializer of a stable local read only by
|
||||
* lending consumers. The copy is ASCII, bounded, and immortal for retain and
|
||||
* release, so it neither pins its source nor needs to be freed; longer or
|
||||
* non-ASCII results keep the ordinary owned string. Each site owns its
|
||||
* buffer: a site runs again only after its previous value is dead, since
|
||||
* nothing can keep it. Coroutine frames do not keep stack buffers across a
|
||||
* suspension, so they are left alone. */
|
||||
export function findScratchStringSlices(
|
||||
fn: IrFunction,
|
||||
lifetimes: CallLifetimes,
|
||||
scalar: ReadonlyMap<string, Slice>,
|
||||
parameters: LentStringParameters = new Map(),
|
||||
): Set<IrExpr> {
|
||||
if (fn.async || fn.generator) return new Set();
|
||||
const bindings = lifetimes.bindings.get(fn.name);
|
||||
const locals = new Map<string, Slice>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (stmt) => {
|
||||
if (
|
||||
stmt.kind === "varDecl" &&
|
||||
bindings?.has(stmt.localId) &&
|
||||
!scalar.has(stmt.localId) &&
|
||||
stmt.init !== null &&
|
||||
isSlice(stmt.init)
|
||||
)
|
||||
locals.set(stmt.localId, stmt.init);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const scan = scanLending(fn, new Set(locals.keys()), locals, parameters);
|
||||
for (const [id, init] of locals) if (!scan.invalid.has(id)) scan.scratch.add(init);
|
||||
return scan.scratch;
|
||||
}
|
||||
|
||||
/** Write a scratch slice into its own stack buffer. The result is owned:
|
||||
* releasing the scratch copy does nothing, and a heap fallback is freed. */
|
||||
export function emitScratchStringSlice(
|
||||
host: LlvmEmitterContext,
|
||||
slice: Slice,
|
||||
receiver: LlValue,
|
||||
args: readonly LlValue[],
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const buffer = B.slot();
|
||||
B.entryAllocas.push(`${buffer} = alloca [${SCRATCH_SLICE_BYTES} x i8], align 8`);
|
||||
host.declare(
|
||||
`declare ptr @scr_str_slice_scratch(ptr, double, double, i1 zeroext, ptr, ${host.sizeType})`,
|
||||
);
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = call ptr @scr_str_slice_scratch(ptr ${receiver.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${buffer}, ${host.sizeType} ${SCRATCH_SLICE_BYTES})`,
|
||||
);
|
||||
return host.own({ name: result, type: slice.type });
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
* helper generators (walkers, dyn). A tagged union is a `%ScrUnion` box; a
|
||||
* nullable union (nullable-unions.ts) is the arm pointer itself: NULL for
|
||||
* its NULL-encoded unit arm and, in `C | null | undefined`, the immortal
|
||||
* NULLABLE_NULL sentinel for `null`. Every structural read of a union value
|
||||
* NULLABLE_NULL sentinel for `null`. A shape union's value is likewise the
|
||||
* record pointer; its tag comes from the record's shape word. Every structural read of a union value
|
||||
* goes through here. */
|
||||
import type { BlockBuilder } from "./blocks.js";
|
||||
import { NULLABLE_NULL, type NullableUnion } from "./nullable-unions.js";
|
||||
import { NULLABLE_NULL, shapeTagFn, type PointerUnion } from "./nullable-unions.js";
|
||||
|
||||
/** The i32 tag of a union value. */
|
||||
export function emitUnionTag(B: BlockBuilder, nullable: NullableUnion | null, u: string): string {
|
||||
export function emitUnionTag(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
|
||||
const t = B.tmp();
|
||||
if (nullable?.repr === "shapes") {
|
||||
B.line(`${t} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`);
|
||||
return t;
|
||||
}
|
||||
if (nullable) {
|
||||
const isNull = B.tmp();
|
||||
B.line(`${isNull} = icmp eq ptr ${u}, null`);
|
||||
@@ -32,8 +37,13 @@ export function emitUnionTag(B: BlockBuilder, nullable: NullableUnion | null, u:
|
||||
|
||||
/** i1: a nullable union value holds its reference arm (neither NULL nor
|
||||
* the `null` sentinel). */
|
||||
export function emitNullablePresent(B: BlockBuilder, nullable: NullableUnion, u: string): string {
|
||||
export function emitNullablePresent(B: BlockBuilder, nullable: PointerUnion, u: string): string {
|
||||
const present = B.tmp();
|
||||
if (nullable.repr === "shapes" && nullable.unitTag < 0) {
|
||||
// No unit arm: every value is a record (NULL only as a pending dummy).
|
||||
B.line(`${present} = icmp ne ptr ${u}, null`);
|
||||
return present;
|
||||
}
|
||||
B.line(`${present} = icmp ne ptr ${u}, null`);
|
||||
if (nullable.nullTag < 0) return present;
|
||||
const notNullArm = B.tmp(),
|
||||
@@ -46,12 +56,18 @@ export function emitNullablePresent(B: BlockBuilder, nullable: NullableUnion, u:
|
||||
/** i1: a nullable union value holds the arm `tag` (`negated`: does not). */
|
||||
export function emitNullableIsTag(
|
||||
B: BlockBuilder,
|
||||
nullable: NullableUnion,
|
||||
nullable: PointerUnion,
|
||||
u: string,
|
||||
tag: number,
|
||||
negated: boolean,
|
||||
): string {
|
||||
if (tag === nullable.refTag) {
|
||||
if (nullable.repr === "shapes" && tag !== nullable.unitTag && tag !== nullable.nullTag) {
|
||||
const actual = emitUnionTag(B, nullable, u),
|
||||
t = B.tmp();
|
||||
B.line(`${t} = icmp ${negated ? "ne" : "eq"} i32 ${actual}, ${tag}`);
|
||||
return t;
|
||||
}
|
||||
if (nullable.repr === "single" && tag === nullable.refTag) {
|
||||
const present = emitNullablePresent(B, nullable, u);
|
||||
if (!negated) return present;
|
||||
const t = B.tmp();
|
||||
@@ -65,7 +81,7 @@ export function emitNullableIsTag(
|
||||
}
|
||||
|
||||
/** The BORROWED payload pointer of a reference arm. */
|
||||
export function emitUnionPeek(B: BlockBuilder, nullable: NullableUnion | null, u: string): string {
|
||||
export function emitUnionPeek(B: BlockBuilder, nullable: PointerUnion | null, u: string): string {
|
||||
if (nullable) return u;
|
||||
const p = B.tmp();
|
||||
const t = B.tmp();
|
||||
@@ -76,7 +92,9 @@ export function emitUnionPeek(B: BlockBuilder, nullable: NullableUnion | null, u
|
||||
|
||||
/** Text-template forms for helpers written as raw LLVM lines: `%out`
|
||||
* receives the tag / payload of `u`. */
|
||||
export function unionTagLines(nullable: NullableUnion | null, u: string, out: string): string[] {
|
||||
export function unionTagLines(nullable: PointerUnion | null, u: string, out: string): string[] {
|
||||
if (nullable?.repr === "shapes")
|
||||
return [` ${out} = call i32 ${shapeTagFn(nullable.unionId)}(ptr ${u})`];
|
||||
if (nullable && nullable.nullTag < 0)
|
||||
return [
|
||||
` ${out}.isnull = icmp eq ptr ${u}, null`,
|
||||
@@ -95,7 +113,7 @@ export function unionTagLines(nullable: NullableUnion | null, u: string, out: st
|
||||
];
|
||||
}
|
||||
|
||||
export function unionPeekLines(nullable: NullableUnion | null, u: string, out: string): string[] {
|
||||
export function unionPeekLines(nullable: PointerUnion | null, u: string, out: string): string[] {
|
||||
// A plain copy of the pointer (NULL included) under the template's name.
|
||||
if (nullable) return [` ${out} = getelementptr i8, ptr ${u}, i64 0`];
|
||||
return [
|
||||
|
||||
@@ -183,7 +183,9 @@ test("a nullable-pointer source rebuilds the target box", () => {
|
||||
expect(retag).not.toContain("retype.share");
|
||||
});
|
||||
|
||||
async function llvmFor(source: string): Promise<{ module: IrModule; ir: string }> {
|
||||
/** `boxed`: keep every union a tagged box (record unions otherwise become
|
||||
* shape unions, which retype without any box). */
|
||||
async function llvmFor(source: string, boxed = true): Promise<{ module: IrModule; ir: string }> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-union-retype-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts"),
|
||||
@@ -194,7 +196,7 @@ async function llvmFor(source: string): Promise<{ module: IrModule; ir: string }
|
||||
throw new Error(result.diagnostics.map((item) => `${item.code}: ${item.message}`).join("\n"));
|
||||
const module = deserializeModule(await readFile(output, "utf8"));
|
||||
expect(validateModule(module)).toEqual([]);
|
||||
return { module, ir: emitLlvmModule(module) };
|
||||
return { module, ir: emitLlvmModule(boxed ? keepUnionsBoxed(module) : module) };
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -253,3 +255,16 @@ console.log(leafValue(parsed));
|
||||
expect(stored).not.toBeNull();
|
||||
expect(ir).toContain(`load i8, ptr ${stored![1]}`);
|
||||
});
|
||||
|
||||
test("record unions retype as the record pointer itself", async () => {
|
||||
const { module, ir } = await llvmFor(VISITOR, false);
|
||||
const helpers = module.functions.filter((fn) => fn.name.startsWith("%union.retag."));
|
||||
expect(helpers.length).toBeGreaterThan(0);
|
||||
for (const helper of helpers) {
|
||||
const symbol = `sc_bf__x25_union_retag_${helper.name.slice("%union.retag.".length)}`;
|
||||
const retag = body(ir, symbol);
|
||||
expect(retag).not.toMatch(/@scr_union_new|retype\.share|sc_retype_guard/);
|
||||
}
|
||||
// No record of the visitor is ever boxed.
|
||||
expect(ir).not.toMatch(/call ptr @scr_union_new_ref/);
|
||||
});
|
||||
|
||||
@@ -284,7 +284,8 @@ export class LlWalkers {
|
||||
throw new InternalCompilerError(
|
||||
`llvm emitter bug: jsonStringify of unknown shape ${shapeId}`,
|
||||
);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
|
||||
if (recordMayKeepSource(shape)) this.emitRecordSourceWriter(B, shapeId);
|
||||
// CYCLE-CAPABLE shapes bracket the walk with the circular-detection
|
||||
// stack; edge labels stamp before members whose walk can re-enter —
|
||||
@@ -437,7 +438,8 @@ export class LlWalkers {
|
||||
throw new InternalCompilerError(`llvm emitter bug: field writer of unknown shape ${shapeId}`);
|
||||
const name = `sc_jwf_${this.recordFieldWriters.size}`;
|
||||
this.recordFieldWriters.set(shapeId, name);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + 1]));
|
||||
const fieldBase = this.host.nullableUnions.fieldBase(shapeId);
|
||||
const fieldIndex = new Map(shape.fields.map((f, i) => [f.name, i + fieldBase]));
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
const cyclic = traceAdapter(this.host, { kind: "record", shapeId }) !== null;
|
||||
const order = recordSourceFieldOrder(shape);
|
||||
@@ -534,7 +536,7 @@ export class LlWalkers {
|
||||
const ovfp = B.tmp();
|
||||
const ovf = B.tmp();
|
||||
B.line(
|
||||
`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + 1}`,
|
||||
`${ovfp} = getelementptr inbounds %${mangleRecordStruct(shape.id)}, ptr %v, i64 0, i32 ${shape.fields.length + this.host.nullableUnions.fieldBase(shape.id)}`,
|
||||
);
|
||||
B.line(`${ovf} = load ptr, ptr ${ovfp} ; overflow map`);
|
||||
if (iv.kind === "dyn") {
|
||||
|
||||
@@ -167,7 +167,7 @@ export interface CcOptions {
|
||||
* on the IR): compiles scr_dgram.c into the binary — the net gating
|
||||
* precedent, so dgram-free binaries keep their exact link line. */
|
||||
dgram?: boolean;
|
||||
/** The program uses node:dns/promises (the dns.lookupPromise libCalls):
|
||||
/** The program uses node:dns/promises (the dns.promisesLookup libCalls):
|
||||
* compiles scr_dns_promises.c over the dgram unit, so callback-only
|
||||
* binaries keep their exact link line. */
|
||||
dnsPromises?: boolean;
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
coverageEnvelope,
|
||||
coveragePasses,
|
||||
renderCoverage,
|
||||
renderExplainDynamic,
|
||||
type CoverageFailOn,
|
||||
} from "../coverage/report.js";
|
||||
import { TIER_MODES, type TierMode } from "../frontend/tier-analysis.js";
|
||||
import { buildEnvelope } from "../diagnostics/envelope.js";
|
||||
import { renderWarnings } from "../diagnostics/render.js";
|
||||
import { isNativeOptimization } from "../backend/optimization.js";
|
||||
@@ -45,8 +47,15 @@ function parseCli(
|
||||
): ReturnType<
|
||||
typeof parseArgs<{ options: typeof CLI_OPTIONS; allowPositionals: true; allowNegative: true }>
|
||||
> {
|
||||
// `--explain-dynamic` takes an optional value: alone it lists every unit.
|
||||
const normalized = args.map((arg) => (arg === "--explain-dynamic" ? "--explain-dynamic=" : arg));
|
||||
try {
|
||||
return parseArgs({ args, options: CLI_OPTIONS, allowPositionals: true, allowNegative: true });
|
||||
return parseArgs({
|
||||
args: normalized,
|
||||
options: CLI_OPTIONS,
|
||||
allowPositionals: true,
|
||||
allowNegative: true,
|
||||
});
|
||||
} catch (err) {
|
||||
// parseArgs appends a paragraph about `--` and positionals to the
|
||||
// unknown-option message; the first sentence is the part that names
|
||||
@@ -91,6 +100,8 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
externalTypeArgs.length > 0 ||
|
||||
values.out !== undefined ||
|
||||
values["emit-ir"] ||
|
||||
values.tier !== undefined ||
|
||||
values["explain-dynamic"] !== undefined ||
|
||||
!values["keep-llvm"]
|
||||
) {
|
||||
fail(
|
||||
@@ -152,10 +163,12 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
values["windows-subsystem"] !== undefined ||
|
||||
values["detached-exit"] ||
|
||||
(values["npm-static"] ?? []).length > 0 ||
|
||||
externalTypeArgs.length > 0
|
||||
externalTypeArgs.length > 0 ||
|
||||
values.tier !== undefined ||
|
||||
values["explain-dynamic"] !== undefined
|
||||
) {
|
||||
fail(
|
||||
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--npm-static/--ffi/--external-types: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, and external type mappings belong to coverage",
|
||||
"scriptc build --lib takes no --backend/--emit/--print/--optimization/--strip/--windows-subsystem/--detached-exit/--npm-static/--ffi/--external-types/--tier/--explain-dynamic: the profile pins the emission and optimization, npm imports are judged automatically, outbound FFI belongs to executable builds, external type mappings belong to coverage, and library artifacts are static-tier only",
|
||||
);
|
||||
}
|
||||
const profilePath = resolve(profileArg);
|
||||
@@ -223,6 +236,15 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
fail(`--fail-on is a coverage option (a build already fails on blockers)\n\n${USAGE}`);
|
||||
}
|
||||
const failOn = failOnRaw as CoverageFailOn | undefined;
|
||||
const tierRaw = values.tier;
|
||||
if (tierRaw !== undefined && !(TIER_MODES as readonly string[]).includes(tierRaw)) {
|
||||
fail(`unknown --tier value "${tierRaw}" (supported: ${TIER_MODES.join(", ")})\n\n${USAGE}`);
|
||||
}
|
||||
const tier = tierRaw as TierMode | undefined;
|
||||
const explainDynamic = values["explain-dynamic"];
|
||||
if (explainDynamic !== undefined && command !== "coverage") {
|
||||
fail(`--explain-dynamic is a coverage option\n\n${USAGE}`);
|
||||
}
|
||||
if (printNativeLinkInfo && command !== "build") {
|
||||
fail(`--print=native-link-info is a build option\n\n${USAGE}`);
|
||||
}
|
||||
@@ -340,6 +362,8 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(npmStatic !== undefined ? { npmStatic } : {}),
|
||||
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
|
||||
...(Object.keys(externalTypes).length > 0 ? { externalTypes } : {}),
|
||||
...(tier !== undefined ? { tier } : {}),
|
||||
...(explainDynamic !== undefined ? { explainDynamic: true } : {}),
|
||||
});
|
||||
if (printDiagnostics) {
|
||||
// Keep stdout pure JSON for tooling.
|
||||
@@ -354,6 +378,18 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
process.stdout.write(
|
||||
renderCoverage(coverage, { color, sourceTexts, root: process.cwd() }) + "\n",
|
||||
);
|
||||
if (explainDynamic !== undefined && coverage.tiers !== undefined) {
|
||||
process.stdout.write(
|
||||
"\n" +
|
||||
renderExplainDynamic(coverage.tiers, {
|
||||
color,
|
||||
sourceTexts,
|
||||
root: process.cwd(),
|
||||
query: explainDynamic,
|
||||
}) +
|
||||
"\n",
|
||||
);
|
||||
}
|
||||
}
|
||||
return coveragePasses(coverage, failOn) ? 0 : 1;
|
||||
}
|
||||
@@ -386,6 +422,7 @@ async function main(args: string[], host: CliHost): Promise<number> {
|
||||
...(npmStatic !== undefined ? { npmStatic } : {}),
|
||||
...(ffiProfilePath !== undefined ? { ffiProfilePath } : {}),
|
||||
...(printNativeLinkInfo ? { nativeLinkInfo: true } : {}),
|
||||
...(tier !== undefined ? { tier } : {}),
|
||||
});
|
||||
if (!result.ok) {
|
||||
if (printDiagnostics) {
|
||||
|
||||
@@ -69,6 +69,16 @@ Options:
|
||||
local declaration file. The declaration supplies types
|
||||
for analysis; the host module remains an explicit
|
||||
external-boundary blocker (repeatable)
|
||||
--tier <auto|static-only>
|
||||
tier policy (default: auto). static-only keeps every
|
||||
construct without a static lowering a compile error, so
|
||||
the program has no dynamic-tier demotions. Both modes
|
||||
currently compile identically
|
||||
--explain-dynamic[=<name>]
|
||||
coverage only: list the functions that operate on dynamic
|
||||
values and the bindings, fields, functions, classes and
|
||||
modules the dynamic tier would take over, with the
|
||||
refusal behind each; a name shows only that unit's chain
|
||||
-h, --help show this help
|
||||
-v, --version print the version
|
||||
`;
|
||||
@@ -90,6 +100,8 @@ export const CLI_OPTIONS = {
|
||||
"npm-static": { type: "string", multiple: true },
|
||||
"provenance-sources": { type: "boolean", default: false },
|
||||
"external-types": { type: "string", multiple: true },
|
||||
tier: { type: "string" },
|
||||
"explain-dynamic": { type: "string" },
|
||||
lib: { type: "boolean", default: false },
|
||||
profile: { type: "string" },
|
||||
help: { type: "boolean", short: "h", default: false },
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { NativeLinkInfo } from "./backend/native-link-info.js";
|
||||
import type { NativeOptimization } from "./backend/optimization.js";
|
||||
import type { ScrDiagnostic } from "./diagnostics/diagnostic.js";
|
||||
import type { CoverageInput } from "./coverage/report.js";
|
||||
import type { TierMode } from "./frontend/tier-analysis.js";
|
||||
|
||||
export type CompileOutputKind = "ir" | "llvm" | "asm" | "obj" | "exe";
|
||||
|
||||
@@ -45,6 +46,10 @@ export interface CompileBaseOptions {
|
||||
/** Attach the machine-readable external link recipe to an object result.
|
||||
* Valid only with outputKind "obj"; it never invokes a linker. */
|
||||
nativeLinkInfo?: boolean;
|
||||
/** `--tier` (frontend/tier-analysis.ts). "static-only" keeps every demotion
|
||||
* request a refusal (design invariant I2). Until demotion lands both modes
|
||||
* compile exactly as before. */
|
||||
tier?: TierMode;
|
||||
}
|
||||
|
||||
/** Executable compile options. This remains the compatibility type for the
|
||||
@@ -161,6 +166,11 @@ export interface AnalyzeOptions {
|
||||
* to analyze project code, but imported runtime values remain explicit
|
||||
* SC1010 blockers rather than being counted as executable. */
|
||||
externalTypes?: Readonly<Record<string, string>>;
|
||||
/** `--tier` (see CompileBaseOptions.tier). */
|
||||
tier?: TierMode;
|
||||
/** `--explain-dynamic`: attach the tier-assignment report
|
||||
* (CoverageInput.tiers). Such analyses bypass the verdict cache. */
|
||||
explainDynamic?: boolean;
|
||||
}
|
||||
|
||||
export interface AnalyzeResult {
|
||||
|
||||
@@ -17,10 +17,13 @@ import {
|
||||
type DiagnosticsEnvelope,
|
||||
type LocatedDiagnostic,
|
||||
type SourcePosition,
|
||||
type TierUnitPosition,
|
||||
type TiersEnvelope,
|
||||
} from "../diagnostics/envelope.js";
|
||||
import { renderDiagnostics } from "../diagnostics/render.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import type { ProvenanceSources } from "../frontend/provenance-registry.js";
|
||||
import type { DemotionUnit, TierReport } from "../frontend/tier-analysis.js";
|
||||
|
||||
export interface CoverageInput {
|
||||
file: string;
|
||||
@@ -51,6 +54,9 @@ export interface CoverageInput {
|
||||
/** --provenance-sources: diagnostics of elided pure-annotated dead
|
||||
* consts (off the build; the provenance section names them). */
|
||||
provenanceElided?: ScrDiagnostic[];
|
||||
/** --explain-dynamic: the report-only tier assignment (which bindings,
|
||||
* fields, functions, classes and modules the dynamic tier would take). */
|
||||
tiers?: TierReport;
|
||||
/** True when tsc preflight failed — no coverage can be computed. */
|
||||
preflightFailed: boolean;
|
||||
}
|
||||
@@ -180,6 +186,12 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
|
||||
|
||||
out.push(` statements analyzed ${total}`);
|
||||
out.push(` compile statically ${ok} ${c(pctColor, c(BOLD, `(${pct}%)`))}`);
|
||||
const dynamic = dynamicStatements(input);
|
||||
if (dynamic > 0) {
|
||||
out.push(
|
||||
` in dynamic tier ${dynamic} ${c(DIM, "(operate on values typed only at runtime; --explain-dynamic lists them)")}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (skipped > 0) {
|
||||
out.push(
|
||||
@@ -432,13 +444,147 @@ export function coverageEnvelope(
|
||||
stats: {
|
||||
statementsTotal: total,
|
||||
statementsStatic: ok,
|
||||
statementsDynamic: 0,
|
||||
statementsDynamic: dynamicStatements(input),
|
||||
statementsFailed: failed,
|
||||
functionsSkipped: skipped,
|
||||
percentStatic: total === 0 ? 100 : Math.floor((ok / total) * 100),
|
||||
},
|
||||
}),
|
||||
...(input.tiers !== undefined ? { tiers: tiersEnvelope(input.tiers, locator) } : {}),
|
||||
groups,
|
||||
diagnostics: located,
|
||||
};
|
||||
}
|
||||
|
||||
/** Reached statements in dynamic-tier functions, never more than the
|
||||
* compiled statements they are part of. */
|
||||
function dynamicStatements(input: CoverageInput): number {
|
||||
const compiled = input.stats.statementsTotal - input.stats.statementsFailed;
|
||||
return Math.max(0, Math.min(input.stats.statementsDynamic ?? 0, compiled));
|
||||
}
|
||||
|
||||
function tiersEnvelope(tiers: TierReport, locator: SourceLocator): TiersEnvelope {
|
||||
const at = (loc: { file: string; start: number }): SourcePosition => ({
|
||||
file: loc.file,
|
||||
...locator.position(loc.file, loc.start),
|
||||
});
|
||||
const unit = (u: DemotionUnit): TierUnitPosition => ({
|
||||
kind: u.kind,
|
||||
name: u.name,
|
||||
...(u.owner !== undefined ? { owner: u.owner } : {}),
|
||||
...at(u.loc),
|
||||
});
|
||||
return {
|
||||
mode: tiers.mode,
|
||||
statementsDynamic: tiers.statementsDynamic,
|
||||
dynamicFunctions: tiers.dynamicFunctions.map((fn) => ({
|
||||
name: fn.name,
|
||||
...at(fn.loc),
|
||||
operation: fn.operation,
|
||||
operationAt: at(fn.operationLoc),
|
||||
statements: fn.statements,
|
||||
})),
|
||||
requests: tiers.requests.map((r) => ({
|
||||
unit: unit(r.unit),
|
||||
enclosing: unit(r.enclosing),
|
||||
seed: r.seed,
|
||||
scope: r.scope,
|
||||
code: r.code,
|
||||
message: r.message,
|
||||
...at(r.loc),
|
||||
})),
|
||||
units: tiers.units.map((u) => ({ ...unit(u), requests: u.requests, sites: u.sites })),
|
||||
};
|
||||
}
|
||||
|
||||
/** IR function names as a user reads them: `%init.3` is a module's top
|
||||
* level, `%Point.norm` the method `Point.norm`; generated helpers keep
|
||||
* their `%`-prefixed names. */
|
||||
function functionDisplayName(name: string): string {
|
||||
if (name.startsWith("%init.")) return "<top level>";
|
||||
if (/^%[A-Za-z_$][\w$]*\.[A-Za-z_$#][\w$]*$/.test(name)) return name.slice(1);
|
||||
return name;
|
||||
}
|
||||
|
||||
function unitLabel(u: DemotionUnit, root: string | undefined): string {
|
||||
if (u.kind === "module") return `module ${displayPath(u.name, root)}`;
|
||||
if (u.kind === "binding" && u.owner !== undefined) return `binding ${u.name} in ${u.owner}`;
|
||||
return `${u.kind} ${u.name}`;
|
||||
}
|
||||
|
||||
function unitMatches(u: DemotionUnit, query: string): boolean {
|
||||
return (
|
||||
u.name === query ||
|
||||
u.name.endsWith(`.${query}`) ||
|
||||
u.name.split(" ")[0] === query ||
|
||||
`${u.owner ?? ""}:${u.name}` === query
|
||||
);
|
||||
}
|
||||
|
||||
/** `scriptc coverage --explain-dynamic[=<unit>]`: the dynamic tier today
|
||||
* and the demotion chains the report-only analysis computed. With a query,
|
||||
* only units whose name matches it (`x`, `Point.x`, `parse:x`). */
|
||||
export function renderExplainDynamic(
|
||||
tiers: TierReport,
|
||||
opts: CoverageRenderOptions & { query?: string } = {},
|
||||
): string {
|
||||
const c = (code: string, s: string) => (opts.color ? code + s + RESET : s);
|
||||
const locator = new SourceLocator(opts.sourceTexts);
|
||||
const where = (loc: { file: string; start: number }) => {
|
||||
const p = locator.position(loc.file, loc.start);
|
||||
return `${displayPath(loc.file, opts.root)}:${p.line}:${p.column}`;
|
||||
};
|
||||
const query = opts.query !== undefined && opts.query !== "" ? opts.query : undefined;
|
||||
const out: string[] = [];
|
||||
out.push(
|
||||
` ${c(BOLD, "dynamic tier")} ${c(DIM, `(tier assignment, report-only; --tier=${tiers.mode})`)}`,
|
||||
);
|
||||
if (query === undefined) {
|
||||
const fns = tiers.dynamicFunctions;
|
||||
out.push(
|
||||
` ${fns.length} function${fns.length === 1 ? "" : "s"} already operate${fns.length === 1 ? "s" : ""} on dynamic values ${c(DIM, `(${tiers.statementsDynamic} statement${tiers.statementsDynamic === 1 ? "" : "s"})`)}`,
|
||||
);
|
||||
for (const fn of fns) {
|
||||
out.push(
|
||||
` ${functionDisplayName(fn.name)} ${c(DIM, `${where(fn.loc)}: ${fn.operation} at ${where(fn.operationLoc)}`)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const live = tiers.requests.filter((r) => r.scope !== "unreached" && r.seed !== "excluded");
|
||||
const excluded = tiers.requests.filter((r) => r.seed === "excluded").length;
|
||||
const unreached = tiers.requests.filter((r) => r.scope === "unreached").length;
|
||||
if (query === undefined) {
|
||||
out.push("");
|
||||
out.push(
|
||||
` ${live.length} demotion request${live.length === 1 ? "" : "s"} ${c(DIM, "(refusals and fences the dynamic tier would replace)")}` +
|
||||
(unreached > 0 ? c(DIM, `, ${unreached} more in unreached code`) : "") +
|
||||
(excluded > 0 ? c(DIM, `, ${excluded} excluded by policy`) : ""),
|
||||
);
|
||||
}
|
||||
const units = tiers.units.filter((u) => query === undefined || unitMatches(u, query));
|
||||
if (query !== undefined && units.length === 0) {
|
||||
out.push(` no demoted unit matches '${query}'`);
|
||||
return out.join("\n");
|
||||
}
|
||||
for (const u of units) {
|
||||
const counts = [
|
||||
u.requests > 0 ? `${u.requests} request${u.requests === 1 ? "" : "s"}` : "",
|
||||
u.sites > 0 ? `${u.sites} site${u.sites === 1 ? "" : "s"} inside` : "",
|
||||
]
|
||||
.filter((s) => s !== "")
|
||||
.join(", ");
|
||||
out.push(` ${unitLabel(u, opts.root)} ${c(DIM, `${where(u.loc)} (${counts})`)}`);
|
||||
if (query === undefined && u.requests === 0) continue;
|
||||
for (const r of live) {
|
||||
if (r.unit.kind !== u.kind || r.unit.name !== u.name || r.unit.loc.start !== u.loc.start)
|
||||
continue;
|
||||
if (r.unit.loc.file !== u.loc.file) continue;
|
||||
out.push(
|
||||
` ${c(DIM, "<-")} ${r.code} ${r.seed} at ${where(r.loc)}: ${capMessage(r.message)}`,
|
||||
);
|
||||
if (r.enclosing.name !== r.unit.name || r.enclosing.kind !== r.unit.kind)
|
||||
out.push(` ${c(DIM, "->")} ${unitLabel(r.enclosing, opts.root)} turns dynamic`);
|
||||
}
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ export function verdictKey(
|
||||
opts: AnalyzeOptions,
|
||||
cache: VerdictCacheOptions,
|
||||
): string | null {
|
||||
// The tier report carries source locations the semantic replay does not
|
||||
// move; explaining analyses always lower.
|
||||
if (opts.explainDynamic === true) return null;
|
||||
let ffi = "";
|
||||
if (opts.ffiProfilePath !== undefined) {
|
||||
try {
|
||||
@@ -120,6 +123,8 @@ export function verdictKey(
|
||||
opts.ffiProfilePath ?? "",
|
||||
ffi,
|
||||
external.join("\n"),
|
||||
// Demotion (stage S6) makes the tier mode part of the verdict.
|
||||
...(opts.tier === "static-only" ? ["tier=static-only"] : []),
|
||||
].join("\0"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,34 @@ export interface DiagnosticGroup {
|
||||
sites: SourcePosition[];
|
||||
}
|
||||
|
||||
export interface TierUnitPosition extends SourcePosition {
|
||||
kind: "binding" | "field" | "function" | "class" | "module";
|
||||
name: string;
|
||||
owner?: string;
|
||||
}
|
||||
|
||||
/** `scriptc coverage --explain-dynamic --print=diagnostics`: the tier
|
||||
* assignment of frontend/tier-analysis.ts with source positions. */
|
||||
export interface TiersEnvelope {
|
||||
mode: "auto" | "static-only";
|
||||
statementsDynamic: number;
|
||||
dynamicFunctions: (SourcePosition & {
|
||||
name: string;
|
||||
operation: string;
|
||||
operationAt: SourcePosition;
|
||||
statements: number;
|
||||
})[];
|
||||
requests: (SourcePosition & {
|
||||
unit: TierUnitPosition;
|
||||
enclosing: TierUnitPosition;
|
||||
seed: string;
|
||||
scope: string;
|
||||
code: string;
|
||||
message: string;
|
||||
})[];
|
||||
units: (TierUnitPosition & { requests: number; sites: number })[];
|
||||
}
|
||||
|
||||
export interface DiagnosticsEnvelope {
|
||||
schema: typeof DIAGNOSTICS_SCHEMA;
|
||||
schemaVersion: typeof DIAGNOSTICS_SCHEMA_VERSION;
|
||||
@@ -97,8 +125,10 @@ export interface DiagnosticsEnvelope {
|
||||
stats?: {
|
||||
statementsTotal: number;
|
||||
statementsStatic: number;
|
||||
/** Always 0: every compiled statement is native. Kept so schema
|
||||
* version 1 consumers keep reading the same fields. */
|
||||
/** The compiled statements (counted in statementsStatic, which covers
|
||||
* every statement that compiles to native code) that belong to
|
||||
* dynamic-tier functions: code operating on values whose types are only
|
||||
* known at runtime. */
|
||||
statementsDynamic: number;
|
||||
statementsFailed: number;
|
||||
functionsSkipped: number;
|
||||
@@ -106,6 +136,8 @@ export interface DiagnosticsEnvelope {
|
||||
};
|
||||
/** build only: the produced artifact path. */
|
||||
artifact?: string;
|
||||
/** coverage --explain-dynamic only: the report-only tier assignment. */
|
||||
tiers?: TiersEnvelope;
|
||||
groups: DiagnosticGroup[];
|
||||
diagnostics: LocatedDiagnostic[];
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ export function prepareExecutableModule(
|
||||
lowered = fe.lower({
|
||||
targetPlatform: buildPlatform,
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
|
||||
});
|
||||
timing("lower");
|
||||
} catch (e) {
|
||||
|
||||
@@ -90,6 +90,8 @@ export function analyzeWithFrontend(
|
||||
coverage: true,
|
||||
targetPlatform: buildPlatform,
|
||||
...(ffi !== null ? { ffiImports: ffi.functions } : {}),
|
||||
...(opts.tier !== undefined ? { tierMode: opts.tier } : {}),
|
||||
...(opts.explainDynamic === true ? { tierReport: { preflight } } : {}),
|
||||
});
|
||||
const provenance = provenanceSources();
|
||||
return {
|
||||
@@ -109,6 +111,7 @@ export function analyzeWithFrontend(
|
||||
...(provenance !== null ? { provenance } : {}),
|
||||
...(lowered.statsByFile ? { statsByFile: lowered.statsByFile } : {}),
|
||||
...(lowered.provenanceElided ? { provenanceElided: lowered.provenanceElided } : {}),
|
||||
...(lowered.tiers !== undefined ? { tiers: lowered.tiers } : {}),
|
||||
preflightFailed: false,
|
||||
},
|
||||
sourceTexts: fe.sourceTexts(),
|
||||
|
||||
@@ -4,6 +4,8 @@ import { isJsSourceFile } from "../../program.js";
|
||||
import { builtinModuleFnOf } from "../surfaces.js";
|
||||
import { registerHttpClientFnBinding } from "../lower-server.js";
|
||||
import { isPromisifiedZlibMember } from "./compression.js";
|
||||
import { isPromisifiedCryptoMember } from "./crypto.js";
|
||||
import { isPromisifiedDnsMember } from "../lower-dgram.js";
|
||||
|
||||
/** Compile-time util.promisify projections. The immutable result binding
|
||||
* owns no runtime slot: calls rewrite to the target's promise lowering,
|
||||
@@ -157,12 +159,16 @@ export function registerPromisifiedBuiltinDecl(
|
||||
}
|
||||
: target?.module === "zlib" && isPromisifiedZlibMember(target.member)
|
||||
? { kind: "promisified-zlib" as const, member: target.member }
|
||||
: null;
|
||||
: target?.module === "crypto" && isPromisifiedCryptoMember(target.member)
|
||||
? { kind: "promisified-crypto" as const, member: target.member }
|
||||
: target?.module === "dns" && isPromisifiedDnsMember(target.member)
|
||||
? { kind: "promisified-dns" as const, member: target.member }
|
||||
: null;
|
||||
if (!projection) {
|
||||
lowerer.noLowering(
|
||||
"util.promisify of this target",
|
||||
argNode ?? e,
|
||||
"child_process.exec, child_process.execFile, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots and the fs callback functions with an fs/promises counterpart are the supported targets",
|
||||
"child_process.exec, child_process.execFile, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions (randomBytes, pbkdf2, scrypt, hkdf, randomFill, randomInt, generateKey), dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart are the supported targets",
|
||||
);
|
||||
}
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { lowerBuiltinByteInput, errorFirstBytesCallback } from "./arguments.js";
|
||||
import { dynUndefinedExpr, numLit } from "../../../ir/build.js";
|
||||
import { dynUndefinedExpr, numLit, varRef } from "../../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { isNodeTypesPath } from "../../dts-paths.js";
|
||||
import {
|
||||
BOOL,
|
||||
BYTES_U8,
|
||||
DYN,
|
||||
F64,
|
||||
@@ -39,7 +40,8 @@ const ZLIB_CALLBACK_FNS: Readonly<Record<string, IrLibFn | undefined>> = {
|
||||
unzip: "zlib.unzipCb",
|
||||
};
|
||||
|
||||
const ZLIB_STREAM_T: IrType = { kind: "object", className: "%Transform" };
|
||||
const ZLIB_STREAM_CLASS = "%Transform";
|
||||
const ZLIB_STREAM_T: IrType = { kind: "object", className: ZLIB_STREAM_CLASS };
|
||||
|
||||
/** A zlib data argument, lowered once: Buffer/Uint8Array bytes, a string
|
||||
* (UTF-8 encoded here), or a checked-dynamic value (the JS lane's untyped
|
||||
@@ -130,6 +132,8 @@ export function zlibStreamModeOf(
|
||||
symbol: ts.Symbol | null | undefined,
|
||||
): number | null {
|
||||
if (!symbol) return null;
|
||||
const aliasMode = zlibClassAliasModeOf(lowerer, symbol, 0);
|
||||
if (aliasMode !== undefined) return aliasMode;
|
||||
const target =
|
||||
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
|
||||
const mode = ZLIB_STREAM_MODES.get(target.name);
|
||||
@@ -494,3 +498,145 @@ export function lowerZlibModuleCall(
|
||||
}
|
||||
throw new InternalCompilerError(`unhandled lowered zlib member ${bi.member}`);
|
||||
}
|
||||
|
||||
/** The zlib stream class an expression names (`zlib.Gzip`, an imported
|
||||
* `Gzip`, a `const C = zlib.Gzip` alias), as its node_zlib_mode, or null. */
|
||||
export function zlibClassModeOfExpr(lowerer: Lowerer, expr: ts.Expression): number | null {
|
||||
let e = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (ts.isIdentifier(e)) return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e));
|
||||
if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
|
||||
return zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(e.name));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The class name of a zlib stream mode (Node's constructor name). */
|
||||
export function zlibClassName(mode: number): string {
|
||||
for (const [name, m] of ZLIB_STREAM_MODES) if (m === mode) return name;
|
||||
return "Zlib";
|
||||
}
|
||||
|
||||
/** The mode of a `const C = zlib.Gzip` binding (or an alias of such a
|
||||
* binding), null when the symbol is a const binding of something else,
|
||||
* undefined when it is not a const binding. Resolved from the declaration,
|
||||
* so uses lowered before the declaration statement see the same answer. */
|
||||
function zlibClassAliasModeOf(
|
||||
lowerer: Lowerer,
|
||||
symbol: ts.Symbol,
|
||||
depth: number,
|
||||
): number | null | undefined {
|
||||
const cached = lowerer.zlibClassAliases.get(symbol);
|
||||
if (cached !== undefined) return cached;
|
||||
if ((symbol.flags & ts.SymbolFlags.BlockScopedVariable) === 0) return undefined;
|
||||
const decl = lowerer.checker.valueDeclarationOf(symbol);
|
||||
if (decl === undefined || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) {
|
||||
return undefined;
|
||||
}
|
||||
// const only: a var or let binding could be reassigned
|
||||
const list = decl.parent;
|
||||
if (!ts.isVariableDeclarationList(list) || (list.flags & ts.NodeFlags.Const) === 0) {
|
||||
return undefined;
|
||||
}
|
||||
let init = decl.initializer;
|
||||
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (init === undefined || depth > 16) return null;
|
||||
let mode: number | null = null;
|
||||
if (ts.isIdentifier(init)) {
|
||||
const next = lowerer.checker.getSymbolAtLocation(init);
|
||||
if (next !== undefined) {
|
||||
const nested = zlibClassAliasModeOf(lowerer, next, depth + 1);
|
||||
mode = nested === undefined ? zlibStreamModeOf(lowerer, next) : nested;
|
||||
}
|
||||
} else if (ts.isPropertyAccessExpression(init) && !init.questionDotToken) {
|
||||
mode = zlibStreamModeOf(lowerer, lowerer.checker.getSymbolAtLocation(init.name));
|
||||
}
|
||||
if (mode !== null) lowerer.zlibClassAliases.set(symbol, mode);
|
||||
return mode;
|
||||
}
|
||||
|
||||
/** True for `const C = zlib.Gzip` (or an alias of an alias): a
|
||||
* compile-time zlib class alias that owns no runtime slot, so the
|
||||
* statement lowering skips it. */
|
||||
export function registerZlibClassAlias(
|
||||
lowerer: Lowerer,
|
||||
nameNode: ts.Node,
|
||||
init: ts.Expression | undefined,
|
||||
): boolean {
|
||||
if (!ts.isIdentifier(nameNode) || init === undefined) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
return symbol !== undefined && zlibClassAliasOf(lowerer, symbol) !== null;
|
||||
}
|
||||
|
||||
/** The mode of a `const C = zlib.Gzip` alias symbol, or null. */
|
||||
export function zlibClassAliasOf(lowerer: Lowerer, symbol: ts.Symbol): number | null {
|
||||
return zlibClassAliasModeOf(lowerer, symbol, 0) ?? null;
|
||||
}
|
||||
|
||||
/** `value instanceof zlib.Gzip` (any spelling of the class, aliases
|
||||
* included). zlib streams are runtime Transforms: a Transform operand asks
|
||||
* the runtime for the stream's mode, and an operand typed as one of
|
||||
* Transform's bases (Duplex, Readable, EventEmitter) is first tested for a
|
||||
* Transform. Any other static type answers false after the operand
|
||||
* evaluates. */
|
||||
export function lowerZlibInstanceOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
mode: number,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const value = lowerer.lowerExpr(expr.left);
|
||||
const type = value.type;
|
||||
const holdsStream = (className: string): boolean =>
|
||||
className === ZLIB_STREAM_CLASS || lowerer.isSubclassOf(ZLIB_STREAM_CLASS, className);
|
||||
const arms = type.kind === "union" ? lowerer.unions.get(type.unionId)?.arms : undefined;
|
||||
if (
|
||||
type.kind === "dyn" ||
|
||||
arms?.some((arm) => arm.kind === "object" && holdsStream(arm.className))
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'instanceof zlib.${zlibClassName(mode)}' on an untyped or union value (give the operand a stream type first)`,
|
||||
);
|
||||
}
|
||||
if (type.kind === "object" && holdsStream(type.className)) {
|
||||
const isMode = (stream: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "zlib.streamIs",
|
||||
args: [stream, numLit(mode, loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
if (type.className === ZLIB_STREAM_CLASS) return isMode(value);
|
||||
const local = lowerer.declareHiddenLocal("%zlibTest", type);
|
||||
const ref = varRef(local.id, type, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
|
||||
result: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: {
|
||||
kind: "instanceOf",
|
||||
value: ref,
|
||||
className: ZLIB_STREAM_CLASS,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
right: isMode({ kind: "downcast", value: ref, type: ZLIB_STREAM_T, loc }),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: value, loc }],
|
||||
result: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrLibFn,
|
||||
type IrType,
|
||||
type IrStmt,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
@@ -1090,3 +1091,176 @@ export function lowerCryptoHashMethodCall(
|
||||
lowerer.checker.getSymbolAtLocation(access.name),
|
||||
);
|
||||
}
|
||||
|
||||
/** The crypto callback functions util.promisify converts. */
|
||||
const PROMISIFIED_CRYPTO_ARITY: Readonly<Record<string, readonly [number, number] | undefined>> = {
|
||||
randomBytes: [1, 1],
|
||||
pbkdf2: [5, 5],
|
||||
scrypt: [3, 4],
|
||||
hkdf: [5, 5],
|
||||
randomFill: [1, 3],
|
||||
randomInt: [1, 2],
|
||||
generateKey: [1, 2],
|
||||
};
|
||||
|
||||
export function isPromisifiedCryptoMember(member: string): boolean {
|
||||
return PROMISIFIED_CRYPTO_ARITY[member] !== undefined;
|
||||
}
|
||||
|
||||
/** A call through `promisify(crypto.randomBytes)` and the other callback
|
||||
* functions: the callback form runs with a callback that settles the
|
||||
* returned promise, so the value or error arrives where the callback
|
||||
* would run. An argument error rejects the promise instead of throwing,
|
||||
* as the throw inside Node's promisify wrapper does. The promise's value
|
||||
* type follows the declared result: a Buffer or the filled view, or a
|
||||
* checked-dynamic value where the declarations give `unknown`. */
|
||||
export function lowerPromisifiedCryptoCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
member: string,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const args = expr.arguments;
|
||||
if (args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(`the promisified crypto.${member} with spread arguments`, expr);
|
||||
}
|
||||
const [min, max] = PROMISIFIED_CRYPTO_ARITY[member]!;
|
||||
if (args.length < min || args.length > max) {
|
||||
lowerer.noLowering(
|
||||
`the promisified crypto.${member} with ${args.length} arguments`,
|
||||
expr,
|
||||
`the promisified form takes the callback form's arguments without the callback`,
|
||||
);
|
||||
}
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
const inner = declared?.kind === "promise" ? declared.inner : null;
|
||||
const promise = (
|
||||
fn: IrLibFn,
|
||||
values: IrExpr[],
|
||||
accepts: (t: IrType) => boolean,
|
||||
what: string,
|
||||
): IrExpr => {
|
||||
if (inner === null || !accepts(inner)) {
|
||||
lowerer.noLowering(
|
||||
`the promisified crypto.${member} with a '${declared ? lowerer.fmt(declared) : "?"}' result`,
|
||||
expr,
|
||||
`the promise's value type must be ${what} or unknown`,
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn, args: values, type: { kind: "promise", inner }, loc };
|
||||
};
|
||||
const bufferOrDyn = (t: IrType) => t.kind === "dyn" || (t.kind === "bytes" && t.elem === "u8");
|
||||
if (member === "randomBytes") {
|
||||
return promise(
|
||||
"crypto.randomBytesPromise",
|
||||
[lowerer.lowerExprExpecting(args[0]!, F64)],
|
||||
bufferOrDyn,
|
||||
"Buffer",
|
||||
);
|
||||
}
|
||||
if (member === "pbkdf2") {
|
||||
return promise(
|
||||
"crypto.pbkdf2Promise",
|
||||
[
|
||||
lowerBuiltinByteInput(lowerer, args[0]!, locOf(args[0]!), "crypto"),
|
||||
lowerBuiltinByteInput(lowerer, args[1]!, locOf(args[1]!), "crypto"),
|
||||
lowerer.lowerExprExpecting(args[2]!, F64),
|
||||
lowerer.lowerExprExpecting(args[3]!, F64),
|
||||
cryptoAlgorithm(lowerer, args[4]!),
|
||||
],
|
||||
bufferOrDyn,
|
||||
"Buffer",
|
||||
);
|
||||
}
|
||||
if (member === "scrypt") {
|
||||
return promise(
|
||||
"crypto.scryptPromise",
|
||||
[
|
||||
cryptoKdfInput(lowerer, args[0]!),
|
||||
cryptoKdfInput(lowerer, args[1]!),
|
||||
lowerer.lowerExprExpecting(args[2]!, F64),
|
||||
args[3]
|
||||
? lowerer.coerceInto(args[3], lowerer.lowerExpr(args[3]), DYN)
|
||||
: dynUndefinedExpr(loc),
|
||||
],
|
||||
bufferOrDyn,
|
||||
"Buffer",
|
||||
);
|
||||
}
|
||||
if (member === "hkdf") {
|
||||
return promise(
|
||||
"crypto.hkdfPromise",
|
||||
[
|
||||
cryptoAlgorithm(lowerer, args[0]!),
|
||||
isKeyObjectValue(lowerer, args[1]!)
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: "crypto.keyObjectInput",
|
||||
args: [lowerer.lowerExprExpecting(args[1]!, DYN)],
|
||||
type: BYTES_U8,
|
||||
loc,
|
||||
}
|
||||
: cryptoKdfInput(lowerer, args[1]!),
|
||||
cryptoKdfInput(lowerer, args[2]!),
|
||||
cryptoKdfInput(lowerer, args[3]!),
|
||||
lowerer.lowerExprExpecting(args[4]!, F64),
|
||||
],
|
||||
(t) => t.kind === "dyn",
|
||||
"ArrayBuffer",
|
||||
);
|
||||
}
|
||||
if (member === "randomFill") {
|
||||
// Node's randomFill counts the arguments before the callback.
|
||||
const form: IrExpr = { kind: "numLit", value: args.length, type: F64, loc };
|
||||
const buffer = lowerer.lowerExpr(args[0]!);
|
||||
if (buffer.type.kind === "bytes" && cryptoNumberArgs(lowerer, args.slice(1))) {
|
||||
const view = buffer.type;
|
||||
return promise(
|
||||
"crypto.randomFillPromise",
|
||||
[
|
||||
buffer,
|
||||
args.length >= 2
|
||||
? lowerer.lowerExprExpecting(args[1]!, F64)
|
||||
: { kind: "numLit", value: 0, type: F64, loc },
|
||||
args.length === 3
|
||||
? lowerer.lowerExprExpecting(args[2]!, F64)
|
||||
: { kind: "numLit", value: 0, type: F64, loc },
|
||||
form,
|
||||
],
|
||||
(t) => t.kind === "dyn" || (t.kind === "bytes" && t.elem === view.elem),
|
||||
"the buffer's own view",
|
||||
);
|
||||
}
|
||||
return promise(
|
||||
"crypto.randomFillPromiseDyn",
|
||||
[
|
||||
lowerer.coerceInto(args[0]!, buffer, DYN),
|
||||
dynArg(lowerer, args[1], loc),
|
||||
dynArg(lowerer, args[2], loc),
|
||||
form,
|
||||
],
|
||||
(t) => t.kind === "dyn",
|
||||
"the buffer's own type",
|
||||
);
|
||||
}
|
||||
if (member === "randomInt") {
|
||||
return promise(
|
||||
"crypto.randomIntPromise",
|
||||
[
|
||||
args.length === 2
|
||||
? lowerer.lowerExprExpecting(args[0]!, F64)
|
||||
: { kind: "numLit", value: 0, type: F64, loc },
|
||||
lowerer.lowerExprExpecting(args[args.length - 1]!, F64),
|
||||
{ kind: "numLit", value: args.length === 2 ? 1 : 0, type: F64, loc },
|
||||
],
|
||||
(t) => t.kind === "dyn" || t.kind === "f64",
|
||||
"number",
|
||||
);
|
||||
}
|
||||
return promise(
|
||||
"crypto.generateKeyPromise",
|
||||
[lowerer.lowerExprExpecting(args[0]!, DYN), dynArg(lowerer, args[1], loc)],
|
||||
(t) => t.kind === "dyn",
|
||||
"KeyObject",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer, own } from "../lowerer.js";
|
||||
import { lowerCheckedPredicateValue } from "../lower-builtin-values.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { BOOL, F64, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
|
||||
import { BOOL, F64, STRING, type IrExpr, type IrLibFn } from "../../../ir/ir.js";
|
||||
import { lowerBuiltinValuePreservingUndefined, lowerOptionalNumberPredicate } from "./arguments.js";
|
||||
import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
import { lowerStaticNumberGlobal } from "../lower-calls.js";
|
||||
@@ -88,6 +88,22 @@ export function lowerNumberStaticCall(
|
||||
const parsed = lowerStaticNumberGlobal(lowerer, member, call, call.arguments, loc);
|
||||
if (parsed !== null) return parsed;
|
||||
}
|
||||
// A checked-dynamic argument (`Number.parseInt(env.FORCE_COLOR, 10)`
|
||||
// over a JavaScript dictionary read): the parsers' own ToString,
|
||||
// exactly the global parseFloat's dyn form.
|
||||
if (probed?.type.kind === "dyn") {
|
||||
const text: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toStringCoerce",
|
||||
args: [probed],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
if (member === "parseFloat")
|
||||
return { kind: "libCall", fn: "num.parseFloat", args: [text], type: F64, loc };
|
||||
const radix = lowerer.lowerExprExpecting(call.arguments[1]!, F64);
|
||||
return { kind: "libCall", fn: "num.parseInt", args: [text, radix], type: F64, loc };
|
||||
}
|
||||
const argText =
|
||||
probed !== null
|
||||
? lowerer.fmt(probed.type)
|
||||
|
||||
@@ -233,6 +233,41 @@ export function lowerProcessStreamProperty(
|
||||
* read in source order). Method members referenced without a call are
|
||||
* rejected specifically. Null for non-process receivers (the chain keeps
|
||||
* trying other property lowerings). */
|
||||
/** The value of a DATA member of the process global taken off by
|
||||
* destructuring (`const { env, argv } = process` in JavaScript, the
|
||||
* supports-color opening): the same values the spelled `process.env` /
|
||||
* `process.argv` reads produce, typed by the binding. Null for members
|
||||
* this does not cover (methods keep the destructuring fence). */
|
||||
export function processDataMemberValue(
|
||||
lowerer: Lowerer,
|
||||
member: string,
|
||||
binding: ts.Node,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
switch (member) {
|
||||
case "env": {
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(binding));
|
||||
if (mapped?.kind !== "record") return null;
|
||||
const helper = envSnapshotHelper(lowerer, mapped.shapeId, loc);
|
||||
return helper === null ? null : { kind: "call", callee: helper, args: [], type: mapped, loc };
|
||||
}
|
||||
case "argv":
|
||||
return { kind: "libCall", fn: "process.argv", args: [], type: arrayOf(STRING), loc };
|
||||
case "platform":
|
||||
return { kind: "libCall", fn: "process.platform", args: [], type: STRING, loc };
|
||||
case "arch":
|
||||
return { kind: "libCall", fn: "process.arch", args: [], type: STRING, loc };
|
||||
case "pid":
|
||||
return { kind: "libCall", fn: "process.pid", args: [], type: F64, loc };
|
||||
case "execPath":
|
||||
return { kind: "libCall", fn: "process.execPath", args: [], type: STRING, loc };
|
||||
case "versions":
|
||||
return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc };
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function lowerProcessProperty(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
|
||||
@@ -1159,7 +1159,9 @@ export class ClassDynamicDispatch {
|
||||
// Runtime-owned roots keep their dedicated property contracts. A
|
||||
// family whose fields all exist from construction and are never
|
||||
// deleted keeps a fixed layout, as TypeScript classes do.
|
||||
this.deletedNames ??= deletedPropertyNames(lowerer.moduleOrder);
|
||||
// null is a result too; undefined means not computed yet.
|
||||
if (this.deletedNames === undefined)
|
||||
this.deletedNames = deletedPropertyNames(lowerer.moduleOrder);
|
||||
if (!root.base?.def.runtime && !fixedFieldFamily(root, this.deletedNames)) track(root);
|
||||
}
|
||||
const loc = this.propertyBag.loc;
|
||||
|
||||
@@ -124,15 +124,6 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca
|
||||
continue;
|
||||
}
|
||||
if (ts.isExpressionStatement(stmt) && !exposes(stmt.expression, depth)) continue;
|
||||
// A check that only reads assigned fields and throws or does nothing
|
||||
// with the receiver (`if (this.size < 0) throw new RangeError()`)
|
||||
// does not expose the instance; assignments in its branches are not
|
||||
// definite and still count as exposures.
|
||||
if (
|
||||
(ts.isIfStatement(stmt) || ts.isThrowStatement(stmt) || ts.isBlock(stmt)) &&
|
||||
!exposes(stmt, depth)
|
||||
)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js";
|
||||
import { countedFor, numLit, varRef } from "../../../ir/build.js";
|
||||
import { objectEnumerationReceiver } from "../object-enumeration-receiver.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
@@ -852,50 +853,57 @@ export function lowerRecordOvfCaptureHelper(
|
||||
lift === "dyn"
|
||||
? { kind: "dynFrom", value: v, type: DYN, loc }
|
||||
: lowerer.applyWidthLift(lift, v, tIv, loc);
|
||||
const body: IrStmt[] = [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "out.0",
|
||||
init: {
|
||||
kind: "recordLit",
|
||||
fields: inits.map((d) => ({
|
||||
name: d.name,
|
||||
value:
|
||||
d.kind === "direct"
|
||||
? lowerer.presenceKeepingCopy(
|
||||
sRef,
|
||||
fromId,
|
||||
d.name,
|
||||
lowerer.applyWidthLift(
|
||||
d.lift,
|
||||
{
|
||||
kind: "recordGet",
|
||||
obj: sRef,
|
||||
shapeId: fromId,
|
||||
field: d.name,
|
||||
type: d.src,
|
||||
loc,
|
||||
},
|
||||
to.fields.find((f) => f.name === d.name)!.type,
|
||||
loc,
|
||||
),
|
||||
const fresh: IrExpr & { kind: "recordLit" } = {
|
||||
kind: "recordLit",
|
||||
fields: inits.map((d) => ({
|
||||
name: d.name,
|
||||
value:
|
||||
d.kind === "direct"
|
||||
? lowerer.presenceKeepingCopy(
|
||||
sRef,
|
||||
fromId,
|
||||
d.name,
|
||||
lowerer.applyWidthLift(
|
||||
d.lift,
|
||||
{
|
||||
kind: "recordGet",
|
||||
obj: sRef,
|
||||
shapeId: fromId,
|
||||
field: d.name,
|
||||
type: d.src,
|
||||
loc,
|
||||
)
|
||||
: ({
|
||||
// No source property of this name: the field starts
|
||||
// absent (an overflow write below can supply it).
|
||||
kind: "fieldAbsent",
|
||||
unionId: d.unionId,
|
||||
type: to.fields.find((f) => f.name === d.name)!.type,
|
||||
loc,
|
||||
} satisfies IrExpr),
|
||||
})),
|
||||
type: toT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
},
|
||||
to.fields.find((f) => f.name === d.name)!.type,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
)
|
||||
: ({
|
||||
// No source property of this name: the field starts
|
||||
// absent (an overflow write below can supply it).
|
||||
kind: "fieldAbsent",
|
||||
unionId: d.unionId,
|
||||
type: to.fields.find((f) => f.name === d.name)!.type,
|
||||
loc,
|
||||
} satisfies IrExpr),
|
||||
})),
|
||||
type: toT,
|
||||
loc,
|
||||
};
|
||||
// The source and its reshape are one object in JavaScript, so a
|
||||
// null-prototype source (a match's groups, Object.groupBy) keeps it.
|
||||
const init: IrExpr =
|
||||
nullProtoRecordShape(lowerer, fromT) && nullProtoRecordShape(lowerer, toT)
|
||||
? {
|
||||
kind: "ternary",
|
||||
cond: recordNullProtoTest(sRef, fromId, loc),
|
||||
then: { ...fresh, nullProto: true },
|
||||
else_: fresh,
|
||||
type: toT,
|
||||
loc,
|
||||
}
|
||||
: fresh;
|
||||
const body: IrStmt[] = [{ kind: "varDecl", localId: "out.0", init, loc }];
|
||||
const fieldStmts = new Map<string, IrStmt>();
|
||||
// Source declared fields in declaration order, skipping unset
|
||||
// optionals (stance 37) and the direct-initialized (consumed) names.
|
||||
|
||||
@@ -6,9 +6,10 @@ import type { IrExpr } from "../../../ir/ir.js";
|
||||
export function isSafeToRepeat(expr: IrExpr): boolean {
|
||||
if (expr.kind === "varRef") return true;
|
||||
if (expr.kind === "recordGet" || expr.kind === "fieldGet") return isSafeToRepeat(expr.obj);
|
||||
// The trust-the-checker narrowing bridges over a pure read: extraction
|
||||
// and downcast are reads too (the +1 on ref payloads is RC bookkeeping,
|
||||
// not an observable effect — each emission owns its own copy).
|
||||
// The narrowing bridges over a pure read: extraction and downcast are
|
||||
// reads too (their guards only compare, and the +1 on ref payloads is RC
|
||||
// bookkeeping, not an observable effect — each emission owns its own
|
||||
// copy).
|
||||
if (expr.kind === "unionNarrow" || expr.kind === "downcast") return isSafeToRepeat(expr.value);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { closeSourceParser, parseSourceFile } from "../ts7/source-parser-node.js";
|
||||
import { isValueReturningConstructorClass } from "./js-dynamic-objects.js";
|
||||
|
||||
afterAll(closeSourceParser);
|
||||
|
||||
function classify(source: string): boolean {
|
||||
const file = parseSourceFile("classes.js", source, "js");
|
||||
let decl: ts.ClassDeclaration | undefined;
|
||||
ts.walkPreorder(file, (node) => {
|
||||
if (!decl && ts.isClassDeclaration(node)) decl = node;
|
||||
});
|
||||
if (!decl) throw new Error("fixture has no class");
|
||||
return isValueReturningConstructorClass(decl);
|
||||
}
|
||||
|
||||
test("a constructor-only class returning a value is claimed", () => {
|
||||
expect(classify(`class Chalk { constructor(options) { return chalkFactory(options); } }`)).toBe(
|
||||
true,
|
||||
);
|
||||
expect(classify(`class C { constructor(a) { if (a) return { a }; return [a]; } ; }`)).toBe(true);
|
||||
expect(classify(`class C { constructor() { const f = () => 1; return { f }; } }`)).toBe(true);
|
||||
});
|
||||
|
||||
test("classes that keep their instance layout are left alone", () => {
|
||||
// No value return, or only nested functions return.
|
||||
expect(classify(`class C { constructor() { return; } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { function f() { return 1; } f(); } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { const f = () => { return 1; }; f(); } }`)).toBe(false);
|
||||
// Other members, heritage, or a constructor that observes `this`.
|
||||
expect(classify(`class C { constructor() { return {}; } m() {} }`)).toBe(false);
|
||||
expect(classify(`class C { x = 1; constructor() { return {}; } }`)).toBe(false);
|
||||
expect(classify(`class C extends B { constructor() { super(); return {}; } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { this.a = 1; return {}; } }`)).toBe(false);
|
||||
expect(classify(`class C { constructor() { const g = () => this; return { g }; } }`)).toBe(false);
|
||||
expect(classify(`class C {}`)).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,312 @@
|
||||
/* JavaScript declarations whose values only the checked-dynamic
|
||||
* representation can hold faithfully. TypeScript infers a fixed object
|
||||
* type for each of them, but the program changes the value's shape or
|
||||
* identity at run time, so a static layout would answer stale members
|
||||
* (or throw on a value that no longer fits):
|
||||
*
|
||||
* - a non-empty object literal bound to a variable that the same file
|
||||
* grows or reshapes dynamically: a keyed write with a runtime key
|
||||
* (`o[k] = v`), a `delete`, or `Object.defineProperty(o, ...)` /
|
||||
* `Object.defineProperties(o, ...)` (ansi-styles' `assembleStyles`).
|
||||
* The object literals nested in its property values belong to the same
|
||||
* value graph and are marked with it;
|
||||
* - a class whose only member is a constructor that returns a value and
|
||||
* never reads `this` (chalk's `class Chalk { constructor(options) {
|
||||
* return chalkFactory(options); } }`): `new C()` answers whatever the
|
||||
* constructor returns, so its instance type has no fixed layout.
|
||||
*
|
||||
* The type mapper answers the checked-dynamic type for the instance and
|
||||
* object-literal types these declarations own (TypeMapperCtx
|
||||
* .jsDynamicDeclaration). The scan is syntactic, runs over JavaScript
|
||||
* files only, and an empty answer leaves every mapping unchanged, so
|
||||
* TypeScript programs and JavaScript that never uses these patterns
|
||||
* compile exactly as before. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { BOOL, DYN, type IrExpr, type IrStmt } from "../../ir/ir.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr } from "../../ir/build.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ParamShape } from "./call-signatures.js";
|
||||
import { newFnCtx } from "./function-context.js";
|
||||
|
||||
const SCANNED_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
||||
ts.SyntaxKind.VariableDeclaration,
|
||||
ts.SyntaxKind.ClassDeclaration,
|
||||
ts.SyntaxKind.BinaryExpression,
|
||||
ts.SyntaxKind.DeleteExpression,
|
||||
ts.SyntaxKind.CallExpression,
|
||||
]);
|
||||
|
||||
function unparenthesized(node: ts.Expression): ts.Expression {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
return node;
|
||||
}
|
||||
|
||||
function isLiteralKey(node: ts.Expression): boolean {
|
||||
const key = unparenthesized(node);
|
||||
return (
|
||||
ts.isStringLiteral(key) || ts.isNumericLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)
|
||||
);
|
||||
}
|
||||
|
||||
function isAssignmentOperator(kind: ts.SyntaxKind): boolean {
|
||||
return kind >= ts.SyntaxKind.FirstAssignment && kind <= ts.SyntaxKind.LastAssignment;
|
||||
}
|
||||
|
||||
/** `class C { constructor(...) { ...; return value; } }` with nothing else:
|
||||
* no heritage, decorators or other members, a constructor body that
|
||||
* returns a value at its own level and never mentions `this`/`super`. */
|
||||
export function isValueReturningConstructorClass(decl: ts.ClassDeclaration): boolean {
|
||||
if (decl.heritageClauses !== undefined && decl.heritageClauses.length > 0) return false;
|
||||
if (decl.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.Decorator)) return false;
|
||||
let ctor: ts.ConstructorDeclaration | undefined;
|
||||
for (const member of decl.members) {
|
||||
if (ts.isSemicolonClassElement(member)) continue;
|
||||
if (!ts.isConstructorDeclaration(member) || ctor !== undefined) return false;
|
||||
ctor = member;
|
||||
}
|
||||
if (ctor?.body === undefined) return false;
|
||||
let returnsValue = false;
|
||||
let observesThis = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (observesThis) return;
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword || node.kind === ts.SyntaxKind.SuperKeyword) {
|
||||
observesThis = true;
|
||||
return;
|
||||
}
|
||||
// Arrow functions share the constructor's `this`; other functions
|
||||
// have their own `this` and their own returns.
|
||||
if (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return;
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return;
|
||||
if (ts.isReturnStatement(node) && node.expression !== undefined && !insideArrow(node, ctor!))
|
||||
returnsValue = true;
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
ctor.body.forEachChild(visit);
|
||||
for (const parameter of ctor.parameters) parameter.forEachChild(visit);
|
||||
return returnsValue && !observesThis;
|
||||
}
|
||||
|
||||
function insideArrow(node: ts.Node, ctor: ts.ConstructorDeclaration): boolean {
|
||||
for (let p = node.parent; p !== undefined && p !== ctor; p = p.parent) {
|
||||
if (ts.isArrowFunction(p)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function markNested(literal: ts.ObjectLiteralExpression, out: Set<ts.Node>): void {
|
||||
if (out.has(literal)) return;
|
||||
out.add(literal);
|
||||
for (const property of literal.properties) {
|
||||
if (!ts.isPropertyAssignment(property)) continue;
|
||||
const value = unparenthesized(property.initializer);
|
||||
if (ts.isObjectLiteralExpression(value)) markNested(value, out);
|
||||
}
|
||||
}
|
||||
|
||||
export function jsDynamicDeclarations(
|
||||
files: readonly ts.SourceFile[],
|
||||
checker: ts.TypeChecker,
|
||||
): Set<ts.Node> {
|
||||
const out = new Set<ts.Node>();
|
||||
for (const file of files) {
|
||||
if (!isJsSourceFile(file) || file.isDeclarationFile) continue;
|
||||
const literals = new Map<ts.Symbol, ts.ObjectLiteralExpression>();
|
||||
const reshaped = new Set<ts.Symbol>();
|
||||
const receiver = (expression: ts.Expression | undefined): void => {
|
||||
if (expression === undefined) return;
|
||||
const target = unparenthesized(expression);
|
||||
if (!ts.isIdentifier(target)) return;
|
||||
const symbol = checker.getSymbolAtLocation(target);
|
||||
if (symbol !== undefined) reshaped.add(symbol);
|
||||
};
|
||||
ts.walkPreorder(
|
||||
file,
|
||||
(node) => {
|
||||
if (ts.isVariableDeclaration(node)) {
|
||||
if (!ts.isIdentifier(node.name) || node.initializer === undefined) return;
|
||||
const init = unparenthesized(node.initializer);
|
||||
// An empty literal's binding is already checked-dynamic
|
||||
// (jsOpenObjectType), and its type describes no layout.
|
||||
if (!ts.isObjectLiteralExpression(init) || init.properties.length === 0) return;
|
||||
const symbol = checker.getSymbolAtLocation(node.name);
|
||||
if (symbol !== undefined) literals.set(symbol, init);
|
||||
return;
|
||||
}
|
||||
if (ts.isClassDeclaration(node)) {
|
||||
if (isValueReturningConstructorClass(node)) out.add(node);
|
||||
return;
|
||||
}
|
||||
if (ts.isBinaryExpression(node)) {
|
||||
if (!isAssignmentOperator(node.operatorToken.kind)) return;
|
||||
const left = unparenthesized(node.left);
|
||||
if (ts.isElementAccessExpression(left) && !isLiteralKey(left.argumentExpression))
|
||||
receiver(left.expression);
|
||||
return;
|
||||
}
|
||||
if (ts.isDeleteExpression(node)) {
|
||||
const operand = unparenthesized(node.expression);
|
||||
if (ts.isElementAccessExpression(operand) || ts.isPropertyAccessExpression(operand))
|
||||
receiver(operand.expression);
|
||||
return;
|
||||
}
|
||||
if (ts.isCallExpression(node)) {
|
||||
const callee = unparenthesized(node.expression);
|
||||
if (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
ts.isIdentifier(callee.expression) &&
|
||||
callee.expression.text === "Object" &&
|
||||
(callee.name.text === "defineProperty" || callee.name.text === "defineProperties")
|
||||
)
|
||||
receiver(node.arguments[0]);
|
||||
}
|
||||
},
|
||||
SCANNED_KINDS,
|
||||
);
|
||||
for (const symbol of reshaped) {
|
||||
const literal = literals.get(symbol);
|
||||
if (literal !== undefined) markNested(literal, out);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `new C(args)` for a value-returning constructor class (above): the
|
||||
* constructor body runs as an ordinary JavaScript function whose result
|
||||
* is the construction's value. A returned object or function is Node's
|
||||
* answer; a fall-through or primitive return would answer the bare
|
||||
* instance, which has no static home here, and throws the deferred
|
||||
* refusal instead. Null when the callee is not such a class. */
|
||||
export function lowerValueReturningConstructorNew(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.NewExpression,
|
||||
): IrExpr | null {
|
||||
const isDynamic = lowerer.typeCtx.jsDynamicDeclaration;
|
||||
if (isDynamic === undefined) return null;
|
||||
const callee = unparenthesized(expr.expression);
|
||||
if (!ts.isIdentifier(callee)) return null;
|
||||
let symbol = lowerer.resolveValueSymbol(callee);
|
||||
if (symbol !== null && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const decl = symbol
|
||||
? lowerer.checker
|
||||
.declarationsOf(symbol)
|
||||
.find((node): node is ts.ClassDeclaration => ts.isClassDeclaration(node) && isDynamic(node))
|
||||
: undefined;
|
||||
if (decl === undefined) return null;
|
||||
const loc = locOf(expr);
|
||||
const ctor = decl.members.find(ts.isConstructorDeclaration)!;
|
||||
const simple = ctor.parameters.every(
|
||||
(p) => ts.isIdentifier(p.name) && p.dotDotDotToken === undefined && p.initializer === undefined,
|
||||
);
|
||||
if (!simple || !ts.isSourceFile(decl.parent)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"constructing a class whose constructor returns a value, with parameter defaults, rest or patterns, or declared inside a function",
|
||||
);
|
||||
}
|
||||
const args = expr.arguments ?? [];
|
||||
if (args.some(ts.isSpreadElement)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
"spread arguments when constructing a class whose constructor returns a value",
|
||||
);
|
||||
}
|
||||
const key = `ctorValue:${decl.getSourceFile().fileName}:${decl.getStart()}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (name === undefined) {
|
||||
name = `%ctorValue.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const savedClass = lowerer.currentClass;
|
||||
lowerer.currentClass = null;
|
||||
lowerer.fnStack.push(newFnCtx(false, null, null, DYN));
|
||||
try {
|
||||
const shapes: ParamShape[] = ctor.parameters.map(() => ({ type: DYN, mode: "required" }));
|
||||
const declared = lowerer.declareParams(ctor.parameters, shapes);
|
||||
const body: IrStmt[] = [
|
||||
...declared.prologue,
|
||||
...lowerer.lowerStmts(ctor.body!.statements),
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: "SC1090",
|
||||
message: `constructors that return no object (the bare instance of '${decl.name?.text ?? "class"}' has no representation here) are not supported yet [SC1090 at ${decl.getSourceFile().fileName}:${ts.getLineAndCharacterOfPosition(decl.getSourceFile(), ctor.getStart()).line + 1}]`,
|
||||
loc: locOf(ctor),
|
||||
},
|
||||
];
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: declared.params,
|
||||
returnType: DYN,
|
||||
locals: lowerer.ctx.locals,
|
||||
body,
|
||||
loc: locOf(ctor),
|
||||
});
|
||||
} finally {
|
||||
lowerer.fnStack.pop();
|
||||
lowerer.currentClass = savedClass;
|
||||
}
|
||||
}
|
||||
const lowered = ctor.parameters.map((_, i) =>
|
||||
args[i] !== undefined ? lowerer.lowerExprExpecting(args[i]!, DYN) : dynUndefinedExpr(loc),
|
||||
);
|
||||
// Surplus arguments still evaluate, left to right, after the others.
|
||||
const surplus: IrStmt[] = args
|
||||
.slice(ctor.parameters.length)
|
||||
.map((arg) => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }));
|
||||
const result = lowerer.declareHiddenLocal("%constructed", DYN);
|
||||
const value: IrExpr = { kind: "varRef", localId: result.id, type: DYN, loc };
|
||||
const sf = expr.getSourceFile();
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, expr.getStart()).line + 1;
|
||||
const objectLike: IrExpr = {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: { kind: "dynTest", test: "function", value, type: BOOL, loc },
|
||||
right: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: { kind: "dynTest", test: "object", value, type: BOOL, loc },
|
||||
right: { kind: "dynTest", test: "null", negated: true, value, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const args0: IrStmt[] = [];
|
||||
const argLocals = lowered.map((arg) => {
|
||||
const local = lowerer.declareHiddenLocal("%constructArg", DYN);
|
||||
args0.push({ kind: "varDecl", localId: local.id, init: arg, loc });
|
||||
return { kind: "varRef", localId: local.id, type: DYN, loc } as IrExpr;
|
||||
});
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
...args0,
|
||||
...surplus,
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: result.id,
|
||||
init: { kind: "call", callee: name, args: argLocals, type: DYN, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: objectLike,
|
||||
then: value,
|
||||
else_: nodeThrowExpr(
|
||||
0,
|
||||
"SC1090",
|
||||
`constructors that return no object are not supported yet [SC1090 at ${sf.fileName}:${line}]`,
|
||||
DYN,
|
||||
loc,
|
||||
),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -106,8 +106,13 @@ import {
|
||||
lowerImportMetaResolveCall,
|
||||
lowerRequireResolveCall,
|
||||
} from "./builtins/module-resolution.js";
|
||||
import { lowerCipherMethodCall, lowerCryptoHashMethodCall } from "./builtins/crypto.js";
|
||||
import {
|
||||
lowerCipherMethodCall,
|
||||
lowerCryptoHashMethodCall,
|
||||
lowerPromisifiedCryptoCall,
|
||||
} from "./builtins/crypto.js";
|
||||
import { lowerErrorIsErrorCall } from "./builtins/errors.js";
|
||||
import { lowerPromisifiedDnsCall } from "./lower-dgram.js";
|
||||
import { lowerDisposableStackUseCall } from "./builtins/disposable-stack.js";
|
||||
import {
|
||||
lowerDirentMethodCall,
|
||||
@@ -126,6 +131,7 @@ import { recordMayKeepSource, throughRecordSource } from "./record-sources.js";
|
||||
import { lowerReflectCall } from "./builtins/reflection.js";
|
||||
import {
|
||||
lowerAbsenceProbe,
|
||||
v8CalleeText,
|
||||
lowerOptionalNumber,
|
||||
lowerPromiseAllTupleCall,
|
||||
lowerPromiseRejectCall,
|
||||
@@ -815,6 +821,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
if (projection?.kind === "promisified-zlib") {
|
||||
return lowerPromisifiedZlibCall(lowerer, expr, projection.member, loc);
|
||||
}
|
||||
if (projection?.kind === "promisified-crypto") {
|
||||
return lowerPromisifiedCryptoCall(lowerer, expr, projection.member, loc);
|
||||
}
|
||||
if (projection?.kind === "promisified-dns") {
|
||||
return lowerPromisifiedDnsCall(lowerer, expr, projection.member, loc);
|
||||
}
|
||||
if (projection?.kind === "promisified-builtin") {
|
||||
return lowerBuiltinCall(
|
||||
lowerer,
|
||||
@@ -1447,7 +1459,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
kind: "dynCall",
|
||||
callee: staticField,
|
||||
receiver: lowerer.lowerExprExpecting(expr.expression.expression, DYN),
|
||||
calleeName: expr.expression.getText(),
|
||||
calleeName: v8CalleeText(expr.expression),
|
||||
args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc: locOf(expr),
|
||||
@@ -1466,7 +1478,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
return {
|
||||
kind: "dynCall",
|
||||
callee,
|
||||
calleeName: expr.expression.getText(),
|
||||
calleeName: v8CalleeText(expr.expression),
|
||||
args,
|
||||
type: DYN,
|
||||
loc: locOf(expr),
|
||||
@@ -1719,7 +1731,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
return {
|
||||
kind: "dynCall",
|
||||
callee: lowerer.coerceToExpected(callee, DYN),
|
||||
calleeName: expr.expression.getText(),
|
||||
calleeName: v8CalleeText(expr.expression),
|
||||
args: expr.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
@@ -1801,7 +1813,7 @@ function lowerCheckedCallableValue(
|
||||
return {
|
||||
kind: "dynCall",
|
||||
callee,
|
||||
calleeName: call.expression.getText(),
|
||||
calleeName: v8CalleeText(call.expression),
|
||||
args: call.arguments.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
@@ -3477,13 +3489,41 @@ function lowerDynReceiverMethodCall(
|
||||
// its name overlaps a primitive prototype (`colors.bold(...)`): the
|
||||
// source value was compiled from that object shape, while genuinely
|
||||
// untyped/any receivers retain the runtime-kind ambiguity and fence.
|
||||
if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable) return null;
|
||||
// A name only String.prototype declares (`bold`, `italics`, `link`,
|
||||
// ...) on a checker-untyped receiver: every other dyn kind answers its
|
||||
// own or inherited member exactly like a non-prototype name (chalk's
|
||||
// `chalk.red.bold(...)` reads a prototype getter of a function), so
|
||||
// only a string receiver keeps the refusal, deferred to run time.
|
||||
const stringOnlyName =
|
||||
checkerUntyped && !ownCallable && DYN_STRING_PROTO_ONLY_NAMES.has(access.name.text);
|
||||
if (DYN_PROTO_METHOD_NAMES.has(access.name.text) && !ownCallable && !stringOnlyName) return null;
|
||||
// Optional forms (`obj.cb?.()`, `obj?.cb()`) belong to the chain
|
||||
// machinery's short-circuit semantics — not modeled here yet.
|
||||
if (call.questionDotToken || access.questionDotToken) return null;
|
||||
const loc = locOf(call);
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%dynCallReceiver", DYN);
|
||||
const receiver: IrExpr = { kind: "varRef", localId: receiverLocal.id, type: DYN, loc };
|
||||
// The string refusal throws in place of the member read (a seqExpr
|
||||
// statement cannot leave the expression).
|
||||
const stringRefusal = (read: IrExpr): IrExpr => {
|
||||
if (!stringOnlyName) return read;
|
||||
const sf = call.getSourceFile();
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, call.getStart(sf)).line + 1;
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc },
|
||||
then: nodeThrowExpr(
|
||||
0,
|
||||
"SC1090",
|
||||
`method calls like '${access.getText()}' on a string are not supported yet [SC1090 at ${sf.fileName}:${line}]`,
|
||||
DYN,
|
||||
loc,
|
||||
),
|
||||
else_: read,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
};
|
||||
const member: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
key: { kind: "strLit", value: access.name.text, type: STRING, loc: locOf(access) },
|
||||
@@ -3495,7 +3535,7 @@ function lowerDynReceiverMethodCall(
|
||||
const callee = varRef(calleeLocal.id, DYN, loc);
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: receiverLocal.id, init: recv, loc },
|
||||
{ kind: "varDecl", localId: calleeLocal.id, init: member, loc },
|
||||
{ kind: "varDecl", localId: calleeLocal.id, init: stringRefusal(member), loc },
|
||||
];
|
||||
if (call.arguments.some(ts.isSpreadElement)) {
|
||||
const spread = lowerSpreadArgsCall(lowerer, call, callee, loc);
|
||||
@@ -3604,6 +3644,31 @@ const DYN_PROTO_METHOD_NAMES = new Set([
|
||||
"toPrecision",
|
||||
]);
|
||||
|
||||
/** The DYN_PROTO_METHOD_NAMES that only String.prototype declares among
|
||||
* the dyn-representable kinds' prototypes. */
|
||||
const DYN_STRING_PROTO_ONLY_NAMES = new Set([
|
||||
"anchor",
|
||||
"big",
|
||||
"blink",
|
||||
"bold",
|
||||
"codePointAt",
|
||||
"fixed",
|
||||
"fontcolor",
|
||||
"fontsize",
|
||||
"isWellFormed",
|
||||
"italics",
|
||||
"link",
|
||||
"localeCompare",
|
||||
"normalize",
|
||||
"small",
|
||||
"strike",
|
||||
"sub",
|
||||
"sup",
|
||||
"toLocaleLowerCase",
|
||||
"toLocaleUpperCase",
|
||||
"toWellFormed",
|
||||
]);
|
||||
|
||||
/** The SHARED prototype names scr_dyn_invoke dispatches at runtime (the
|
||||
* subset of DYN_PROTO_METHOD_NAMES with a receiver-kind dispatch): the
|
||||
* runtime runs the real method for the receiver's kind, throws Node's
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { checkedIterableSpread } from "./checked-iterable-spread.js";
|
||||
import { lowerReadlineUiNew } from "./builtins/readline.js";
|
||||
@@ -12,7 +13,7 @@ import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js";
|
||||
import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js";
|
||||
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { lowerZlibStreamNew, zlibStreamModeOf } from "./builtins/compression.js";
|
||||
import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js";
|
||||
import type { FnCtx, Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -146,7 +147,7 @@ import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
import { constructionEscape, mentionsReceiver } from "./construction-escapes.js";
|
||||
import { constructionEscape } from "./construction-escapes.js";
|
||||
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
|
||||
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
import { reflectedClassStaticMethodValue } from "./class-method-values.js";
|
||||
@@ -471,11 +472,6 @@ export interface ClassInfo {
|
||||
* unassigned read throws the catchable TypeError instead of yielding
|
||||
* an undefined the declared type cannot hold (SEMANTICS.md). */
|
||||
deferredInitFields?: Set<string>;
|
||||
/** Fields (inherited included) that code construction exposes the
|
||||
* instance to can read before their assignment: the slot is the
|
||||
* undefined-armed union, allocation writes undefined, and reads yield the
|
||||
* union value, so an early read sees undefined as in Node. */
|
||||
earlyReadFields?: Set<string>;
|
||||
/** True when construction may hand the instance to other code (a method
|
||||
* call, an argument, a callback, or a base constructor that does) — a
|
||||
* subclass's own fields can then be read before their assignment. */
|
||||
@@ -2352,7 +2348,11 @@ export function collectClassShapeInner(
|
||||
(member.type !== undefined ||
|
||||
(lowerer.typeOf(member.name).flags & ts.TypeFlags.Any) === 0)
|
||||
) {
|
||||
lowerer.unsupported("SC1090", member.name, "'unknown'-typed static fields");
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
member.name,
|
||||
`${dynFieldTypeSpelling(lowerer, member.name)}-typed static fields`,
|
||||
);
|
||||
}
|
||||
staticFields.push({
|
||||
name: member.name.text,
|
||||
@@ -2427,12 +2427,29 @@ export function collectClassShapeInner(
|
||||
ts.isPropertyDeclaration(member) &&
|
||||
!member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)
|
||||
) {
|
||||
if (!isJsSourceFile(member.getSourceFile()))
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
memberName,
|
||||
"redeclaring Error.cause (pass the cause in the Error constructor options)",
|
||||
);
|
||||
// A TypeScript redeclaration is the JavaScript field: Node
|
||||
// defines the Error's own `cause` (undefined without an
|
||||
// initializer) after super() returns. `declare` and `abstract`
|
||||
// declarations are erased and define nothing. Every `.cause`
|
||||
// read of an Error-rooted class is the checked-dynamic property
|
||||
// (fieldTarget's errorCause container), so only types that read
|
||||
// back unchanged from it are accepted.
|
||||
if (!isJsSourceFile(member.getSourceFile())) {
|
||||
if (
|
||||
modifiers?.some(
|
||||
(m) =>
|
||||
m.kind === ts.SyntaxKind.DeclareKeyword ||
|
||||
m.kind === ts.SyntaxKind.AbstractKeyword,
|
||||
)
|
||||
)
|
||||
continue;
|
||||
if (!lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)))
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
memberName,
|
||||
"redeclaring Error.cause with a type other than 'unknown' or 'object' (declare it 'cause?: unknown' and narrow at the read)",
|
||||
);
|
||||
}
|
||||
fieldOrder.push({
|
||||
name: "cause",
|
||||
type: DYN,
|
||||
@@ -2796,7 +2813,11 @@ export function collectClassShapeInner(
|
||||
!isJsSourceFile(member.getSourceFile()) &&
|
||||
!lowerer.classFieldStoresDyn(lowerer.typeOf(member.name))
|
||||
) {
|
||||
lowerer.unsupported("SC1090", member.name, "'unknown'-typed class fields");
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
member.name,
|
||||
`${dynFieldTypeSpelling(lowerer, member.name)}-typed class fields`,
|
||||
);
|
||||
}
|
||||
const undefinedInitializer =
|
||||
type.kind === "dyn" && !member.initializer ? locOf(member) : undefined;
|
||||
@@ -2922,7 +2943,7 @@ export function collectClassShapeInner(
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
p.name,
|
||||
"redeclaring Error.cause (pass the cause in the Error constructor options)",
|
||||
"redeclaring Error.cause as a parameter property (declare 'cause?: unknown' and assign it in the constructor)",
|
||||
);
|
||||
}
|
||||
const shape = lowerer.paramShape(p);
|
||||
@@ -2930,7 +2951,11 @@ export function collectClassShapeInner(
|
||||
if (type.kind === "void") lowerer.badType(p.name, lowerer.typeOf(p.name));
|
||||
// The class-field dyn rule verbatim (KEEP NARROW).
|
||||
if (type.kind === "dyn" && !lowerer.classFieldStoresDyn(lowerer.typeOf(p))) {
|
||||
lowerer.unsupported("SC1090", p.name, "'unknown'-typed class fields");
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
p.name,
|
||||
`${dynFieldTypeSpelling(lowerer, p)}-typed class fields`,
|
||||
);
|
||||
}
|
||||
// `override x` (and any same-named inherited member) would
|
||||
// redeclare a base slot — the declared-field rule verbatim.
|
||||
@@ -3481,54 +3506,55 @@ export function collectClassShapeInner(
|
||||
const derived =
|
||||
decl.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword) ??
|
||||
false;
|
||||
// A generic family is not emitted; its instances and each mixin
|
||||
// application analyze their own construction with their actual base.
|
||||
const escape: { escapes: boolean; assigned: Set<string> | null } = familyMode
|
||||
? { escapes: derived, assigned: null }
|
||||
: constructionEscape({
|
||||
ctor,
|
||||
fields: fieldOrder.flatMap((f) =>
|
||||
paramProps.some((pp) => pp.name === f.name)
|
||||
? []
|
||||
: [{ name: f.name, initializer: f.initializer }],
|
||||
),
|
||||
paramProps: paramProps.map((pp) => pp.name),
|
||||
derived,
|
||||
baseEscapes:
|
||||
base === null ||
|
||||
callableBase !== undefined ||
|
||||
factoryBaseExpression !== undefined ||
|
||||
base.constructionEscapes === true,
|
||||
isField: (name) =>
|
||||
fields.has(name) &&
|
||||
!methods.has(`set:${name}`) &&
|
||||
!lowerer.findMethodOn(base, `set:${name}`),
|
||||
privateMethodBody: (call) => {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee)) return null;
|
||||
const name = callee.name.text;
|
||||
const declaration = decl.members.find(
|
||||
(member): member is ts.MethodDeclaration =>
|
||||
ts.isMethodDeclaration(member) &&
|
||||
member.body !== undefined &&
|
||||
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
||||
member.name.text === name,
|
||||
);
|
||||
if (!declaration?.body || declaration.asteriskToken) return null;
|
||||
const modifiers = ts.getModifiers(declaration) ?? [];
|
||||
if (
|
||||
modifiers.some(
|
||||
(m) =>
|
||||
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
|
||||
// Generic instances and mixins share layouts across declarations, so
|
||||
// they only report whether construction exposes the instance.
|
||||
const escape: { escapes: boolean; assigned: Set<string> | null } =
|
||||
familyMode || mixin
|
||||
? { escapes: derived, assigned: null }
|
||||
: constructionEscape({
|
||||
ctor,
|
||||
fields: fieldOrder.flatMap((f) =>
|
||||
paramProps.some((pp) => pp.name === f.name)
|
||||
? []
|
||||
: [{ name: f.name, initializer: f.initializer }],
|
||||
),
|
||||
paramProps: paramProps.map((pp) => pp.name),
|
||||
derived,
|
||||
baseEscapes:
|
||||
base === null ||
|
||||
callableBase !== undefined ||
|
||||
factoryBaseExpression !== undefined ||
|
||||
base.constructionEscapes === true,
|
||||
isField: (name) =>
|
||||
fields.has(name) &&
|
||||
!methods.has(`set:${name}`) &&
|
||||
!lowerer.findMethodOn(base, `set:${name}`),
|
||||
privateMethodBody: (call) => {
|
||||
const callee = call.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee)) return null;
|
||||
const name = callee.name.text;
|
||||
const declaration = decl.members.find(
|
||||
(member): member is ts.MethodDeclaration =>
|
||||
ts.isMethodDeclaration(member) &&
|
||||
member.body !== undefined &&
|
||||
(ts.isIdentifier(member.name) || ts.isPrivateIdentifier(member.name)) &&
|
||||
member.name.text === name,
|
||||
);
|
||||
if (!declaration?.body || declaration.asteriskToken) return null;
|
||||
const modifiers = ts.getModifiers(declaration) ?? [];
|
||||
if (
|
||||
modifiers.some(
|
||||
(m) =>
|
||||
m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.AsyncKeyword,
|
||||
)
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const isPrivate =
|
||||
ts.isPrivateIdentifier(declaration.name) ||
|
||||
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
|
||||
return isPrivate ? declaration.body : null;
|
||||
},
|
||||
});
|
||||
return null;
|
||||
const isPrivate =
|
||||
ts.isPrivateIdentifier(declaration.name) ||
|
||||
modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword);
|
||||
return isPrivate ? declaration.body : null;
|
||||
},
|
||||
});
|
||||
// The deferred definite-assignment check: a field on the unguarded
|
||||
// list passes only with an unconditional `this.x = ...` at the
|
||||
// constructor's TOP LEVEL — the same standard the JS-class path
|
||||
@@ -3584,30 +3610,18 @@ export function collectClassShapeInner(
|
||||
}
|
||||
}
|
||||
|
||||
// Fields assigned after construction can expose the instance (a method
|
||||
// call, a base constructor that calls an override) can be read while
|
||||
// Node still holds undefined. Their slots admit undefined, allocation
|
||||
// writes it, and reads yield it, as in Node.
|
||||
const earlyReadFields = new Set<string>(base?.earlyReadFields ?? []);
|
||||
if (escape.assigned && escape.escapes) {
|
||||
if (escape.assigned && !inst) {
|
||||
for (const f of fieldOrder) {
|
||||
if (f.redeclared || escape.assigned.has(f.name) || deferredInitFields.has(f.name)) continue;
|
||||
if (paramProps.some((pp) => pp.name === f.name)) continue;
|
||||
const declared = fields.get(f.name);
|
||||
if (declared === undefined || !earlyReadable(lowerer, declared)) continue;
|
||||
// A declared `T | undefined` field already starts undefined.
|
||||
if (declared.kind === "union" && lowerer.armTag(declared.unionId, UNDEFINED_T) >= 0)
|
||||
if (declared?.kind !== "object" || lowerer.classes.get(declared.className)?.def.runtime)
|
||||
continue;
|
||||
const armed = lowerer.runtimeOptionalType(declared);
|
||||
if (armed.kind !== "union") continue;
|
||||
const armed = lowerer.withUndefinedArm(declared);
|
||||
if (armed === null || armed.kind !== "union") continue;
|
||||
fields.set(f.name, armed);
|
||||
f.type = armed;
|
||||
// Primitive and union slots yield the undefined value itself.
|
||||
// Reference slots keep the deferred-init reads: consumers that
|
||||
// observe undefined (printing, equality, typeof, nullish defaults)
|
||||
// see it, and member access on it throws a TypeError, as in Node.
|
||||
if (declared.kind === "union" || PRIMITIVE_EARLY_READS.has(declared.kind))
|
||||
earlyReadFields.add(f.name);
|
||||
else deferredInitFields.add(f.name);
|
||||
deferredInitFields.add(f.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3712,43 +3726,6 @@ export function collectClassShapeInner(
|
||||
(name) => !hasAccessor(name) && !methods.has(name) && !lowerer.findMethodOn(base, name),
|
||||
)
|
||||
: null;
|
||||
// Constructor statements that run after the instance may have been
|
||||
// exposed (a method call, `this` passed along, a base constructor
|
||||
// that exposes it): a field they create can be read before its
|
||||
// assignment, while Node holds undefined.
|
||||
const exposedBefore = new Set<ts.Statement>();
|
||||
{
|
||||
let exposed = false;
|
||||
for (const stmt of ctor?.body?.statements ?? []) {
|
||||
const expression = ts.isExpressionStatement(stmt) ? stmt.expression : null;
|
||||
if (
|
||||
expression &&
|
||||
ts.isCallExpression(expression) &&
|
||||
expression.expression.kind === ts.SyntaxKind.SuperKeyword
|
||||
) {
|
||||
if (base?.constructionEscapes === true || expression.arguments.some(mentionsReceiver))
|
||||
exposed = true;
|
||||
continue;
|
||||
}
|
||||
const assigned =
|
||||
expression &&
|
||||
ts.isBinaryExpression(expression) &&
|
||||
expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(expression.left) &&
|
||||
expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
? expression
|
||||
: null;
|
||||
if (exposed || (assigned && mentionsReceiver(assigned.right))) exposedBefore.add(stmt);
|
||||
const plainAssignment =
|
||||
assigned !== null &&
|
||||
ts.isPropertyAccessExpression(assigned.left) &&
|
||||
!mentionsReceiver(assigned.right) &&
|
||||
!hasAccessor(assigned.left.name.text) &&
|
||||
!methods.has(assigned.left.name.text) &&
|
||||
!lowerer.findMethodOn(base, assigned.left.name.text);
|
||||
if (!plainAssignment && mentionsReceiver(stmt)) exposed = true;
|
||||
}
|
||||
}
|
||||
if (ctor?.body) {
|
||||
for (const stmt of ctor.body.statements) {
|
||||
const lhs =
|
||||
@@ -3819,15 +3796,8 @@ export function collectClassShapeInner(
|
||||
type = inheritedJsFieldType(lowerer, decl, name, type);
|
||||
// super() can invoke a subclass setter or method before its
|
||||
// constructor assignments run. These slots start undefined.
|
||||
const sealed = sealedPrefix?.created.has(name) === true;
|
||||
if (base && lowerer.dynConvertible(type) && !sealed) type = DYN;
|
||||
if (exposedBefore.has(stmt) && !sealed && earlyReadable(lowerer, type)) {
|
||||
const armed = lowerer.runtimeOptionalType(type);
|
||||
if (armed.kind === "union") {
|
||||
type = armed;
|
||||
earlyReadFields.add(name);
|
||||
}
|
||||
}
|
||||
if (base && lowerer.dynConvertible(type) && !sealedPrefix?.created.has(name))
|
||||
type = DYN;
|
||||
fields.set(name, type);
|
||||
fieldOrder.push({ name, type, initializer: undefined });
|
||||
continue;
|
||||
@@ -4088,7 +4058,6 @@ export function collectClassShapeInner(
|
||||
? { classDecorators: { nodes: classDecoratorNodes } }
|
||||
: {}),
|
||||
...(deferredInitFields.size > 0 ? { deferredInitFields } : {}),
|
||||
...(earlyReadFields.size > 0 ? { earlyReadFields } : {}),
|
||||
...(escape.escapes ? { constructionEscapes: true as const } : {}),
|
||||
};
|
||||
// GENERIC members get their declaring-class backlink now that the
|
||||
@@ -6263,33 +6232,6 @@ function overrideReturnAbi(
|
||||
return declared;
|
||||
}
|
||||
|
||||
/** Field kinds whose early reads yield the undefined value (earlyReadFields). */
|
||||
const PRIMITIVE_EARLY_READS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"f64",
|
||||
"bool",
|
||||
"string",
|
||||
"bigint",
|
||||
]);
|
||||
|
||||
/** Field types whose slot can admit undefined for reads before the field's
|
||||
* assignment. Date values are scalars, so their unassigned slot is memory
|
||||
* safe; checked-dynamic slots already hold undefined. */
|
||||
function earlyReadable(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (isUnitType(type)) return false;
|
||||
switch (type.kind) {
|
||||
case "dyn":
|
||||
case "void":
|
||||
case "date":
|
||||
case "generator":
|
||||
case "caught":
|
||||
return false;
|
||||
case "object":
|
||||
return lowerer.classes.get(type.className)?.def.runtime !== true;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** The nearest declaration of `name` at or above `info` — the method a
|
||||
* receiver of that static class runs when nothing below overrides it. */
|
||||
export function findMethodOn(
|
||||
@@ -6476,7 +6418,7 @@ export function collectInheritedJsFieldWrites(
|
||||
* hierarchy identifies the original declarations through aliases as well. */
|
||||
export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.SourceFile[]): void {
|
||||
const families: [ts.MethodDeclaration, ts.MethodDeclaration][] = [];
|
||||
collectClassMethodMutations(lowerer, files);
|
||||
lowerer.speculativeTypeQueries(() => collectClassMethodMutations(lowerer, files));
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
||||
const accessors = node.members.filter(
|
||||
@@ -7618,6 +7560,14 @@ export function throwingSetterFn(lowerer: Lowerer, info: ClassInfo, prop: string
|
||||
};
|
||||
}
|
||||
|
||||
/** The declared type a refused checked-dynamic class field names in its
|
||||
* diagnostic (`'any'`, `'WeakMap<object, string>'`): `unknown` and
|
||||
* `object` fields store checked-dynamic values, so the refusal is about
|
||||
* whatever else the declaration spells. */
|
||||
function dynFieldTypeSpelling(lowerer: Lowerer, node: ts.Node): string {
|
||||
return `'${lowerer.checker.typeToString(lowerer.typeOf(node))}'`;
|
||||
}
|
||||
|
||||
/** The base a constructor chain actually CALLS into: generic FAMILIES are
|
||||
* never constructed (no `%<family>.constructor` exists), so an
|
||||
* instantiation's construction-relevant base is the family's own base —
|
||||
@@ -7719,11 +7669,7 @@ function paramPropInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrLoca
|
||||
obj: { kind: "varRef", localId: thisLocal.id, type: thisType, loc },
|
||||
className: info.def.name,
|
||||
field: pp.name,
|
||||
// An early-readable field's slot admits undefined (earlyReadFields).
|
||||
value: lowerer.coerceToExpected(
|
||||
{ kind: "varRef", localId: local!.id, type: local!.type, loc },
|
||||
info.fields.get(pp.name) ?? local!.type,
|
||||
),
|
||||
value: { kind: "varRef", localId: local!.id, type: local!.type, loc },
|
||||
loc,
|
||||
});
|
||||
}
|
||||
@@ -8531,6 +8477,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
undefined,
|
||||
lowerer.checker.getSymbolAtLocation(expr.expression),
|
||||
);
|
||||
const valueReturning = lowerValueReturningConstructorNew(lowerer, expr);
|
||||
if (valueReturning) return valueReturning;
|
||||
const instanceConstructor = lowerInstanceConstructorNew(lowerer, expr);
|
||||
if (instanceConstructor) return instanceConstructor;
|
||||
const objectFactory = lowerObjectFactoryNew(lowerer, expr);
|
||||
@@ -8681,7 +8629,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
// `new zlib.Gzip(options?)` — the zlib stream classes.
|
||||
const zlibModeNs = zlibStreamModeOf(lowerer, resolved);
|
||||
if (zlibModeNs !== null) {
|
||||
const what = `new zlib.${expr.expression.name.text}`;
|
||||
const what = `new zlib.${zlibClassName(zlibModeNs)}`;
|
||||
return lowerZlibStreamNew(lowerer, expr.arguments ?? [], zlibModeNs, what, expr, loc);
|
||||
}
|
||||
// `new N.C(...)` / `new a.Point(...)` — construction through a
|
||||
@@ -8939,7 +8887,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
if (streamInfo) return lowerStreamNew(lowerer, expr, streamInfo);
|
||||
const zlibMode = zlibStreamModeOf(lowerer, symbol);
|
||||
if (zlibMode !== null) {
|
||||
const what = `new zlib.${symbol!.name}`;
|
||||
const what = `new zlib.${zlibClassName(zlibMode)}`;
|
||||
return lowerZlibStreamNew(lowerer, expr.arguments ?? [], zlibMode, what, expr, loc);
|
||||
}
|
||||
// `new URL(input)`: the WHATWG URL class (stdlib/@types provenance —
|
||||
|
||||
@@ -308,7 +308,7 @@ function lowerDnsPromisesCall(
|
||||
}
|
||||
const promise: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: service ? "dns.lookupServicePromise" : "dns.lookupPromise",
|
||||
fn: service ? "dns.promisesLookupService" : "dns.promisesLookup",
|
||||
args: [dyn(args[0]!), args.length === 2 ? dyn(args[1]!) : dynUndefinedExpr(loc)],
|
||||
type: { kind: "promise", inner: DYN },
|
||||
loc,
|
||||
@@ -776,3 +776,98 @@ export function lowerDgramMethodCall(
|
||||
lowerer.checker.getSymbolAtLocation(access.name),
|
||||
);
|
||||
}
|
||||
|
||||
/** True for a dns callback function util.promisify converts. */
|
||||
export function isPromisifiedDnsMember(member: string): boolean {
|
||||
return member === "lookup" || member === "lookupService";
|
||||
}
|
||||
|
||||
/** A call through `promisify(dns.lookup)` or `promisify(dns.lookupService)`:
|
||||
* the callback form runs with a callback that settles a promise of the
|
||||
* checked-dynamic `{ address, family }` (the address array with
|
||||
* options.all) or `{ hostname, service }`, which Node's custom promisified
|
||||
* forms produce. An argument error rejects the promise instead of
|
||||
* throwing. A declared result type other than `unknown` is reached through
|
||||
* an async helper that awaits that promise and checks the value. */
|
||||
export function lowerPromisifiedDnsCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
member: string,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const args = expr.arguments;
|
||||
if (args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(`the promisified dns.${member} with spread arguments`, expr);
|
||||
}
|
||||
const service = member === "lookupService";
|
||||
if (service ? args.length !== 2 : args.length < 1 || args.length > 2) {
|
||||
lowerer.noLowering(
|
||||
`the promisified dns.${member} with ${args.length} arguments`,
|
||||
expr,
|
||||
service
|
||||
? "the form is lookupService(address, port)"
|
||||
: "the supported forms are lookup(hostname) and lookup(hostname, options)",
|
||||
);
|
||||
}
|
||||
const dyn = (node: ts.Expression | undefined): IrExpr =>
|
||||
node ? lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN) : dynUndefinedExpr(loc);
|
||||
const promiseDyn: IrType = { kind: "promise", inner: DYN };
|
||||
const call: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: service ? "dns.lookupServicePromise" : "dns.lookupPromise",
|
||||
args: [dyn(args[0]), dyn(args[1])],
|
||||
type: promiseDyn,
|
||||
loc,
|
||||
};
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (declared?.kind !== "promise") {
|
||||
lowerer.noLowering(`the promisified dns.${member} with this result type`, expr);
|
||||
}
|
||||
const value = declared.inner;
|
||||
if (value.kind === "dyn") return call;
|
||||
if (
|
||||
!canDynCheckTo(
|
||||
value,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
`the promisified dns.${member} with a '${lowerer.fmt(declared)}' result`,
|
||||
expr,
|
||||
"the result type must have a checked conversion from the lookup's value",
|
||||
);
|
||||
}
|
||||
const key = `dnsPromise:${JSON.stringify(value)}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
name = `%dnsPromise.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "p.0", name: "p", type: promiseDyn }],
|
||||
returnType: value,
|
||||
async: true,
|
||||
locals: [{ id: "p.0", name: "p", type: promiseDyn, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "dynCheck",
|
||||
value: {
|
||||
kind: "awaitExpr",
|
||||
value: { kind: "varRef", localId: "p.0", type: promiseDyn, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: value,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [call], type: declared, loc };
|
||||
}
|
||||
|
||||
@@ -31,7 +31,14 @@ import { literalUnionArm } from "../union-discriminants.js";
|
||||
* (FieldTarget). */
|
||||
import { isFsStreamMember, lowerFsStreamProperty } from "./builtins/fs-streams.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "./builtins/compression.js";
|
||||
import {
|
||||
isZlibStreamMember,
|
||||
lowerZlibInstanceOf,
|
||||
lowerZlibStreamBytesWritten,
|
||||
zlibClassAliasOf,
|
||||
zlibClassModeOfExpr,
|
||||
zlibClassName,
|
||||
} from "./builtins/compression.js";
|
||||
import { constituentTypes } from "../ts7/checker.js";
|
||||
import { dirname } from "node:path";
|
||||
import * as posix from "node:path/posix";
|
||||
@@ -131,7 +138,7 @@ import {
|
||||
lowerSafeBytesRead,
|
||||
} from "./array-values.js";
|
||||
import { strCharsCall } from "./containers/array-construction.js";
|
||||
import { lowerOptionalStringIndex } from "./string-index.js";
|
||||
import { lowerOptionalStringIndex, lowerUnionStringIndex } from "./string-index.js";
|
||||
import { tryLowerIndexedComparison } from "./indexed-comparison.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
import { TEMPORAL_GLOBAL, unsupportedModuleFeatureOf } from "../builtin-modules.js";
|
||||
@@ -197,6 +204,7 @@ import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import {
|
||||
fenceNodeModuleMutation,
|
||||
isNodeModuleValue,
|
||||
lowerCjsTopLevelThis,
|
||||
lowerNodeModuleIdentifier,
|
||||
lowerNodeModuleProperty,
|
||||
lowerRequireCacheElement,
|
||||
@@ -609,6 +617,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (isNodeModuleValue(lowerer, expr.expression)) {
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
}
|
||||
// `typeof zlib.Gzip`, `typeof C` over a zlib class alias.
|
||||
if (zlibClassModeOfExpr(lowerer, expr.expression) !== null) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(expr.expression)) {
|
||||
const member = expr.expression;
|
||||
const prototype = member.expression;
|
||||
@@ -694,6 +706,30 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
if (isTemporalGlobalRoot(lowerer, tested, sym)) {
|
||||
return { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
}
|
||||
// `const { inspect } = require("util")` / `import { inspect } from
|
||||
// "node:util"`: a binding of a builtin module's exported function,
|
||||
// class or object. JavaScript sources represent such a value without
|
||||
// its own lowering as an identity token (a string), so the operand's
|
||||
// representation cannot answer. Node's answer follows the export's
|
||||
// kind, and the binding read has no effects.
|
||||
const imported = isJsSourceFile(tested.getSourceFile())
|
||||
? lowerer.builtinImportOf(tested)
|
||||
: null;
|
||||
if (
|
||||
imported &&
|
||||
builtinModuleConstOf(lowerer, imported.module, imported.member) === undefined
|
||||
) {
|
||||
const t = lowerer.typeOf(tested);
|
||||
if (
|
||||
lowerer.checker.getCallSignatures(t).length > 0 ||
|
||||
lowerer.checker.getConstructSignatures(t).length > 0
|
||||
) {
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
}
|
||||
if ((t.flags & ts.TypeFlags.Object) !== 0) {
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
}
|
||||
}
|
||||
if (lowerer.isStdlibSymbol(sym)) {
|
||||
const t = lowerer.typeOf(expr.expression);
|
||||
if (
|
||||
@@ -857,7 +893,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
projection?.kind === "promisified-exec-file" ||
|
||||
projection?.kind === "promisified-exec" ||
|
||||
projection?.kind === "promisified-builtin" ||
|
||||
projection?.kind === "promisified-zlib"
|
||||
projection?.kind === "promisified-zlib" ||
|
||||
projection?.kind === "promisified-crypto" ||
|
||||
projection?.kind === "promisified-dns"
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -879,6 +917,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
`a builtin function alias as an escaping value (call '${expr.text}' directly)`,
|
||||
);
|
||||
}
|
||||
// `const C = zlib.Gzip` owns no runtime slot: construction,
|
||||
// instanceof, typeof and .name lower at their use sites.
|
||||
if (sym && zlibClassAliasOf(lowerer, sym) !== null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`a zlib class alias as an escaping value (use '${expr.text}' with new, instanceof, typeof or .name)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Union-typed bindings read through tsc's control-flow narrowing:
|
||||
// when the checker types this USE as a single arm, maybeNarrow
|
||||
@@ -1666,6 +1713,9 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
// constant. TypeScript keeps the fence (noImplicitThis makes it a
|
||||
// compile-time story there).
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
// Module scope of a CommonJS file: `this` is module.exports.
|
||||
const exportsThis = lowerCjsTopLevelThis(lowerer, expr);
|
||||
if (exportsThis) return exportsThis;
|
||||
return { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc };
|
||||
}
|
||||
lowerer.unsupported("SC1080", expr);
|
||||
@@ -1826,6 +1876,11 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const table = lowerInternTableProperty(lowerer, expr);
|
||||
if (table) return table;
|
||||
}
|
||||
// `zlib.Gzip.name`, `C.name` over a zlib class alias.
|
||||
if (expr.name.text === "name" && !expr.questionDotToken) {
|
||||
const mode = zlibClassModeOfExpr(lowerer, expr.expression);
|
||||
if (mode !== null) return { kind: "strLit", value: zlibClassName(mode), type: STRING, loc };
|
||||
}
|
||||
// Expando statics may still be absent even when JS inference sees
|
||||
// the eventual assignment's scalar type. Preserve the checked value.
|
||||
const staticField = lowerStaticFieldRead(lowerer, expr);
|
||||
@@ -3157,57 +3212,12 @@ export function lowerTernary(
|
||||
* union-typed reference at use sites (`if (r.kind === "ok") { ...r... }`
|
||||
* types `r` as the ok-arm inside the branch); the IR value is still the
|
||||
* tagged union, so the read is bridged with a `unionNarrow` extracting
|
||||
* the arm's payload. The extraction is tag-UNCHECKED at runtime —
|
||||
* soundness rests entirely on tsc having proven the tag (the project's
|
||||
* trust-the-checker thesis; see docs/ir.md). A checker type that maps to
|
||||
* the arm's payload. The extraction relies on tsc having proven the tag;
|
||||
* backends verify it once and trap on a mismatch, so a wrong proof stops
|
||||
* the program instead of reading another arm. A checker type that maps to
|
||||
* the same union (unnarrowed use), to a SUB-union (partial narrowing —
|
||||
* unrepresentable without a re-tag), or to nothing (`never` in an
|
||||
* exhaustive default) leaves the expression union-typed. */
|
||||
/** Uses that read an early-read field's value as Node does when it is
|
||||
* undefined: printing, templates, operators, tests, member access, calls of
|
||||
* program functions (whose parameters the optional-value analysis widens),
|
||||
* and stores into bindings, fields and returns. Other uses, such as library
|
||||
* arguments, extract the declared type with a check. */
|
||||
function observesEarlyRead(node: ts.Node): boolean {
|
||||
let child = node;
|
||||
let parent = node.parent;
|
||||
while (parent && ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (!parent) return false;
|
||||
if (
|
||||
ts.isTemplateSpan(parent) ||
|
||||
ts.isBinaryExpression(parent) ||
|
||||
ts.isPrefixUnaryExpression(parent) ||
|
||||
ts.isPostfixUnaryExpression(parent) ||
|
||||
ts.isTypeOfExpression(parent) ||
|
||||
ts.isConditionalExpression(parent) ||
|
||||
ts.isIfStatement(parent) ||
|
||||
ts.isWhileStatement(parent) ||
|
||||
ts.isReturnStatement(parent) ||
|
||||
ts.isVariableDeclaration(parent) ||
|
||||
ts.isPropertyAssignment(parent) ||
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
(ts.isPropertyAccessExpression(parent) && parent.expression === child) ||
|
||||
(ts.isElementAccessExpression(parent) && parent.expression === child)
|
||||
)
|
||||
return true;
|
||||
if (ts.isCallExpression(parent) && parent.arguments.includes(child as ts.Expression)) {
|
||||
const callee = parent.expression;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
ts.isIdentifier(callee.expression) &&
|
||||
callee.expression.text === "console"
|
||||
)
|
||||
return true;
|
||||
return (
|
||||
!ts.isPropertyAccessExpression(callee) || callee.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr {
|
||||
if (
|
||||
(expr.type.kind === "union" || expr.type.kind === "dyn") &&
|
||||
@@ -3394,8 +3404,8 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
}
|
||||
// instanceof narrowing for classes: tsc types this USE as a subclass of
|
||||
// the IR value's class (only a dynamic instanceof test narrows a class
|
||||
// type), so the read bridges with an unchecked static downcast — the
|
||||
// same trust-the-checker contract as the union bridge below.
|
||||
// type), so the read bridges with a static downcast that backends verify
|
||||
// at run time, like the union bridge below.
|
||||
if (expr.type.kind === "object") {
|
||||
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (narrowed?.kind === "union") {
|
||||
@@ -3418,25 +3428,6 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
if (expr.type.kind !== "union") return expr;
|
||||
if (ts.isPropertyAccessExpression(node) && isOptionalProcessStreamProperty(lowerer, node))
|
||||
return expr;
|
||||
// A field read before its assignment holds undefined although its
|
||||
// declared type omits it; the declared type is no narrowing proof. A flow
|
||||
// narrowing past the declared type (`if (this.x !== null)`) extracts the
|
||||
// narrowed arm with a check, since the slot may still hold undefined.
|
||||
if (
|
||||
expr.kind === "fieldGet" &&
|
||||
lowerer.classes.get(expr.className)?.earlyReadFields?.has(expr.field) === true
|
||||
) {
|
||||
const symbol = ts.isPropertyAccessExpression(node)
|
||||
? lowerer.checker.getSymbolAtLocation(node.name)
|
||||
: undefined;
|
||||
const declared = symbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(symbol)) : null;
|
||||
const narrowedArm = lowerer.mapTypeOf(lowerer.typeOf(node));
|
||||
if (narrowedArm === null || narrowedArm.kind === "union") return expr;
|
||||
if (declared !== null && typeEquals(declared, narrowedArm) && observesEarlyRead(node))
|
||||
return expr;
|
||||
const helper = lowerer.narrowedArmHelper(expr.type.unionId, narrowedArm, expr.loc);
|
||||
return helper ? lowerer.checkedNarrowCall(helper, expr, narrowedArm, expr.loc) : expr;
|
||||
}
|
||||
|
||||
// Represented fields and index values can include undefined even when
|
||||
// checker inference or lib.d.ts declares strings (RegExp captures).
|
||||
@@ -3664,6 +3655,22 @@ function checkedStaleArm(
|
||||
return helper ? lowerer.checkedNarrowCall(helper, value, arm, value.loc) : null;
|
||||
}
|
||||
|
||||
/** V8's spelling of a callee in "is not a function": a string-literal
|
||||
* element key reads as a property name (`o["a"]` is `o.a`), other keys
|
||||
* keep their brackets. */
|
||||
export function v8CalleeText(node: ts.Node): string {
|
||||
if (ts.isElementAccessExpression(node)) {
|
||||
const key = node.argumentExpression;
|
||||
const object = v8CalleeText(node.expression);
|
||||
return ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)
|
||||
? `${object}.${key.text}`
|
||||
: `${object}[${key.getText()}]`;
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(node) && !node.questionDotToken)
|
||||
return `${v8CalleeText(node.expression)}.${node.name.text}`;
|
||||
return node.getText();
|
||||
}
|
||||
|
||||
/** Node's TypeError for the operation consuming `node` when it holds a
|
||||
* value of type `found`, or null where Node would not throw. */
|
||||
function staleReadMessage(node: ts.Node, found: IrType): string | null {
|
||||
@@ -3674,7 +3681,7 @@ function staleReadMessage(node: ts.Node, found: IrType): string | null {
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (ts.isCallExpression(parent) && parent.expression === child && !parent.questionDotToken) {
|
||||
return found.kind === "func" ? null : `${node.getText()} is not a function`;
|
||||
return found.kind === "func" ? null : `${v8CalleeText(node)} is not a function`;
|
||||
}
|
||||
const unit = found.kind === "undefinedT" ? "undefined" : found.kind === "nullT" ? "null" : null;
|
||||
if (unit === null) return null;
|
||||
@@ -4065,7 +4072,7 @@ function runtimeOptionalAssertionErases(
|
||||
* (`w = 5; if (w !== null)`), the comparison is statically decided and
|
||||
* folds to a bool literal — that drops only a side-effect-free read
|
||||
* (flow narrowing applies to references, never to calls), and soundness
|
||||
* is the same trust-the-checker bet unionNarrow already makes. Null when
|
||||
* rests on the same narrowing proof unionNarrow relies on. Null when
|
||||
* neither side is a unit literal (not a unit comparison). */
|
||||
export function lowerUnitComparison(
|
||||
lowerer: Lowerer,
|
||||
@@ -5068,6 +5075,34 @@ export function lowerIntrinsicProperty(
|
||||
return { kind: "libCall", fn: "dyn.arrayPrototype", args: [], type: DYN, loc: locOf(expr) };
|
||||
if (lowerer.stdlibGlobalMember(expr, "Object") === "prototype")
|
||||
return { kind: "libCall", fn: "dyn.objectPrototype", args: [], type: DYN, loc: locOf(expr) };
|
||||
// Function.prototype as a VALUE (a prototype-chain link:
|
||||
// `Object.setPrototypeOf(F.prototype, Function.prototype)`): one
|
||||
// anonymous function answering undefined, length 0 — a zero-capture
|
||||
// closure of a compiler helper, so every read is the same function
|
||||
// (closure identity) and shares its property table. Reads through it
|
||||
// find the function members the checked runtime models for every
|
||||
// function (apply, constructor, name, length). Member forms keep their
|
||||
// own lowerings.
|
||||
if (lowerer.stdlibGlobalMember(expr, "Function") === "prototype") {
|
||||
const loc = locOf(expr);
|
||||
const name = "%dyn.functionPrototype";
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name))
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: DYN,
|
||||
body: [{ kind: "return", value: dynUndefinedExpr(loc), loc }],
|
||||
loc,
|
||||
});
|
||||
return {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "closure", fnName: name, captures: [], type: funcOf([], DYN), loc },
|
||||
fnName: "",
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (lowerer.stdlibGlobalMember(expr, "Array") === "isArray")
|
||||
return lowerArrayIsArrayValue(lowerer, locOf(expr));
|
||||
if (lowerer.stdlibGlobalMember(expr, "Array") === "from")
|
||||
@@ -6973,6 +7008,19 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
locOf(expr),
|
||||
);
|
||||
}
|
||||
// The checker narrowed the receiver to string, but its stored value
|
||||
// is still the union (an array-derived binding, a typeof guard).
|
||||
if (index.type.kind === "f64") {
|
||||
const read = lowerUnionStringIndex(
|
||||
lowerer,
|
||||
recv,
|
||||
index,
|
||||
expr.argumentExpression,
|
||||
lowerer.withUndefinedArm(STRING),
|
||||
locOf(expr),
|
||||
);
|
||||
if (read) return read;
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
@@ -7151,15 +7199,173 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
|
||||
return lowerer.maybeNarrow({ kind: "arrayGet", arr, index, type: elemT, loc: locOf(expr) }, expr);
|
||||
}
|
||||
|
||||
/** A record receiver that an outer keyed read left optional
|
||||
* (`nested[a][b]`, where `nested[a]` can miss). The receiver evaluates
|
||||
* once into `stmts`; `present` is its record arm, valid only once the
|
||||
* access has tested `stable` against the undefined arm. */
|
||||
interface OptionalRecordReceiver {
|
||||
stmts: IrStmt[];
|
||||
stable: IrExpr;
|
||||
present: IrExpr;
|
||||
unionId: string;
|
||||
undefTag: number;
|
||||
}
|
||||
|
||||
function optionalRecordReceiver(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): OptionalRecordReceiver | null {
|
||||
if (value.type.kind !== "union") return null;
|
||||
const unionId = value.type.unionId;
|
||||
const undefTag = lowerer.armTag(unionId, UNDEFINED_T);
|
||||
if (undefTag < 0 || lowerer.unions.get(unionId)?.arms.length !== 2) return null;
|
||||
const record = lowerer.stripUndefinedArm(value.type);
|
||||
if (record.kind !== "record") return null;
|
||||
const stmts: IrStmt[] = [];
|
||||
const stable = stabilized(lowerer, value, "%recordReceiver", stmts);
|
||||
const present: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId,
|
||||
tag: lowerer.armTag(unionId, record),
|
||||
value: stable,
|
||||
type: record,
|
||||
loc,
|
||||
};
|
||||
return { stmts, stable, present, unionId, undefTag };
|
||||
}
|
||||
|
||||
/** `value`, evaluated once at this point of `out`. */
|
||||
function stabilized(lowerer: Lowerer, value: IrExpr, name: string, out: IrStmt[]): IrExpr {
|
||||
if (isSafeToRepeat(value)) return value;
|
||||
const tmp = lowerer.declareHiddenLocal(name, value.type);
|
||||
out.push({ kind: "varDecl", localId: tmp.id, init: value, loc: value.loc });
|
||||
return varRef(tmp.id, value.type, value.loc);
|
||||
}
|
||||
|
||||
function undefinedReceiver(receiver: OptionalRecordReceiver, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "unionIsTag",
|
||||
unionId: receiver.unionId,
|
||||
tag: receiver.undefTag,
|
||||
negated: false,
|
||||
value: receiver.stable,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Node's TypeError for a keyed read or write on an undefined receiver. */
|
||||
function undefinedReceiverThrow(
|
||||
key: IrExpr,
|
||||
action: "read" | "set",
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const head = `Cannot ${action} properties of undefined (${action === "read" ? "reading" : "setting"} '`;
|
||||
const message: IrExpr = {
|
||||
kind: "strConcat",
|
||||
left: {
|
||||
kind: "strConcat",
|
||||
left: { kind: "strLit", value: head, type: STRING, loc },
|
||||
right: key,
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
right: { kind: "strLit", value: "')", type: STRING, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "error.nodeThrow",
|
||||
args: [numLit(1, loc), { kind: "strLit", value: "", type: STRING, loc }, message],
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The receiver and key of a keyed access whose receiver can be undefined,
|
||||
* evaluated in order into `stmts`, with the receiver's record arm and the
|
||||
* key's property-name string. Null when the receiver is not optional. */
|
||||
function guardedRecordAccess(
|
||||
lowerer: Lowerer,
|
||||
access: ts.ElementAccessExpression,
|
||||
rawObj: IrExpr,
|
||||
): { receiver: OptionalRecordReceiver; key: IrExpr; name: IrExpr } | null {
|
||||
const receiver = optionalRecordReceiver(lowerer, rawObj, locOf(access.expression));
|
||||
if (!receiver) return null;
|
||||
const keyNode = access.argumentExpression;
|
||||
const litKey = recordKeyLiteralText(keyNode) ?? recordKeyTypeLiteralText(lowerer, keyNode);
|
||||
const key =
|
||||
litKey !== null
|
||||
? ({ kind: "strLit", value: litKey, type: STRING, loc: locOf(keyNode) } satisfies IrExpr)
|
||||
: stabilized(lowerer, lowerer.lowerExpr(keyNode), "%recordKey", receiver.stmts);
|
||||
// A symbol key names no string property; its message spelling is not
|
||||
// modeled.
|
||||
const name =
|
||||
key.type.kind === "symbol"
|
||||
? ({ kind: "strLit", value: "Symbol()", type: STRING, loc: locOf(keyNode) } satisfies IrExpr)
|
||||
: lowerRecordPropertyKey(lowerer, key, keyNode);
|
||||
return { receiver, key, name };
|
||||
}
|
||||
|
||||
/** `nested[a][b] = v` where `nested[a]` can miss: the receiver, the key and
|
||||
* the value evaluate in order, then an undefined receiver throws Node's
|
||||
* TypeError and a present one takes the write. Only member and identifier
|
||||
* receivers are probed, since a probe that finds no optional receiver
|
||||
* lowers the receiver again. */
|
||||
function lowerGuardedRecordWrite(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
target: ts.ElementAccessExpression,
|
||||
): IrStmt | null {
|
||||
let receiverNode: ts.Expression = target.expression;
|
||||
while (ts.isParenthesizedExpression(receiverNode)) receiverNode = receiverNode.expression;
|
||||
if (
|
||||
lowerer.chainRecvByNode.has(target.expression) ||
|
||||
!(
|
||||
ts.isElementAccessExpression(receiverNode) ||
|
||||
ts.isPropertyAccessExpression(receiverNode) ||
|
||||
ts.isIdentifier(receiverNode)
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const guarded = guardedRecordAccess(lowerer, target, lowerer.lowerExpr(target.expression));
|
||||
if (!guarded) return null;
|
||||
const { receiver, key, name } = guarded;
|
||||
const loc = locOf(expr);
|
||||
const value = stabilized(lowerer, lowerer.lowerExpr(expr.right), "%recordValue", receiver.stmts);
|
||||
const write = lowerer.withExpressionOverride(target.expression, receiver.present, () =>
|
||||
lowerer.withExpressionOverride(target.argumentExpression, key, () =>
|
||||
lowerer.withExpressionOverride(expr.right, value, () => lowerElementWrite(lowerer, expr)),
|
||||
),
|
||||
);
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
...receiver.stmts,
|
||||
{
|
||||
kind: "if",
|
||||
cond: undefinedReceiver(receiver, loc),
|
||||
then: [{ kind: "exprStmt", expr: undefinedReceiverThrow(name, "set", VOID, loc), loc }],
|
||||
else_: [write],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `r[k]` over a (non-tuple) record shape. A LITERAL key naming a declared
|
||||
* field is the bracket spelling of field access (the ONLY spelling tsc
|
||||
* permits for signature-declared fields under
|
||||
* noPropertyAccessFromIndexSignature) — an ordinary recordGet. Everything
|
||||
* else is a runtime-keyed read (recordKeyGet): declared fields answer
|
||||
* first via an emitted string-switch, index-signature shapes fall through
|
||||
* to the overflow map. The result type is the CHECKER's for the access —
|
||||
* the index signature's value type (dyn for `unknown`), or its
|
||||
* undefined-armed union under noUncheckedIndexedAccess. Every declared
|
||||
* to the overflow map. The result type is the index signature's value type
|
||||
* armed with undefined (dyn for `unknown`): a missing key reads undefined,
|
||||
* as in JavaScript, whatever noUncheckedIndexedAccess says. Every declared
|
||||
* field must be able to SURFACE as that type (equal, an arm of it, or —
|
||||
* for dyn results — JSON-safe for the dyn conversion); shapes mixing in
|
||||
* fields outside that stay fenced. Declared-only shapes support reads
|
||||
@@ -7183,9 +7389,39 @@ export function lowerRecordKeyRead(
|
||||
// a generic instance) is the same static read — identifier evaluation
|
||||
// is pure, so skipping it matches JS exactly.
|
||||
const litKey = recordKeyLiteralText(keyNode) ?? recordKeyTypeLiteralText(lowerer, keyNode);
|
||||
// An outer keyed read can leave the receiver undefined: evaluate the
|
||||
// receiver and the key, then throw Node's TypeError or read the present
|
||||
// record.
|
||||
// The receiver lowers FIRST (both branches below read it, and JS
|
||||
// evaluates the receiver before the key).
|
||||
const obj = lowerer.lowerExpr(expr.expression);
|
||||
{
|
||||
const guarded = guardedRecordAccess(lowerer, expr, obj);
|
||||
if (guarded) {
|
||||
const { receiver, key, name } = guarded;
|
||||
const read = lowerer.withExpressionOverride(expr.expression, receiver.present, () =>
|
||||
litKey !== null
|
||||
? lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefined)
|
||||
: lowerer.withExpressionOverride(keyNode, key, () =>
|
||||
lowerRecordKeyRead(lowerer, expr, shapeId, shape, includeUndefined),
|
||||
),
|
||||
);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: receiver.stmts,
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: undefinedReceiver(receiver, loc),
|
||||
then: undefinedReceiverThrow(name, "read", read.type, loc),
|
||||
else_: read,
|
||||
type: read.type,
|
||||
loc,
|
||||
},
|
||||
type: read.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
// A record-mapped CHECKER type over a VALUE living in the checked-dynamic tree (a JS
|
||||
// file-scope object-literal global): the checked-dynamic keyed read —
|
||||
// dynKeyGet against the runtime keys (a missing key answers the checked-dynamic tree
|
||||
@@ -7299,21 +7535,29 @@ export function lowerRecordKeyRead(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// The DECLARED result type of the access — the index signature's value
|
||||
// type (armed with undefined under noUncheckedIndexedAccess), or the
|
||||
// declared fields' one common type on signature-free shapes (tsc's
|
||||
// keyof check proved membership; a smuggled miss traps). The checker
|
||||
// may have NARROWED this occurrence (assignment CFA on literal keys) —
|
||||
// maybeNarrow bridges to the narrowed arm exactly like a field read.
|
||||
// The result type of the access — the index signature's value type
|
||||
// armed with undefined, or the declared fields' one common type on
|
||||
// signature-free shapes (tsc's keyof check proved membership; a
|
||||
// smuggled miss traps). On signature-free shapes the checker may have
|
||||
// NARROWED this occurrence (assignment CFA on literal keys) — maybeNarrow
|
||||
// bridges to the narrowed arm exactly like a field read.
|
||||
let declared: IrType | null = null;
|
||||
// An index-signature read is a JavaScript property read: a missing key
|
||||
// answers undefined even without noUncheckedIndexedAccess. The read keeps
|
||||
// that explicit optional union, as an ordinary array read does.
|
||||
let optionalRead = false;
|
||||
// A read whose every consumer treats undefined like NaN (see
|
||||
// nan-coded-reads.ts) is a plain number that is NaN for a missing key.
|
||||
const nanCoded =
|
||||
!includeUndefined &&
|
||||
shape.indexValue?.kind === "f64" &&
|
||||
lowerer.nanCodedReads?.isNumericRead(expr) === true;
|
||||
if (shape.indexValue) {
|
||||
declared = shape.indexValue;
|
||||
if (
|
||||
declared.kind !== "dyn" &&
|
||||
(includeUndefined || lowerer.program.getCompilerOptions().noUncheckedIndexedAccess)
|
||||
) {
|
||||
if (declared.kind !== "dyn" && !nanCoded) {
|
||||
declared = lowerer.withUndefinedArmOf(declared);
|
||||
if (!declared) lowerer.badType(expr, lowerer.typeOf(expr));
|
||||
optionalRead = true;
|
||||
}
|
||||
} else if (
|
||||
shape.fields.length > 0 &&
|
||||
@@ -7346,10 +7590,12 @@ export function lowerRecordKeyRead(
|
||||
shapeId,
|
||||
key,
|
||||
...(overflowOnly ? { overflowOnly: true as const } : {}),
|
||||
...(nanCoded ? { missNaN: true as const } : {}),
|
||||
type: declared,
|
||||
loc,
|
||||
};
|
||||
return includeUndefined ? read : lowerer.maybeNarrow(read, expr);
|
||||
if (nanCoded) return read;
|
||||
return includeUndefined || optionalRead ? read : lowerer.maybeNarrow(read, expr);
|
||||
}
|
||||
|
||||
/** ToPropertyKey for represented record keys. Missing array-derived string
|
||||
@@ -7457,7 +7703,8 @@ export function lowerPresenceOperand(lowerer: Lowerer, node: ts.Expression): IrE
|
||||
* side of `||`/`??`). JavaScript answers undefined for missing record keys
|
||||
* and array indices even when noUncheckedIndexedAccess is disabled. Keep
|
||||
* that value in an explicit union until the surrounding guard/default has
|
||||
* consumed it; ordinary unchecked reads retain the typed trap. Assertions
|
||||
* consumed it, including class-field and declared-only reads that otherwise
|
||||
* keep their typed storage. Assertions
|
||||
* around the read are erased here because this context handles the missing
|
||||
* arm instead of consuming it as the asserted type. */
|
||||
export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
@@ -8255,6 +8502,8 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
|
||||
// the field's type through the emitted helper (a mismatch throws the
|
||||
// catchable TypeError — SEMANTICS.md).
|
||||
if (shape && !shape.tuple) {
|
||||
const guarded = lowerGuardedRecordWrite(lowerer, expr, target);
|
||||
if (guarded) return guarded;
|
||||
// Literal keys — string literals AND numeric literals in their
|
||||
// canonical string spelling (`b[1] = v` writes field/key "1", JS's
|
||||
// own key derivation) — name declared fields directly. A key
|
||||
@@ -10046,6 +10295,19 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
lowerUnionTypeofTest(lowerer, expr, loc);
|
||||
if (test) return test;
|
||||
}
|
||||
// `C === zlib.Gzip` between two zlib class references: the classes
|
||||
// are compile-time values, so the answer folds.
|
||||
if (
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
|
||||
) {
|
||||
const leftClass = zlibClassModeOfExpr(lowerer, expr.left);
|
||||
const rightClass = leftClass === null ? null : zlibClassModeOfExpr(lowerer, expr.right);
|
||||
if (leftClass !== null && rightClass !== null) {
|
||||
const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
return { kind: "boolLit", value: (leftClass === rightClass) !== negated, type: BOOL, loc };
|
||||
}
|
||||
}
|
||||
|
||||
const strictEquality =
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
@@ -11297,6 +11559,34 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
|
||||
// same lift the union-operand path above takes, arriving here
|
||||
// with two plain arm values instead.
|
||||
const target = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
// An always-truthy left operand (a record, object, array or function
|
||||
// value: every JS object is truthy): `o && x` evaluates `o` for its
|
||||
// effects and answers `x`; `o || x` answers `o` and never evaluates
|
||||
// `x`. supports-color's `stream && stream.isTTY` is the shape. Ahead
|
||||
// of the union lift below exactly where that lift would store the
|
||||
// object in a result union with no arm for it; a union holding the
|
||||
// operand keeps the lift.
|
||||
if (
|
||||
REF_TRUTHY_KINDS.has(kind) &&
|
||||
(target?.kind !== "union" ||
|
||||
(op === ts.SyntaxKind.AmpersandAmpersandToken &&
|
||||
lowerer.armTag(target.unionId, left.type) < 0))
|
||||
) {
|
||||
const and = op === ts.SyntaxKind.AmpersandAmpersandToken;
|
||||
const answer = and ? right : left;
|
||||
const answerT = target ?? answer.type;
|
||||
if (answerT.kind !== "void") {
|
||||
const value = lowerer.coerceInto(and ? expr.right : expr.left, answer, answerT);
|
||||
if (!and) return value;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: left, loc }],
|
||||
result: value,
|
||||
type: answerT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (target?.kind === "union") {
|
||||
lowerer.requireTruthyUnion(target.unionId, expr);
|
||||
return {
|
||||
@@ -11997,8 +12287,8 @@ function lowerCjsExportTableThisMember(
|
||||
/** A read of a catch binding — the caught analog of maybeNarrow. Where
|
||||
* tsc's control-flow narrowing has proven a supported test (`instanceof`
|
||||
* over a hierarchy class, `typeof` over a primitive), the read bridges
|
||||
* with a kind-unchecked `caughtNarrow` extraction (trust-the-checker,
|
||||
* exactly like unionNarrow). Every OTHER read is the narrowness fence:
|
||||
* with a `caughtNarrow` extraction whose kind backends verify at run time,
|
||||
* exactly like unionNarrow. Every OTHER read is the narrowness fence:
|
||||
* the binding's payload is a typed exception snapshot, not a dyn, so
|
||||
* un-narrowed uses have nothing sound to lower to. */
|
||||
export function caughtRead(
|
||||
@@ -12091,6 +12381,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
|
||||
const sockTest = lowerSocketInstanceOf(lowerer, expr, loc);
|
||||
if (sockTest !== null) return sockTest;
|
||||
const zlibMode = zlibClassModeOfExpr(lowerer, expr.right);
|
||||
if (zlibMode !== null) return lowerZlibInstanceOf(lowerer, expr, zlibMode, loc);
|
||||
|
||||
if (lowerer.isStdlibGlobal(expr.right, "Set") || lowerer.isStdlibGlobal(expr.right, "Map")) {
|
||||
const collection = lowerer.isStdlibGlobal(expr.right, "Map") ? "map" : "set";
|
||||
@@ -13713,6 +14005,8 @@ const MATCH_ROW_CALLBACK_METHODS = new Set([
|
||||
"filter",
|
||||
"find",
|
||||
"findLast",
|
||||
"findIndex",
|
||||
"findLastIndex",
|
||||
"some",
|
||||
"every",
|
||||
"flatMap",
|
||||
@@ -13826,7 +14120,10 @@ function matchAllRegexOf(lowerer: Lowerer, e: ts.Expression): ts.Expression | nu
|
||||
* receiver isn't a match slice or the regex isn't traceable — the
|
||||
* caller's member fence (with the groups hint) names the gap. */
|
||||
function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
if (expr.name.text !== "groups" || expr.questionDotToken !== undefined) return null;
|
||||
if (expr.name.text !== "groups") return null;
|
||||
// `m?.groups` re-dispatches through the optional-chain machinery, which
|
||||
// narrows the receiver to its match arm first.
|
||||
if (expr.questionDotToken !== undefined && !lowerer.chainHandled.has(expr)) return null;
|
||||
const loc = locOf(expr);
|
||||
const strArr = arrayOf(STRING);
|
||||
const recvT = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
|
||||
@@ -14777,19 +15074,35 @@ export function fieldTarget(
|
||||
// all-unknown-fields cast — `(err as { code?: unknown }).code`):
|
||||
// decline, and the dyn keyed-read fallback answers.
|
||||
if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
|
||||
// A nested property read such as `rows[i].value` has the same
|
||||
// unchecked outer array-read issue as tuple indexing. Validate
|
||||
// the record arm before fieldGet/recordGet so a missing outer
|
||||
// value raises a catchable TypeError rather than reaching the
|
||||
// backend with a union receiver.
|
||||
// A nested property read such as `rows[i].value` or
|
||||
// `table[k].value`: the outer read can miss. Validate the record
|
||||
// arm before fieldGet/recordGet so a missing outer value raises
|
||||
// Node's TypeError rather than reaching the backend with a union
|
||||
// receiver.
|
||||
const present = lowerer.stripUndefinedArm(obj.type);
|
||||
const parent = access.parent;
|
||||
const written =
|
||||
parent !== undefined &&
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.left === access &&
|
||||
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken;
|
||||
const helper =
|
||||
present.kind === "record"
|
||||
? lowerer.narrowedArmHelper(obj.type.unionId, present, locOf(access.expression))
|
||||
: null;
|
||||
obj = helper
|
||||
? lowerer.checkedNarrowCall(helper, obj, present, locOf(access.expression))
|
||||
: lowerer.maybeNarrow(obj, access.expression);
|
||||
obj =
|
||||
(present.kind === "record"
|
||||
? lowerer.runtimeOptionalPropertyReceiver(
|
||||
access.expression,
|
||||
obj,
|
||||
present,
|
||||
access.name.text,
|
||||
written ? "set" : "read",
|
||||
)
|
||||
: null) ??
|
||||
(helper
|
||||
? lowerer.checkedNarrowCall(helper, obj, present, locOf(access.expression))
|
||||
: lowerer.maybeNarrow(obj, access.expression));
|
||||
}
|
||||
if (obj.type.kind !== "record") return null;
|
||||
const actualShape = lowerer.shapes.get(obj.type.shapeId);
|
||||
@@ -15107,8 +15420,8 @@ export function fieldGetExpr(
|
||||
}
|
||||
// Overflow dot reads: exactly the bracket read with a literal key —
|
||||
// recordKeyGet, overflowOnly (the name declares no field by
|
||||
// construction), typed as the index value armed with undefined under
|
||||
// noUncheckedIndexedAccess (mirroring lowerRecordKeyRead).
|
||||
// construction), typed as the index value armed with undefined
|
||||
// (mirroring lowerRecordKeyRead).
|
||||
if (target.container === "recordOvf") {
|
||||
let obj = target.obj;
|
||||
if (obj.type.kind === "union" && ts.isPropertyAccessExpression(blame)) {
|
||||
@@ -15134,7 +15447,7 @@ export function fieldGetExpr(
|
||||
`reading '${target.field}' on a record without an index signature`,
|
||||
);
|
||||
let t: IrType = shape.indexValue;
|
||||
if (lowerer.program.getCompilerOptions().noUncheckedIndexedAccess) {
|
||||
if (t.kind !== "dyn") {
|
||||
const armed = lowerer.withUndefinedArmOf(t);
|
||||
if (!armed) lowerer.badType(blame, lowerer.typeOf(blame));
|
||||
t = armed;
|
||||
@@ -15201,20 +15514,6 @@ export function fieldGetExpr(
|
||||
const helper = lowerer.deferredReadHelper(target.fieldType.unionId, inner, loc);
|
||||
if (helper) return { kind: "call", callee: helper, args: [read], type: inner, loc };
|
||||
}
|
||||
// An early-read field (ClassInfo.earlyReadFields) yields its undefined
|
||||
// where the use reads it as Node does; elsewhere the declared type is
|
||||
// extracted with a check.
|
||||
if (
|
||||
target.fieldType.kind === "union" &&
|
||||
lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true &&
|
||||
!observesEarlyRead(blame)
|
||||
) {
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(blame));
|
||||
if (declared && declared.kind !== "union") {
|
||||
const helper = lowerer.narrowedArmHelper(target.fieldType.unionId, declared, loc);
|
||||
if (helper) return lowerer.checkedNarrowCall(helper, read, declared, loc);
|
||||
}
|
||||
}
|
||||
return read;
|
||||
}
|
||||
return {
|
||||
@@ -15755,22 +16054,6 @@ function lowerFieldCompoundValue(
|
||||
if (op === "+" && read.type.kind === "string") {
|
||||
appended = lowerer.ensureString(rhs, rhsNode ?? access);
|
||||
value = { kind: "strConcat", left: read, right: appended, type: STRING, loc };
|
||||
} else if (
|
||||
op === "+" &&
|
||||
read.type.kind === "union" &&
|
||||
lowerer.stripUndefinedArm(read.type).kind === "string" &&
|
||||
target.container === "class" &&
|
||||
lowerer.classes.get(target.className)?.earlyReadFields?.has(target.field) === true
|
||||
) {
|
||||
// A string field read before its assignment holds undefined, which
|
||||
// concatenates as "undefined".
|
||||
value = {
|
||||
kind: "strConcat",
|
||||
left: lowerer.ensureString(read, access),
|
||||
right: lowerer.ensureString(rhs, rhsNode ?? access),
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
} else if (numericRead.type.kind === "f64" && numericRhs.type.kind === "f64") {
|
||||
value = { kind: "bin", op, left: numericRead, right: numericRhs, type: F64, loc };
|
||||
} else if (bigintValue) {
|
||||
|
||||
@@ -80,6 +80,7 @@ import {
|
||||
createRequireProgramModuleDecl,
|
||||
} from "./builtins/module-bindings.js";
|
||||
import { isPromisifyCall, registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
|
||||
import { registerZlibClassAlias } from "./builtins/compression.js";
|
||||
import { stripTypeCasts } from "./builtins/arguments.js";
|
||||
import {
|
||||
bindingContextualGenericFnNodeOf,
|
||||
@@ -1700,6 +1701,7 @@ export function collectGlobals(
|
||||
// resolution and value reads reuse the source's interned closure.
|
||||
if (isConst && registerOverloadedCallableAlias(lowerer, decl)) continue;
|
||||
if (isConst && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) continue;
|
||||
if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) continue;
|
||||
// `const f = (x) => ...` at file scope of an npm-static JS file
|
||||
// whose params are implicit-any: the implicit-monomorphization
|
||||
// twin of the generic binding above — no global exists; calls and
|
||||
@@ -2662,6 +2664,17 @@ export function lowerFileInit(
|
||||
loc: loc0,
|
||||
});
|
||||
}
|
||||
// Top-level `this` of a CommonJS module: its object exists before the
|
||||
// module's first statement (lowerCjsTopLevelThis).
|
||||
const exportsThis = lowerer.cjsExportsThis.get(sf);
|
||||
if (exportsThis) {
|
||||
header.push({
|
||||
kind: "assign",
|
||||
localId: exportsThis.id,
|
||||
value: { kind: "dynObjLit", fields: [], type: DYN, loc: loc0 },
|
||||
loc: loc0,
|
||||
});
|
||||
}
|
||||
for (const { stmt, dep } of orderedImportsOf(lowerer.program, sf)) {
|
||||
const npm = lowerer.npmInitActions.get(sf)?.get(stmt);
|
||||
if (npm !== undefined && npm.length > 0) {
|
||||
|
||||
@@ -8,10 +8,11 @@ import * as win32 from "node:path/win32";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { FileParts } from "./lower-modules.js";
|
||||
import { isNodeEsmFile, locOf } from "../program.js";
|
||||
import { isCjsJsFile, isNodeEsmFile, locOf } from "../program.js";
|
||||
import { runtimePathForTarget } from "../runtime-resolve.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
@@ -224,6 +225,135 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
return rendered === "ScriptcModule" || rendered === "NodeModule" || rendered === "NodeJS.Module";
|
||||
}
|
||||
|
||||
/** `module.exports` used only for its truthiness (the UMD test `typeof
|
||||
* module !== "undefined" && module.exports`): the exports object starts as
|
||||
* a fresh object, so it stays truthy unless the module replaces it with a
|
||||
* value that can be falsy. True when every `module.exports = value` in the
|
||||
* file assigns an object or function. */
|
||||
function moduleExportsAlwaysTruthy(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
|
||||
if (!truthinessOnly(expr)) return false;
|
||||
const sf = expr.getSourceFile();
|
||||
if (isNodeEsmFile(sf, lowerer.program)) return false;
|
||||
let truthy = true;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
!ts.isBinaryExpression(node) ||
|
||||
node.operatorToken.kind < ts.SyntaxKind.FirstAssignment ||
|
||||
node.operatorToken.kind > ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
return undefined;
|
||||
let target = node.left;
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(target) ||
|
||||
target.name.text !== "exports" ||
|
||||
!isCjsModuleGlobal(lowerer, target.expression)
|
||||
)
|
||||
return undefined;
|
||||
const type = lowerer.typeOf(node.right);
|
||||
const objectLike = (t: ts.Type): boolean =>
|
||||
t.isUnionType()
|
||||
? ts.constituentTypes(t).every(objectLike)
|
||||
: (t.flags & ts.TypeFlags.Object) !== 0 || (t.flags & ts.TypeFlags.NonPrimitive) !== 0;
|
||||
if (node.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !objectLike(type)) {
|
||||
truthy = false;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return truthy;
|
||||
}
|
||||
|
||||
/** Whether only the truthiness of `node` can be observed: a condition, a
|
||||
* `!` operand, or a `&&`/`||` operand of an expression that is itself used
|
||||
* only for its truthiness. */
|
||||
function truthinessOnly(node: ts.Expression): boolean {
|
||||
let child: ts.Node = node;
|
||||
for (let parent = node.parent; ; child = parent, parent = parent.parent) {
|
||||
if (ts.isParenthesizedExpression(parent)) continue;
|
||||
if (ts.isPrefixUnaryExpression(parent))
|
||||
return parent.operator === ts.SyntaxKind.ExclamationToken;
|
||||
if (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
(parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
|
||||
parent.operatorToken.kind === ts.SyntaxKind.BarBarToken)
|
||||
)
|
||||
continue;
|
||||
if (ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent))
|
||||
return parent.expression === child;
|
||||
if (ts.isForStatement(parent)) return parent.condition === child;
|
||||
if (ts.isConditionalExpression(parent)) return parent.condition === child;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** `this` at the top level of a CommonJS module (directly, or inside
|
||||
* arrow functions there): Node calls the module wrapper with `this` bound
|
||||
* to the module's original exports object. */
|
||||
export function isCjsTopLevelThis(lowerer: Lowerer, node: ts.Node): boolean {
|
||||
if (node.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
for (let p: ts.Node | undefined = node.parent; p && !ts.isSourceFile(p); p = p.parent) {
|
||||
if (ts.isArrowFunction(p)) continue;
|
||||
if (
|
||||
ts.isFunctionLike(p) ||
|
||||
ts.isClassDeclaration(p) ||
|
||||
ts.isClassExpression(p) ||
|
||||
ts.isClassStaticBlockDeclaration(p)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return isCjsJsFile(node.getSourceFile(), lowerer.program);
|
||||
}
|
||||
|
||||
export function usesCjsTopLevelThis(lowerer: Lowerer, sf: ts.SourceFile): boolean {
|
||||
if (!isCjsJsFile(sf, lowerer.program)) return false;
|
||||
let found = false;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
node !== sf &&
|
||||
!ts.isArrowFunction(node) &&
|
||||
(ts.isFunctionLike(node) ||
|
||||
ts.isClassDeclaration(node) ||
|
||||
ts.isClassExpression(node) ||
|
||||
ts.isClassStaticBlockDeclaration(node))
|
||||
)
|
||||
return "skip";
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The value of top-level `this` in a CommonJS module: the module's own
|
||||
* object standing for its original exports, created when the module starts
|
||||
* (lowerFileInit). Exports themselves are the static export table, so a
|
||||
* property access through `this` would read or write an object no requirer
|
||||
* sees; it keeps a named refusal instead. Typeof, truthiness, identity and
|
||||
* passing the value along (the UMD wrapper `(function (root) { ... })(this)`)
|
||||
* behave as in Node. */
|
||||
export function lowerCjsTopLevelThis(lowerer: Lowerer, expr: ts.Expression): IrExpr | null {
|
||||
if (!isCjsTopLevelThis(lowerer, expr)) return null;
|
||||
const global = lowerer.cjsExportsThis.get(expr.getSourceFile());
|
||||
if (!global) return null;
|
||||
let receiver: ts.Node = expr;
|
||||
while (ts.isParenthesizedExpression(receiver.parent)) receiver = receiver.parent;
|
||||
const access = receiver.parent;
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(access) || ts.isElementAccessExpression(access)) &&
|
||||
access.expression === receiver
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
access,
|
||||
"property access through top-level 'this' in a CommonJS module (it is module.exports; spell exports.<name>)",
|
||||
);
|
||||
}
|
||||
return varRef(global.id, DYN, locOf(expr));
|
||||
}
|
||||
|
||||
function optionalModule(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
@@ -309,6 +439,8 @@ export function lowerNodeModuleProperty(
|
||||
if (!direct && !nodeModuleType(lowerer, expr.expression)) return null;
|
||||
if (expr.questionDotToken) return null;
|
||||
const member = expr.name.text;
|
||||
if (member === "exports" && direct && moduleExportsAlwaysTruthy(lowerer, expr))
|
||||
return boolLit(true, locOf(expr));
|
||||
if (member === "exports" || member === "require") return null;
|
||||
let moduleRef = direct
|
||||
? cjsModuleRef(lowerer, expr.expression)
|
||||
|
||||
@@ -10,6 +10,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* flow (if/while/for/for-of/do, switch, try/catch, abrupt completions),
|
||||
* explicit resource management, and blocked-binding poisoning. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { processDataMemberValue } from "./builtins/process.js";
|
||||
import { requireInitFlagStmt } from "./require-initialization.js";
|
||||
import { isJsonRequireDecl } from "./json-require.js";
|
||||
import { bindingSource } from "../binding-source.js";
|
||||
@@ -147,6 +148,7 @@ import {
|
||||
} from "./builtins/module-bindings.js";
|
||||
import { lowerNodeModuleCall } from "./builtins/module-resolution.js";
|
||||
import { registerBuiltinCallableAlias } from "./builtins/callable-aliases.js";
|
||||
import { registerZlibClassAlias } from "./builtins/compression.js";
|
||||
import { lowerEnumDeclaration } from "./lower-enums.js";
|
||||
import {
|
||||
abstractPropertyDeclOf,
|
||||
@@ -2717,6 +2719,14 @@ export function lowerBindingPattern(
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
srcType.kind === "array" &&
|
||||
isMatchSliceType(lowerer, srcType) &&
|
||||
isRegexMatchResultPattern(lowerer, pattern)
|
||||
) {
|
||||
lowerMatchResultObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out);
|
||||
return;
|
||||
}
|
||||
if (srcType.kind !== "record") {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
@@ -2816,18 +2826,20 @@ export function lowerBindingPattern(
|
||||
// An UNDECLARED key of an INDEX-SIGNATURE shape (`const { m, n, p }
|
||||
// = ....groups` over `{ [key: string]: string }` — the groups
|
||||
// record, `Record<string, T>` tables generally): the overflow-map
|
||||
// read, exactly the dot access's recordKeyGet — a missing key
|
||||
// traps when the value type carries no undefined arm (the checker
|
||||
// claimed V), or binds the undefined arm (where a default applies
|
||||
// like any field's).
|
||||
// read, exactly the dot access's recordKeyGet — a missing key binds
|
||||
// the undefined arm (where a default applies like any field's).
|
||||
const loc = locOf(el);
|
||||
const readType =
|
||||
shape.indexValue.kind === "dyn"
|
||||
? shape.indexValue
|
||||
: (lowerer.withUndefinedArmOf(shape.indexValue) ?? shape.indexValue);
|
||||
let value: IrExpr = {
|
||||
kind: "recordKeyGet",
|
||||
obj: srcRef(),
|
||||
shapeId: srcType.shapeId,
|
||||
key: { kind: "strLit", value: propName, type: STRING, loc },
|
||||
overflowOnly: true,
|
||||
type: shape.indexValue,
|
||||
type: readType,
|
||||
loc,
|
||||
};
|
||||
if (el.initializer) value = applyBindingDefault(lowerer, el, value);
|
||||
@@ -2985,6 +2997,124 @@ function patternKeyNameOf(lowerer: Lowerer, prop: ts.PropertyName): string | nul
|
||||
return null;
|
||||
}
|
||||
|
||||
/** True when an object pattern destructures a regex match result: the
|
||||
* checker types its source as the standard library's RegExpExecArray or
|
||||
* RegExpMatchArray. */
|
||||
function isRegexMatchResultPattern(lowerer: Lowerer, pattern: ts.ObjectBindingPattern): boolean {
|
||||
const parent = pattern.parent;
|
||||
const source =
|
||||
ts.isVariableDeclaration(parent) && parent.initializer !== undefined
|
||||
? lowerer.typeOf(parent.initializer)
|
||||
: lowerer.typeOf(pattern);
|
||||
const type = lowerer.checker.getNonNullableType(source);
|
||||
const symbol = type.getAliasSymbol() ?? type.getSymbol();
|
||||
return (
|
||||
symbol !== undefined &&
|
||||
(symbol.name === "RegExpExecArray" || symbol.name === "RegExpMatchArray") &&
|
||||
lowerer.isStdlibSymbol(symbol)
|
||||
);
|
||||
}
|
||||
|
||||
/** `const { 0: whole, 1: first, index, input, groups } = re.exec(s)!`: an
|
||||
* object pattern over a regex match result. The result is an honest
|
||||
* string[] slice whose `index` and `input` are the special-form reads that
|
||||
* `m.index` and `m.input` lower to; numeric keys are the positional reads
|
||||
* of the equivalent array pattern, and `groups` is the projection `m.groups`
|
||||
* builds when the regex is statically known. Rest elements, defaults on
|
||||
* the special properties, and every other key keep a named fence. */
|
||||
function lowerMatchResultObjectPattern(
|
||||
lowerer: Lowerer,
|
||||
pattern: ts.ObjectBindingPattern,
|
||||
srcRef: () => IrExpr,
|
||||
srcType: IrType & { kind: "array" },
|
||||
isLet: boolean,
|
||||
out: IrStmt[],
|
||||
): void {
|
||||
let groups: { name: string; index: number }[] | null | undefined;
|
||||
for (const el of pattern.elements) {
|
||||
if (el.name === undefined) continue;
|
||||
const loc = locOf(el);
|
||||
if (el.dotDotDotToken) {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
el,
|
||||
"rest bindings over regex match results (the rest copies the positions and the index, input and groups properties — destructure them by name)",
|
||||
);
|
||||
}
|
||||
const prop = el.propertyName ?? el.name;
|
||||
const propName =
|
||||
ts.isIdentifier(prop) ||
|
||||
ts.isComputedPropertyName(prop) ||
|
||||
ts.isStringLiteralLike(prop) ||
|
||||
ts.isNumericLiteral(prop)
|
||||
? patternKeyNameOf(lowerer, prop as ts.PropertyName)
|
||||
: null;
|
||||
if (propName === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
el,
|
||||
"destructuring with computed keys that do not fold to one property name",
|
||||
);
|
||||
}
|
||||
let value: IrExpr;
|
||||
if (/^(?:0|[1-9]\d{0,8})$/.test(propName)) {
|
||||
const index = Number(propName);
|
||||
const position: IrExpr = { kind: "numLit", value: index, type: F64, loc };
|
||||
value = lowerSafeIndexRead(lowerer, srcRef(), position, loc) ?? {
|
||||
kind: "arrayGet",
|
||||
arr: srcRef(),
|
||||
index: position,
|
||||
type: srcType.elem,
|
||||
loc,
|
||||
};
|
||||
if (el.initializer) {
|
||||
value = arrayPositionDefault(lowerer, el, value, srcRef, index, srcType.elem, out);
|
||||
}
|
||||
} else if (propName === "index" || propName === "input") {
|
||||
if (el.initializer) {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
el,
|
||||
`binding defaults on the '${propName}' of a regex match result`,
|
||||
);
|
||||
}
|
||||
value = lowerer.maybeNarrow(
|
||||
{
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(srcRef(), DYN),
|
||||
key: { kind: "strLit", value: propName, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
el.name,
|
||||
);
|
||||
} else if (propName === "groups") {
|
||||
if (groups === undefined) {
|
||||
const parent = pattern.parent;
|
||||
groups =
|
||||
ts.isVariableDeclaration(parent) && parent.initializer !== undefined
|
||||
? matchResultNamedGroupsOf(lowerer, parent.initializer)
|
||||
: null;
|
||||
}
|
||||
if (groups === null || groups.length === 0 || el.initializer) {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
el,
|
||||
"destructuring 'groups' from this regex match result (it lowers when the regex is statically known and has named groups, without a default)",
|
||||
);
|
||||
}
|
||||
value = lowerGroupsProjection(lowerer, srcRef(), groups, loc);
|
||||
} else {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
el,
|
||||
`destructuring the property '${propName}' of a regex match result (numeric positions, index, input and groups compile)`,
|
||||
);
|
||||
}
|
||||
lowerer.bindPatternTarget(el.name, value, isLet, out);
|
||||
}
|
||||
}
|
||||
|
||||
/** Decode one non-rest object-assignment property into its static source
|
||||
* field, destination expression, and optional default initializer. */
|
||||
function destructuringPropertyOf(
|
||||
@@ -3148,6 +3278,9 @@ function stdlibGlobalTokenDestructure(
|
||||
token: string;
|
||||
alias: string | null;
|
||||
g: IrGlobal | undefined;
|
||||
/** A data member's value (`const { env } = process`), bound instead
|
||||
* of a token. */
|
||||
value?: IrExpr;
|
||||
}[] = [];
|
||||
for (const el of decl.name.elements) {
|
||||
if (el.name === undefined) continue;
|
||||
@@ -3183,8 +3316,9 @@ function stdlibGlobalTokenDestructure(
|
||||
"destructuring with computed keys that do not fold to one property name",
|
||||
);
|
||||
}
|
||||
let token: string;
|
||||
let token = "";
|
||||
let alias: string | null = null;
|
||||
let value: IrExpr | null = null;
|
||||
if (globalName === "globalThis") {
|
||||
// The member must BE a stdlib global (user script vars are
|
||||
// properties of `typeof globalThis` too — their values are real
|
||||
@@ -3204,6 +3338,13 @@ function stdlibGlobalTokenDestructure(
|
||||
token = `[builtin console.${propName}]`;
|
||||
} else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) {
|
||||
token = `[builtin ${globalName}.${propName}]`;
|
||||
} else if (
|
||||
globalName === "process" &&
|
||||
ts.isIdentifier(el.name) &&
|
||||
(value = processDataMemberValue(lowerer, propName, el.name, locOf(el))) !== null
|
||||
) {
|
||||
// A data member reads its value once, exactly like the spelled
|
||||
// `process.<member>` read the destructuring performs.
|
||||
} else {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
@@ -3220,6 +3361,10 @@ function stdlibGlobalTokenDestructure(
|
||||
);
|
||||
}
|
||||
const g = lowerer.globalsBySymbol.get(symbol);
|
||||
if (value !== null) {
|
||||
binds.push({ name: el.name, token, alias, g, value });
|
||||
continue;
|
||||
}
|
||||
if (g !== undefined && g.type.kind !== "string" && g.type.kind !== "dyn") {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
@@ -3232,6 +3377,21 @@ function stdlibGlobalTokenDestructure(
|
||||
const out: IrStmt[] = [];
|
||||
for (const b of binds) {
|
||||
const loc = locOf(b.name);
|
||||
if (b.value !== undefined) {
|
||||
if (b.g !== undefined) {
|
||||
out.push({
|
||||
kind: "assign",
|
||||
localId: b.g.id,
|
||||
value: lowerer.coerceInto(b.name, b.value, b.g.type),
|
||||
...(b.g.tdz ? { initializes: true as const } : {}),
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
const local = lowerer.declareLocal(b.name, b.name.text, b.value.type, isLet);
|
||||
out.push({ kind: "varDecl", localId: local.id, init: b.value, loc });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const token: IrExpr = { kind: "strLit", value: b.token, type: STRING, loc };
|
||||
if (b.alias !== null) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(b.name);
|
||||
@@ -4263,6 +4423,7 @@ export function lowerVarDecl(
|
||||
// reads reuse its interned closure identity.
|
||||
if (!isLet && registerOverloadedCallableAlias(lowerer, decl)) return null;
|
||||
if (!isLet && registerBuiltinCallableAlias(lowerer, decl.name, decl.initializer)) return null;
|
||||
if (registerZlibClassAlias(lowerer, decl.name, decl.initializer)) return null;
|
||||
|
||||
// `const knownBy = (cmd) => ...` — an IMPLICIT-ANY function-value
|
||||
// binding in npm-static JS (function-scope bindings register here, in
|
||||
@@ -4351,16 +4512,21 @@ export function lowerVarDecl(
|
||||
if (wrapped) return { kind: "assign", localId: g.id, value: wrapped, loc: locOf(decl) };
|
||||
return null;
|
||||
}
|
||||
const raw = lowerVariableInitializer(
|
||||
lowerer,
|
||||
decl.initializer,
|
||||
g.type.kind === "dyn" &&
|
||||
(isNativeBuiltinValueInitializer(lowerer, decl.initializer) ||
|
||||
ts.isArrayLiteralExpression(decl.initializer) ||
|
||||
ts.isObjectLiteralExpression(decl.initializer))
|
||||
? DYN
|
||||
: undefined,
|
||||
);
|
||||
const guardedRead = isGuardedRecordRead(lowerer, decl)
|
||||
? lowerAbsenceProbe(lowerer, decl.initializer)
|
||||
: null;
|
||||
const raw =
|
||||
guardedRead ??
|
||||
lowerVariableInitializer(
|
||||
lowerer,
|
||||
decl.initializer,
|
||||
g.type.kind === "dyn" &&
|
||||
(isNativeBuiltinValueInitializer(lowerer, decl.initializer) ||
|
||||
ts.isArrayLiteralExpression(decl.initializer) ||
|
||||
ts.isObjectLiteralExpression(decl.initializer))
|
||||
? DYN
|
||||
: undefined,
|
||||
);
|
||||
if (
|
||||
isJsSourceFile(decl.getSourceFile()) &&
|
||||
!decl.type &&
|
||||
@@ -5156,29 +5322,111 @@ export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclar
|
||||
}
|
||||
|
||||
function immediatelyGuardedAbsenceProbe(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
|
||||
if (decl.type || !ts.isIdentifier(decl.name) || !decl.initializer) return false;
|
||||
const guard = immediateAbsenceGuard(lowerer, decl);
|
||||
// `if (!x)`: the falsy test sees a missing value.
|
||||
if (guard === "falsy") return true;
|
||||
// `if (x === undefined)` and its spellings compare the binding with the
|
||||
// missing value itself, so the read must be able to produce it. Only
|
||||
// index-signature record reads take this form: array reads already
|
||||
// carry their undefined arm, and their ordinary lowering keeps its
|
||||
// bounds proofs.
|
||||
return guard === "compare" && isIndexSignatureRecordRead(lowerer, decl);
|
||||
}
|
||||
|
||||
/** A file-scope or block-scope `let e = map[k]` over an index-signature
|
||||
* record whose next statement guards `e` for absence (`if (e ===
|
||||
* undefined)`, `if (!e)`, ...): the get-or-create idiom. Its read keeps the
|
||||
* missing key as undefined even without noUncheckedIndexedAccess. */
|
||||
export function isGuardedRecordRead(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
|
||||
return immediateAbsenceGuard(lowerer, decl) !== null && isIndexSignatureRecordRead(lowerer, decl);
|
||||
}
|
||||
|
||||
function isIndexSignatureRecordRead(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
|
||||
if (!decl.initializer) return false;
|
||||
let init = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (!ts.isElementAccessExpression(init)) return false;
|
||||
// Under noUncheckedIndexedAccess the checker already spells the missing
|
||||
// arm, and the ordinary read carries it.
|
||||
const read = lowerer.typeOf(init);
|
||||
if (
|
||||
(read.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0 ||
|
||||
(read.isUnionType() &&
|
||||
ts.constituentTypes(read).some((t) => (t.flags & ts.TypeFlags.Undefined) !== 0))
|
||||
)
|
||||
return false;
|
||||
const receiver = lowerer.mapTypeOf(lowerer.typeOf(init.expression));
|
||||
const shape = receiver?.kind === "record" ? lowerer.shapes.get(receiver.shapeId) : undefined;
|
||||
return !!shape && !shape.tuple && !!shape.indexValue && shape.indexValue.kind !== "dyn";
|
||||
}
|
||||
|
||||
/** How the statement right after an unannotated declaration tests the
|
||||
* declared binding for absence: `if (!x)` ("falsy"), or a comparison with
|
||||
* the missing value — `x === undefined`, `x == null`, `x !== void 0`,
|
||||
* `typeof x === "undefined"` and their mirrored and negated spellings
|
||||
* ("compare"). Null for anything else. */
|
||||
function immediateAbsenceGuard(
|
||||
lowerer: Lowerer,
|
||||
decl: ts.VariableDeclaration,
|
||||
): "falsy" | "compare" | null {
|
||||
if (decl.type || !ts.isIdentifier(decl.name) || !decl.initializer) return null;
|
||||
const statement = decl.parent?.parent;
|
||||
if (!statement || !ts.isVariableStatement(statement)) return false;
|
||||
if (!statement || !ts.isVariableStatement(statement)) return null;
|
||||
const container = statement.parent;
|
||||
const statements =
|
||||
ts.isBlock(container) || ts.isSourceFile(container) ? container.statements : null;
|
||||
if (!statements) return false;
|
||||
if (!statements) return null;
|
||||
const index = statements.indexOf(statement);
|
||||
const next = index >= 0 ? statements[index + 1] : undefined;
|
||||
if (!next || !ts.isIfStatement(next)) return false;
|
||||
let condition = next.expression;
|
||||
while (ts.isParenthesizedExpression(condition)) condition = condition.expression;
|
||||
if (
|
||||
!ts.isPrefixUnaryExpression(condition) ||
|
||||
condition.operator !== ts.SyntaxKind.ExclamationToken
|
||||
)
|
||||
return false;
|
||||
let operand = condition.operand;
|
||||
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
|
||||
return (
|
||||
ts.isIdentifier(operand) &&
|
||||
lowerer.resolveValueSymbol(operand) === lowerer.resolveValueSymbol(decl.name)
|
||||
);
|
||||
if (!next || !ts.isIfStatement(next)) return null;
|
||||
const binding = lowerer.resolveValueSymbol(decl.name);
|
||||
const peel = (node: ts.Expression): ts.Expression => {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
return node;
|
||||
};
|
||||
const isBinding = (node: ts.Expression): boolean => {
|
||||
const operand = peel(node);
|
||||
return ts.isIdentifier(operand) && lowerer.resolveValueSymbol(operand) === binding;
|
||||
};
|
||||
const condition = peel(next.expression);
|
||||
if (ts.isPrefixUnaryExpression(condition))
|
||||
return condition.operator === ts.SyntaxKind.ExclamationToken && isBinding(condition.operand)
|
||||
? "falsy"
|
||||
: null;
|
||||
if (!ts.isBinaryExpression(condition)) return null;
|
||||
const op = condition.operatorToken.kind;
|
||||
const strict =
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
const loose =
|
||||
op === ts.SyntaxKind.EqualsEqualsToken || op === ts.SyntaxKind.ExclamationEqualsToken;
|
||||
if (!strict && !loose) return null;
|
||||
const isUndefined = (node: ts.Expression): boolean => {
|
||||
const operand = peel(node);
|
||||
if (ts.isVoidExpression(operand)) return true;
|
||||
return (
|
||||
ts.isIdentifier(operand) &&
|
||||
operand.text === "undefined" &&
|
||||
(lowerer.typeOf(operand).flags & ts.TypeFlags.Undefined) !== 0
|
||||
);
|
||||
};
|
||||
const isNull = (node: ts.Expression): boolean => peel(node).kind === ts.SyntaxKind.NullKeyword;
|
||||
const missing = (node: ts.Expression): boolean => isUndefined(node) || (loose && isNull(node));
|
||||
const isTypeofBinding = (node: ts.Expression): boolean => {
|
||||
const operand = peel(node);
|
||||
return ts.isTypeOfExpression(operand) && isBinding(operand.expression);
|
||||
};
|
||||
const isUndefinedTag = (node: ts.Expression): boolean => {
|
||||
const operand = peel(node);
|
||||
return ts.isStringLiteralLike(operand) && operand.text === "undefined";
|
||||
};
|
||||
const { left, right } = condition;
|
||||
return (isBinding(left) && missing(right)) ||
|
||||
(missing(left) && isBinding(right)) ||
|
||||
(isTypeofBinding(left) && isUndefinedTag(right)) ||
|
||||
(isUndefinedTag(left) && isTypeofBinding(right))
|
||||
? "compare"
|
||||
: null;
|
||||
}
|
||||
|
||||
function falsyExitRuntimeOptionalLocals(lowerer: Lowerer, stmt: ts.IfStatement): IrLocal[] {
|
||||
@@ -5405,7 +5653,7 @@ function lowerBranchSwitch(
|
||||
* cell, deliberately narrower than dyn: the supported uses are the
|
||||
* narrowing tests (`e instanceof C` over hierarchy classes, `typeof e
|
||||
* === "string"/"number"/"boolean"`), reads under a proven narrow
|
||||
* (caughtRead bridges them with caughtNarrow, trust-the-checker like
|
||||
* (caughtRead bridges them with caughtNarrow, verified at run time like
|
||||
* unionNarrow), and rethrow (`throw e`). Unsupported raw uses and
|
||||
* assignments are fenced (SC1063 and friends); destructuring patterns
|
||||
* are SC1062. Captures retain the snapshot beyond the catch scope.
|
||||
|
||||
@@ -66,7 +66,7 @@ function lowerMockTrackerMember(
|
||||
|
||||
const TEST_FN_HINT = "test bodies take () or (t: TestContext) and return void or Promise<void>";
|
||||
const TESTCTX_SURFACE_HINT =
|
||||
"t.test, t.skip, t.todo, t.diagnostic, t.plan, t.before, t.after, t.beforeEach, t.afterEach, t.name, t.fullName, and t.assert.* are the supported TestContext members";
|
||||
"t.test, t.skip, t.todo, t.diagnostic, t.plan, t.before, t.after, t.beforeEach, t.afterEach, t.name, t.fullName, t.filePath, t.attempt, t.passed, t.workerId, t.tags, and t.assert.* are the supported TestContext members";
|
||||
|
||||
/** Registration calls return Node's Promise<void>, but the promise only
|
||||
* resolves through the runner — consuming it outside `await t.test(...)`
|
||||
@@ -409,11 +409,16 @@ function lowerSuite(
|
||||
}
|
||||
if (methodMode === 1 || methodMode === 2) opts.mode = methodMode;
|
||||
const cb = lowerer.lowerExpr(fnNode);
|
||||
if (cb.type.kind !== "func" || cb.type.params.length !== 0) {
|
||||
// The body may take the SuiteContext (flag 2, as test bodies take theirs).
|
||||
if (
|
||||
cb.type.kind !== "func" ||
|
||||
cb.type.params.length > 1 ||
|
||||
(cb.type.params.length === 1 && cb.type.params[0]!.kind !== "testCtx")
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
`${what} whose body is not a zero-parameter function`,
|
||||
`${what} whose body is not a function of the SuiteContext`,
|
||||
fnNode,
|
||||
"suite bodies take no parameters (they run at registration)",
|
||||
"suite bodies take no parameters or the SuiteContext (they run at registration)",
|
||||
);
|
||||
}
|
||||
if (cb.type.ret.kind !== "void" && !isUnitType(cb.type.ret)) {
|
||||
@@ -427,7 +432,10 @@ function lowerSuite(
|
||||
const mode = numLit(opts.mode, loc);
|
||||
const msg = opts.msg ?? strLit("", loc);
|
||||
// describe.only and { only: true } mark the suite (flag 4, as for tests).
|
||||
const flags = numLit(optionFlags(opts) | (methodMode === 3 ? 4 : 0), loc);
|
||||
const flags = numLit(
|
||||
optionFlags(opts) | (methodMode === 3 ? 4 : 0) | (cb.type.params.length === 1 ? 2 : 0),
|
||||
loc,
|
||||
);
|
||||
return withOptionValues(
|
||||
{
|
||||
kind: "libCall",
|
||||
@@ -456,14 +464,16 @@ function lowerHook(
|
||||
lowerer.noLowering(
|
||||
`${what} with ${args.length} arguments`,
|
||||
expr,
|
||||
"the supported form is one zero-parameter function (hook options have no lowering)",
|
||||
"the supported form is one function (hook options have no lowering)",
|
||||
);
|
||||
}
|
||||
const { cb, flags } = lowerBodyArg(lowerer, args[0]!, what, false);
|
||||
// A hook may take its context: the suite's for before/after, the test's
|
||||
// for beforeEach/afterEach.
|
||||
const { cb, flags } = lowerBodyArg(lowerer, args[0]!, what, true);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "test.hook",
|
||||
args: [numLit(which, loc), cb, numLit(flags, loc)],
|
||||
args: [numLit(which, loc), cb, numLit(flags, loc), strLit(atStringOf(expr), loc)],
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
@@ -919,7 +929,8 @@ export function lowerTestMethodCall(
|
||||
);
|
||||
}
|
||||
const receiver = lowerer.lowerExpr(access.expression);
|
||||
const { cb, flags } = lowerBodyArg(lowerer, args[0]!, what, false);
|
||||
// The hook may take the test's context.
|
||||
const { cb, flags } = lowerBodyArg(lowerer, args[0]!, what, true);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "test.ctxHook",
|
||||
@@ -962,6 +973,78 @@ export function lowerTestMethodCall(
|
||||
);
|
||||
}
|
||||
|
||||
/** t.filePath (the entry file, as Node's entryFile), t.attempt (0: tests
|
||||
* run once), t.passed, t.workerId (Number(NODE_TEST_WORKER_ID) ||
|
||||
* undefined) and t.tags ([]: the tags option has no lowering), each
|
||||
* coerced to the member's declared type. */
|
||||
function lowerTestCtxValue(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
member: "filePath" | "attempt" | "passed" | "workerId" | "tags",
|
||||
): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const receiver = lowerer.lowerExpr(expr.expression);
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (!declared) {
|
||||
lowerer.noLowering(`TestContext.${member} of this type`, expr, TESTCTX_SURFACE_HINT);
|
||||
}
|
||||
const after = (value: IrExpr): IrExpr => ({
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: receiver, loc }],
|
||||
result: lowerer.coerceToExpected(value, declared),
|
||||
type: declared,
|
||||
loc,
|
||||
});
|
||||
switch (member) {
|
||||
case "filePath":
|
||||
return after(strLit(lowerer.entry.fileName, loc));
|
||||
case "attempt":
|
||||
return after(numLit(0, loc));
|
||||
case "tags":
|
||||
return after({ kind: "arrayLit", elems: [], type: declared, loc });
|
||||
case "passed":
|
||||
return lowerer.coerceToExpected(
|
||||
{ kind: "libCall", fn: "test.ctxPassed", args: [receiver], type: BOOL, loc },
|
||||
declared,
|
||||
);
|
||||
case "workerId": {
|
||||
const id = lowerer.declareHiddenLocal("%workerId", F64);
|
||||
const missing = lowerer.wrappedUndefined(declared, loc);
|
||||
if (!missing)
|
||||
lowerer.noLowering("TestContext.workerId of this type", expr, TESTCTX_SURFACE_HINT);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "exprStmt", expr: receiver, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: id.id,
|
||||
init: { kind: "libCall", fn: "test.workerId", args: [], type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "!==",
|
||||
left: varRef(id.id, F64, loc),
|
||||
right: numLit(0, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: lowerer.coerceToExpected(varRef(id.id, F64, loc), declared),
|
||||
else_: missing,
|
||||
type: declared,
|
||||
loc,
|
||||
},
|
||||
type: declared,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** `t.name` as a VALUE — one entry in lower-exprs' property chain. Null
|
||||
* for other receivers/members (the chain keeps trying); unlowered
|
||||
* TestContext members fence member-qualified. */
|
||||
@@ -985,7 +1068,17 @@ export function lowerTestCtxProperty(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (expr.name.text === "assert") return null; // claimed by the call path
|
||||
const member = expr.name.text;
|
||||
if (
|
||||
member === "filePath" ||
|
||||
member === "attempt" ||
|
||||
member === "passed" ||
|
||||
member === "workerId" ||
|
||||
member === "tags"
|
||||
) {
|
||||
return lowerTestCtxValue(lowerer, expr, member);
|
||||
}
|
||||
if (member === "assert") return null; // claimed by the call path
|
||||
lowerer.noLowering(
|
||||
`TestContext.${expr.name.text}`,
|
||||
expr,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
|
||||
import {
|
||||
lifetimeRegionType,
|
||||
lowerLifetimeRegionCall,
|
||||
@@ -110,6 +111,13 @@ import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-d
|
||||
import { ClassProtocols } from "./class-protocols.js";
|
||||
import { earlyReadVarSymbols } from "./var-hoisting.js";
|
||||
import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
import { assignLegacyTiers, type DynamicFunction } from "../../ir/tiers.js";
|
||||
import {
|
||||
analyzeTiers,
|
||||
dynamicStatementCount,
|
||||
type TierMode,
|
||||
type TierReport,
|
||||
} from "../tier-analysis.js";
|
||||
import { DeferredModuleInitializers } from "./deferred-module-initializers.js";
|
||||
import { finalizeClassMethodValues } from "./class-method-values.js";
|
||||
import type { ClassSymbolKey } from "./symbol-fields.js";
|
||||
@@ -270,7 +278,7 @@ import {
|
||||
appendForkModules,
|
||||
appendWorkerModules,
|
||||
} from "./lower-modules.js";
|
||||
import { prepareCjsModuleGraph } from "./lower-node-module.js";
|
||||
import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js";
|
||||
import { registerBuiltinEventTargetClasses } from "./lower-event-target.js";
|
||||
import {
|
||||
type ClassInfo,
|
||||
@@ -532,6 +540,7 @@ import {
|
||||
lowerForOf,
|
||||
lowerForStatement,
|
||||
matchGroupsPatternSource,
|
||||
isGuardedRecordRead,
|
||||
} from "./lower-stmts.js";
|
||||
import {
|
||||
type FieldTarget,
|
||||
@@ -608,7 +617,9 @@ export type StaticCallableProjection =
|
||||
| { kind: "promisified-exec-file" }
|
||||
| { kind: "promisified-exec" }
|
||||
| { kind: "promisified-builtin"; module: string; member: string }
|
||||
| { kind: "promisified-zlib"; member: string };
|
||||
| { kind: "promisified-zlib"; member: string }
|
||||
| { kind: "promisified-crypto"; member: string }
|
||||
| { kind: "promisified-dns"; member: string };
|
||||
|
||||
/** Own-property lookup for the surface tables. They are plain object
|
||||
* literals, so a bare `table[name]` would also find Object.prototype
|
||||
@@ -627,6 +638,9 @@ export interface LowerStats {
|
||||
|
||||
/** Functions whose signature couldn't be analyzed (bodies not counted). */
|
||||
functionsSkipped: number;
|
||||
/** Coverage only: reached statements that compile into dynamic-tier
|
||||
* functions (tier-analysis.ts), a subset of the compiled statements. */
|
||||
statementsDynamic?: number;
|
||||
}
|
||||
|
||||
/** Every identifier a binding name binds: the identifier itself, or all
|
||||
@@ -701,6 +715,11 @@ export interface LowerResult {
|
||||
/** Compile-time-detectable behavior differences from Node (SC6xxx
|
||||
* divergence warnings) in the program's own modules. Never failures. */
|
||||
divergences?: ScrDiagnostic[];
|
||||
/** Functions lowering placed in the dynamic tier (ir/tiers.ts), present
|
||||
* when there are any. */
|
||||
dynamicFunctions?: DynamicFunction[];
|
||||
/** LowerOptions.tierReport: the report-only tier assignment. */
|
||||
tiers?: TierReport;
|
||||
}
|
||||
|
||||
export interface LowerOptions {
|
||||
@@ -750,6 +769,12 @@ export interface LowerOptions {
|
||||
/** The mapped entries plus relative declaration dependencies, attributed
|
||||
* to their owning external specifier. */
|
||||
externalTypeSpecifiersByFile?: ReadonlyMap<string, readonly string[]>;
|
||||
/** `--tier` (tier-analysis.ts); "auto" when absent. */
|
||||
tierMode?: TierMode;
|
||||
/** Coverage only: compute the tier-assignment report (`--explain-dynamic`).
|
||||
* `preflight` holds the import and package refusals coverage reports
|
||||
* beside the lowering's own blockers; they request module demotions. */
|
||||
tierReport?: { preflight: readonly ScrDiagnostic[] };
|
||||
}
|
||||
|
||||
interface RuntimeFenceFallback {
|
||||
@@ -825,6 +850,8 @@ export interface LowererMode {
|
||||
externalTypes?: ReadonlyMap<string, string>;
|
||||
/** LowerOptions.externalTypeSpecifiersByFile, shared by every pass. */
|
||||
externalTypeSpecifiersByFile?: ReadonlyMap<string, readonly string[]>;
|
||||
/** LowerOptions.tierMode, shared by every pass. */
|
||||
tierMode?: TierMode;
|
||||
}
|
||||
|
||||
function directExternalTypeSpecifiersByFile(
|
||||
@@ -916,6 +943,7 @@ export function lowerToIr(
|
||||
const externalTypes = options.externalTypes ?? new Map<string, string>();
|
||||
const externalTypeSpecifiersByFile =
|
||||
options.externalTypeSpecifiersByFile ?? directExternalTypeSpecifiersByFile(externalTypes);
|
||||
const tierMode = options.tierMode ?? "auto";
|
||||
const validation = new Lowerer(program, entry, moduleOrder, {
|
||||
frontendServices: options.frontendServices,
|
||||
projectCheckJs: options.projectCheckJs ?? false,
|
||||
@@ -927,6 +955,7 @@ export function lowerToIr(
|
||||
workerTargets,
|
||||
externalTypes,
|
||||
externalTypeSpecifiersByFile,
|
||||
tierMode,
|
||||
});
|
||||
const ffiValidation = validateFfiImports(validation);
|
||||
timing("ffi-validate");
|
||||
@@ -949,6 +978,7 @@ export function lowerToIr(
|
||||
workerTargets,
|
||||
externalTypes,
|
||||
externalTypeSpecifiersByFile,
|
||||
tierMode,
|
||||
ffiBindingSymbols: ffiValidation.symbolsByName,
|
||||
});
|
||||
for (const d of extraModuleCycleDiags) reachableEmit.pushDiag(d);
|
||||
@@ -979,6 +1009,7 @@ export function lowerToIr(
|
||||
ffiBindingSymbols: ffiValidation.symbolsByName,
|
||||
externalTypes,
|
||||
externalTypeSpecifiersByFile,
|
||||
tierMode,
|
||||
});
|
||||
for (const d of extraModuleCycleDiags) emit.pushDiag(d);
|
||||
for (const d of ffiValidation.diagnostics) emit.pushDiag(d);
|
||||
@@ -1037,19 +1068,44 @@ export function lowerToIr(
|
||||
ffiBindingSymbols: ffiValidation.symbolsByName,
|
||||
externalTypes,
|
||||
externalTypeSpecifiersByFile,
|
||||
tierMode,
|
||||
});
|
||||
const rem = remainder.run();
|
||||
timing("coverage-remainder");
|
||||
// Tiers (tier-analysis.ts): reached statements in dynamic-tier functions,
|
||||
// and with tierReport the report-only demotion analysis.
|
||||
const dynamicFunctions = resultLowerer.dynamicBodies;
|
||||
const tiers =
|
||||
options.tierReport === undefined
|
||||
? undefined
|
||||
: analyzeTiers(program, {
|
||||
mode: tierMode,
|
||||
dynamicFunctions,
|
||||
reached: [...options.tierReport.preflight, ...result.diagnostics],
|
||||
deferred: result.runtimeFences,
|
||||
unreached: [...rem.diagnostics, ...rem.runtimeFences],
|
||||
});
|
||||
// A dynamic function's own statements can include ones that failed.
|
||||
const statementsDynamic = Math.min(
|
||||
tiers?.statementsDynamic ?? dynamicStatementCount(program, dynamicFunctions),
|
||||
result.stats.statementsTotal - result.stats.statementsFailed,
|
||||
);
|
||||
if (tiers !== undefined) tiers.statementsDynamic = statementsDynamic;
|
||||
timing("tier-analysis", { dynamic: dynamicFunctions.length });
|
||||
// Unreached JS statements whose fences deferred to runtime count as
|
||||
// failed statements in the remainder's stats, so their diagnostics must
|
||||
// travel with them: a failed statement is never left unexplained.
|
||||
return {
|
||||
const covered: LowerResult = {
|
||||
...result,
|
||||
stats: { ...result.stats, statementsDynamic },
|
||||
unreached: {
|
||||
diagnostics: rem.diagnostics,
|
||||
stats: rem.stats,
|
||||
...(rem.runtimeFences.length > 0 ? { runtimeFences: rem.runtimeFences } : {}),
|
||||
},
|
||||
};
|
||||
if (tiers !== undefined) covered.tiers = tiers;
|
||||
return covered;
|
||||
}
|
||||
|
||||
/** The native namespace type produced by a literal static import() of a
|
||||
@@ -1802,6 +1858,11 @@ export class Lowerer {
|
||||
* typeParamBindings — the checker has no `T` to substitute, so the
|
||||
* binding rides the node-type accessor instead of mapType. */
|
||||
implicitParamTypes: Map<ts.Symbol, ts.Type> | null = null;
|
||||
/** Immutable bindings that name a zlib stream class (`const C =
|
||||
* zlib.Gzip`), keyed by the binding's symbol: the class's
|
||||
* node_zlib_mode. No runtime slot exists: new, typeof, .name and ===
|
||||
* resolve at compile time, and instanceof checks the stream's mode. */
|
||||
readonly zlibClassAliases = new Map<ts.Symbol, number>();
|
||||
/** Whether some net.Socket in this program may get an encoding: a
|
||||
* setEncoding call whose receiver is a socket or an untyped value.
|
||||
* Computed once on first use (null until then). */
|
||||
@@ -1851,6 +1912,9 @@ export class Lowerer {
|
||||
* carry undefined at runtime. Identifier reads keep the union tag so
|
||||
* typeof and later boundaries observe the real value. */
|
||||
readonly runtimeOptionalGlobals = new Set<IrGlobal>();
|
||||
/** Per CommonJS file using top-level `this`: the global holding the
|
||||
* object that stands for its original exports (lowerCjsTopLevelThis). */
|
||||
readonly cjsExportsThis = new Map<ts.SourceFile, IrGlobal>();
|
||||
/** Bindings whose unchecked string arithmetic can produce either a number
|
||||
* (NaN for an absent read) or a string. Preserve that result union at bare
|
||||
* reads instead of applying the checker-bare string narrowing adapter. */
|
||||
@@ -1965,6 +2029,26 @@ export class Lowerer {
|
||||
return fallback ?? null;
|
||||
}
|
||||
|
||||
/** The index value of a keyed read of a record's index signature that
|
||||
* can answer undefined for a missing key while its checker type does not
|
||||
* admit undefined, or null. A literal key naming a declared field is a
|
||||
* field read. */
|
||||
optionalIndexSignatureRead(e: ts.ElementAccessExpression): IrType | null {
|
||||
const receiver = panicSafe(() => this.mapTypeOf(this.typeOf(e.expression)), null);
|
||||
if (receiver?.kind !== "record") return null;
|
||||
const shape = this.shapes.get(receiver.shapeId);
|
||||
if (!shape || shape.tuple || !shape.indexValue || shape.indexValue.kind === "dyn") return null;
|
||||
const key = e.argumentExpression;
|
||||
if (
|
||||
(ts.isStringLiteral(key) || ts.isNumericLiteral(key)) &&
|
||||
shape.fields.some((field) => field.name === key.text)
|
||||
)
|
||||
return null;
|
||||
const read = panicSafe(() => this.mapTypeOf(this.typeOf(e)), null);
|
||||
if (read?.kind === "union" && this.armTag(read.unionId, UNDEFINED_T) >= 0) return null;
|
||||
return shape.indexValue;
|
||||
}
|
||||
|
||||
runtimeOptionalIdentifierValue(
|
||||
node: ts.Expression,
|
||||
): { value: IrExpr; present: IrType; unionId: string } | null {
|
||||
@@ -2048,6 +2132,7 @@ export class Lowerer {
|
||||
value: IrExpr,
|
||||
expected: IrType,
|
||||
member: string,
|
||||
action: "read" | "set" = "read",
|
||||
): IrExpr | null {
|
||||
const source = this.runtimeOptionalSourceValue(node, value);
|
||||
if (source?.type.kind !== "union") return null;
|
||||
@@ -2090,7 +2175,7 @@ export class Lowerer {
|
||||
then: nodeThrowExpr(
|
||||
1,
|
||||
"",
|
||||
`Cannot read properties of ${unit} (reading '${member}')`,
|
||||
`Cannot ${action} properties of ${unit} (${action === "read" ? "reading" : "setting"} '${member}')`,
|
||||
expected,
|
||||
loc,
|
||||
),
|
||||
@@ -2664,6 +2749,14 @@ export class Lowerer {
|
||||
/** Coverage remainder mode: the reachability gate inverts (see wantBody)
|
||||
* and no module is built. */
|
||||
readonly remainder: boolean;
|
||||
/** `--tier` (tier-analysis.ts). Stage S2 is report-only: both modes
|
||||
* refuse and fence exactly as before. Stage S6 demotes the units of
|
||||
* committed refusals in "auto" and keeps them refusals in "static-only". */
|
||||
readonly tierMode: TierMode;
|
||||
/** Functions finishModule placed in the dynamic tier (ir/tiers.ts). */
|
||||
dynamicFunctions: DynamicFunction[] = [];
|
||||
/** Their IR, for coverage's dynamic statement count. */
|
||||
dynamicBodies: IrFunction[] = [];
|
||||
/** Deferred collection diagnostics (failed signatures/class shapes) by
|
||||
* declaration symbol: an unreached declaration must not fail the build,
|
||||
* so its diagnostics wait until a reference makes them relevant. */
|
||||
@@ -2750,6 +2843,7 @@ export class Lowerer {
|
||||
this.frontendServices = mode.frontendServices;
|
||||
this.reachable = mode.reachable ?? null;
|
||||
this.remainder = mode.remainder ?? false;
|
||||
this.tierMode = mode.tierMode ?? "auto";
|
||||
this.alreadyFlushed = mode.alreadyFlushed ?? new Set();
|
||||
this.targetPlatform = mode.targetPlatform ?? process.platform;
|
||||
this.startupCrash = mode.startupCrash ?? null;
|
||||
@@ -2839,7 +2933,8 @@ export class Lowerer {
|
||||
return null;
|
||||
}
|
||||
const className = this.classNamer(decl);
|
||||
if (!this.collectingExprClasses.has(decl)) this.lowerClassExpressionInfo(decl);
|
||||
if (!this.collectingExprClasses.has(decl) && !this.collectLocalClassForType(decl))
|
||||
return null;
|
||||
return { kind: "object", className };
|
||||
},
|
||||
mixinIntersectionInstance: (widened) => mixinIntersectionInstanceType(this, widened),
|
||||
@@ -2874,6 +2969,8 @@ export class Lowerer {
|
||||
this.fileTag.set(sf, sf === entry ? "" : `%m${i}.`);
|
||||
});
|
||||
if (this.moduleOrder.length === 0) this.fileTag.set(entry, "");
|
||||
const jsDynamic = jsDynamicDeclarations(this.moduleOrder, this.checker);
|
||||
if (jsDynamic.size > 0) this.typeCtx.jsDynamicDeclaration = (decl) => jsDynamic.has(decl);
|
||||
this.registerBuiltinErrorClasses();
|
||||
registerBuiltinEmitterClass(this);
|
||||
registerBuiltinStreamClasses(this);
|
||||
@@ -3670,6 +3767,16 @@ export class Lowerer {
|
||||
const id = `%g.${tag}%loaded`;
|
||||
this.moduleGuardOf.set(fp.sf, id);
|
||||
this.globalsList.push({ id, name: "%loaded", type: BOOL, mutable: true });
|
||||
if (usesCjsTopLevelThis(this, fp.sf)) {
|
||||
const exportsThis: IrGlobal = {
|
||||
id: `%g.${tag}%exportsThis`,
|
||||
name: "%exportsThis",
|
||||
type: DYN,
|
||||
mutable: true,
|
||||
};
|
||||
this.globalsList.push(exportsThis);
|
||||
this.cjsExportsThis.set(fp.sf, exportsThis);
|
||||
}
|
||||
}
|
||||
|
||||
// A module is intrinsically async when an await/for-await occurs
|
||||
@@ -3924,6 +4031,33 @@ export class Lowerer {
|
||||
}
|
||||
}
|
||||
|
||||
/** A file-scope get-or-create read (`let e = map[k]; if (e ===
|
||||
* undefined) ...` over an index-signature record) keeps a missing key as
|
||||
* undefined (isGuardedRecordRead). Its pre-registered global takes the
|
||||
* undefined arm before any body reads it; the declaration lowers its
|
||||
* initializer as the matching absence probe. */
|
||||
analyzeGuardedRecordReadGlobals(parts: FileParts[]): void {
|
||||
for (const { sf } of parts) {
|
||||
for (const statement of sf.statements) {
|
||||
if (!ts.isVariableStatement(statement)) continue;
|
||||
for (const decl of statement.declarationList.declarations) {
|
||||
if (!ts.isIdentifier(decl.name) || !isGuardedRecordRead(this, decl)) continue;
|
||||
const global = this.globalOf(decl.name);
|
||||
const symbol = this.resolveValueSymbol(decl.name);
|
||||
if (!global || !symbol || global.tdz || this.runtimeOptionalBindingTypes.has(symbol))
|
||||
continue;
|
||||
const widened =
|
||||
global.type.kind === "union"
|
||||
? this.withUndefinedArmOf(global.type)
|
||||
: this.withUndefinedArm(global.type);
|
||||
if (!widened || typeEquals(widened, global.type)) continue;
|
||||
global.type = widened;
|
||||
this.runtimeOptionalBindingTypes.set(symbol, widened);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzeRuntimeOptionalArrayReads(parts: FileParts[]): void {
|
||||
const provenance = RuntimeOptionalProvenance.fromEnvironment();
|
||||
// The site whose value the current promotion follows (debug only).
|
||||
@@ -3948,18 +4082,6 @@ export class Lowerer {
|
||||
if (!s) return null;
|
||||
return s.flags & ts.SymbolFlags.Alias ? this.checker.getAliasedSymbol(s) : s;
|
||||
};
|
||||
// Fields that can be read before their assignment hold undefined then;
|
||||
// their reads are runtime-optional values like missing array reads.
|
||||
for (const info of this.classes.values()) {
|
||||
if (!info.earlyReadFields || !info.decl?.name) continue;
|
||||
const classSymbol = symbolOf(info.decl.name);
|
||||
if (!classSymbol) continue;
|
||||
const instance = this.checker.getDeclaredTypeOfSymbol(classSymbol);
|
||||
for (const name of info.earlyReadFields) {
|
||||
const property = this.checker.getPropertyOfType(instance, name);
|
||||
if (property) optionalSymbols.add(property);
|
||||
}
|
||||
}
|
||||
const signatureOf = (node: ts.Identifier): FnSig | null => {
|
||||
const symbol = symbolOf(node);
|
||||
if (!symbol) return null;
|
||||
@@ -4105,6 +4227,7 @@ export class Lowerer {
|
||||
const t = mappedOrNull(receiver);
|
||||
return t?.kind === "bytes" ? t.elem : null;
|
||||
},
|
||||
isNumberRecordRead: (read) => this.optionalIndexSignatureRead(read)?.kind === "f64",
|
||||
isNumber: (node) => mappedOrNull(node)?.kind === "f64",
|
||||
isString: (node) => mappedOrNull(node)?.kind === "string",
|
||||
hasLength: (node) => {
|
||||
@@ -4240,8 +4363,26 @@ export class Lowerer {
|
||||
// Typed-array reads answer undefined for an invalid index too, unless
|
||||
// every consumer treats undefined like NaN.
|
||||
if (kind === "bytes") return !nanCoded.isNumericRead(e);
|
||||
if (kind === "record")
|
||||
return this.optionalIndexSignatureRead(e) !== null && !nanCoded.isNumericRead(e);
|
||||
return kind === "array";
|
||||
};
|
||||
/** `r.name` where the name resolves to the receiver's index signature. */
|
||||
const isRecordIndexPropertyRead = (e: ts.PropertyAccessExpression): boolean => {
|
||||
// A name that resolves to a declared member is not an index read, and
|
||||
// only a receiver with an index signature maps to an indexed record.
|
||||
if (symbolOf(e.name)) return false;
|
||||
const receiverType = this.typeOf(e.expression);
|
||||
if (this.checker.getIndexInfosOfType(receiverType).length === 0) return false;
|
||||
const receiver = this.mapTypeOf(receiverType);
|
||||
if (receiver?.kind !== "record") return false;
|
||||
const shape = this.shapes.get(receiver.shapeId);
|
||||
if (!shape || shape.tuple || !shape.indexValue || shape.indexValue.kind === "dyn")
|
||||
return false;
|
||||
if (shape.fields.some((field) => field.name === e.name.text)) return false;
|
||||
const read = this.mapTypeOf(this.typeOf(e));
|
||||
return !(read?.kind === "union" && this.armTag(read.unionId, UNDEFINED_T) >= 0);
|
||||
};
|
||||
const isDynamicObjectEntryRead = (node: ts.Expression): boolean => {
|
||||
const read = peel(node);
|
||||
if (!ts.isElementAccessExpression(read) || !ts.isIdentifier(read.expression)) return false;
|
||||
@@ -4330,6 +4471,7 @@ export class Lowerer {
|
||||
if (assertsDefined(node)) return false;
|
||||
const e = peel(node);
|
||||
if (isIndexedRead(e)) return true;
|
||||
if (ts.isPropertyAccessExpression(e) && isRecordIndexPropertyRead(e)) return true;
|
||||
if (ts.isPropertyAccessExpression(e) && isOptionalProcessStreamProperty(this, e)) return true;
|
||||
if (ts.isPropertyAccessExpression(e) && isSettledIteratorValueRead(this, e)) return true;
|
||||
if (ts.isPropertyAccessExpression(e) && isOsConstantsSymbol(symbolOf(e.expression), this))
|
||||
@@ -4554,6 +4696,7 @@ export class Lowerer {
|
||||
return out;
|
||||
};
|
||||
const scanPattern = (name: ts.BindingName, init: ts.Expression): boolean => {
|
||||
if (ts.isObjectBindingPattern(name)) return scanRecordPattern(name, init);
|
||||
if (!ts.isArrayBindingPattern(name)) return false;
|
||||
const sourceType = this.mapTypeOf(this.typeOf(init));
|
||||
if (sourceType?.kind !== "array") return false;
|
||||
@@ -4589,6 +4732,40 @@ export class Lowerer {
|
||||
});
|
||||
return changed;
|
||||
};
|
||||
/** `const { a, b } = table` over an index-signature record: a name that
|
||||
* is not a declared field reads the index signature and can be
|
||||
* missing. */
|
||||
const scanRecordPattern = (name: ts.ObjectBindingPattern, init: ts.Expression): boolean => {
|
||||
const sourceType = this.mapTypeOf(this.typeOf(init));
|
||||
if (sourceType?.kind !== "record") return false;
|
||||
const shape = this.shapes.get(sourceType.shapeId);
|
||||
const indexValue = shape?.indexValue;
|
||||
if (!shape || shape.tuple || !indexValue || indexValue.kind === "dyn") return false;
|
||||
let changed = false;
|
||||
for (const el of name.elements) {
|
||||
if (el.dotDotDotToken || el.initializer || !ts.isIdentifier(el.name)) continue;
|
||||
const key = el.propertyName ?? el.name;
|
||||
if (!ts.isIdentifier(key) && !ts.isStringLiteral(key) && !ts.isNumericLiteral(key))
|
||||
continue;
|
||||
if (shape.fields.some((field) => field.name === key.text)) continue;
|
||||
const symbol = symbolOf(el.name);
|
||||
if (!symbol || optionalSymbols.has(symbol)) continue;
|
||||
const read = this.mapTypeOf(this.typeOf(el.name));
|
||||
if (read?.kind === "union" && this.armTag(read.unionId, UNDEFINED_T) >= 0) continue;
|
||||
const widened = this.withUndefinedArmOf(indexValue);
|
||||
if (!widened) continue;
|
||||
optionalSymbols.add(symbol);
|
||||
provenance?.note("pattern-binding", el.name, init);
|
||||
this.runtimeOptionalBindingTypes.set(symbol, widened);
|
||||
const global = this.globalsBySymbol.get(symbol);
|
||||
if (global) {
|
||||
global.type = widened;
|
||||
this.runtimeOptionalGlobals.add(global);
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
const hofCallbackIndices = (name: string, hasInitialValue: boolean): number[] | null => {
|
||||
if (name === "reduce" || name === "reduceRight") return hasInitialValue ? [1] : [0, 1];
|
||||
if (
|
||||
@@ -5878,25 +6055,61 @@ export class Lowerer {
|
||||
addClassFields(recordFlows.bindingClass(symbol), fields);
|
||||
for (const [symbol, fields] of returnFields)
|
||||
addClassFields(recordFlows.returnClass(symbol), fields);
|
||||
// A record field that flows into other slots (`fill(args, state.parsed)`)
|
||||
// widens with its owner's class: every member takes the field's widened
|
||||
// layout, so the field and the slots it reaches share one record.
|
||||
const nestedFields = new Map<number, Map<string, number>>();
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const { owner, name, classId } of recordFlows.fields()) {
|
||||
if (nestedFields.get(owner)?.has(name)) continue;
|
||||
if (!(classFields.has(classId) || nestedFields.has(classId))) continue;
|
||||
if (!recordFlows.closed(classId)) continue;
|
||||
const byName = nestedFields.get(owner) ?? new Map<string, number>();
|
||||
byName.set(name, classId);
|
||||
nestedFields.set(owner, byName);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
const settledFields = (
|
||||
id: number | null,
|
||||
own: ReadonlySet<string> | undefined,
|
||||
): ReadonlySet<string> | undefined =>
|
||||
id === null
|
||||
? own
|
||||
: classFields.has(id) && recordFlows.closed(id)
|
||||
? classFields.get(id)
|
||||
: (classFields.has(id) || nestedFields.has(id)) && recordFlows.closed(id)
|
||||
? (classFields.get(id) ?? new Set<string>())
|
||||
: undefined;
|
||||
const widenRecordFields = (type: IrType, fields: ReadonlySet<string>): IrType => {
|
||||
const widenRecordFields = (
|
||||
type: IrType,
|
||||
fields: ReadonlySet<string>,
|
||||
id: number | null = null,
|
||||
seen: ReadonlySet<number> = new Set(),
|
||||
): IrType => {
|
||||
let promoted = type;
|
||||
const fieldOf = (name: string) =>
|
||||
promoted.kind === "record"
|
||||
? this.shapes.get(promoted.shapeId)?.fields.find((f) => f.name === name)
|
||||
: undefined;
|
||||
for (const field of fields) {
|
||||
const source =
|
||||
promoted.kind === "record"
|
||||
? this.shapes.get(promoted.shapeId)?.fields.find((f) => f.name === field)
|
||||
: undefined;
|
||||
const source = fieldOf(field);
|
||||
if (source)
|
||||
promoted = this.runtimeOptionalRecordField(promoted, field, addUndefined(source.type));
|
||||
}
|
||||
if (id === null || seen.has(id)) return promoted;
|
||||
const nested = nestedFields.get(id);
|
||||
if (!nested) return promoted;
|
||||
for (const [field, inner] of nested) {
|
||||
const source = fieldOf(field);
|
||||
if (source?.type.kind !== "record") continue;
|
||||
const widened = widenRecordFields(
|
||||
source.type,
|
||||
classFields.get(inner) ?? new Set<string>(),
|
||||
inner,
|
||||
new Set([...seen, id]),
|
||||
);
|
||||
promoted = this.runtimeOptionalRecordField(promoted, field, widened);
|
||||
}
|
||||
return promoted;
|
||||
};
|
||||
for (const [symbol, id] of recordFlows.bindings())
|
||||
@@ -5911,9 +6124,10 @@ export class Lowerer {
|
||||
for (const [bound, { target, index }] of recordParameterSlots) {
|
||||
const shape = signatureBySymbol.get(target)?.params[index];
|
||||
if (!shape || shape.type.kind !== "record") continue;
|
||||
const fields = settledFields(recordFlows.bindingClass(bound), optionalFields.get(bound));
|
||||
const id = recordFlows.bindingClass(bound);
|
||||
const fields = settledFields(id, optionalFields.get(bound));
|
||||
if (!fields) continue;
|
||||
const promoted = widenRecordFields(shape.type, fields);
|
||||
const promoted = widenRecordFields(shape.type, fields, id);
|
||||
if (typeEquals(promoted, shape.type)) continue;
|
||||
shape.type = promoted;
|
||||
this.runtimeOptionalBindingTypes.set(bound, promoted);
|
||||
@@ -5922,7 +6136,7 @@ export class Lowerer {
|
||||
const id = recordFlows.returnClass(symbol);
|
||||
const fields = id === null ? undefined : settledFields(id, undefined);
|
||||
if (fields && sig.returnType.kind === "record") {
|
||||
const next = widenRecordFields(sig.returnType, fields);
|
||||
const next = widenRecordFields(sig.returnType, fields, id);
|
||||
sig.returnType = next;
|
||||
if (sig.top) sig.top.returnType = next;
|
||||
if (sig.method) sig.method.ret = next;
|
||||
@@ -6008,7 +6222,8 @@ export class Lowerer {
|
||||
if (global) global.type = promoted;
|
||||
}
|
||||
}
|
||||
const fields = settledFields(recordFlows.bindingClass(symbol), optionalFields.get(symbol));
|
||||
const flowClass = recordFlows.bindingClass(symbol);
|
||||
const fields = settledFields(flowClass, optionalFields.get(symbol));
|
||||
if (!dynamicObjectEntryRows.has(symbol) && !optionalSymbols.has(symbol) && !fields) return;
|
||||
const current = this.mapTypeOf(this.typeOf(node.name));
|
||||
if (!current) return;
|
||||
@@ -6025,7 +6240,8 @@ export class Lowerer {
|
||||
};
|
||||
}
|
||||
}
|
||||
if (fields && promoted.kind === "record") promoted = widenRecordFields(promoted, fields);
|
||||
if (fields && promoted.kind === "record")
|
||||
promoted = widenRecordFields(promoted, fields, flowClass);
|
||||
if (!typeEquals(promoted, current)) {
|
||||
this.runtimeOptionalBindingTypes.set(symbol, promoted);
|
||||
const global = ts.isIdentifier(node.name)
|
||||
@@ -6084,8 +6300,11 @@ export class Lowerer {
|
||||
for (const fp of parts) this.checker.prefetchSourceFile(fp.sf);
|
||||
}
|
||||
this.collectProgram(parts);
|
||||
this.analyzeRuntimeOptionalArrayReads(parts);
|
||||
this.analyzeDateJsonResults(parts);
|
||||
this.speculativeTypeQueries(() => {
|
||||
this.analyzeRuntimeOptionalArrayReads(parts);
|
||||
this.analyzeDateJsonResults(parts);
|
||||
this.analyzeGuardedRecordReadGlobals(parts);
|
||||
});
|
||||
// Decorated classes analyze AFTER the whole collection pass: a
|
||||
// decorator's return type may name a subclass declared below the
|
||||
// class, and reference lowering needs each class's rebindability
|
||||
@@ -6336,11 +6555,18 @@ export class Lowerer {
|
||||
};
|
||||
if (module) sanitizeUnregisteredClassTypes(module, (name) => this.classes.has(name));
|
||||
if (module) checkCycleBindingAccesses(this, module);
|
||||
// Execution tiers from the final bodies (ir/tiers.ts). Assigned with or
|
||||
// without a module: coverage reports the dynamic tier of programs that
|
||||
// still have blockers.
|
||||
this.dynamicFunctions = assignLegacyTiers(functions);
|
||||
if (this.dynamicFunctions.length > 0)
|
||||
this.dynamicBodies = functions.filter((fn) => fn.tier === "dynamic");
|
||||
return {
|
||||
module,
|
||||
diagnostics: this.diags,
|
||||
runtimeFences: this.runtimeFences,
|
||||
stats: this.stats,
|
||||
...(this.dynamicFunctions.length > 0 ? { dynamicFunctions: this.dynamicFunctions } : {}),
|
||||
...(this.statsByFile.size > 0 ? { statsByFile: this.statsByFile } : {}),
|
||||
...(this.provenanceElided.length > 0 ? { provenanceElided: this.provenanceElided } : {}),
|
||||
};
|
||||
@@ -6470,8 +6696,11 @@ export class Lowerer {
|
||||
collectionEdges.add(name);
|
||||
};
|
||||
this.collectProgram(parts);
|
||||
this.analyzeRuntimeOptionalArrayReads(parts);
|
||||
this.analyzeDateJsonResults(parts);
|
||||
this.speculativeTypeQueries(() => {
|
||||
this.analyzeRuntimeOptionalArrayReads(parts);
|
||||
this.analyzeDateJsonResults(parts);
|
||||
this.analyzeGuardedRecordReadGlobals(parts);
|
||||
});
|
||||
// Decorated classes analyze post-collection here too: the %init seeds
|
||||
// lower the decoration calls, whose edges (decorator bodies, construct
|
||||
// thunks) reachable emit must see.
|
||||
@@ -8535,9 +8764,18 @@ export class Lowerer {
|
||||
// initial contextual layout was chosen. Its known discriminator still
|
||||
// selects the destination arm; validate that payload before wrapping.
|
||||
// Work with the already lowered value so property effects run once.
|
||||
// A type assertion keeps the literal's own record layout, which can
|
||||
// equal another arm's layout (`{ kind: "return", value, loc } as Stmt`
|
||||
// when a `throw` arm has the same fields), so assertions are looked
|
||||
// through as well.
|
||||
if (expr.type.kind === "record" && expected.kind === "union") {
|
||||
let literal = node;
|
||||
while (ts.isParenthesizedExpression(literal) || ts.isSatisfiesExpression(literal))
|
||||
while (
|
||||
ts.isParenthesizedExpression(literal) ||
|
||||
ts.isSatisfiesExpression(literal) ||
|
||||
ts.isAsExpression(literal) ||
|
||||
ts.isTypeAssertion(literal)
|
||||
)
|
||||
literal = literal.expression;
|
||||
const def = this.unions.get(expected.unionId);
|
||||
if (def?.discriminant && ts.isObjectLiteralExpression(literal)) {
|
||||
@@ -9614,6 +9852,49 @@ export class Lowerer {
|
||||
return lowerClassExpressionInfo(this, expr);
|
||||
}
|
||||
|
||||
/** Depth of the whole-program analyses that query types across every
|
||||
* file before reachability is known (speculativeTypeQueries). */
|
||||
private speculativeTypeQueryDepth = 0;
|
||||
|
||||
/** Runs a whole-program analysis whose type queries are speculative: they
|
||||
* visit every file, reached or not, and no statement or declaration owns
|
||||
* a refusal they trigger (collectLocalClassForType). */
|
||||
speculativeTypeQueries(analysis: () => void): void {
|
||||
this.speculativeTypeQueryDepth++;
|
||||
try {
|
||||
analysis();
|
||||
} finally {
|
||||
this.speculativeTypeQueryDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
/** Collects a function-local class because a type query named its
|
||||
* instance type. Collection and body lowering own a refusal of that
|
||||
* class: the poison reaches the declaration or statement that asked, as
|
||||
* before. A speculative whole-program analysis owns none, so there the
|
||||
* class is unmappable for the query and its diagnostics are dropped;
|
||||
* lowering the class's own declaration collects it again and reports
|
||||
* them if the program reaches it. */
|
||||
collectLocalClassForType(decl: ts.ClassLikeDeclaration): boolean {
|
||||
if (this.speculativeTypeQueryDepth === 0 || this.fnStack.length > 0) {
|
||||
this.lowerClassExpressionInfo(decl);
|
||||
return true;
|
||||
}
|
||||
const sink: ScrDiagnostic[] = [];
|
||||
const previousSink = this.diagSink;
|
||||
this.diagSink = sink;
|
||||
try {
|
||||
this.lowerClassExpressionInfo(decl);
|
||||
} catch (e) {
|
||||
if (e instanceof PoisonError) return false;
|
||||
throw e;
|
||||
} finally {
|
||||
this.diagSink = previousSink;
|
||||
}
|
||||
for (const d of sink) this.pushDiag(d);
|
||||
return true;
|
||||
}
|
||||
|
||||
lowerClassExpression(expr: ts.ClassLikeDeclaration): IrExpr {
|
||||
return lowerClassExpression(this, expr);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ export interface NanCodedHost {
|
||||
declarationOf(symbol: ts.Symbol): ts.Node | undefined;
|
||||
/** The element kind of a typed-array receiver, or null. */
|
||||
bytesElemOf(receiver: ts.Expression): IrBytesElem | null;
|
||||
/** The read is a keyed read of a record's `number` index signature,
|
||||
* which answers undefined for a missing key. */
|
||||
isNumberRecordRead(read: ts.ElementAccessExpression): boolean;
|
||||
/** The static IR type of the node is a plain number. */
|
||||
isNumber(node: ts.Node): boolean;
|
||||
isString(node: ts.Expression): boolean;
|
||||
@@ -315,7 +318,10 @@ export class NanCodedReads {
|
||||
return;
|
||||
}
|
||||
if (ts.isElementAccessExpression(node) && this.isReadCandidate(node)) {
|
||||
const elem = this.host.bytesElemOf(node.expression);
|
||||
// A record's number values may hold a genuine NaN, like f64 elements.
|
||||
const elem =
|
||||
this.host.bytesElemOf(node.expression) ??
|
||||
(this.host.isNumberRecordRead(node) ? "f64" : null);
|
||||
if (elem) candidates.set(node, elem);
|
||||
}
|
||||
});
|
||||
@@ -393,7 +399,13 @@ export class NanCodedReads {
|
||||
// native build of the compiler.
|
||||
const cached = this.symbolCache.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
const symbol = this.host.symbolOf(node);
|
||||
let symbol = this.host.symbolOf(node);
|
||||
// A method read through an instance (`h.get`) can resolve to another
|
||||
// symbol than its declaration name; slots and call sites key on the
|
||||
// declaration's own symbol.
|
||||
const decl = symbol ? this.host.declarationOf(symbol) : undefined;
|
||||
if (decl && ts.isMethodDeclaration(decl) && decl.name !== node && ts.isIdentifier(decl.name))
|
||||
symbol = this.symbolOf(decl.name) ?? symbol;
|
||||
this.symbolCache.set(node, symbol);
|
||||
return symbol;
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ import type { Lowerer } from "./lowerer.js";
|
||||
* after assignments and tests, and keeps that narrowing across calls: a
|
||||
* callback, method or getter that clears or replaces the property (or a
|
||||
* closure that reassigns the binding) leaves the reference typed by its old
|
||||
* value. Lowering bridges a narrowed union read with an UNCHECKED payload
|
||||
* extraction and folds `?.`, truthiness and `typeof` by the narrowed type,
|
||||
* so such a read would interpret the wrong arm. This analysis decides when
|
||||
* value. Lowering bridges a narrowed union read with a payload extraction
|
||||
* that traps on the wrong arm and folds `?.`, truthiness and `typeof` by the
|
||||
* narrowed type, so such a read would abort or fold to the old value instead
|
||||
* of throwing the catchable TypeError. This analysis decides when
|
||||
* the narrowing is provably current:
|
||||
*
|
||||
* - every property in the reference is construction-only (no store to its
|
||||
@@ -30,6 +31,10 @@ import type { Lowerer } from "./lowerer.js";
|
||||
* A loop around the use that starts inside that span contributes its
|
||||
* whole body, since its back edge reaches the use again.
|
||||
*
|
||||
* A binding initialized or assigned from a read whose narrowing may be
|
||||
* stale inherits that narrowing (`const held: Expr = current` keeps the arm
|
||||
* `current` was typed with), so its narrowed reads are never trusted.
|
||||
*
|
||||
* TypeScript never carries a property narrowing into a nested function,
|
||||
* and every narrowing (an assignment, test, predicate or assertion of the
|
||||
* reference) is an occurrence of the reference, so a narrowed use always
|
||||
@@ -42,6 +47,7 @@ export class NarrowingStability {
|
||||
private readonly verdicts = new Map<ts.Node, boolean>();
|
||||
private readonly effects = new Map<ts.Node, boolean>();
|
||||
private readonly capturedMutable = new Map<ts.Symbol, boolean>();
|
||||
private readonly staleBindings = new Map<ts.Symbol, boolean>();
|
||||
private readonly identifierWrites = new Map<ts.SourceFile, Map<string, ts.Identifier[]>>();
|
||||
private readonly widened = new Map<ts.Node, ts.Type | null>();
|
||||
private readonly plainContainers = new Map<ts.Node, boolean>();
|
||||
@@ -90,7 +96,7 @@ export class NarrowingStability {
|
||||
}
|
||||
if (ts.isIdentifier(node)) {
|
||||
const symbol = this.bindingOf(node);
|
||||
if (!symbol || !this.isCapturedMutable(symbol)) return false;
|
||||
if (!symbol || !this.mayHoldStaleNarrowing(symbol)) return false;
|
||||
return !this.trusted(node);
|
||||
}
|
||||
return false;
|
||||
@@ -113,7 +119,8 @@ export class NarrowingStability {
|
||||
declared = this.declaredPropertyType(node);
|
||||
} else {
|
||||
const symbol = this.bindingOf(node);
|
||||
declared = symbol && this.isCapturedMutable(symbol) ? checker.getTypeOfSymbol(symbol) : null;
|
||||
declared =
|
||||
symbol && this.mayHoldStaleNarrowing(symbol) ? checker.getTypeOfSymbol(symbol) : null;
|
||||
}
|
||||
if (declared === null) return null;
|
||||
const narrowedTs = checker.getTypeAtLocation(node);
|
||||
@@ -245,6 +252,10 @@ export class NarrowingStability {
|
||||
)
|
||||
return false;
|
||||
}
|
||||
if (ts.isIdentifier(chain.root)) {
|
||||
const root = this.bindingOf(chain.root);
|
||||
if (root !== null && this.inheritsStaleNarrowing(root)) return false;
|
||||
}
|
||||
const rootStable =
|
||||
chain.root.kind === ts.SyntaxKind.ThisKeyword ||
|
||||
(ts.isIdentifier(chain.root) && !this.rootCapturedMutable(chain.root));
|
||||
@@ -346,6 +357,63 @@ export class NarrowingStability {
|
||||
return !program.wildcard && !program.mutable.has(name) && !program.accessors.has(name);
|
||||
}
|
||||
|
||||
/** A binding whose narrowed reads may observe a narrowing that no longer
|
||||
* holds: another function assigns it, or it inherits a stale narrowing. */
|
||||
private mayHoldStaleNarrowing(symbol: ts.Symbol): boolean {
|
||||
return this.isCapturedMutable(symbol) || this.inheritsStaleNarrowing(symbol);
|
||||
}
|
||||
|
||||
/** The binding is initialized or assigned from a narrowed read that may be
|
||||
* stale, so TypeScript's narrowing of the binding may be stale too. */
|
||||
private inheritsStaleNarrowing(symbol: ts.Symbol): boolean {
|
||||
const cached = this.staleBindings.get(symbol);
|
||||
if (cached !== undefined) return cached;
|
||||
// A cycle of bindings assigned from each other contributes nothing.
|
||||
this.staleBindings.set(symbol, false);
|
||||
let result = false;
|
||||
try {
|
||||
const decl = this.lowerer.checker.valueDeclarationOf(symbol);
|
||||
if (decl !== undefined && ts.isVariableDeclaration(decl)) {
|
||||
if (decl.initializer !== undefined && this.mayBeStale(decl.initializer)) result = true;
|
||||
else {
|
||||
for (const write of this.writesIn(decl.getSourceFile()).get(symbol.name) ?? []) {
|
||||
const assignment = write.parent;
|
||||
if (
|
||||
ts.isBinaryExpression(assignment) &&
|
||||
assignment.left === write &&
|
||||
assignment.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
this.lowerer.resolveValueSymbol(write) === symbol &&
|
||||
this.mayBeStale(assignment.right)
|
||||
) {
|
||||
result = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.staleBindings.set(symbol, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** A reference TypeScript narrows below its declared type where the
|
||||
* narrowing may no longer hold. */
|
||||
private mayBeStale(expression: ts.Expression): boolean {
|
||||
let e = expression;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isIdentifier(e) && !ts.isPropertyAccessExpression(e)) return false;
|
||||
const checker = this.lowerer.checker;
|
||||
let declared: ts.Type | null;
|
||||
if (ts.isPropertyAccessExpression(e)) declared = this.declaredPropertyType(e);
|
||||
else {
|
||||
const symbol = this.bindingOf(e);
|
||||
declared = symbol === null ? null : checker.getTypeOfSymbol(symbol);
|
||||
}
|
||||
if (declared === null || checker.getTypeAtLocation(e) === declared) return false;
|
||||
return !this.trusted(e);
|
||||
}
|
||||
|
||||
/** A `let`/`var`/parameter binding assigned from a function other than
|
||||
* the one that declares it: any call may change it. */
|
||||
private isCapturedMutable(symbol: ts.Symbol): boolean {
|
||||
|
||||
@@ -53,6 +53,8 @@ function classes(source: string): {
|
||||
: null,
|
||||
declarationOf: (symbol) => declarations.get(symbol),
|
||||
recordShape: (node) => {
|
||||
// Field slots: a binding's field read where a record is expected.
|
||||
if (ts.isPropertyAccessExpression(node)) return "R";
|
||||
const declaration = node.parent;
|
||||
if (!declaration) return null;
|
||||
if (ts.isParameter(declaration)) return annotated(declaration.type);
|
||||
@@ -183,3 +185,19 @@ test("assignments join their target and a foreign value opens it", () => {
|
||||
expect(flows.closed("target")).toBe(true);
|
||||
expect(flows.closed("foreign")).toBe(false);
|
||||
});
|
||||
|
||||
test("a binding's record field joins the slots it reaches and widens with its owner", () => {
|
||||
const flows = classes(`
|
||||
function fill(into: R): void { into.v = 1; }
|
||||
const state = { parsed: { v: 0 } };
|
||||
fill(state.parsed);
|
||||
function mark(m: R): void { m.v = 2; }
|
||||
const spread = { parsed: { v: 0 } };
|
||||
mark(spread.parsed);
|
||||
console.log({ ...spread });
|
||||
`);
|
||||
expect(flows.closed("into")).toBe(true);
|
||||
expect(flows.closed("state")).toBe(true);
|
||||
// A spread copies the field out of its owner, so the record keeps its layout.
|
||||
expect(flows.closed("m")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -85,6 +85,20 @@ export class RecordFlowClasses {
|
||||
/** Names written as shorthand properties (`{ p }`): the checker resolves
|
||||
* those to the property, so a binding of that name stays open. */
|
||||
private readonly shorthandNames = new Set<string>();
|
||||
/** Record fields of tracked bindings that flow between slots
|
||||
* (`fill(args, state.parsed)`), by owner symbol and field name. A field
|
||||
* slot widens only with its owner, whose layout holds it. */
|
||||
private readonly fieldSlots = new Map<ts.Symbol, Map<string, number>>();
|
||||
/** The owner binding slot of each field slot. */
|
||||
private readonly fieldOwners = new Map<number, number>();
|
||||
/** Object literals entering each slot; their property values enter the
|
||||
* class's field slots. */
|
||||
private readonly literalSources: { slot: number; literal: ts.ObjectLiteralExpression }[] = [];
|
||||
/** Slots whose field values are copied out whole (a spread, a
|
||||
* destructuring, `Object.values`), so a nested record would convert. */
|
||||
private readonly copiedOut = new Set<number>();
|
||||
/** Counts merges and openings, so field joins run until stable. */
|
||||
private changes = 0;
|
||||
|
||||
constructor(
|
||||
private readonly files: readonly ts.SourceFile[],
|
||||
@@ -94,6 +108,7 @@ export class RecordFlowClasses {
|
||||
this.collectFunctions();
|
||||
this.collectSlots();
|
||||
this.connect();
|
||||
this.connectFields();
|
||||
}
|
||||
|
||||
/** The class id of a binding or parameter slot, or null when the symbol
|
||||
@@ -160,9 +175,27 @@ export class RecordFlowClasses {
|
||||
shape = own;
|
||||
}
|
||||
this.shapeChecked.set(root, uniform);
|
||||
// A field slot takes a widened layout only with its owner's class.
|
||||
for (const [slot, owner] of this.fieldOwners) {
|
||||
if (!uniform) break;
|
||||
if (this.find(slot) === root && !this.closed(owner)) uniform = false;
|
||||
}
|
||||
this.shapeChecked.set(root, uniform);
|
||||
return uniform;
|
||||
}
|
||||
|
||||
/** Every field slot: its owner's class, the field name and its class. */
|
||||
fields(): { owner: number; name: string; classId: number }[] {
|
||||
const out: { owner: number; name: string; classId: number }[] = [];
|
||||
for (const byName of this.fieldSlots.values()) {
|
||||
for (const [name, slot] of byName) {
|
||||
const owner = this.fieldOwners.get(slot)!;
|
||||
out.push({ owner: this.find(owner), name, classId: this.find(slot) });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private newSlot(node: ts.Node, isReturn = false): number {
|
||||
const id = this.parent.length;
|
||||
this.parent.push(id);
|
||||
@@ -189,10 +222,109 @@ export class RecordFlowClasses {
|
||||
if (ra === rb) return;
|
||||
this.parent[rb] = ra;
|
||||
this.open[ra] = this.open[ra]! || this.open[rb]!;
|
||||
this.changes++;
|
||||
}
|
||||
|
||||
private markOpen(slot: number): void {
|
||||
this.open[this.find(slot)] = true;
|
||||
const root = this.find(slot);
|
||||
if (!this.open[root]) this.changes++;
|
||||
this.open[root] = true;
|
||||
}
|
||||
|
||||
/** The slot of a tracked binding's record field, created on first use. */
|
||||
private fieldSlot(owner: ts.Symbol, name: string, node: ts.Node): number | undefined {
|
||||
const ownerSlot = this.bindingSlots.get(owner);
|
||||
if (ownerSlot === undefined) return undefined;
|
||||
let byName = this.fieldSlots.get(owner);
|
||||
if (!byName) {
|
||||
byName = new Map();
|
||||
this.fieldSlots.set(owner, byName);
|
||||
}
|
||||
let slot = byName.get(name);
|
||||
if (slot === undefined) {
|
||||
slot = this.newSlot(node);
|
||||
byName.set(name, slot);
|
||||
this.fieldOwners.set(slot, ownerSlot);
|
||||
this.changes++;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
/** `binding.name` where the binding is a tracked slot. */
|
||||
private fieldAccess(node: ts.Node): { owner: ts.Symbol; name: string } | null {
|
||||
if (!ts.isPropertyAccessExpression(node) || !ts.isIdentifier(node.expression)) return null;
|
||||
if (!ts.isIdentifier(node.name)) return null;
|
||||
const owner = this.host.symbolOf(node.expression);
|
||||
return owner && this.bindingSlots.has(owner) ? { owner, name: node.name.text } : null;
|
||||
}
|
||||
|
||||
/** Joins each field slot with the same field of every member of its
|
||||
* owner's class, gives it the values that literals and assignments store
|
||||
* in the field, and opens it with its owner, until no class changes. */
|
||||
private connectFields(): void {
|
||||
const visitedUses = new Set<string>();
|
||||
const visitedLiterals = new Set<string>();
|
||||
for (let before = -1; before !== this.changes;) {
|
||||
before = this.changes;
|
||||
// Grouped once per pass; joins made during a pass show in the next.
|
||||
const members = new Map<number, ts.Symbol[]>();
|
||||
for (const [symbol, slot] of this.bindingSlots) addTo(members, this.find(slot), symbol);
|
||||
const literals = new Map<number, number[]>();
|
||||
this.literalSources.forEach(({ slot }, i) => addTo(literals, this.find(slot), i));
|
||||
const copied = new Set([...this.copiedOut].map((slot) => this.find(slot)));
|
||||
for (const [symbol, byName] of [...this.fieldSlots]) {
|
||||
const root = this.find(this.bindingSlots.get(symbol)!);
|
||||
for (const [name, field] of [...byName]) {
|
||||
for (const member of members.get(root) ?? []) {
|
||||
const peer = this.fieldSlot(member, name, this.slotNodes[field]!)!;
|
||||
this.union(field, peer);
|
||||
const key = `${this.bindingSlots.get(member)}:${name}`;
|
||||
if (visitedUses.has(key)) continue;
|
||||
visitedUses.add(key);
|
||||
for (const reference of this.referencesOf(member)) this.fieldUse(reference, name, peer);
|
||||
}
|
||||
for (const i of literals.get(root) ?? []) {
|
||||
const key = `${i}:${name}`;
|
||||
if (visitedLiterals.has(key)) continue;
|
||||
visitedLiterals.add(key);
|
||||
for (const property of this.literalSources[i]!.literal.properties) {
|
||||
if (ts.isSpreadAssignment(property)) {
|
||||
this.markOpen(field);
|
||||
continue;
|
||||
}
|
||||
const propertyName = property.name;
|
||||
if (
|
||||
!propertyName ||
|
||||
!(ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName)) ||
|
||||
propertyName.text !== name
|
||||
)
|
||||
continue;
|
||||
if (ts.isPropertyAssignment(property)) this.source(property.initializer, field);
|
||||
else if (ts.isShorthandPropertyAssignment(property))
|
||||
this.source(property.name as ts.Identifier, field);
|
||||
else this.markOpen(field);
|
||||
}
|
||||
}
|
||||
if (this.open[this.find(root)] || copied.has(root)) this.markOpen(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks one reference to a field slot's owner: a read of the field
|
||||
* (`owner.name`, `owner["name"]`) must be a member use, and a computed
|
||||
* key can reach the field too. */
|
||||
private fieldUse(reference: ts.Identifier, name: string, field: number): void {
|
||||
const access = reference.parent;
|
||||
if (!access || (access as { expression?: ts.Node }).expression !== reference) return;
|
||||
if (ts.isPropertyAccessExpression(access)) {
|
||||
if (access.name.text === name && !this.use(access, field)) this.markOpen(field);
|
||||
return;
|
||||
}
|
||||
if (!ts.isElementAccessExpression(access)) return;
|
||||
const key = peel(access.argumentExpression);
|
||||
if (!ts.isStringLiteral(key) && !ts.isNoSubstitutionTemplateLiteral(key)) this.markOpen(field);
|
||||
else if (key.text === name && !this.use(access, field)) this.markOpen(field);
|
||||
}
|
||||
|
||||
private index(): void {
|
||||
@@ -473,7 +605,15 @@ export class RecordFlowClasses {
|
||||
* when the value can be an object some other storage already holds. */
|
||||
private source(node: ts.Expression, slot: number): void {
|
||||
const e = peel(node);
|
||||
if (ts.isObjectLiteralExpression(e)) return;
|
||||
if (ts.isObjectLiteralExpression(e)) {
|
||||
this.literalSources.push({ slot, literal: e });
|
||||
return;
|
||||
}
|
||||
const access = this.fieldAccess(e);
|
||||
if (access) {
|
||||
this.union(slot, this.fieldSlot(access.owner, access.name, e)!);
|
||||
return;
|
||||
}
|
||||
if (ts.isIdentifier(e)) {
|
||||
const symbol = this.host.symbolOf(e);
|
||||
const other = symbol ? this.bindingSlots.get(symbol) : undefined;
|
||||
@@ -519,7 +659,7 @@ export class RecordFlowClasses {
|
||||
/** True when this reference to a member only reads or writes its fields,
|
||||
* passes the record to another member (a flow `source` joins), or
|
||||
* inspects it without keeping or converting it. */
|
||||
private use(reference: ts.Identifier, slot: number): boolean {
|
||||
private use(reference: ts.Expression, slot: number): boolean {
|
||||
let value: ts.Node = reference;
|
||||
for (;;) {
|
||||
const user = value.parent;
|
||||
@@ -536,7 +676,10 @@ export class RecordFlowClasses {
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(user) && user.expression === value) return true;
|
||||
if (ts.isElementAccessExpression(user) && user.expression === value) return true;
|
||||
if (ts.isSpreadAssignment(user)) return true;
|
||||
if (ts.isSpreadAssignment(user)) {
|
||||
this.copiedOut.add(slot);
|
||||
return true;
|
||||
}
|
||||
if (ts.isTemplateSpan(user) || ts.isTypeOfExpression(user)) return true;
|
||||
if (ts.isExpressionStatement(user)) return true;
|
||||
if (ts.isPrefixUnaryExpression(user) && user.operator === ts.SyntaxKind.ExclamationToken)
|
||||
@@ -588,7 +731,8 @@ export class RecordFlowClasses {
|
||||
op === ts.SyntaxKind.AmpersandAmpersandEqualsToken;
|
||||
// A record assigned to a member joins it when the member's own
|
||||
// reference is visited, below.
|
||||
if (assigns && user.right === value) return this.isMember(user.left);
|
||||
if (assigns && user.right === value)
|
||||
return this.isMember(user.left) || this.isFieldMember(user.left);
|
||||
if (assigns && user.left === value && value === reference) {
|
||||
this.source(user.right, slot);
|
||||
return true;
|
||||
@@ -597,7 +741,10 @@ export class RecordFlowClasses {
|
||||
return false;
|
||||
}
|
||||
if (ts.isVariableDeclaration(user) && user.initializer === value) {
|
||||
if (ts.isObjectBindingPattern(user.name)) return true;
|
||||
if (ts.isObjectBindingPattern(user.name)) {
|
||||
this.copiedOut.add(slot);
|
||||
return true;
|
||||
}
|
||||
return this.isMember(user.name);
|
||||
}
|
||||
if (ts.isReturnStatement(user)) {
|
||||
@@ -629,11 +776,13 @@ export class RecordFlowClasses {
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) {
|
||||
const methods = READING_CALLS.get(callee.expression.text);
|
||||
return (
|
||||
const reads =
|
||||
!!methods &&
|
||||
methods.has(callee.name.text) &&
|
||||
this.host.isGlobal(callee.expression, callee.expression.text)
|
||||
);
|
||||
this.host.isGlobal(callee.expression, callee.expression.text);
|
||||
if (reads && (callee.name.text === "values" || callee.name.text === "entries"))
|
||||
this.copiedOut.add(slot);
|
||||
return reads;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -646,6 +795,19 @@ export class RecordFlowClasses {
|
||||
const symbol = this.host.symbolOf(node);
|
||||
return !!symbol && this.bindingSlots.has(symbol);
|
||||
}
|
||||
|
||||
/** A tracked binding's field as an assignment target: the stored record
|
||||
* joins the field slot when the field's uses are visited. */
|
||||
private isFieldMember(node: ts.Node): boolean {
|
||||
const access = this.fieldAccess(node);
|
||||
return !!access && this.fieldSlot(access.owner, access.name, node) !== undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function addTo<T>(map: Map<number, T[]>, key: number, value: T): void {
|
||||
const list = map.get(key);
|
||||
if (list) list.push(value);
|
||||
else map.set(key, [value]);
|
||||
}
|
||||
|
||||
/** The return expressions of a function body, excluding nested functions. */
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
import { BOOL, F64, STRING, type IrExpr, type IrType, type SrcLoc } from "../../ir/ir.js";
|
||||
import type * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
isUnitType,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { numLit, varRef } from "../../ir/build.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
|
||||
@@ -66,3 +77,77 @@ export function lowerOptionalStringIndex(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A number-keyed read of a receiver the checker narrowed to string whose
|
||||
* stored value is still a union: an array-derived binding keeps its
|
||||
* undefined arm (`const t = tokens[i]`), and a `typeof t === "string"`
|
||||
* guard narrows the checker's type without retagging the storage. Both
|
||||
* operands are captured first, as for a plain string receiver. A unit arm
|
||||
* performs the checked-dynamic keyed read on that unit, which throws Node's
|
||||
* "Cannot read properties of undefined (reading '<key>')"; another value
|
||||
* arm, which the checker has excluded, fails the checked extraction; the
|
||||
* string arm reads like any string receiver. Null when the union has no
|
||||
* string arm. */
|
||||
export function lowerUnionStringIndex(
|
||||
lowerer: Lowerer,
|
||||
receiver: IrExpr,
|
||||
index: IrExpr,
|
||||
keyNode: ts.Node,
|
||||
type: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (receiver.type.kind !== "union") return null;
|
||||
const unionType = receiver.type;
|
||||
const unionId = unionType.unionId;
|
||||
const def = lowerer.unions.get(unionId);
|
||||
const stringTag = lowerer.armTag(unionId, STRING);
|
||||
if (!def || stringTag < 0) return null;
|
||||
const receiverLocal = lowerer.declareHiddenLocal("%indexedUnion", unionType);
|
||||
const indexLocal = lowerer.declareHiddenLocal("%unionStringIndex", F64);
|
||||
const stored = (): IrExpr => varRef(receiverLocal.id, unionType, loc);
|
||||
const key = (): IrExpr => varRef(indexLocal.id, F64, loc);
|
||||
const stmts: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: receiverLocal.id, init: receiver, loc },
|
||||
{ kind: "varDecl", localId: indexLocal.id, init: index, loc },
|
||||
];
|
||||
let text: IrExpr;
|
||||
if (def.arms.every((arm, tag) => tag === stringTag || isUnitType(arm))) {
|
||||
text = { kind: "unionNarrow", unionId, tag: stringTag, value: stored(), type: STRING, loc };
|
||||
} else {
|
||||
const helper = lowerer.narrowedArmHelper(unionId, STRING, loc);
|
||||
if (helper === null) return null;
|
||||
text = lowerer.checkedNarrowCall(helper, stored(), STRING, loc);
|
||||
}
|
||||
let result = lowerOptionalStringIndex(lowerer, text, key(), type, loc);
|
||||
for (let tag = def.arms.length - 1; tag >= 0; tag--) {
|
||||
const arm = def.arms[tag]!;
|
||||
if (!isUnitType(arm)) continue;
|
||||
const unit = arm.kind === "nullT" ? "null" : "undefined";
|
||||
const thrown: IrExpr = {
|
||||
kind: "dynCheck",
|
||||
value: {
|
||||
kind: "dynKeyGet",
|
||||
value: {
|
||||
kind: "dynFrom",
|
||||
value: { kind: "unitLit", unit, type: arm, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
key: lowerer.ensureString(key(), keyNode),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: stored(), type: BOOL, loc },
|
||||
then: lowerer.coerceToExpected(thrown, type),
|
||||
else_: result,
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "seqExpr", stmts, result, type, loc };
|
||||
}
|
||||
|
||||
@@ -2060,8 +2060,13 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
|
||||
crypto: {
|
||||
// The composed randomBytes(n).toString(enc) chain keeps its one-libCall
|
||||
// lowering (lowerCryptoComposedCall), while the callback overload has
|
||||
// its own adapter spelling. Both remain the randomBytes surface.
|
||||
randomBytes: ["crypto.randomBytesToString", "crypto.randomBytesCb"],
|
||||
// its own adapter spelling. Both remain the randomBytes surface, as does
|
||||
// the util.promisify form.
|
||||
randomBytes: [
|
||||
"crypto.randomBytesToString",
|
||||
"crypto.randomBytesCb",
|
||||
"crypto.randomBytesPromise",
|
||||
],
|
||||
// Omitting size fills from offset through the rest of the Buffer and
|
||||
// uses a separate runtime entry point for Node's exact bounds checks.
|
||||
// Views of every kind return themselves; other buffers take the Dyn form.
|
||||
@@ -2071,9 +2076,13 @@ export const BUILTIN_MODULE_FN_ALIASES: Record<
|
||||
"crypto.randomFillViewRest",
|
||||
"crypto.randomFillSyncDyn",
|
||||
],
|
||||
randomFill: ["crypto.randomFillCbDyn"],
|
||||
randomFill: [
|
||||
"crypto.randomFillCbDyn",
|
||||
"crypto.randomFillPromise",
|
||||
"crypto.randomFillPromiseDyn",
|
||||
],
|
||||
timingSafeEqual: ["crypto.timingSafeEqualDyn"],
|
||||
randomInt: ["crypto.randomIntMax", "crypto.randomIntCb"],
|
||||
randomInt: ["crypto.randomIntMax", "crypto.randomIntCb", "crypto.randomIntPromise"],
|
||||
randomUUID: ["crypto.randomUUIDOpts"],
|
||||
},
|
||||
os: {
|
||||
@@ -2891,7 +2900,7 @@ export const BUILTIN_MODULE_FENCE_HINTS: Record<
|
||||
},
|
||||
util: {
|
||||
promisify:
|
||||
"the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots and the fs callback functions with an fs/promises counterpart; call the bound function directly",
|
||||
"the lowered shapes are const bindings over child_process.execFile, child_process.exec, setTimeout, setImmediate, stream.finished, stream.pipeline, the zlib callback one-shots, the crypto callback functions, dns.lookup, dns.lookupService and the fs callback functions with an fs/promises counterpart; call the bound function directly",
|
||||
},
|
||||
"stream/consumers": {
|
||||
blob:
|
||||
@@ -3054,8 +3063,8 @@ export function stdlibMemberFence(lowerer: Lowerer, access: ts.PropertyAccessExp
|
||||
hint =
|
||||
"'.groups' lowers when the match's regex is STATICALLY known — a regex literal, or a " +
|
||||
"const initialized with one (the group-name table is built at compile time) — and the " +
|
||||
"read is not an optional-chain step: narrow the match instead (if (m) { m.groups } or " +
|
||||
"m!.groups)";
|
||||
"match comes from exec, match, a for-of over matchAll, or the element parameter of an " +
|
||||
"array-method callback over matchAll rows";
|
||||
} else if (recvIr?.kind === "array" && member === "flat") {
|
||||
hint =
|
||||
"flat has no lowering (flatMap does) — flatten into an accumulator instead: " +
|
||||
|
||||
@@ -15,7 +15,21 @@ export function isPrunedNpmReexport(program: ts.Program, stmt: ts.ExportDeclarat
|
||||
return program.analysis.prunedNpmReexports.has(stmt);
|
||||
}
|
||||
|
||||
function packageJson(path: string): Record<string, unknown> | null {
|
||||
type PackageJson = Record<string, unknown> | null;
|
||||
|
||||
/** Parsed package.json files by path, kept for one plan: every purity check
|
||||
* reads the metadata of its file's package again. */
|
||||
type PackageJsonCache = Map<string, PackageJson>;
|
||||
|
||||
function packageJson(path: string, cache: PackageJsonCache): PackageJson {
|
||||
const cached = cache.get(path);
|
||||
if (cached !== undefined) return cached;
|
||||
const json = readPackageJson(path);
|
||||
cache.set(path, json);
|
||||
return json;
|
||||
}
|
||||
|
||||
function readPackageJson(path: string): PackageJson {
|
||||
const source = trackedReadFile(path);
|
||||
if (source === null) return null;
|
||||
try {
|
||||
@@ -58,12 +72,16 @@ function sideEffectPattern(pattern: string): RegExp {
|
||||
/** A nested format scope such as dist/esm/package.json is not the package's
|
||||
* published metadata. Stay inside the resolved package while finding its
|
||||
* named root, including pnpm's realpath and workspace-linked installs. */
|
||||
function packageRootJsonPath(fromFile: string, packageName: string): string | null {
|
||||
function packageRootJsonPath(
|
||||
fromFile: string,
|
||||
packageName: string,
|
||||
cache: PackageJsonCache,
|
||||
): string | null {
|
||||
let dir = dirname(resolve(fromFile));
|
||||
let root: string | null = null;
|
||||
while (npmPackageNameOf(dir.replaceAll("\\", "/")) === packageName) {
|
||||
const path = join(dir, "package.json");
|
||||
if (packageJson(path)?.["name"] === packageName) root = path;
|
||||
if (packageJson(path, cache)?.["name"] === packageName) root = path;
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
@@ -73,9 +91,13 @@ function packageRootJsonPath(fromFile: string, packageName: string): string | nu
|
||||
|
||||
// An external package's executable sources may not be in this program.
|
||||
// Preserve the conservative whole-dependency promise in that case.
|
||||
function pureExternalPackage(path: string, visiting = new Set<string>()): boolean {
|
||||
function pureExternalPackage(
|
||||
path: string,
|
||||
cache: PackageJsonCache,
|
||||
visiting = new Set<string>(),
|
||||
): boolean {
|
||||
if (visiting.has(path)) return true;
|
||||
const json = packageJson(path);
|
||||
const json = packageJson(path, cache);
|
||||
const value = json?.["sideEffects"];
|
||||
if (json === null || !(value === false || (Array.isArray(value) && value.length === 0)))
|
||||
return false;
|
||||
@@ -85,8 +107,8 @@ function pureExternalPackage(path: string, visiting = new Set<string>()): boolea
|
||||
if (!dependencies || typeof dependencies !== "object" || Array.isArray(dependencies)) continue;
|
||||
for (const name of Object.keys(dependencies)) {
|
||||
const resolved = resolveBareModule(path, name, "js-only");
|
||||
const root = resolved === null ? null : packageRootJsonPath(resolved.typesFile, name);
|
||||
if (root === null || !pureExternalPackage(root, visiting)) return false;
|
||||
const root = resolved === null ? null : packageRootJsonPath(resolved.typesFile, name, cache);
|
||||
if (root === null || !pureExternalPackage(root, cache, visiting)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -135,10 +157,11 @@ export function planNpmStaticReexports(
|
||||
for (const path of extraRoots) request(program.getSourceFile(path) ?? null, null);
|
||||
|
||||
const pureMemo = new Map<ts.SourceFile, boolean>();
|
||||
const packageJsons: PackageJsonCache = new Map();
|
||||
const filePure = (sf: ts.SourceFile): boolean => {
|
||||
const pkg = npmPackageNameOf(sf.fileName) ?? npmStaticPackageOfPath(sf.fileName);
|
||||
const path = pkg === null ? null : packageRootJsonPath(sf.fileName, pkg);
|
||||
const json = path === null ? null : packageJson(path);
|
||||
const path = pkg === null ? null : packageRootJsonPath(sf.fileName, pkg, packageJsons);
|
||||
const json = path === null ? null : packageJson(path, packageJsons);
|
||||
const value = json?.["sideEffects"];
|
||||
if (value === false) return true;
|
||||
if (
|
||||
@@ -195,8 +218,10 @@ export function planNpmStaticReexports(
|
||||
const resolved = resolveBareModule(sf.fileName, edge.spec, "js-only");
|
||||
const pkg = resolved === null ? null : npmPackageNameOf(resolved.typesFile);
|
||||
const root =
|
||||
pkg === null || resolved === null ? null : packageRootJsonPath(resolved.typesFile, pkg);
|
||||
return root !== null && pureExternalPackage(root);
|
||||
pkg === null || resolved === null
|
||||
? null
|
||||
: packageRootJsonPath(resolved.typesFile, pkg, packageJsons);
|
||||
return root !== null && pureExternalPackage(root, packageJsons);
|
||||
});
|
||||
};
|
||||
const pure = visit(root);
|
||||
|
||||
@@ -193,6 +193,10 @@ export function npmStaticTransformPkgJson(pkg: Record<string, unknown>): void {
|
||||
for (const value of Object.values(obj)) transform(value);
|
||||
};
|
||||
if (pkg["exports"] !== undefined) transform(pkg["exports"]);
|
||||
// The "imports" field maps "#" specifiers with the same condition objects
|
||||
// (chalk 5's `"#supports-color": { node, default }`), so Node's "node"
|
||||
// target hoists there too.
|
||||
if (pkg["imports"] !== undefined) transform(pkg["imports"]);
|
||||
}
|
||||
|
||||
/** npmStaticTransformPkgJson over a package.json TEXT (the fs shadow's
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
setProjectPathMappings,
|
||||
setProjectRealm,
|
||||
} from "./resolve.js";
|
||||
import { setNpmStaticPackages } from "./npm-static.js";
|
||||
|
||||
const fixturesRoot = join(import.meta.dirname, "../../../..", "tests/fixtures");
|
||||
|
||||
@@ -60,3 +61,56 @@ test("source-only workspace packages are project modules while JavaScript packag
|
||||
clearResolveCaches();
|
||||
}
|
||||
});
|
||||
|
||||
test("package imports inside a --npm-static package answer the shipped JavaScript", async () => {
|
||||
// chalk 5's shape: a declaration twin beside every runtime file, and a
|
||||
// `{ node, default }` imports entry whose node target is what Node runs.
|
||||
const dir = realpathSync(await mkdtemp(join(tmpdir(), "scriptc-resolve-imports-")));
|
||||
try {
|
||||
const pkg = join(dir, "node_modules", "colors");
|
||||
const vendor = join(pkg, "source", "vendor");
|
||||
await mkdir(join(vendor, "styles"), { recursive: true });
|
||||
await mkdir(join(vendor, "detect"), { recursive: true });
|
||||
await Promise.all([
|
||||
writeFile(
|
||||
join(pkg, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "colors",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
exports: "./source/index.js",
|
||||
imports: {
|
||||
"#styles": "./source/vendor/styles/index.js",
|
||||
"#detect": {
|
||||
node: "./source/vendor/detect/index.js",
|
||||
default: "./source/vendor/detect/browser.js",
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
writeFile(join(pkg, "source", "index.js"), 'import styles from "#styles";\n'),
|
||||
writeFile(join(pkg, "source", "index.d.ts"), "export {};\n"),
|
||||
writeFile(join(vendor, "styles", "index.js"), "export default {};\n"),
|
||||
writeFile(join(vendor, "styles", "index.d.ts"), "export {};\n"),
|
||||
writeFile(join(vendor, "detect", "index.js"), "export default 1;\n"),
|
||||
writeFile(join(vendor, "detect", "index.d.ts"), "export {};\n"),
|
||||
writeFile(join(vendor, "detect", "browser.js"), "export default 0;\n"),
|
||||
writeFile(join(vendor, "detect", "browser.d.ts"), "export {};\n"),
|
||||
]);
|
||||
const from = join(pkg, "source", "index.js");
|
||||
|
||||
clearResolveCaches();
|
||||
setNpmStaticPackages([]);
|
||||
expect(resolveProjectModule(from, "#styles")).toBe(join(vendor, "styles", "index.d.ts"));
|
||||
|
||||
clearResolveCaches();
|
||||
setNpmStaticPackages(["colors"]);
|
||||
expect(resolveProjectModule(from, "#styles")).toBe(join(vendor, "styles", "index.js"));
|
||||
expect(resolveProjectModule(from, "#detect")).toBe(join(vendor, "detect", "index.js"));
|
||||
expect(resolveProjectModule(from, "#missing")).toBeNull();
|
||||
} finally {
|
||||
setNpmStaticPackages([]);
|
||||
clearResolveCaches();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -617,6 +617,18 @@ export function resolveProjectImport(fromFile: string, specifier: string): strin
|
||||
// Node's validity rule: only the bare "#" is never resolvable ("#/..."
|
||||
// matches "#/*"-pattern keys on current Node).
|
||||
if (specifier === "#") return null;
|
||||
// Inside an opted-in --npm-static package, an imports entry resolves
|
||||
// the way the tsgo host's fs shadow does: the transformed document
|
||||
// (the "node" condition hoisted), the runtime JS conditions, and the
|
||||
// shipped JS rather than a declaration twin (see resolveRelativeModule).
|
||||
if (npmStaticPackageOfPath(resolve(fromFile)) !== null) {
|
||||
const transformed = JSON.parse(JSON.stringify(pkg)) as PkgJson & Record<string, unknown>;
|
||||
npmStaticTransformPkgJson(transformed);
|
||||
const jsTarget = resolvePackageImports(transformed.imports, specifier, JS_ONLY_CONDITIONS);
|
||||
if (jsTarget === null || !jsTarget.startsWith("./")) return null;
|
||||
const jsPath = join(pkgDir, jsTarget);
|
||||
return normalizeResolvedAnswer(loadAsJsFile(jsPath) ?? loadAsJsDirectory(jsPath));
|
||||
}
|
||||
target = resolvePackageImports(pkg.imports, specifier, EXPORT_CONDITIONS);
|
||||
// An imports target that is not "./"-relative is PACKAGE_RESOLVE'd as
|
||||
// a bare specifier (Node's PACKAGE_IMPORTS_EXPORTS_RESOLVE): the
|
||||
|
||||
@@ -0,0 +1,629 @@
|
||||
/* Tier assignment, report-only (two-tier design, stage S2).
|
||||
*
|
||||
* The design assigns every binding, field, function instance, class and
|
||||
* module a tier. Static is today's compiler; dynamic code operates on
|
||||
* dynamic values. Demotion from static to dynamic is monotone and is seeded
|
||||
* by `any`, untyped JavaScript, types and operations without a static
|
||||
* lowering, reflection a static layout cannot answer, and dynamic modules
|
||||
* (DESIGN.md §6).
|
||||
*
|
||||
* Stage S2 computes what WOULD be demoted without demoting anything:
|
||||
*
|
||||
* 1. Existing dynamic code. Functions whose bodies already use the `dyn`
|
||||
* vocabulary are the dynamic tier today (ir/tiers.ts assigns them). The
|
||||
* source statements whose lowering performs a dynamic operation are the
|
||||
* coverage report's statementsDynamic.
|
||||
* 2. Demotion requests. Every refusal or runtime fence the lowerer commits
|
||||
* (today's poison sites: the SC1xxx/SC2xxx families) and every refused
|
||||
* import or package becomes a request, attributed to the unit whose
|
||||
* static representation could not express the construct. The lowerer
|
||||
* still refuses or fences exactly as before; the requests are recorded
|
||||
* from the committed diagnostics, so speculative probes that roll back
|
||||
* record nothing. Under `--tier=static-only` every request stays today's
|
||||
* refusal; stage S6 turns requests into demotions in the default mode.
|
||||
* 3. Demoted units. A request demotes its unit, and the function instance
|
||||
* containing the site turns dynamic (it would hold dynamic operations,
|
||||
* invariant I1). Units are deduplicated; each keeps its requests, which
|
||||
* is the chain `--explain-dynamic` prints.
|
||||
*
|
||||
* Attribution is syntactic and approximate: the site's innermost node, the
|
||||
* operand it acts on, and that operand's declaration. Stage S6 replaces it
|
||||
* with requests the failing lowering names exactly. */
|
||||
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
||||
import type { IrFunction, SrcLoc } from "../ir/ir.js";
|
||||
import { dynamicOperationSites, type DynamicFunction } from "../ir/tiers.js";
|
||||
import * as ts from "./ts7/adapter.js";
|
||||
|
||||
/** `--tier`: "auto" (the default) will demote; "static-only" keeps every
|
||||
* demotion request a refusal (design invariant I2). Identical in S2. */
|
||||
export type TierMode = "auto" | "static-only";
|
||||
|
||||
export const TIER_MODES: readonly TierMode[] = ["auto", "static-only"];
|
||||
|
||||
export type DemotionUnitKind = "binding" | "field" | "function" | "class" | "module";
|
||||
|
||||
export interface DemotionUnit {
|
||||
kind: DemotionUnitKind;
|
||||
/** Display name: `x`, `Point.x`, `parse`, `Parser.next`, `Point`, or the
|
||||
* module's file path. */
|
||||
name: string;
|
||||
/** Bindings: the function declaring a local, absent for module scope. */
|
||||
owner?: string;
|
||||
/** The unit's declaration (the site itself when none is known). */
|
||||
loc: SrcLoc;
|
||||
}
|
||||
|
||||
/** Why a unit is demoted (DESIGN.md §6.2): a type with no static
|
||||
* representation, an operation with no static lowering, reflection on a
|
||||
* class, a use of a binding whose declaration was refused, a module or
|
||||
* package that cannot be static, or a construct excluded by policy (no
|
||||
* demotion: it stays a refusal). */
|
||||
export type DemotionSeed = "type" | "operation" | "reflection" | "cascade" | "module" | "excluded";
|
||||
|
||||
export type DemotionScope = "reached" | "deferred" | "unreached";
|
||||
|
||||
export interface DemotionRequest {
|
||||
unit: DemotionUnit;
|
||||
/** The function instance (or module top level) containing the site. */
|
||||
enclosing: DemotionUnit;
|
||||
seed: DemotionSeed;
|
||||
scope: DemotionScope;
|
||||
code: string;
|
||||
message: string;
|
||||
loc: SrcLoc;
|
||||
}
|
||||
|
||||
export interface DemotedUnit extends DemotionUnit {
|
||||
/** Requests naming this unit directly. */
|
||||
requests: number;
|
||||
/** Requests whose sites this function instance or module contains. */
|
||||
sites: number;
|
||||
}
|
||||
|
||||
export interface TierDynamicFunction extends DynamicFunction {
|
||||
/** Source statements with a dynamic operation in this function. */
|
||||
statements: number;
|
||||
}
|
||||
|
||||
export interface TierReport {
|
||||
mode: TierMode;
|
||||
/** Reached source statements that perform a dynamic operation. */
|
||||
statementsDynamic: number;
|
||||
dynamicFunctions: TierDynamicFunction[];
|
||||
requests: DemotionRequest[];
|
||||
/** Units the requests would demote (reached and deferred scopes),
|
||||
* deduplicated, by kind then source order. */
|
||||
units: DemotedUnit[];
|
||||
}
|
||||
|
||||
/* ── statements of dynamic functions ─────────────────────────────────── */
|
||||
|
||||
function isClassLike(node: ts.Node): boolean {
|
||||
return ts.isClassDeclaration(node) || ts.isClassExpression(node);
|
||||
}
|
||||
|
||||
/** The innermost node spanning exactly or containing [start, end). */
|
||||
function nodeAt(sf: ts.SourceFile, start: number, end: number): ts.Node {
|
||||
let node: ts.Node = sf;
|
||||
for (;;) {
|
||||
let next: ts.Node | undefined;
|
||||
node.forEachChild((child) => {
|
||||
if (child.getStart(sf) <= start && end <= child.getEnd()) {
|
||||
next = child;
|
||||
return true;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
if (next === undefined) return node;
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
|
||||
/** The source statement an IR location belongs to: the innermost
|
||||
* statement containing it, or an arrow function's expression body. Null
|
||||
* for synthetic locations. */
|
||||
function statementAt(program: ts.Program, loc: SrcLoc): ts.Node | null {
|
||||
if (loc.start === 0 && loc.end === 0) return null;
|
||||
const sf = program.getSourceFile(loc.file);
|
||||
if (sf === undefined) return null;
|
||||
let node: ts.Node | undefined = nodeAt(sf, loc.start, loc.end);
|
||||
while (node !== undefined && !ts.isSourceFile(node)) {
|
||||
const parent: ts.Node | undefined = node.parent;
|
||||
if (parent !== undefined && ts.isArrowFunction(parent) && parent.body === node) return node;
|
||||
if (ts.isStatement(node) && !ts.isBlock(node)) return node;
|
||||
node = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Dynamic statements are the source statements whose lowering performs a
|
||||
* dynamic operation (in the function itself or in a generated helper it
|
||||
* calls). They count once however many functions and operations they
|
||||
* involve. A function's tier is the unit of I1, but static operations
|
||||
* inside a dynamic function stay static, so the statement is the honest
|
||||
* measure of how much code is dynamic. */
|
||||
function dynamicFunctionReports(
|
||||
program: ts.Program,
|
||||
bodies: readonly IrFunction[],
|
||||
): { functions: TierDynamicFunction[]; statements: number } {
|
||||
const byLoc = new Map<string, ts.Node | null>();
|
||||
const all = new Set<ts.Node>();
|
||||
const functions: TierDynamicFunction[] = [];
|
||||
for (const fn of bodies) {
|
||||
const sites = dynamicOperationSites(fn);
|
||||
if (sites.length === 0) continue;
|
||||
const own = new Set<ts.Node>();
|
||||
for (const site of sites) {
|
||||
const key = `${site.loc.file}\0${site.loc.start}\0${site.loc.end}`;
|
||||
let statement = byLoc.get(key);
|
||||
if (statement === undefined) {
|
||||
statement = statementAt(program, site.loc);
|
||||
byLoc.set(key, statement);
|
||||
}
|
||||
if (statement === null) continue;
|
||||
own.add(statement);
|
||||
all.add(statement);
|
||||
}
|
||||
functions.push({
|
||||
name: fn.name,
|
||||
loc: fn.loc,
|
||||
operation: sites[0]!.operation,
|
||||
operationLoc: sites[0]!.loc,
|
||||
statements: own.size,
|
||||
});
|
||||
}
|
||||
return { functions, statements: all.size };
|
||||
}
|
||||
|
||||
/* ── demotion requests ───────────────────────────────────────────────── */
|
||||
|
||||
const TYPE_CODES = new Set([
|
||||
"SC2001",
|
||||
"SC2002",
|
||||
"SC2003",
|
||||
"SC2005",
|
||||
"SC2006",
|
||||
"SC2007",
|
||||
"SC2008",
|
||||
"SC2009",
|
||||
"SC2011",
|
||||
"SC1063",
|
||||
"SC1100",
|
||||
"SC1101",
|
||||
]);
|
||||
const MODULE_CODES = new Set([
|
||||
"SC1010",
|
||||
"SC1012",
|
||||
"SC1013",
|
||||
"SC1014",
|
||||
"SC1015",
|
||||
"SC1016",
|
||||
"SC2013",
|
||||
]);
|
||||
/** Runtime code generation (`eval`, `new Function`): excluded by policy,
|
||||
* since the dynamic tier compiles ahead of time (DESIGN.md §12 class X). */
|
||||
const EXCLUDED_CODES = new Set(["SC2010"]);
|
||||
const EXCLUDED_SURFACE = /^'(?:new Function|Function|eval)'/;
|
||||
|
||||
/** Refusals and fences that name a static-tier gap. Type errors,
|
||||
* environment and target refusals, divergences and internal errors are
|
||||
* not demotion requests. */
|
||||
export function isDemotionCode(code: string): boolean {
|
||||
return /^SC[12]\d{3}$/.test(code);
|
||||
}
|
||||
|
||||
function isFunctionLikeOrFile(node: ts.Node): boolean {
|
||||
return (
|
||||
ts.isSourceFile(node) ||
|
||||
ts.isFunctionLikeDeclaration(node) ||
|
||||
ts.isClassStaticBlockDeclaration(node)
|
||||
);
|
||||
}
|
||||
|
||||
function nameText(name: ts.Node | undefined): string | null {
|
||||
if (name === undefined) return null;
|
||||
if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name)) return name.text;
|
||||
if (ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
||||
return null;
|
||||
}
|
||||
|
||||
function className(node: ts.Node | undefined): string {
|
||||
if (node === undefined) return "<anonymous class>";
|
||||
if ((ts.isClassDeclaration(node) || ts.isClassExpression(node)) && node.name !== undefined)
|
||||
return node.name.text;
|
||||
const parent = node.parent;
|
||||
if (parent !== undefined && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name))
|
||||
return parent.name.text;
|
||||
return "<anonymous class>";
|
||||
}
|
||||
|
||||
/** The display name of a function-like node, as a user would look it up. */
|
||||
function functionName(node: ts.Node): string {
|
||||
if (ts.isSourceFile(node)) return "<module top level>";
|
||||
if (ts.isClassStaticBlockDeclaration(node)) return `${className(node.parent)}.<static block>`;
|
||||
if (ts.isConstructorDeclaration(node)) return `${className(node.parent)}.constructor`;
|
||||
const own = ts.isFunctionLikeDeclaration(node) ? nameText(node.name) : null;
|
||||
const parent = node.parent;
|
||||
if (parent !== undefined && isClassLike(parent) && own !== null)
|
||||
return `${className(parent)}.${own}`;
|
||||
if (own !== null) return own;
|
||||
if (parent !== undefined) {
|
||||
if (ts.isVariableDeclaration(parent) || ts.isPropertyAssignment(parent)) {
|
||||
const bound = nameText(parent.name);
|
||||
if (bound !== null) return bound;
|
||||
}
|
||||
if (
|
||||
ts.isPropertyDeclaration(parent) &&
|
||||
parent.parent !== undefined &&
|
||||
isClassLike(parent.parent)
|
||||
) {
|
||||
const field = nameText(parent.name);
|
||||
if (field !== null) return `${className(parent.parent)}.${field}`;
|
||||
}
|
||||
if (ts.isBinaryExpression(parent) && parent.right === node) {
|
||||
const left = parent.left;
|
||||
if (ts.isIdentifier(left)) return left.text;
|
||||
if (ts.isPropertyAccessExpression(left)) return left.getText();
|
||||
}
|
||||
}
|
||||
const sf = node.getSourceFile();
|
||||
const pos = ts.getLineAndCharacterOfPosition(sf, node.getStart(sf));
|
||||
return `<anonymous ${pos.line + 1}:${pos.character + 1}>`;
|
||||
}
|
||||
|
||||
function locOfNode(node: ts.Node): SrcLoc {
|
||||
const sf = node.getSourceFile();
|
||||
return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() };
|
||||
}
|
||||
|
||||
function enclosingFunction(node: ts.Node): ts.Node {
|
||||
let current: ts.Node | undefined = node.parent;
|
||||
while (current !== undefined && !isFunctionLikeOrFile(current)) current = current.parent;
|
||||
return current ?? node.getSourceFile();
|
||||
}
|
||||
|
||||
function functionUnit(node: ts.Node, instance: string | null): DemotionUnit {
|
||||
// Top-level code runs in the module's initializer function.
|
||||
if (ts.isSourceFile(node))
|
||||
return {
|
||||
kind: "function",
|
||||
name: "<top level>",
|
||||
loc: { file: node.fileName, start: 0, end: 0 },
|
||||
};
|
||||
const name = functionName(node);
|
||||
return {
|
||||
kind: "function",
|
||||
name: instance === null ? name : `${name} ${instance}`,
|
||||
loc: locOfNode(node),
|
||||
};
|
||||
}
|
||||
|
||||
/** Generic instances append their instantiation context to diagnostics
|
||||
* (Lowerer.pushDiag: ` (instantiating 'f' with <T>)`); the instance, not
|
||||
* the source function, is the unit. Returns the type-argument text. */
|
||||
function instanceOf(message: string): string | null {
|
||||
const at = message.lastIndexOf(" (instantiating ");
|
||||
if (at < 0 || !message.endsWith(")")) return null;
|
||||
const context = message.slice(at + 2, -1);
|
||||
const withAt = context.indexOf(" with ");
|
||||
return withAt < 0 ? context : context.slice(withAt + 6);
|
||||
}
|
||||
|
||||
/** Strip wrappers and walk receivers down to the value an operation acts
|
||||
* on: `a.b.c(…)` → `a`, `(x as T)[k]` → `x`. Records whether the chain
|
||||
* passes through `.prototype`. */
|
||||
function rootOf(expr: ts.Node): { root: ts.Node; viaPrototype: boolean } {
|
||||
let node = expr;
|
||||
let viaPrototype = false;
|
||||
for (;;) {
|
||||
if (
|
||||
ts.isParenthesizedExpression(node) ||
|
||||
ts.isAsExpression(node) ||
|
||||
ts.isNonNullExpression(node) ||
|
||||
ts.isAssertionExpression(node) ||
|
||||
ts.isSatisfiesExpression(node) ||
|
||||
ts.isSpreadElement(node) ||
|
||||
ts.isAwaitExpression(node)
|
||||
) {
|
||||
node = node.expression!;
|
||||
} else if (ts.isPropertyAccessExpression(node)) {
|
||||
if (node.name.text === "prototype") viaPrototype = true;
|
||||
node = node.expression;
|
||||
} else if (ts.isElementAccessExpression(node) || ts.isCallExpression(node)) {
|
||||
node = node.expression;
|
||||
} else if (
|
||||
ts.isDeleteExpression(node) ||
|
||||
ts.isTypeOfExpression(node) ||
|
||||
ts.isVoidExpression(node)
|
||||
) {
|
||||
node = node.expression;
|
||||
} else {
|
||||
return { root: node, viaPrototype };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const REFLECTION_NAMESPACES = new Set(["Object", "Reflect"]);
|
||||
|
||||
/** The expression an operation at `node` acts on. Reflection calls
|
||||
* (`Object.defineProperty(x, …)`, `Reflect.set(x, …)`) act on their first
|
||||
* argument; `new C()` and `x instanceof C` on the class value. */
|
||||
function subjectOf(node: ts.Node): ts.Node {
|
||||
let current = node;
|
||||
// A refused callee or member name: the whole call or access is the site.
|
||||
const parent = current.parent;
|
||||
if (parent !== undefined && ts.isPropertyAccessExpression(parent) && parent.name === current)
|
||||
current = parent;
|
||||
if (ts.isCallExpression(current)) {
|
||||
const callee = current.expression;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(callee) &&
|
||||
ts.isIdentifier(callee.expression) &&
|
||||
REFLECTION_NAMESPACES.has(callee.expression.text) &&
|
||||
current.arguments.length > 0
|
||||
) {
|
||||
return current.arguments[0]!;
|
||||
}
|
||||
return callee;
|
||||
}
|
||||
if (ts.isNewExpression(current)) return current.expression;
|
||||
if (ts.isBinaryExpression(current)) {
|
||||
if (current.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) return current.right;
|
||||
if (current.operatorToken.kind === ts.SyntaxKind.EqualsToken) return current.left;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/** The declaration owning a type-refused node: the binding, parameter,
|
||||
* field or function whose declared type has no static representation. */
|
||||
function typeOwnerOf(node: ts.Node): ts.Node | null {
|
||||
let current: ts.Node | undefined = node;
|
||||
while (current !== undefined && !ts.isSourceFile(current)) {
|
||||
if (
|
||||
ts.isVariableDeclaration(current) ||
|
||||
ts.isParameter(current) ||
|
||||
ts.isBindingElement(current) ||
|
||||
ts.isPropertyDeclaration(current) ||
|
||||
ts.isPropertySignature(current) ||
|
||||
ts.isCatchClause(current)
|
||||
) {
|
||||
return current;
|
||||
}
|
||||
if (ts.isFunctionLikeDeclaration(current) || isClassLike(current)) return current;
|
||||
if (ts.isStatement(current)) return null;
|
||||
current = current.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isModuleObjectName(node: ts.Node): boolean {
|
||||
return ts.isIdentifier(node) && (node.text === "exports" || node.text === "module");
|
||||
}
|
||||
|
||||
class UnitResolver {
|
||||
constructor(private readonly checker: ts.TypeChecker) {}
|
||||
|
||||
/** The unit a declaration node denotes. */
|
||||
declarationUnit(decl: ts.Node): DemotionUnit | null {
|
||||
if (isClassLike(decl)) return { kind: "class", name: className(decl), loc: locOfNode(decl) };
|
||||
if (ts.isFunctionLikeDeclaration(decl)) return functionUnit(decl, null);
|
||||
if (ts.isPropertyDeclaration(decl) || ts.isPropertySignature(decl)) {
|
||||
const field = nameText(decl.name) ?? "<computed>";
|
||||
const owner = decl.parent;
|
||||
const ownerName = owner !== undefined && isClassLike(owner) ? className(owner) : null;
|
||||
return {
|
||||
kind: ownerName === null ? "binding" : "field",
|
||||
name: ownerName === null ? field : `${ownerName}.${field}`,
|
||||
loc: locOfNode(decl),
|
||||
};
|
||||
}
|
||||
if (ts.isCatchClause(decl)) {
|
||||
const binding = decl.variableDeclaration;
|
||||
if (binding === undefined) return null;
|
||||
return this.declarationUnit(binding);
|
||||
}
|
||||
if (ts.isVariableDeclaration(decl) || ts.isParameter(decl) || ts.isBindingElement(decl)) {
|
||||
const name = nameText(decl.name) ?? decl.name?.getText() ?? "<binding>";
|
||||
const scope = enclosingFunction(decl);
|
||||
const unit: DemotionUnit = { kind: "binding", name, loc: locOfNode(decl) };
|
||||
if (ts.isParameter(decl) && ts.isFunctionLikeDeclaration(decl.parent))
|
||||
unit.owner = functionName(decl.parent);
|
||||
else if (!ts.isSourceFile(scope)) unit.owner = functionName(scope);
|
||||
return unit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The unit an operand expression denotes. */
|
||||
valueUnit(expr: ts.Node): { unit: DemotionUnit; reflection: boolean } | null {
|
||||
const { root, viaPrototype } = rootOf(expr);
|
||||
const sf = root.getSourceFile();
|
||||
if (root.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
const owner = enclosingFunction(root);
|
||||
if (ts.isSourceFile(owner))
|
||||
return {
|
||||
unit: { kind: "module", name: sf.fileName, loc: locOfNode(sf) },
|
||||
reflection: false,
|
||||
};
|
||||
const cls = owner.parent;
|
||||
if (cls !== undefined && isClassLike(cls)) {
|
||||
// `this.f` names a field; `this` itself is the instance.
|
||||
const access = root.parent;
|
||||
if (
|
||||
access !== undefined &&
|
||||
ts.isPropertyAccessExpression(access) &&
|
||||
access.expression === root
|
||||
) {
|
||||
return {
|
||||
unit: {
|
||||
kind: "field",
|
||||
name: `${className(cls)}.${access.name.text}`,
|
||||
loc: locOfNode(access),
|
||||
},
|
||||
reflection: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
unit: { kind: "class", name: className(cls), loc: locOfNode(cls) },
|
||||
reflection: viaPrototype,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!ts.isIdentifier(root)) return null;
|
||||
let symbol = this.checker.getSymbolAtLocation(root);
|
||||
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
symbol = this.checker.getAliasedSymbol(symbol);
|
||||
const decls = symbol === undefined ? [] : this.checker.declarationsOf(symbol);
|
||||
const decl = decls.find((d) => !d.getSourceFile().isDeclarationFile);
|
||||
if (decl === undefined) {
|
||||
// CommonJS `exports`/`module` as values: the module object.
|
||||
if (isModuleObjectName(root))
|
||||
return {
|
||||
unit: { kind: "module", name: sf.fileName, loc: locOfNode(sf) },
|
||||
reflection: false,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
if (ts.isSourceFile(decl))
|
||||
return {
|
||||
unit: { kind: "module", name: decl.fileName, loc: locOfNode(decl) },
|
||||
reflection: false,
|
||||
};
|
||||
const unit = this.declarationUnit(decl);
|
||||
if (unit === null) return null;
|
||||
// `C.prototype.m = …`, `Object.defineProperty(C.prototype, …)`: the
|
||||
// class (or the constructor function's prototype object).
|
||||
if (viaPrototype && (unit.kind === "class" || unit.kind === "function"))
|
||||
return { unit: { ...unit, kind: "class" }, reflection: true };
|
||||
return { unit, reflection: unit.kind === "class" };
|
||||
}
|
||||
}
|
||||
|
||||
function seedOf(code: string, message: string): DemotionSeed {
|
||||
if (EXCLUDED_CODES.has(code) || EXCLUDED_SURFACE.test(message)) return "excluded";
|
||||
if (code === "SC2004") return "cascade";
|
||||
if (MODULE_CODES.has(code)) return "module";
|
||||
if (TYPE_CODES.has(code)) return "type";
|
||||
return "operation";
|
||||
}
|
||||
|
||||
/** One request for a committed refusal or fence diagnostic. */
|
||||
function requestFor(
|
||||
program: ts.Program,
|
||||
resolver: UnitResolver,
|
||||
diag: ScrDiagnostic,
|
||||
scope: DemotionScope,
|
||||
): DemotionRequest | null {
|
||||
if (!isDemotionCode(diag.code)) return null;
|
||||
let seed = seedOf(diag.code, diag.message);
|
||||
const base = { code: diag.code, message: diag.message, loc: diag.loc, scope };
|
||||
const sf = program.getSourceFile(diag.loc.file);
|
||||
if (sf === undefined) {
|
||||
const unit: DemotionUnit = { kind: "module", name: diag.loc.file, loc: diag.loc };
|
||||
return { ...base, unit, enclosing: unit, seed };
|
||||
}
|
||||
const moduleUnit: DemotionUnit = { kind: "module", name: sf.fileName, loc: locOfNode(sf) };
|
||||
const node = nodeAt(sf, diag.loc.start, diag.loc.end);
|
||||
const instance = instanceOf(diag.message);
|
||||
const owner = ts.isSourceFile(node) ? sf : enclosingFunction(node);
|
||||
const enclosing = functionUnit(owner, instance);
|
||||
if (seed === "module" || ts.isSourceFile(node))
|
||||
return { ...base, unit: moduleUnit, enclosing, seed };
|
||||
let unit: DemotionUnit | null = null;
|
||||
if (seed === "type") {
|
||||
const decl = typeOwnerOf(node);
|
||||
if (decl !== null) unit = resolver.declarationUnit(decl);
|
||||
}
|
||||
if (unit === null) {
|
||||
const value = resolver.valueUnit(subjectOf(node));
|
||||
if (value !== null) {
|
||||
unit = value.unit;
|
||||
if (value.reflection && seed === "operation") seed = "reflection";
|
||||
}
|
||||
}
|
||||
return { ...base, unit: unit ?? enclosing, enclosing, seed };
|
||||
}
|
||||
|
||||
function unitKey(unit: DemotionUnit): string {
|
||||
return `${unit.kind}\0${unit.name}\0${unit.owner ?? ""}\0${unit.loc.file}\0${unit.loc.start}`;
|
||||
}
|
||||
|
||||
const KIND_ORDER: readonly DemotionUnitKind[] = ["module", "class", "field", "function", "binding"];
|
||||
|
||||
function demotedUnits(requests: readonly DemotionRequest[]): DemotedUnit[] {
|
||||
const units = new Map<string, DemotedUnit>();
|
||||
const entry = (unit: DemotionUnit): DemotedUnit => {
|
||||
const key = unitKey(unit);
|
||||
let found = units.get(key);
|
||||
if (found === undefined) units.set(key, (found = { ...unit, requests: 0, sites: 0 }));
|
||||
return found;
|
||||
};
|
||||
for (const request of requests) {
|
||||
if (request.scope === "unreached" || request.seed === "excluded") continue;
|
||||
entry(request.unit).requests++;
|
||||
entry(request.enclosing).sites++;
|
||||
}
|
||||
return [...units.values()].sort(
|
||||
(a, b) =>
|
||||
KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind) ||
|
||||
a.loc.file.localeCompare(b.loc.file) ||
|
||||
a.loc.start - b.loc.start ||
|
||||
a.name.localeCompare(b.name),
|
||||
);
|
||||
}
|
||||
|
||||
export interface TierAnalysisInput {
|
||||
mode: TierMode;
|
||||
/** The dynamic-tier IR functions of the reached program. */
|
||||
dynamicFunctions: readonly IrFunction[];
|
||||
reached: readonly ScrDiagnostic[];
|
||||
deferred: readonly ScrDiagnostic[];
|
||||
unreached: readonly ScrDiagnostic[];
|
||||
}
|
||||
|
||||
/** Coverage's statementsDynamic (see dynamicFunctionReports). */
|
||||
export function dynamicStatementCount(
|
||||
program: ts.Program,
|
||||
dynamicFunctions: readonly IrFunction[],
|
||||
): number {
|
||||
return dynamicFunctionReports(program, dynamicFunctions).statements;
|
||||
}
|
||||
|
||||
export function analyzeTiers(program: ts.Program, input: TierAnalysisInput): TierReport {
|
||||
const resolver = new UnitResolver(program.getTypeChecker());
|
||||
const requests: DemotionRequest[] = [];
|
||||
const add = (diags: readonly ScrDiagnostic[], scope: DemotionScope): void => {
|
||||
for (const diag of diags) {
|
||||
let request: DemotionRequest | null;
|
||||
try {
|
||||
request = requestFor(program, resolver, diag, scope);
|
||||
} catch {
|
||||
// Attribution is best effort; a report never fails an analysis.
|
||||
if (!isDemotionCode(diag.code)) continue;
|
||||
const unit: DemotionUnit = { kind: "module", name: diag.loc.file, loc: diag.loc };
|
||||
request = {
|
||||
unit,
|
||||
enclosing: unit,
|
||||
seed: seedOf(diag.code, diag.message),
|
||||
scope,
|
||||
code: diag.code,
|
||||
message: diag.message,
|
||||
loc: diag.loc,
|
||||
};
|
||||
}
|
||||
if (request !== null) requests.push(request);
|
||||
}
|
||||
};
|
||||
add(input.reached, "reached");
|
||||
add(input.deferred, "deferred");
|
||||
add(input.unreached, "unreached");
|
||||
const dynamic = dynamicFunctionReports(program, input.dynamicFunctions);
|
||||
return {
|
||||
mode: input.mode,
|
||||
statementsDynamic: dynamic.statements,
|
||||
dynamicFunctions: dynamic.functions,
|
||||
requests,
|
||||
units: demotedUnits(requests),
|
||||
};
|
||||
}
|
||||
@@ -996,6 +996,10 @@ export type TypeParamTsResolver = (t: ts.Type) => ts.Type | null;
|
||||
* one new field here, not a new parameter at every call site. */
|
||||
export interface TypeMapperCtx {
|
||||
checker: ts.TypeChecker;
|
||||
/** JavaScript declarations whose instance or object-literal types only
|
||||
* the checked-dynamic representation holds (js-dynamic-objects.ts).
|
||||
* Absent when the program has none: every mapping is unchanged. */
|
||||
jsDynamicDeclaration?: (decl: ts.Node) => boolean;
|
||||
/** Set while mapping a generic callable member's erased type
|
||||
* (erasedGenericMemberType): the signature's own type parameters
|
||||
* resolve to `unknown` instead of refusing the mapping. */
|
||||
@@ -1368,6 +1372,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const intrinsic = type.getSymbol();
|
||||
if (intrinsic?.name === "globalThis" && checker.declarationsOf(intrinsic).length === 0)
|
||||
return DYN;
|
||||
if (ctx.jsDynamicDeclaration !== undefined && intrinsic !== undefined) {
|
||||
const objectLiteral = (intrinsic.flags & ts.SymbolFlags.ObjectLiteral) !== 0;
|
||||
const classInstance =
|
||||
(intrinsic.flags & ts.SymbolFlags.Class) !== 0 && type.isClassOrInterface();
|
||||
if (
|
||||
(objectLiteral || classInstance) &&
|
||||
checker.declarationsOf(intrinsic).some((decl) => ctx.jsDynamicDeclaration!(decl))
|
||||
)
|
||||
return DYN;
|
||||
}
|
||||
{
|
||||
const moduleId = ctx.moduleNamespaceId?.(type) ?? null;
|
||||
if (moduleId !== null) return { kind: "moduleNs", moduleId };
|
||||
@@ -2952,11 +2966,13 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
return { kind: "dgramSocket" };
|
||||
}
|
||||
// node:test's TestContext — the test-body parameter (`test('x', (t) =>
|
||||
// ...)`). @types/node's `class TestContext` and the fallback
|
||||
// declarations' interface both live inside `declare module "node:test"`
|
||||
// (isDeclaredInAmbientModule answers for both spellings).
|
||||
// ...)`) — and SuiteContext, the describe-body and suite-hook parameter:
|
||||
// both are the runner's node for the test or suite. @types/node's
|
||||
// classes and the fallback declarations' interfaces all live inside
|
||||
// `declare module "node:test"` (isDeclaredInAmbientModule answers for
|
||||
// both spellings).
|
||||
if (
|
||||
psym?.name === "TestContext" &&
|
||||
(psym?.name === "TestContext" || psym?.name === "SuiteContext") &&
|
||||
checker
|
||||
.declarationsOf(psym)
|
||||
.some(
|
||||
@@ -3560,12 +3576,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
),
|
||||
};
|
||||
}
|
||||
// The promisified exec/execFile result `{ stdout: string; stderr: string }`:
|
||||
// an anonymous type literal (@types/node's __promisify__ overloads in
|
||||
// child_process, the shipped fallback's promisify overloads in util),
|
||||
// so it maps by provenance and exact shape, interned as the
|
||||
// %execFileAsync/%execAsync helpers build it. Node builds the object as
|
||||
// { stdout, stderr }. The Buffer-typed literals do not match and stay
|
||||
// The promisified exec/execFile result `{ stdout: string; stderr: string }`
|
||||
// and the promisified dns.lookupService result `{ hostname: string;
|
||||
// service: string }`: anonymous type literals (@types/node's
|
||||
// __promisify__ overloads in child_process and dns, the shipped
|
||||
// fallback's promisify overloads in util), so they map by provenance and
|
||||
// exact shape. The exec result is interned as the %execFileAsync/
|
||||
// %execAsync helpers build it; Node builds { stdout, stderr } and
|
||||
// { hostname, service }. The Buffer-typed literals do not match and stay
|
||||
// unmapped.
|
||||
if (
|
||||
psym &&
|
||||
@@ -3575,21 +3593,38 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
(d) =>
|
||||
ts.isTypeLiteralNode(d) &&
|
||||
ctx.isStdlibFile(d.getSourceFile()) &&
|
||||
(isDeclaredInAmbientModule(d, "child_process") || isDeclaredInAmbientModule(d, "util")),
|
||||
(isDeclaredInAmbientModule(d, "child_process") ||
|
||||
isDeclaredInAmbientModule(d, "util") ||
|
||||
isDeclaredInAmbientModule(d, "dns")),
|
||||
)
|
||||
) {
|
||||
const props = checker.getPropertiesOfType(widened);
|
||||
if (
|
||||
const names =
|
||||
props.length === 2 &&
|
||||
props.every(
|
||||
(p) =>
|
||||
(p.name === "stdout" || p.name === "stderr") &&
|
||||
(p.flags & ts.SymbolFlags.Optional) === 0 &&
|
||||
(checker.getTypeOfSymbol(p).flags & ts.TypeFlags.String) !== 0,
|
||||
) &&
|
||||
props[0]!.name !== props[1]!.name
|
||||
) {
|
||||
return { kind: "record", shapeId: execResultShape(ctx.shapes) };
|
||||
)
|
||||
? props
|
||||
.map((p) => p.name)
|
||||
.sort()
|
||||
.join(",")
|
||||
: "";
|
||||
if (names === "stderr,stdout") return { kind: "record", shapeId: execResultShape(ctx.shapes) };
|
||||
if (names === "hostname,service") {
|
||||
return {
|
||||
kind: "record",
|
||||
shapeId: ctx.shapes.intern(
|
||||
[
|
||||
{ name: "hostname", type: STRING },
|
||||
{ name: "service", type: STRING },
|
||||
],
|
||||
false,
|
||||
undefined,
|
||||
["hostname", "service"],
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
// crypto.X509Certificate — the Dirent-style data record: THREE visible
|
||||
|
||||
@@ -172,11 +172,22 @@ export {
|
||||
} from "./diagnostics/render.js";
|
||||
export {
|
||||
renderCoverage,
|
||||
renderExplainDynamic,
|
||||
coverageEnvelope,
|
||||
coveragePasses,
|
||||
type CoverageInput,
|
||||
type CoverageFailOn,
|
||||
} from "./coverage/report.js";
|
||||
export {
|
||||
TIER_MODES,
|
||||
type DemotedUnit,
|
||||
type DemotionRequest,
|
||||
type DemotionSeed,
|
||||
type DemotionUnit,
|
||||
type DemotionUnitKind,
|
||||
type TierMode,
|
||||
type TierReport,
|
||||
} from "./frontend/tier-analysis.js";
|
||||
export {
|
||||
buildEnvelope,
|
||||
DIAGNOSTICS_SCHEMA_VERSION,
|
||||
@@ -208,6 +219,13 @@ export {
|
||||
} from "./compat/fetch-profile.js";
|
||||
export { LIB_FN_SIGS } from "./ir/builtin-signatures.js";
|
||||
export { validateModule } from "./ir/validate.js";
|
||||
export {
|
||||
assignLegacyTiers,
|
||||
dynamicOperationOf,
|
||||
functionTier,
|
||||
type DynamicFunction,
|
||||
type IrTier,
|
||||
} from "./ir/tiers.js";
|
||||
export {
|
||||
deserializeModule,
|
||||
deserializeModuleBytes,
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { findUnaliasedLocalArrays, preservesArrayLength } from "./array-length-stability.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
arrayOf,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
|
||||
const loc = { file: "array-length-stability.ts", start: 0, end: 0 };
|
||||
const ARR = arrayOf(F64);
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });
|
||||
const arr = (localId: string): IrExpr => ({ kind: "varRef", localId, type: ARR, loc });
|
||||
const method = (
|
||||
receiver: string,
|
||||
name: Extract<IrExpr, { kind: "arrIntrinsic" }>["method"],
|
||||
args: IrExpr[] = [],
|
||||
): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: name,
|
||||
receiver: arr(receiver),
|
||||
args,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const length = (receiver: string): IrExpr => method(receiver, "length");
|
||||
const read = (receiver: string, index: IrExpr): IrExpr => method(receiver, "getNumber", [index]);
|
||||
const plus = (left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left,
|
||||
right,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const stmt = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc });
|
||||
const set = (receiver: string, index: IrExpr, value: IrExpr): IrStmt => ({
|
||||
kind: "arraySet",
|
||||
arr: arr(receiver),
|
||||
index,
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
const cond: IrExpr = { kind: "bin", op: "<", left: ref("i"), right: length("a"), type: BOOL, loc };
|
||||
const sum: IrStmt = {
|
||||
kind: "assign",
|
||||
localId: "s",
|
||||
value: plus(ref("s"), read("a", ref("i"))),
|
||||
loc,
|
||||
};
|
||||
const locals = new Map<string, IrLocal>([
|
||||
["i", { id: "i", name: "i", type: F64, mutable: true }],
|
||||
["s", { id: "s", name: "s", type: F64, mutable: true }],
|
||||
["a", { id: "a", name: "a", type: ARR, mutable: false }],
|
||||
["b", { id: "b", name: "b", type: ARR, mutable: false }],
|
||||
["out", { id: "out", name: "out", type: ARR, mutable: false }],
|
||||
]);
|
||||
const preserves = (
|
||||
body: IrStmt[],
|
||||
inRange: string | null = "i",
|
||||
unaliased: ReadonlySet<string> = new Set(),
|
||||
scope = locals,
|
||||
): boolean => preservesArrayLength([cond, ...body], { receiver: "a", inRange }, scope, unaliased);
|
||||
|
||||
test("reads, local arithmetic and element writes at the in-range counter keep the length", () => {
|
||||
expect(preserves([sum])).toBe(true);
|
||||
expect(preserves([set("a", ref("i"), num(1)), set("b", ref("i"), read("a", ref("i")))])).toBe(
|
||||
true,
|
||||
);
|
||||
expect(preserves([stmt(method("a", "indexOf", [num(3)]))])).toBe(true);
|
||||
});
|
||||
|
||||
test("writes past the counter, without an in-range counter, or to the counter can grow it", () => {
|
||||
expect(preserves([set("a", plus(ref("i"), num(1)), num(1))])).toBe(false);
|
||||
expect(preserves([set("b", ref("s"), num(1))])).toBe(false);
|
||||
expect(preserves([set("a", ref("i"), num(1))], null)).toBe(false);
|
||||
expect(preserves([{ kind: "assign", localId: "i", value: num(0), loc }, sum])).toBe(false);
|
||||
});
|
||||
|
||||
test("resizing methods, length stores, calls and escapes refuse the proof", () => {
|
||||
for (const name of ["push", "pop", "shift", "unshift", "splice", "fill"] as const)
|
||||
expect(preserves([stmt(method("a", name, [num(1)]))])).toBe(false);
|
||||
// An unrelated array may alias the receiver.
|
||||
expect(preserves([stmt(method("b", "push", [num(1)]))])).toBe(false);
|
||||
expect(preserves([{ kind: "arraySetLength", arr: arr("a"), length: num(0), loc }])).toBe(false);
|
||||
expect(preserves([{ kind: "arrayDelete", arr: arr("a"), index: ref("i"), loc }])).toBe(false);
|
||||
expect(preserves([stmt({ kind: "call", callee: "f", args: [], type: F64, loc })])).toBe(false);
|
||||
expect(preserves([stmt({ kind: "call", callee: "f", args: [arr("a")], type: F64, loc })])).toBe(
|
||||
false,
|
||||
);
|
||||
expect(preserves([{ kind: "varDecl", localId: "b", init: arr("a"), loc }])).toBe(false);
|
||||
expect(preserves([{ kind: "assign", localId: "a", value: arr("b"), loc }])).toBe(false);
|
||||
});
|
||||
|
||||
test("an unaliased local array other than the receiver may grow", () => {
|
||||
const push = stmt(method("out", "push", [read("a", ref("i"))]));
|
||||
expect(preserves([push], "i", new Set(["out"]))).toBe(true);
|
||||
expect(preserves([set("out", ref("s"), num(1))], "i", new Set(["out"]))).toBe(true);
|
||||
expect(preserves([push])).toBe(false);
|
||||
// The receiver itself is never resized, even when no alias exists.
|
||||
expect(preserves([stmt(method("a", "push", [num(1)]))], "i", new Set(["a"]))).toBe(false);
|
||||
});
|
||||
|
||||
test("captured receivers have no local proof", () => {
|
||||
const boxed = new Map(locals);
|
||||
boxed.set("a", { ...locals.get("a")!, boxed: true });
|
||||
expect(preserves([sum], "i", new Set(), boxed)).toBe(false);
|
||||
});
|
||||
|
||||
function fn(body: IrStmt[], params: string[] = []): IrFunction {
|
||||
return {
|
||||
loc,
|
||||
name: "f",
|
||||
params: params.map((localId) => ({ localId, type: ARR })),
|
||||
returnType: ARR,
|
||||
locals: [...locals.values()],
|
||||
body,
|
||||
} as IrFunction;
|
||||
}
|
||||
const fresh: IrExpr = { kind: "arrayLit", elems: [], type: ARR, loc };
|
||||
|
||||
test("fresh local arrays read only as receivers have no alias", () => {
|
||||
const body: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: "out", init: fresh, loc },
|
||||
stmt(method("out", "push", [num(1)])),
|
||||
{ kind: "return", value: arr("out"), loc },
|
||||
];
|
||||
expect([...findUnaliasedLocalArrays(fn(body))]).toEqual(["out"]);
|
||||
// Parameters, copies into another binding, and arguments escape.
|
||||
expect(findUnaliasedLocalArrays(fn(body, ["out"])).size).toBe(0);
|
||||
const copied = [...body, { kind: "varDecl", localId: "b", init: arr("out"), loc } as IrStmt];
|
||||
expect(findUnaliasedLocalArrays(fn(copied)).has("out")).toBe(false);
|
||||
const passed = [...body, stmt({ kind: "call", callee: "g", args: [arr("out")], type: F64, loc })];
|
||||
expect(findUnaliasedLocalArrays(fn(passed)).has("out")).toBe(false);
|
||||
const rebound = [...body, { kind: "assign", localId: "out", value: arr("a"), loc } as IrStmt];
|
||||
expect(findUnaliasedLocalArrays(fn(rebound)).has("out")).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
import type { IrExpr, IrFunction, IrLocal, IrStmt } from "./ir.js";
|
||||
import { isBorrowSafeMath } from "./analysis.js";
|
||||
import { byteNumberAccess } from "./byte-numbers.js";
|
||||
import { everyExprChild, everyStmtChild } from "./traverse.js";
|
||||
|
||||
/** Array methods that read the receiver without changing its length or its
|
||||
* slot states and run no user code. */
|
||||
const READING_ARRAY_METHODS = new Set<string>([
|
||||
"length",
|
||||
"getNumber",
|
||||
"indexEq",
|
||||
"indexOf",
|
||||
"includes",
|
||||
"nextPresent",
|
||||
]);
|
||||
|
||||
/** String methods that read an immutable string and run no user code. */
|
||||
const READING_STRING_METHODS = new Set<string>(["length", "charCodeAt", "codePointAt"]);
|
||||
|
||||
/** Expressions that compute from their operands and run no user code. Their
|
||||
* children are checked separately. */
|
||||
const PLAIN_EXPRESSIONS = new Set<string>([
|
||||
"numLit",
|
||||
"strLit",
|
||||
"boolLit",
|
||||
"unitLit",
|
||||
"varRef",
|
||||
"bin",
|
||||
"unary",
|
||||
"incDec",
|
||||
"toBool",
|
||||
"logical",
|
||||
"ternary",
|
||||
"strEq",
|
||||
"strCmp",
|
||||
"fieldGet",
|
||||
"recordGet",
|
||||
"unionNarrow",
|
||||
"unionIsTag",
|
||||
"unionDisc",
|
||||
"unionWrap",
|
||||
"upcast",
|
||||
"downcast",
|
||||
"instanceOf",
|
||||
"nullish",
|
||||
"seqExpr",
|
||||
]);
|
||||
|
||||
/** Statements whose own effect is a local write or control flow inside the
|
||||
* loop. Their children are checked separately. */
|
||||
const PLAIN_STATEMENTS = new Set<string>([
|
||||
"varDecl",
|
||||
"assign",
|
||||
"exprStmt",
|
||||
"if",
|
||||
"block",
|
||||
"break",
|
||||
"continue",
|
||||
"for",
|
||||
"while",
|
||||
"doWhile",
|
||||
]);
|
||||
|
||||
/** Array methods whose result is never the receiver itself (unlike
|
||||
* `reverse`, `fill` or `copyWithin`, which return it). */
|
||||
const CONTAINED_RECEIVER_METHODS = new Set<string>([
|
||||
...READING_ARRAY_METHODS,
|
||||
"push",
|
||||
"pop",
|
||||
"shift",
|
||||
"unshift",
|
||||
"splice",
|
||||
"join",
|
||||
"slice",
|
||||
]);
|
||||
|
||||
/** Local arrays that no other binding can hold: every write of the local
|
||||
* stores a fresh array literal and every read of it is the receiver of an
|
||||
* element access or array method (or the value of a `return`, which ends the
|
||||
* activation). No alias of such an array exists, so mutating it cannot change
|
||||
* the length of an array held by any other binding. */
|
||||
export function findUnaliasedLocalArrays(fn: IrFunction): ReadonlySet<string> {
|
||||
const candidates = new Set<string>();
|
||||
const captured = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
for (const local of fn.locals)
|
||||
if (local.type.kind === "array" && !local.boxed && !local.tdz && !captured.has(local.id))
|
||||
candidates.add(local.id);
|
||||
for (const param of fn.params) candidates.delete(param.localId);
|
||||
const written = new Set<string>();
|
||||
const fresh = (value: IrExpr | null): boolean =>
|
||||
value !== null && (value.kind === "arrayLit" || value.kind === "arrayNewLen");
|
||||
const receiver = (e: IrExpr): void => {
|
||||
if (e.kind !== "varRef") expr(e);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
candidates.delete(e.localId);
|
||||
return true;
|
||||
case "assignExpr":
|
||||
candidates.delete(e.localId);
|
||||
break;
|
||||
case "arrIntrinsic":
|
||||
if (!CONTAINED_RECEIVER_METHODS.has(e.method)) break;
|
||||
receiver(e.receiver);
|
||||
e.args.forEach(expr);
|
||||
return true;
|
||||
case "arrayGet":
|
||||
case "arrayHas":
|
||||
case "arrayState":
|
||||
receiver(e.arr);
|
||||
expr(e.index);
|
||||
return true;
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
case "assign": {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
if (fresh(value)) written.add(s.localId);
|
||||
else if (value) candidates.delete(s.localId);
|
||||
if (value) expr(value);
|
||||
return true;
|
||||
}
|
||||
case "arraySet":
|
||||
receiver(s.arr);
|
||||
expr(s.index);
|
||||
expr(s.value);
|
||||
return true;
|
||||
case "return":
|
||||
if (s.value) receiver(s.value);
|
||||
return true;
|
||||
case "forOf":
|
||||
case "tryCatch":
|
||||
if (s.kind === "forOf") candidates.delete(s.localId);
|
||||
if (s.kind === "tryCatch" && s.catchLocalId) candidates.delete(s.catchLocalId);
|
||||
break;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
// A binding with no visible write takes its value some other way.
|
||||
for (const id of candidates) if (!written.has(id)) candidates.delete(id);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export interface ArrayLengthLoop {
|
||||
/** The local holding the array whose length must not change. */
|
||||
receiver: string;
|
||||
/** A binding that holds a value below the array's length wherever the
|
||||
* loop's condition, body or update reads it (the induction variable of
|
||||
* `for (let i = 0; i < a.length; i++)` when the body never writes it, as
|
||||
* long as the length itself has not changed). An element write at this
|
||||
* index replaces an existing element or sets a non-index property. */
|
||||
inRange: string | null;
|
||||
}
|
||||
|
||||
/** Whether running `nodes` (a loop's condition, body and update) can never
|
||||
* change the length of the array held by local `loop.receiver`. The proof is
|
||||
* syntactic and conservative: every expression and statement must be one
|
||||
* that runs no user code (no calls, callbacks, closures, iterators,
|
||||
* suspension or exception regions); the binding is never written and only
|
||||
* ever read as the receiver of an element access or a reading array method,
|
||||
* so the array cannot escape to code that resizes it; and nothing in the
|
||||
* nodes resizes any array that may be the same one. Element writes are
|
||||
* accepted at the in-range binding (no array grows by them: an alias of the
|
||||
* receiver has the same length) and anywhere in an unaliased local array.
|
||||
* Array methods that resize an array are accepted only on unaliased local
|
||||
* arrays other than the receiver. */
|
||||
export function preservesArrayLength(
|
||||
nodes: readonly (IrExpr | IrStmt)[],
|
||||
loop: ArrayLengthLoop,
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
unaliased: ReadonlySet<string>,
|
||||
): boolean {
|
||||
const local = locals.get(loop.receiver);
|
||||
if (!local || local.type.kind !== "array" || local.boxed || local.tdz) return false;
|
||||
const separate = (e: IrExpr): boolean =>
|
||||
e.kind === "varRef" && e.localId !== loop.receiver && unaliased.has(e.localId);
|
||||
// A receiver position: the receiver binding itself is read there without
|
||||
// escaping.
|
||||
const receiver = (e: IrExpr): boolean =>
|
||||
(e.kind === "varRef" && e.localId === loop.receiver) || expr(e);
|
||||
const written = (id: string): boolean => id === loop.receiver || id === loop.inRange;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
return e.localId !== loop.receiver;
|
||||
case "assignExpr":
|
||||
case "incDec":
|
||||
return !written(e.localId) && everyExprChild(e, expr, stmt);
|
||||
case "arrayGet":
|
||||
case "arrayHas":
|
||||
case "arrayState":
|
||||
return receiver(e.arr) && expr(e.index);
|
||||
case "arrIntrinsic":
|
||||
if (READING_ARRAY_METHODS.has(e.method)) return receiver(e.receiver) && e.args.every(expr);
|
||||
return separate(e.receiver) && e.args.every(expr);
|
||||
case "strIntrinsic":
|
||||
return (
|
||||
READING_STRING_METHODS.has(e.method) &&
|
||||
e.receiver.type.kind === "string" &&
|
||||
expr(e.receiver) &&
|
||||
e.args.every(expr)
|
||||
);
|
||||
case "bytesIntrinsic": {
|
||||
const numeric = byteNumberAccess(e);
|
||||
return (
|
||||
(numeric ? !numeric.write : e.method === "get" || e.method === "length") &&
|
||||
expr(e.receiver) &&
|
||||
e.args.every(expr)
|
||||
);
|
||||
}
|
||||
case "libCall":
|
||||
return (
|
||||
isBorrowSafeMath(e.fn) &&
|
||||
e.args.every((arg) => arg.type.kind === "f64") &&
|
||||
e.args.every(expr)
|
||||
);
|
||||
case "strConcat":
|
||||
return (
|
||||
e.left.type.kind === "string" &&
|
||||
e.right.type.kind === "string" &&
|
||||
expr(e.left) &&
|
||||
expr(e.right)
|
||||
);
|
||||
default:
|
||||
return PLAIN_EXPRESSIONS.has(e.kind) && everyExprChild(e, expr, stmt);
|
||||
}
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
if (written(s.localId)) return false;
|
||||
break;
|
||||
case "arraySet":
|
||||
return (
|
||||
(separate(s.arr) ||
|
||||
(s.index.kind === "varRef" &&
|
||||
loop.inRange !== null &&
|
||||
s.index.localId === loop.inRange)) &&
|
||||
receiver(s.arr) &&
|
||||
expr(s.index) &&
|
||||
expr(s.value)
|
||||
);
|
||||
case "return":
|
||||
return s.value === null || expr(s.value);
|
||||
default:
|
||||
if (!PLAIN_STATEMENTS.has(s.kind)) return false;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
return nodes.every((node) => ("type" in node ? expr(node as IrExpr) : stmt(node as IrStmt)));
|
||||
}
|
||||
@@ -754,8 +754,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// Node's argument checks (ERR_INVALID_ARG_TYPE/VALUE, ERR_SOCKET_BAD_PORT).
|
||||
"dns.lookupDyn",
|
||||
"dns.lookupService",
|
||||
"dns.lookupPromise",
|
||||
"dns.lookupServicePromise",
|
||||
"dns.promisesLookup",
|
||||
"dns.promisesLookupService",
|
||||
"dns.setDefaultResultOrder",
|
||||
"net.connect",
|
||||
"net.connectCb",
|
||||
|
||||
@@ -1410,8 +1410,8 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* port) over the callback forms; the promise fulfills with the dyn
|
||||
* { address, family } (array for all: true) / { hostname, service }.
|
||||
* Argument errors throw synchronously (may-throw). */
|
||||
"dns.lookupPromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"dns.lookupServicePromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"dns.promisesLookup": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"dns.promisesLookupService": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"dns.getServers": { argTypes: [], result: DYN },
|
||||
"dns.getDefaultResultOrder": { argTypes: [], result: STRING },
|
||||
"dns.setDefaultResultOrder": { argTypes: [DYN], result: VOID },
|
||||
@@ -1442,11 +1442,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
argTypes: [STRING, F64, STRING, F64, STRING, F64, F64, F64],
|
||||
result: VOID,
|
||||
},
|
||||
"test.suite": {
|
||||
argTypes: [STRING, F64, STRING, { kind: "func", params: [], ret: VOID }, STRING, F64, F64],
|
||||
result: VOID,
|
||||
},
|
||||
"test.hook": { argTypes: [F64, null, F64], result: VOID },
|
||||
// The suite body takes no parameters or its SuiteContext (flag 2).
|
||||
"test.suite": { argTypes: [STRING, F64, STRING, null, STRING, F64, F64], result: VOID },
|
||||
// The trailing string is the hook call's "file:line:col".
|
||||
"test.hook": { argTypes: [F64, null, F64, STRING], result: VOID },
|
||||
"test.sub": {
|
||||
argTypes: [TESTCTX_T, STRING, F64, STRING, null, F64, STRING, F64, F64, F64],
|
||||
result: { kind: "promise", inner: VOID },
|
||||
@@ -1460,6 +1459,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"test.ctxDiagnostic": { argTypes: [TESTCTX_T, STRING], result: VOID },
|
||||
"test.ctxName": { argTypes: [TESTCTX_T], result: STRING },
|
||||
"test.ctxFullName": { argTypes: [TESTCTX_T], result: STRING },
|
||||
// t.passed, and t.workerId's Number(NODE_TEST_WORKER_ID) with 0 for
|
||||
// undefined.
|
||||
"test.ctxPassed": { argTypes: [TESTCTX_T], result: BOOL },
|
||||
"test.workerId": { argTypes: [], result: F64 },
|
||||
// t.before/t.after/t.beforeEach/t.afterEach (which: 0 before, 1 after,
|
||||
// 2 beforeEach, 3 afterEach); a throwing t.before is captured on the test.
|
||||
"test.ctxHook": { argTypes: [TESTCTX_T, F64, null, F64], result: VOID },
|
||||
@@ -2590,6 +2593,25 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"crypto.hkdfCb": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64, null], result: VOID },
|
||||
"crypto.scrypt": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN], result: BYTES_U8 },
|
||||
"crypto.scryptCb": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN, null], result: VOID },
|
||||
/** util.promisify over the callback forms: the callback form's
|
||||
* arguments without the callback → a promise the callback form settles,
|
||||
* or rejected with an argument error; never throws. The result's value
|
||||
* type is the declared one (a Buffer, the filled view, a number, or a
|
||||
* checked-dynamic value); the validator checks it. */
|
||||
"crypto.randomBytesPromise": { argTypes: [F64], result: VOID },
|
||||
"crypto.pbkdf2Promise": { argTypes: [BYTES_U8, BYTES_U8, F64, F64, STRING], result: VOID },
|
||||
"crypto.scryptPromise": { argTypes: [BYTES_U8, BYTES_U8, F64, DYN], result: VOID },
|
||||
"crypto.hkdfPromise": { argTypes: [STRING, BYTES_U8, BYTES_U8, BYTES_U8, F64], result: VOID },
|
||||
"crypto.randomFillPromise": { argTypes: [null, F64, F64, F64], result: VOID },
|
||||
"crypto.randomFillPromiseDyn": { argTypes: [DYN, DYN, DYN, F64], result: VOID },
|
||||
"crypto.randomIntPromise": { argTypes: [F64, F64, F64], result: VOID },
|
||||
"crypto.generateKeyPromise": { argTypes: [DYN, DYN], result: VOID },
|
||||
/** util.promisify(dns.lookup) and util.promisify(dns.lookupService):
|
||||
* [hostname, options] and [address, port] → a promise of the
|
||||
* checked-dynamic result object (the address array with options.all),
|
||||
* or rejected with the error; never throws. */
|
||||
"dns.lookupPromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"dns.lookupServicePromise": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
// The Buffer statics and the fs/zlib Buffer forms: fixed always-u8
|
||||
// signatures (Buffer IS a Uint8Array — one bytes kind).
|
||||
/** The Buffer statics with fixed (always-u8) signatures. fromStr is
|
||||
@@ -2777,6 +2799,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* receivers are borrowed. */
|
||||
"zlib.streamNew": { argTypes: [F64, DYN], result: VOID },
|
||||
"zlib.streamBytesWritten": { argTypes: [null], result: F64 },
|
||||
/** `stream instanceof zlib.Gzip`: the borrowed stream and the class's
|
||||
* node_zlib_mode; true when the stream is a zlib stream of that mode. */
|
||||
"zlib.streamIs": { argTypes: [null, F64], result: BOOL },
|
||||
"zlib.streamFlush": { argTypes: [null, DYN], result: VOID },
|
||||
"zlib.streamFlushCb": { argTypes: [null, DYN, null], result: VOID },
|
||||
"zlib.streamParams": { argTypes: [null, DYN, DYN], result: VOID },
|
||||
|
||||
@@ -118,6 +118,9 @@ export function boundedIntegerLoopFacts(
|
||||
stable(e.receiver.localId)
|
||||
)
|
||||
return { min: 0, max: Number.MAX_SAFE_INTEGER };
|
||||
// Every evaluation of an array length lies in its range, so the
|
||||
// condition bounds each pass even when the body resizes the array.
|
||||
if (isArrayLength(e)) return ARRAY_LENGTH_RANGE;
|
||||
return null;
|
||||
}
|
||||
let step: IntegerRange;
|
||||
@@ -134,12 +137,15 @@ export function boundedIntegerLoopFacts(
|
||||
if (step.min <= 0 || step.max > 2147483647) return result;
|
||||
}
|
||||
const limit = bound(loop.cond.right);
|
||||
if (!limit || limit.max < start.min) return result;
|
||||
const distance = limit.max - start.min;
|
||||
// The counter is an integer, so a strict bound admits passes up to one
|
||||
// below the limit.
|
||||
const last = limit && (loop.cond.op === "<" ? limit.max - 1 : limit.max);
|
||||
if (!limit || last === null || last < start.min) return result;
|
||||
const distance = last - start.min;
|
||||
const iterations = Math.floor(distance / step.min) + 1;
|
||||
if (!Number.isSafeInteger(distance) || !Number.isSafeInteger(iterations)) return result;
|
||||
// Header facts also cover the failing final condition and zero iterations.
|
||||
const maximum = Math.max(start.max, limit.max + step.max);
|
||||
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);
|
||||
@@ -154,6 +160,20 @@ export function boundedIntegerLoopFacts(
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Every length an array can have: SCR_ARR_MAX_LENGTH (scr_array.c). */
|
||||
export const ARRAY_LENGTH_RANGE: IntegerRange = { min: 0, max: 4294967295 };
|
||||
|
||||
/** `a.length` of a typed array value (an ordinary JS array). */
|
||||
function isArrayLength(e: IrExpr): boolean {
|
||||
return (
|
||||
e.kind === "arrIntrinsic" &&
|
||||
e.method === "length" &&
|
||||
e.args.length === 0 &&
|
||||
e.type.kind === "f64" &&
|
||||
e.receiver.type.kind === "array"
|
||||
);
|
||||
}
|
||||
|
||||
/** The most UTF-16 code units a runtime string holds: SCR_STR_MAX_CAP
|
||||
* (scr_runtime.h) caps a string's UTF-8 bytes, and a UTF-16 length never
|
||||
* exceeds the UTF-8 byte length. */
|
||||
@@ -241,6 +261,9 @@ export function guardedLoopCounterFacts(
|
||||
stable(e.receiver.localId)
|
||||
)
|
||||
return { min: 0, max: STRING_MAX_UNITS };
|
||||
// An array length can change on any pass, but never leaves its range,
|
||||
// which is all a ceiling needs.
|
||||
if (isArrayLength(e)) return ARRAY_LENGTH_RANGE;
|
||||
return null;
|
||||
};
|
||||
// The bounds a condition's conjuncts put on counters while it holds.
|
||||
|
||||
@@ -208,3 +208,111 @@ test("a string read at the counter is an integer consumer", () => {
|
||||
s.body = [read(ref("limit"))];
|
||||
expect(matchIntegerCountedForLoop(s, withText)).toBeNull();
|
||||
});
|
||||
|
||||
test("a stable array length bounds a counted loop; a resizing body keeps it a condition", () => {
|
||||
const ARR = arrayOf(F64);
|
||||
const array = (localId: string): IrExpr => ({ kind: "varRef", localId, type: ARR, loc });
|
||||
const length: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: array("a"),
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const read = (index: IrExpr): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "getNumber",
|
||||
receiver: array("a"),
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
// for (let i = 0; i < a.length; i += 2) a[i];
|
||||
const strided = (body: IrStmt[]): IrStmt & { kind: "for" } => {
|
||||
const s = counted(length);
|
||||
s.update = {
|
||||
kind: "assign",
|
||||
localId: "i",
|
||||
value: { kind: "bin", op: "+", left: ref("i"), right: number(2), type: F64, loc },
|
||||
loc,
|
||||
};
|
||||
s.body = [{ kind: "exprStmt", expr: read(ref("i")), loc }, ...body];
|
||||
return s;
|
||||
};
|
||||
const ranges = new Map([[length, { min: 0, max: 4294967295 }]]);
|
||||
expect(matchIntegerCountedForLoop(strided([]), locals, ranges, new Set())).toMatchObject({
|
||||
localId: "i",
|
||||
guarded: false,
|
||||
range: { min: 0, max: 4294967294 },
|
||||
});
|
||||
// Without the alias facts there is no stability proof.
|
||||
expect(matchIntegerCountedForLoop(strided([]), locals, ranges)).toBeNull();
|
||||
const push: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: array("a"),
|
||||
args: [number(1)],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
expect(matchIntegerCountedForLoop(strided([push]), locals, ranges, new Set())).toBeNull();
|
||||
// An element write at the counter stays below the length; one past it can grow the array.
|
||||
const write = (index: IrExpr): IrStmt => ({
|
||||
kind: "arraySet",
|
||||
arr: array("a"),
|
||||
index,
|
||||
value: number(0),
|
||||
loc,
|
||||
});
|
||||
expect(
|
||||
matchIntegerCountedForLoop(strided([write(ref("i"))]), locals, ranges, new Set()),
|
||||
).not.toBeNull();
|
||||
const past = { kind: "bin", op: "+", left: ref("i"), right: number(1), type: F64, loc } as const;
|
||||
expect(matchIntegerCountedForLoop(strided([write(past)]), locals, ranges, new Set())).toBeNull();
|
||||
});
|
||||
|
||||
test("a descending loop from an array's last index takes an integer counter for its reads", () => {
|
||||
const ARR = arrayOf(F64);
|
||||
const length: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: { kind: "varRef", localId: "a", type: ARR, loc },
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const start: IrExpr = { kind: "bin", op: "-", left: length, right: number(1), type: F64, loc };
|
||||
const s = loop(start);
|
||||
s.cond = { kind: "bin", op: ">=", left: ref("i"), right: number(0), type: BOOL, loc };
|
||||
s.update = {
|
||||
kind: "assign",
|
||||
localId: "i",
|
||||
value: { kind: "bin", op: "-", left: ref("i"), right: number(1), type: F64, loc },
|
||||
loc,
|
||||
};
|
||||
s.body = [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "getNumber",
|
||||
receiver: { kind: "varRef", localId: "a", type: ARR, loc },
|
||||
args: [ref("i")],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const ranges = new Map([[start, { min: -1, max: 4294967294 }]]);
|
||||
expect(matchIntegerCountedForLoop(s, locals, ranges)).toMatchObject({
|
||||
step: -1,
|
||||
guarded: false,
|
||||
range: { min: 0, max: 4294967294 },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IntegerRange, IntegerRanges } from "./integer-ranges.js";
|
||||
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js";
|
||||
import { byteNumberAccess } from "./byte-numbers.js";
|
||||
import { preservesArrayLength } from "./array-length-stability.js";
|
||||
|
||||
/** A canonical byte loop whose induction variable is mathematically an
|
||||
* unsigned integer for every body entry. Backends may keep this binding in
|
||||
@@ -47,15 +48,17 @@ function constantStep(update: IrStmt | null, id: string): number | null {
|
||||
}
|
||||
|
||||
/** Pure initial values may be evaluated again on a fallback path. Bounds
|
||||
* additionally have to remain invariant throughout the loop. Array length
|
||||
* is allowed only in the initializer; its mutable value is never hoisted
|
||||
* from a condition. Typed-array lengths are fixed for a given receiver. */
|
||||
* additionally have to remain invariant throughout the loop. An array length
|
||||
* is hoisted from a condition only when `stableLength` proves the loop never
|
||||
* changes it; otherwise it is allowed only in the initializer. Typed-array
|
||||
* lengths are fixed for a given receiver. */
|
||||
function stableLoopValue(
|
||||
value: IrExpr,
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
body: IrStmt[],
|
||||
invariant: boolean,
|
||||
counter: string,
|
||||
stableLength: (receiver: string) => boolean = () => false,
|
||||
): boolean {
|
||||
switch (value.kind) {
|
||||
case "numLit":
|
||||
@@ -77,16 +80,16 @@ function stableLoopValue(
|
||||
value.op === "&" ||
|
||||
value.op === ">>>" ||
|
||||
value.op === "|") &&
|
||||
stableLoopValue(value.left, locals, body, invariant, counter) &&
|
||||
stableLoopValue(value.right, locals, body, invariant, counter)
|
||||
stableLoopValue(value.left, locals, body, invariant, counter, stableLength) &&
|
||||
stableLoopValue(value.right, locals, body, invariant, counter, stableLength)
|
||||
);
|
||||
case "bytesIntrinsic":
|
||||
case "arrIntrinsic":
|
||||
return (
|
||||
(value.method === "length" ||
|
||||
(value.kind === "bytesIntrinsic" && value.method === "byteLength")) &&
|
||||
(value.kind === "bytesIntrinsic" || !invariant) &&
|
||||
value.receiver.kind === "varRef" &&
|
||||
(value.kind === "bytesIntrinsic" || !invariant || stableLength(value.receiver.localId)) &&
|
||||
stableLoopValue(value.receiver, locals, body, invariant, counter)
|
||||
);
|
||||
default:
|
||||
@@ -103,6 +106,7 @@ export function matchIntegerCountedForLoop(
|
||||
stmt: IrStmt & { kind: "for" },
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
ranges: IntegerRanges = new Map(),
|
||||
unaliased: ReadonlySet<string> | null = null,
|
||||
): IntegerCountedForLoop | null {
|
||||
const init = stmt.init;
|
||||
if (init?.kind !== "varDecl" || !init.init) return null;
|
||||
@@ -135,7 +139,38 @@ export function matchIntegerCountedForLoop(
|
||||
op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record<string, string>)[op] ?? "";
|
||||
}
|
||||
if (step > 0 ? op !== "<" && op !== "<=" : op !== ">" && op !== ">=") return null;
|
||||
if (!stableLoopValue(limit, locals, stmt.body, true, local.id)) return null;
|
||||
// An ascending counter compared below `a.length - c` holds an index below
|
||||
// the length on every pass while that length is unchanged.
|
||||
const belowLength = (receiver: string): boolean => {
|
||||
let offset = 0,
|
||||
length = limit;
|
||||
if (
|
||||
length.kind === "bin" &&
|
||||
length.op === "-" &&
|
||||
length.right.kind === "numLit" &&
|
||||
Number.isFinite(length.right.value)
|
||||
) {
|
||||
offset = length.right.value;
|
||||
length = length.left;
|
||||
}
|
||||
return (
|
||||
step > 0 &&
|
||||
length.kind === "arrIntrinsic" &&
|
||||
length.method === "length" &&
|
||||
length.receiver.kind === "varRef" &&
|
||||
length.receiver.localId === receiver &&
|
||||
(op === "<" ? offset >= 0 : offset >= 1)
|
||||
);
|
||||
};
|
||||
const stableLength = (receiver: string): boolean =>
|
||||
unaliased !== null &&
|
||||
preservesArrayLength(
|
||||
[cond, ...stmt.body],
|
||||
{ receiver, inRange: belowLength(receiver) ? local.id : null },
|
||||
locals,
|
||||
unaliased,
|
||||
);
|
||||
if (!stableLoopValue(limit, locals, stmt.body, true, local.id, stableLength)) return null;
|
||||
const inclusive = op === "<=" || op === ">=";
|
||||
const guardLimit = step > 0 ? 2 ** 53 - (inclusive ? 1 : 0) : -(2 ** 53) + (inclusive ? 1 : 0);
|
||||
const boundRange =
|
||||
@@ -178,10 +213,19 @@ export function matchIntegerCountedForLoop(
|
||||
// A string read at the counter indexes with the integer directly.
|
||||
if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && counter(e.args[0]))
|
||||
useful = true;
|
||||
// So does an array element read; only a loop that needs no versioned
|
||||
// copy takes the integer counter for it.
|
||||
if (
|
||||
!guarded &&
|
||||
((e.kind === "arrayGet" && counter(e.index)) ||
|
||||
(e.kind === "arrIntrinsic" && e.method === "getNumber" && e.args[0] && counter(e.args[0])))
|
||||
)
|
||||
useful = true;
|
||||
return (!guarded || ++cost <= 80) && everyExprChild(e, expr, bodyStmt);
|
||||
};
|
||||
const bodyStmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "bytesSet" && counter(s.index)) useful = true;
|
||||
if (!guarded && s.kind === "arraySet" && counter(s.index)) useful = true;
|
||||
if (
|
||||
guarded &&
|
||||
(s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile")
|
||||
|
||||
@@ -334,7 +334,8 @@ test("filtered strided loops retain decoded values and bound secondary cursors",
|
||||
const ranges = analyzeIntegerRanges(f);
|
||||
expect(ranges.get(cursor)).toEqual({ min: 0, max: 80 });
|
||||
expect(ranges.get(decoded)).toEqual({ min: 0, max: 4294967295 });
|
||||
expect(ranges.get(conditionRead)).toEqual({ min: 0, max: 124 });
|
||||
// The last pass enters at most at 99, so the failing test reads at most 123.
|
||||
expect(ranges.get(conditionRead)).toEqual({ min: 0, max: 123 });
|
||||
for (const mutation of ["fraction", "repeat", "overflow", "counter"] as const) {
|
||||
const changed = structuredClone(f);
|
||||
const body = (changed.body[1] as IrStmt & { kind: "for" }).body;
|
||||
@@ -795,6 +796,77 @@ test("a while condition on a string's length bounds a counter, and guarded incre
|
||||
expect(run((inside) => [observe(inside), bump, rebound]).after).toBeNull();
|
||||
});
|
||||
|
||||
test("an array length bounds while and for counters even when the body resizes the array", () => {
|
||||
// let i = 0; while (i < items.length) { observe(i); items.push(i); i++; } observe(i);
|
||||
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 push: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
loc,
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: items,
|
||||
args: [ref("i")],
|
||||
type: F64,
|
||||
},
|
||||
};
|
||||
const bump: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "incDec", op: "+", prefix: false, localId: "i", type: F64 },
|
||||
};
|
||||
const inside = ref("i"),
|
||||
after = ref("i");
|
||||
const f = fn([
|
||||
{ loc, kind: "assign", localId: "i", value: num(0) },
|
||||
{
|
||||
loc,
|
||||
kind: "while",
|
||||
cond: condition("<", ref("i"), length),
|
||||
body: [observe(inside), push, bump],
|
||||
},
|
||||
observe(after),
|
||||
]);
|
||||
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: 4294967294 });
|
||||
expect(ranges.get(after)).toEqual({ min: 0, max: 4294967295 });
|
||||
// A secondary cursor of `for (let j = 0; j < items.length; j++) k++` stays a uint32.
|
||||
const cursor = ref("k");
|
||||
const loop: IrStmt = {
|
||||
loc,
|
||||
kind: "for",
|
||||
init: { loc, kind: "varDecl", localId: "j", init: num(0) },
|
||||
cond: condition("<", ref("j"), length),
|
||||
update: { loc, kind: "assign", localId: "j", value: bin("+", ref("j"), num(1)) },
|
||||
body: [
|
||||
push,
|
||||
observe(cursor),
|
||||
{ loc, kind: "assign", localId: "k", value: bin("+", ref("k"), num(1)) },
|
||||
],
|
||||
};
|
||||
const g = fn([{ loc, kind: "assign", localId: "k", value: num(0) }, loop]);
|
||||
g.locals.push(
|
||||
{ id: "j", name: "j", type: F64, mutable: true },
|
||||
{ id: "k", name: "k", type: F64, mutable: true },
|
||||
{ id: "items", name: "items", type: ARR, mutable: false },
|
||||
);
|
||||
expect(analyzeIntegerRanges(g).get(cursor)).toEqual({ min: 0, max: 4294967295 });
|
||||
});
|
||||
|
||||
test("whole-program slot facts seed parameters, field reads and call results", () => {
|
||||
const field: IrExpr = {
|
||||
loc,
|
||||
|
||||
@@ -344,8 +344,8 @@ export type IrType =
|
||||
* Runtime representation is a refcounted snapshot box (ScrCaught) holding
|
||||
* the taken exception: a kind tag plus the payload. Even NARROWER than
|
||||
* dyn: the only expressions a caught value may appear in are `caughtTest`
|
||||
* (kind/instanceof tests), `caughtNarrow` (checker-trusted extraction
|
||||
* under a proven test), and the `rethrow` statement — the frontend
|
||||
* (kind/instanceof tests), `caughtNarrow` (extraction under a proven
|
||||
* test, verified at run time), and the `rethrow` statement — the frontend
|
||||
* rejects every other use with the narrowing hint. */
|
||||
| { kind: "caught" }
|
||||
| { kind: "promise"; inner: IrType } // heap, refcounted, settled-once
|
||||
@@ -1656,6 +1656,14 @@ export interface IrFunction {
|
||||
sourceName?: string;
|
||||
/** Ordinary authored function declarations/expressions own a prototype. */
|
||||
ownsPrototype?: true;
|
||||
/** The function's execution tier (ir/tiers.ts). Absent means static: the
|
||||
* function may use dynamic values only through boundary operations
|
||||
* (dynFrom, dynCheck, kind tests) and moves, which the validator enforces
|
||||
* (invariant I1). "dynamic" admits the dynamic operation vocabulary
|
||||
* (dynKeyGet, dynCall, dyn.* builtins, ...). Lowering assigns the field
|
||||
* from the body (assignLegacyTiers); stage S2 of the two-tier design
|
||||
* keeps it out of serialized IR, so loaders derive it again. */
|
||||
tier?: "dynamic";
|
||||
body: IrStmt[];
|
||||
loc: SrcLoc;
|
||||
}
|
||||
@@ -2953,11 +2961,12 @@ export type IrExpr =
|
||||
* fulfillment payload (scr_await_void) and rejections flow untyped.
|
||||
* Ownership of the operand transfers to the result, like upcast. */
|
||||
| { kind: "promiseVoidWiden"; value: IrExpr; type: IrType; loc: SrcLoc }
|
||||
/** Checker-trusted narrowing of a base-class value to a subclass (`type`
|
||||
* is the subclass). The frontend emits this only where tsc's control-flow
|
||||
* narrowing has already proven the dynamic class (an `instanceof` guard)
|
||||
* — the same trust-the-checker contract as unionNarrow: no runtime check,
|
||||
* a pointer reinterpret with ownership transferring like upcast. */
|
||||
/** Narrowing of a base-class value to a subclass (`type` is the
|
||||
* subclass). The frontend emits this only where tsc's control-flow
|
||||
* narrowing has already proven the dynamic class (an `instanceof` guard),
|
||||
* like unionNarrow. Backends verify the class once and trap on a mismatch
|
||||
* (a compiler bug or a lying type predicate or assertion function), then
|
||||
* reinterpret the pointer with ownership transferring like upcast. */
|
||||
| { kind: "downcast"; value: IrExpr; type: IrType; loc: SrcLoc }
|
||||
/** `x instanceof C` where x's static class and C are both in extends-
|
||||
* hierarchies: an O(1) preorder-interval check against the vtable the
|
||||
@@ -3034,26 +3043,30 @@ export type IrExpr =
|
||||
* Declared fields are tried FIRST (an emitted string-switch — field
|
||||
* access exactness is preserved: a declared name always answers from the
|
||||
* struct slot), then the overflow map on index-signature shapes. `type`
|
||||
* is the CHECKER's type for the access: the index signature's value type
|
||||
* (dyn for `unknown`; with noUncheckedIndexedAccess, its
|
||||
* `V | undefined` union). A MISSING key produces: the undefined dyn
|
||||
* singleton when `type` is dyn; the undefined arm when `type` is an
|
||||
* undefined-armed union; otherwise a TRAP — the checker claimed V and no
|
||||
* undefined is representable (the array OOB policy; on declared-only
|
||||
* shapes tsc's keyof check makes the trap unreachable without an `as`
|
||||
* smuggle). Declared-field values surface as `type`: V-typed fields read
|
||||
* directly, dyn results build a dyn COPY of the field value (the dynFrom
|
||||
* conversion — deep for composites, documented), union results wrap.
|
||||
* is the index signature's value type armed with undefined (dyn for
|
||||
* `unknown`), so a missing key reads undefined as in JavaScript, or the
|
||||
* declared fields' common type on signature-free shapes. A MISSING key
|
||||
* produces: the undefined dyn singleton when `type` is dyn; the undefined
|
||||
* arm when `type` is an undefined-armed union; NaN with `missNaN`;
|
||||
* otherwise a TRAP (declared-only shapes, where tsc's keyof check makes
|
||||
* the trap unreachable without an `as` smuggle). Declared-field values
|
||||
* surface as `type`: V-typed fields read directly, dyn results build a
|
||||
* dyn COPY of the field value (the dynFrom conversion — deep for
|
||||
* composites, documented), union results wrap.
|
||||
* The key and object are borrowed; refcounted results are owned (+1).
|
||||
* `overflowOnly` (set when the key is a LITERAL that names no declared
|
||||
* field): the read touches only the overflow map — declared fields need
|
||||
* not surface as `type`, and the emitted helper skips the string-switch. */
|
||||
* not surface as `type`, and the emitted helper skips the string-switch.
|
||||
* `missNaN` (with an f64 `type`): a missing key produces NaN instead of
|
||||
* trapping, for reads whose every consumer treats undefined like NaN
|
||||
* (nan-coded-reads.ts). */
|
||||
| {
|
||||
kind: "recordKeyGet";
|
||||
obj: IrExpr;
|
||||
shapeId: string;
|
||||
key: IrExpr;
|
||||
overflowOnly?: true;
|
||||
missNaN?: true;
|
||||
type: IrType;
|
||||
loc: SrcLoc;
|
||||
}
|
||||
@@ -3386,12 +3399,13 @@ export type IrExpr =
|
||||
type: IrType;
|
||||
loc: SrcLoc;
|
||||
}
|
||||
/** Checker-trusted extraction of a catch binding's payload as `type` —
|
||||
* the caught analog of unionNarrow: the frontend emits this only where
|
||||
* tsc's control-flow narrowing has already proven the matching test
|
||||
* (`e instanceof C` / `typeof e === "string"`), so the read is
|
||||
* kind-UNCHECKED at runtime. `type` is f64, bool, string, or a
|
||||
* class object; refcounted results come out retained (+1). */
|
||||
/** Extraction of a catch binding's payload as `type` — the caught
|
||||
* analog of unionNarrow: the frontend emits this only where tsc's
|
||||
* control-flow narrowing has already proven the matching test
|
||||
* (`e instanceof C` / `typeof e === "string"`). Backends verify the kind
|
||||
* or class once and trap on a mismatch (a compiler bug or a lying type
|
||||
* predicate or assertion function). `type` is f64, bool, string, or a class object; refcounted results come
|
||||
* out retained (+1). */
|
||||
| { kind: "caughtNarrow"; value: IrExpr; type: IrType; loc: SrcLoc }
|
||||
/** CHECKED extraction of a catch binding's payload as a hierarchy-class
|
||||
* instance — the caught analog of dynCheck, emitted for `e as C` casts
|
||||
@@ -3417,11 +3431,13 @@ export type IrExpr =
|
||||
* varRef (borrowed); `type` is dyn; the result is a fresh tree (+1),
|
||||
* never aliasing the payload. Never throws. */
|
||||
| { kind: "caughtToDyn"; value: IrExpr; type: IrType; loc: SrcLoc }
|
||||
/** Union payload extraction, tag-UNCHECKED: `value` is union-typed,
|
||||
* `type` is arms[tag], and the backend reads the payload assuming the tag
|
||||
* — SOUNDNESS RESTS ON tsc's control-flow narrowing (the frontend emits
|
||||
* this only where the checker has already narrowed the expression to that
|
||||
* arm; see docs/ir.md). Refcounted payloads come out retained (+1). */
|
||||
/** Union payload extraction: `value` is union-typed and `type` is
|
||||
* arms[tag]. The frontend emits this only where tsc's control-flow
|
||||
* narrowing has already narrowed the expression to that arm; backends
|
||||
* verify the tag once and trap on a mismatch (a compiler bug or a lying
|
||||
* type predicate or assertion function), so a wrong proof cannot read the
|
||||
* payload as another arm.
|
||||
* Refcounted payloads come out retained (+1). */
|
||||
| { kind: "unionNarrow"; unionId: string; tag: number; value: IrExpr; type: IrType; loc: SrcLoc }
|
||||
/** Discriminant read `r.kind` on a union receiver: every arm is a
|
||||
* record/class possessing field `field` with the SAME primitive IR type
|
||||
|
||||
@@ -424,7 +424,7 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
|
||||
}
|
||||
}
|
||||
if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true;
|
||||
if (fn === "dns.lookupPromise" || fn === "dns.lookupServicePromise")
|
||||
if (fn === "dns.promisesLookup" || fn === "dns.promisesLookupService")
|
||||
features.dnsPromises = true;
|
||||
if (fn.startsWith("tlsca.")) features.tlsCa = true;
|
||||
// scr_cipher.c over the mbedTLS archive; DEP0182 reports through the
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
* text parts and decoded from bytes one function at a time; neither direction
|
||||
* materializes the complete document as a single string. */
|
||||
import type { IrExpr, IrFunction, IrModule, IrStmt } from "./ir.js";
|
||||
import { deriveModuleTiers } from "./tiers.js";
|
||||
|
||||
// Version 16 removes embedded-engine types, operations, and module metadata.
|
||||
// Older modules can require runtime support that no longer exists.
|
||||
@@ -42,6 +43,19 @@ function encodeNumber(_key: string, value: unknown): unknown {
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Stage S2 of the two-tier design keeps function tiers out of IR
|
||||
* artifacts, so a report-only release changes no serialized byte; loaders
|
||||
* derive them again (deriveModuleTiers). No IR node other than a function
|
||||
* carries a `tier` key. */
|
||||
function encodeWithoutTier(key: string, value: unknown): unknown {
|
||||
return key === "tier" ? undefined : encodeNumber(key, value);
|
||||
}
|
||||
|
||||
function encodeFunction(fn: IrFunction): string | undefined {
|
||||
if (fn.tier !== undefined) return JSON.stringify(fn, encodeWithoutTier);
|
||||
return ordinaryNumbers(fn) ? JSON.stringify(fn) : JSON.stringify(fn, encodeNumber);
|
||||
}
|
||||
|
||||
/** The compact encoding as ordered parts: the module header up to the
|
||||
* function array, one part per function (with separate comma parts), and the
|
||||
* header's remainder. Concatenating the parts yields exactly
|
||||
@@ -58,9 +72,7 @@ export function serializeModuleParts(mod: IrModule): string[] {
|
||||
for (let index = 0; index < mod.functions.length; index++) {
|
||||
const fn = mod.functions[index]!;
|
||||
if (index > 0) parts.push(",");
|
||||
parts.push(
|
||||
(ordinaryNumbers(fn) ? JSON.stringify(fn) : JSON.stringify(fn, encodeNumber)) ?? "null",
|
||||
);
|
||||
parts.push(encodeFunction(fn) ?? "null");
|
||||
}
|
||||
parts.push("]" + header.slice(offset + slot.length));
|
||||
return parts;
|
||||
@@ -70,7 +82,7 @@ export function serializeModuleParts(mod: IrModule): string[] {
|
||||
* readable default. Both return one string, so modules whose encoding
|
||||
* exceeds the host's string limit must use `serializeModuleParts`. */
|
||||
export function serializeModule(mod: IrModule, compact = false): string {
|
||||
if (!compact) return JSON.stringify(mod, encodeNumber, 2);
|
||||
if (!compact) return JSON.stringify(mod, encodeWithoutTier, 2);
|
||||
return serializeModuleParts(mod).join("");
|
||||
}
|
||||
|
||||
@@ -96,7 +108,7 @@ function checkedVersion(mod: IrModule): IrModule {
|
||||
}
|
||||
|
||||
export function deserializeModule(json: string): IrModule {
|
||||
return checkedVersion(JSON.parse(json, decodeNumber) as IrModule);
|
||||
return deriveModuleTiers(checkedVersion(JSON.parse(json, decodeNumber) as IrModule));
|
||||
}
|
||||
|
||||
/** A node as it reads back from the compact encoding: a fresh object, as
|
||||
@@ -249,5 +261,5 @@ export function deserializeModuleBytes(bytes: Buffer): IrModule {
|
||||
);
|
||||
}
|
||||
mod.functions = functions;
|
||||
return mod;
|
||||
return deriveModuleTiers(mod);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, DYN, F64, STRING, VOID, type IrExpr, type IrFunction, type IrModule } from "./ir.js";
|
||||
import {
|
||||
deserializeModule,
|
||||
deserializeModuleBytes,
|
||||
serializeModule,
|
||||
serializeModuleParts,
|
||||
} from "./serialize.js";
|
||||
import {
|
||||
assignLegacyTiers,
|
||||
dynamicOperationOf,
|
||||
firstDynamicOperation,
|
||||
functionTier,
|
||||
} from "./tiers.js";
|
||||
import { validateModule } from "./validate.js";
|
||||
|
||||
const loc = { file: "tiers.ts", start: 0, end: 0 };
|
||||
const dynParam = { kind: "varRef", localId: "v", type: DYN, loc } as const satisfies IrExpr;
|
||||
|
||||
function fn(name: string, value: IrExpr): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: [{ localId: "v", name: "v", type: DYN }],
|
||||
returnType: value.type,
|
||||
locals: [{ id: "v", name: "v", type: DYN, mutable: false }],
|
||||
body: [{ kind: "return", value, loc }],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function moduleOf(...functions: IrFunction[]): IrModule {
|
||||
const main: IrFunction = {
|
||||
name: "%main",
|
||||
params: [],
|
||||
returnType: VOID,
|
||||
locals: [],
|
||||
body: [],
|
||||
loc,
|
||||
};
|
||||
return { irVersion: 16, sourceFile: "tiers.ts", functions: [main, ...functions], entry: "%main" };
|
||||
}
|
||||
|
||||
const keyGet: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
key: { kind: "strLit", value: "name", type: STRING, loc },
|
||||
value: dynParam,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
|
||||
test("dynamic operations and boundary operations are told apart", () => {
|
||||
expect(dynamicOperationOf(keyGet)).toBe("dynKeyGet");
|
||||
expect(
|
||||
dynamicOperationOf({
|
||||
kind: "libCall",
|
||||
fn: "dyn.add",
|
||||
args: [dynParam, dynParam],
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
).toBe("dyn.add");
|
||||
expect(
|
||||
dynamicOperationOf({
|
||||
kind: "call",
|
||||
callee: "%dyn.class.property.12",
|
||||
args: [dynParam],
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
).toBe("%dyn.class.property");
|
||||
// Boundary operations: conversions and kind tests.
|
||||
expect(dynamicOperationOf({ kind: "dynCheck", value: dynParam, type: F64, loc })).toBeNull();
|
||||
expect(
|
||||
dynamicOperationOf({
|
||||
kind: "dynFrom",
|
||||
value: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
).toBeNull();
|
||||
expect(
|
||||
dynamicOperationOf({ kind: "dynTest", test: "string", value: dynParam, type: BOOL, loc }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
dynamicOperationOf({ kind: "libCall", fn: "dyn.typedRefIs", args: [dynParam], type: DYN, loc }),
|
||||
).toBeNull();
|
||||
// Moves and calls of ordinary functions.
|
||||
expect(dynamicOperationOf(dynParam)).toBeNull();
|
||||
expect(
|
||||
dynamicOperationOf({ kind: "call", callee: "helper", args: [dynParam], type: DYN, loc }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("lowering's classification puts exactly the functions with dynamic operations in the dynamic tier", () => {
|
||||
const reader = fn("reader", keyGet);
|
||||
const mover = fn("mover", dynParam);
|
||||
const dynamic = assignLegacyTiers([reader, mover]);
|
||||
expect(dynamic.map((d) => [d.name, d.operation])).toEqual([["reader", "dynKeyGet"]]);
|
||||
expect(functionTier(reader)).toBe("dynamic");
|
||||
expect(functionTier(mover)).toBe("static");
|
||||
expect(firstDynamicOperation(mover)).toBeNull();
|
||||
});
|
||||
|
||||
test("the validator enforces I1: static functions perform no dynamic operation", () => {
|
||||
const reader = fn("reader", keyGet);
|
||||
const errors = validateModule(moduleOf(reader));
|
||||
expect(errors.map((e) => e.message)).toEqual([
|
||||
"in reader: static-tier function performs the dynamic operation dynKeyGet",
|
||||
]);
|
||||
reader.tier = "dynamic";
|
||||
expect(validateModule(moduleOf(reader))).toEqual([]);
|
||||
// A logical operator over dynamic values applies ToBoolean to them.
|
||||
const either = fn("either", {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: dynParam,
|
||||
right: dynParam,
|
||||
type: DYN,
|
||||
loc,
|
||||
});
|
||||
expect(validateModule(moduleOf(either)).map((e) => e.message)).toEqual([
|
||||
"in either: static-tier function performs the dynamic operation logical",
|
||||
]);
|
||||
});
|
||||
|
||||
test("serialized IR carries no tiers in stage S2, and loading derives them again", () => {
|
||||
const reader = fn("reader", keyGet);
|
||||
const mod = moduleOf(reader);
|
||||
const untiered = serializeModule(mod, true);
|
||||
assignLegacyTiers(mod.functions);
|
||||
expect(reader.tier).toBe("dynamic");
|
||||
expect(serializeModule(mod, true)).toBe(untiered);
|
||||
expect(serializeModuleParts(mod).join("")).toBe(untiered);
|
||||
expect(serializeModule(mod)).not.toContain('"tier"');
|
||||
for (const loaded of [
|
||||
deserializeModule(untiered),
|
||||
deserializeModuleBytes(Buffer.from(untiered)),
|
||||
]) {
|
||||
expect(loaded.functions.map(functionTier)).toEqual(["static", "dynamic"]);
|
||||
expect(validateModule(loaded)).toEqual([]);
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user