mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Merge main
This commit is contained in:
@@ -172,7 +172,9 @@ jobs:
|
||||
SCRIPTC_SAN: ${{ matrix.flavor == 'san' && '1' || '' }}
|
||||
# Match macOS, where Apple ASan has no LeakSanitizer: the RC audit owns
|
||||
# leak detection, including intentional abandonment. Plain shards also
|
||||
# build some sanitized probes, so this applies to both flavors.
|
||||
# build some sanitized probes, so this applies to both flavors. The
|
||||
# san shards' case-sharded corpus turns LeakSanitizer on
|
||||
# (scripts/ci-test-plan.mjs, tests/harness/lsan.supp).
|
||||
ASAN_OPTIONS: detect_leaks=0
|
||||
# Within-file partition for the large case-driven harnesses
|
||||
# (differential, llvm-differential, npm, server, Test262, coverage, and
|
||||
|
||||
@@ -18,6 +18,7 @@ Application-shaped workloads for measuring the speed of compiled executables. Ea
|
||||
| `typed-array-scan`, `typed-array-scan-workers` | cpu | element-by-element scan loops over `Uint8Array`, `Uint16Array`, `Int32Array` and `Float64Array`; the second entry is a worker-enabled executable (it can start a worker), so each access also checks for shared storage |
|
||||
| `intern-table`, `intern-table-map`, `intern-table-1`, `intern-table-map-1` | cpu | worker threads interning the same type-like objects under string keys and records under number keys, then hits and misses: one shared `InternTable` per kind of key, or a module-level `Map` per thread (the `-map` entries), with 8 threads or 1 (the `-1` entries); under Node.js every thread has its own tables |
|
||||
| `thread-slots`, `thread-slots-cache` | cpu | worker threads reading one published type graph from an `InternTable` and keeping per-thread state for each type they visit: a `ThreadSlots` family, or a direct-mapped cache by id in front of a `Map` by id (the `-cache` entry); under Node.js both are per thread |
|
||||
| `channel-messages` | cpu | 200,000 MessageChannel messages in one thread, half numbers and half small objects, all posted before the port delivers any |
|
||||
| `atomics-contended` | cpu | four worker threads updating `SharedArrayBuffer` counters with `Atomics`: one shared counter, one counter per worker, a `compareExchange` loop, and `Atomics.wait`/`notify` handoffs |
|
||||
| `value-objects` | cpu | small immutable records and class instances: vector helpers, method chains, particle updates |
|
||||
| `value-records` | cpu | records that hold strings, spread state updates and destructured tuples: token helpers, an immutable reducer, tuple returns |
|
||||
|
||||
@@ -147,6 +147,13 @@
|
||||
"20"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "channel-messages",
|
||||
"entry": "workloads/channel-messages.ts",
|
||||
"args": [
|
||||
"200000"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "atomics-contended",
|
||||
"entry": "workloads/atomics-contended.ts",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// MessageChannel messages within one thread: the program posts the given
|
||||
// number of messages, half of them numbers and half small objects, before
|
||||
// the port delivers any, so all of them are queued at once. Argument: the
|
||||
// number of messages.
|
||||
import { MessageChannel } from "node:worker_threads";
|
||||
|
||||
const count = Number(process.argv[2] ?? "200000");
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
let received = 0;
|
||||
let sum = 0;
|
||||
port1.on("message", (value: number | { id: number; name: string }) => {
|
||||
sum += typeof value === "number" ? value : value.id + value.name.length;
|
||||
if (++received === count) {
|
||||
console.log(`${count} messages, sum ${sum}`);
|
||||
port1.close();
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (i % 2 === 0) port2.postMessage(i);
|
||||
else port2.postMessage({ id: i, name: "item" });
|
||||
}
|
||||
@@ -228,6 +228,10 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/process-arguments-and-errors.mdx</include>
|
||||
|
||||
### Module path globals
|
||||
|
||||
<include>../limitations/module-path-globals.mdx</include>
|
||||
|
||||
<span id="embedded-javascript-engine" />
|
||||
|
||||
## Runtime code evaluation
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
- **Exit status:** Numeric `process.exitCode = value` writes in statement position set the implicit exit status, which `process.exit()` uses when called without an argument.
|
||||
`process.exit(code)` and `process.exitCode` throw a `RangeError` (`ERR_OUT_OF_RANGE`) for a code that is not a safe integer, and convert other codes to int32, as Node does.
|
||||
Reads, resets, and assignments used as values are unsupported.
|
||||
Reading `process.exitCode` answers the assigned code, or `undefined` before any assignment; `process.exit(code)` assigns it before the `'exit'` listeners run.
|
||||
Resets (`process.exitCode = undefined`) and assignments used as values are unsupported.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
- **Object freezing:** `Object.freeze` and `Object.isFrozen` support plain checked dictionaries and primitives.
|
||||
Freezing is shallow and preserves accessors.
|
||||
Aliased typed records, arrays, proxies, and other native objects cannot be frozen.
|
||||
Class instances support `Object.freeze`, `Object.seal`, `Object.preventExtensions`, `Object.isFrozen`, `Object.isSealed`, and `Object.isExtensible` when the static type of the argument names the class.
|
||||
A write to a read-only field throws Node's `TypeError` in strict mode code and leaves the field unchanged in sloppy mode code.
|
||||
`Object.defineProperty` with a string-literal key changes the value, `writable`, `enumerable`, and `configurable` attributes of a declared field; an object-literal accessor descriptor (`get` or `set`) for a declared field is refused at compile time with `SC2020`, and another accessor descriptor for a declared field throws `SC2020` when the call runs.
|
||||
Freezing, sealing, or preventing extensions of a class instance held in an untyped value throws `SC2020`.
|
||||
A JavaScript class field that is first created after its instance became non-extensible is added instead of throwing.
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
The cursor never stops between the two UTF-16 units of a supplementary character: where Node's Up or Down movement through a multiline line would, the cursor moves to the character's start or end.
|
||||
In JavaScript, a `'history'` listener receives a copy of the history; its changes are written back when the listener returns, but the copy is not `rl.history` itself.
|
||||
`rl.history`, `historySize`, `historyIndex`, `crlfDelay`, `tabSize`, `escapeCodeTimeout`, `line`, `cursor`, and `terminal` read as in Node, except that `rl.cursor` of an interface that is not a terminal throws instead of answering `undefined`.
|
||||
`rl[Symbol.dispose]()` closes the interface, and `instanceof` with `readline.Interface` or `readlinePromises.Interface` answers whether the interface came from that module, as in Node.
|
||||
In JavaScript, the input and output can be any expression that evaluates to a stream, such as a function's result or an object's member; a value that is not a stream is unsupported.
|
||||
In JavaScript, `question()` and `setPrompt()` accept a value that is not a string: when such a prompt is written, an interface that is not a terminal throws `ERR_INVALID_ARG_TYPE` as Node does (the `readline/promises` question rejects with it) and the question stays pending, and a terminal interface writes `String(value)`.
|
||||
A terminal interface's prompt that is an object or a symbol, `getPrompt()` while the prompt is not a string, and `getCursorPos()` with an object or symbol prompt are unsupported.
|
||||
`readline.cursorTo`, `moveCursor`, `clearLine`, and `clearScreenDown` write to `process.stdout`, `process.stderr`, a writable stream, or nothing for `null` and `undefined`; their callbacks take no parameters.
|
||||
The `readline/promises` `Readline` class writes to the same outputs.
|
||||
`readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences.
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
Class instances, builtin objects, and object literals whose methods use `this` throw `SC2020`.
|
||||
Calls through the object record it as `this`, including calls of a method read off the object and called on its own, where Node records `undefined`.
|
||||
`object.method === mock` is `false`, because the field holds a wrapper of the mock.
|
||||
`mock.fn()` without an original, `mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`.
|
||||
`mock.fn()` without an original returns `undefined` and records the arguments of each call, as in Node; with a function type whose return type does not include `undefined`, such as `mock.fn<() => number>()`, it throws `SC2020`.
|
||||
`mock.getter`, `mock.setter`, `mock.property`, `mock.module` and mock options throw `SC2020`.
|
||||
- **Mock timers:** `mock.timers.enable`, `tick`, `runAll`, `reset`, `setTime` and `[Symbol.dispose]()` compile, on the module's `mock` and on `t.mock`, for the `setTimeout`, `setInterval`, `setImmediate`, `Date`, `scheduler.wait` and `AbortSignal.timeout` APIs.
|
||||
`enable()` takes no options or an object literal whose `apis`, when given, is an array literal.
|
||||
Options and `apis` lists that are not literals, and the `MockTimers` object used as a value, throw `SC2020`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
- **Function values:** Immutable aliases of overloaded program functions retain per-call overload resolution and identity.
|
||||
A function stored in a slot with a different signature remains the same function for equality, array searches, `length`, and own properties.
|
||||
Typed rest-parameter functions can be stored and called indirectly.
|
||||
Typed rest-parameter functions can be stored and called indirectly, including through a slot with fixed parameters, where the slot's arguments arrive in the rest array.
|
||||
Builtin function values have API-specific restrictions; see [Builtin function values](#builtin-function-values).
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
`__dirname` and `__filename` are compile-time constants: the absolute path of the module's source directory and file at build time, the same strings as `import.meta.dirname` and `import.meta.filename`.
|
||||
A `.ts` or `.tsx` file keeps them even when it uses `import` syntax, which Node.js would load as an ES module without these globals, because TypeScript projects are commonly shipped as CommonJS bundles where they exist.
|
||||
In such a file `typeof __dirname` is `"string"`, where Node.js running the source directly reports `"undefined"`.
|
||||
JavaScript ES modules (`.mjs`, or `.js` in a `"type": "module"` package) do not have them, as in Node.js.
|
||||
@@ -1,3 +1,4 @@
|
||||
- **Null and optional values:** `undefined` and `null` compile as standalone values and union members.
|
||||
Optional record and class fields and optional, default, and rest parameters also compile, including rest parameters typed `unknown[]` or `any[]`, whose functions take any number of arguments.
|
||||
A function value with fixed parameters cannot be stored where such a variadic function type is expected.
|
||||
A function value whose fixed parameters are all `unknown` or `any`, or that has none, can be stored where such a variadic function type without fixed parameters is expected: each call passes the arguments in order, `undefined` for the ones the caller left out.
|
||||
Other function values with fixed parameters cannot be stored there.
|
||||
|
||||
@@ -55940,7 +55940,9 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:process.'disconnect'",
|
||||
"tests": [
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts"
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -55982,7 +55984,9 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:process.'message'",
|
||||
"tests": [
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts"
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -56365,7 +56369,9 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:process.process.disconnect",
|
||||
"tests": [
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts"
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -57013,7 +57019,9 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:process.process.send",
|
||||
"tests": [
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts"
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -57467,7 +57475,9 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:process.connected",
|
||||
"tests": [
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts"
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -57582,7 +57592,10 @@
|
||||
"tests/corpus/2941-process-exit-code.ts",
|
||||
"tests/corpus/2942-process-exit-code-override.ts",
|
||||
"tests/corpus/2943-process-exit-code-invalid.ts",
|
||||
"tests/corpus/2944-process-exit-code-wrap.ts"
|
||||
"tests/corpus/2944-process-exit-code-wrap.ts",
|
||||
"tests/corpus/6422-process-exitcode-read.ts",
|
||||
"tests/corpus/6423-process-exitcode-default.ts",
|
||||
"tests/corpus/6424-process-exit-keeps-exitcode.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -58969,7 +58982,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59001,7 +59016,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59229,7 +59246,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59290,7 +59309,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59322,7 +59343,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59405,7 +59428,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:readline.createInterface",
|
||||
"tests": [
|
||||
"tests/corpus/1475-readline-closed-stdin.ts"
|
||||
"tests/corpus/1475-readline-closed-stdin.ts",
|
||||
"tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59544,7 +59568,8 @@
|
||||
"confidence": "high",
|
||||
"source": "compiler-dedicated:rl.close",
|
||||
"tests": [
|
||||
"tests/corpus/1475-readline-closed-stdin.ts"
|
||||
"tests/corpus/1475-readline-closed-stdin.ts",
|
||||
"tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59574,7 +59599,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59604,7 +59631,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59634,7 +59663,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59664,7 +59695,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59694,7 +59727,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59724,7 +59759,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59754,7 +59791,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59784,7 +59823,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59904,7 +59945,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59934,7 +59977,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59964,7 +60009,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -60022,7 +60069,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -60052,7 +60101,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -66493,6 +66544,7 @@
|
||||
"source": "compiler-feature:test.mock.fn",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/mock-fn.ts",
|
||||
"tests/fixtures/node-test/cases/mock-fn-default.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
}
|
||||
@@ -76165,6 +76217,8 @@
|
||||
"tests/fixtures/node-types/promisify-crypto.ts",
|
||||
"tests/corpus/5632-promisify-dns.ts",
|
||||
"tests/fixtures/node-types/promisify-dns.ts",
|
||||
"tests/corpus/6428-promisify-callback-functions.ts",
|
||||
"tests/fixtures/node-types/promisify-callback-functions.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -79409,6 +79409,11 @@
|
||||
"tests/corpus/5210-process-memory-usage.ts",
|
||||
"tests/corpus/5211-process-identity.ts",
|
||||
"tests/corpus/5809-process-exit-and-rejections.cjs",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts",
|
||||
"tests/corpus/6422-process-exitcode-read.ts",
|
||||
"tests/corpus/6423-process-exitcode-default.ts",
|
||||
"tests/corpus/6424-process-exit-keeps-exitcode.ts",
|
||||
"tests/corpus/990-process-basics.ts",
|
||||
"tests/corpus/node24-env-files-errors.ts",
|
||||
"tests/corpus/node24-env-files.ts",
|
||||
@@ -79486,7 +79491,9 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:process.'disconnect'",
|
||||
"tests": [
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts"
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -79525,7 +79532,9 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:process.'message'",
|
||||
"tests": [
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts"
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -79938,7 +79947,9 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:process.process.disconnect",
|
||||
"tests": [
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts"
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -80583,7 +80594,9 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:process.process.send",
|
||||
"tests": [
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts"
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -81002,7 +81015,9 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:process.connected",
|
||||
"tests": [
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts"
|
||||
"tests/corpus/2946-child-fork-ipc/main.ts",
|
||||
"tests/corpus/6420-process-send-presence.ts",
|
||||
"tests/corpus/6421-process-send-presence-fork/main.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -81111,7 +81126,10 @@
|
||||
"tests/corpus/2941-process-exit-code.ts",
|
||||
"tests/corpus/2942-process-exit-code-override.ts",
|
||||
"tests/corpus/2943-process-exit-code-invalid.ts",
|
||||
"tests/corpus/2944-process-exit-code-wrap.ts"
|
||||
"tests/corpus/2944-process-exit-code-wrap.ts",
|
||||
"tests/corpus/6422-process-exitcode-read.ts",
|
||||
"tests/corpus/6423-process-exitcode-default.ts",
|
||||
"tests/corpus/6424-process-exit-keeps-exitcode.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -82492,7 +82510,10 @@
|
||||
"tests/corpus/5791-readline-keypress-interfaces.cjs",
|
||||
"tests/corpus/5792-readline-keypress-listeners.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs",
|
||||
"tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
@@ -82546,7 +82567,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -82577,7 +82600,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -82797,7 +82822,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -82879,7 +82906,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -82910,7 +82939,9 @@
|
||||
"tests/corpus/5298-readline-abort.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5872-readline-positional-create-interface.mjs",
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs"
|
||||
"tests/corpus/5873-readline-promises-namespace.cjs",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -82990,7 +83021,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:readline.createInterface",
|
||||
"tests": [
|
||||
"tests/corpus/1475-readline-closed-stdin.ts"
|
||||
"tests/corpus/1475-readline-closed-stdin.ts",
|
||||
"tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83146,7 +83178,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "compiler-dedicated:rl.close",
|
||||
"tests": [
|
||||
"tests/corpus/1475-readline-closed-stdin.ts"
|
||||
"tests/corpus/1475-readline-closed-stdin.ts",
|
||||
"tests/corpus/5878-readline-dynamic-streams.mjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83175,7 +83208,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83204,7 +83239,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83233,7 +83270,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83262,7 +83301,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83291,7 +83332,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83320,7 +83363,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83349,7 +83394,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83378,7 +83425,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83494,7 +83543,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83523,7 +83574,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83552,7 +83605,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83608,7 +83663,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -83637,7 +83694,9 @@
|
||||
"tests/corpus/5295-readline-promises-question.ts",
|
||||
"tests/corpus/5296-readline-lines.ts",
|
||||
"tests/corpus/5297-readline-callback-events.cjs",
|
||||
"tests/corpus/5299-readline-stdin.ts"
|
||||
"tests/corpus/5299-readline-stdin.ts",
|
||||
"tests/corpus/5876-readline-dispose-instanceof.ts",
|
||||
"tests/corpus/5877-readline-question-query.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
@@ -91104,6 +91163,7 @@
|
||||
"tests/fixtures/node-test/cases/ctx-plan.ts",
|
||||
"tests/fixtures/node-test/cases/get-test-context-cancelled.ts",
|
||||
"tests/fixtures/node-test/cases/get-test-context.ts",
|
||||
"tests/fixtures/node-test/cases/mock-fn-default.ts",
|
||||
"tests/fixtures/node-test/cases/mock-fn.ts",
|
||||
"tests/fixtures/node-test/cases/mock-method.ts",
|
||||
"tests/fixtures/node-test/cases/mock-timers-date.ts",
|
||||
@@ -92515,6 +92575,7 @@
|
||||
"evidence": "compiler-feature:test.mock.fn",
|
||||
"tests": [
|
||||
"tests/fixtures/node-test/cases/mock-fn.ts",
|
||||
"tests/fixtures/node-test/cases/mock-fn-default.ts",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -101196,6 +101257,7 @@
|
||||
"tests/corpus/5832-util-types-brands-dynamic.js",
|
||||
"tests/corpus/5835-util-format-integer-specifiers.ts",
|
||||
"tests/corpus/5884-inspect-layout-options.ts",
|
||||
"tests/corpus/6428-promisify-callback-functions.ts",
|
||||
"tests/corpus/node24-deep-equality-properties.js",
|
||||
"tests/corpus/node24-deep-equality.cjs",
|
||||
"tests/corpus/node24-deep-equality.ts",
|
||||
@@ -101222,6 +101284,7 @@
|
||||
"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-callback-functions.ts",
|
||||
"tests/fixtures/node-types/promisify-crypto.ts",
|
||||
"tests/fixtures/node-types/promisify-dns.ts",
|
||||
"tests/fixtures/node-types/promisify-fs.ts",
|
||||
@@ -102880,6 +102943,8 @@
|
||||
"tests/fixtures/node-types/promisify-crypto.ts",
|
||||
"tests/corpus/5632-promisify-dns.ts",
|
||||
"tests/fixtures/node-types/promisify-dns.ts",
|
||||
"tests/corpus/6428-promisify-callback-functions.ts",
|
||||
"tests/fixtures/node-types/promisify-callback-functions.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
"node-builtin.process.versions": ["tests/corpus/process-versions-storage.ts"],
|
||||
"node-builtin.process.rows": ["tests/corpus/2992-stream-rows.ts", "tests/harness/terminal-geometry.test.ts"],
|
||||
"node-builtin.process.columns": ["tests/corpus/1448-stream-columns.ts", "tests/harness/terminal-geometry.test.ts"],
|
||||
"node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts"],
|
||||
"node-builtin.process.exitCode": ["tests/corpus/5809-process-exit-and-rejections.cjs", "tests/corpus/2941-process-exit-code.ts", "tests/corpus/2942-process-exit-code-override.ts", "tests/corpus/2943-process-exit-code-invalid.ts", "tests/corpus/2944-process-exit-code-wrap.ts", "tests/corpus/6422-process-exitcode-read.ts", "tests/corpus/6423-process-exitcode-default.ts", "tests/corpus/6424-process-exit-keeps-exitcode.ts"],
|
||||
"node-builtin.punycode": ["tests/corpus/6122-punycode-conversions.ts", "tests/corpus/6124-punycode-require-warnings.cjs", "tests/corpus/6126-punycode-dependency/main.cjs"],
|
||||
"node-builtin.querystring": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2917-builtin-callable-widths.ts"],
|
||||
"node-builtin.readline": ["tests/corpus/1475-readline-closed-stdin.ts"],
|
||||
@@ -199,13 +199,13 @@
|
||||
{"chapter": "events", "symbols": ["CustomEvent", "detail"], "status": "partial", "evidence": ["tests/corpus/5130-event-target-dispatch.js", "tests/corpus/5132-event-target-typed.ts"]},
|
||||
{"chapter": "events", "symbols": ["events.errorMonitor"], "status": "partial", "evidence": ["tests/corpus/5134-events-error-monitor.ts", "tests/corpus/5136-events-error-monitor-string-name.ts", "tests/corpus/5137-events-error-monitor-computed-name.js", "tests/corpus/5138-events-error-monitor-shared-listener.ts"]},
|
||||
{"chapter": "events", "symbols": ["events.addAbortListener"], "status": "partial", "evidence": ["tests/corpus/5133-abort-signal-event-target.ts"]},
|
||||
{"chapter": "readline", "symbols": ["readlinePromises.Interface", "readlinePromises.createInterface", "rl.question"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", "tests/corpus/5873-readline-promises-namespace.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["readlinePromises.Interface", "readlinePromises.createInterface", "rl.question"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5298-readline-abort.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5872-readline-positional-create-interface.mjs", "tests/corpus/5873-readline-promises-namespace.cjs", "tests/corpus/5876-readline-dispose-instanceof.ts", "tests/corpus/5877-readline-question-query.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["'history'", "rl.line"], "status": "partial", "evidence": ["tests/corpus/5780-readline-history-options.ts", "tests/corpus/5781-readline-history-options.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["'SIGINT'", "'SIGTSTP'", "'SIGCONT'", "line", "cursor", "completer"], "status": "partial", "evidence": ["tests/corpus/5782-readline-terminal-editing.ts", "tests/corpus/5783-readline-terminal-completion.cjs", "tests/corpus/5784-readline-terminal-promises.ts", "tests/corpus/5785-readline-terminal-keys.ts", "tests/corpus/5786-readline-terminal-history.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["readline.cursorTo", "readline.moveCursor", "readline.clearLine", "readline.clearScreenDown", "rl.getCursorPos"], "status": "partial", "evidence": ["tests/corpus/5787-readline-cursor-helpers.cjs", "tests/corpus/5789-readline-cursor-pos.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["readlinePromises.Readline", "rl.clearLine", "rl.clearScreenDown", "rl.commit", "rl.cursorTo", "rl.moveCursor", "rl.rollback"], "status": "partial", "evidence": ["tests/corpus/5788-readline-promises-readline.ts", "tests/corpus/5789-readline-cursor-pos.cjs"]},
|
||||
{"chapter": "readline", "symbols": ["readline.emitKeypressEvents"], "status": "partial", "evidence": ["tests/corpus/5790-readline-keypress-events.ts", "tests/corpus/5791-readline-keypress-interfaces.cjs", "tests/corpus/5792-readline-keypress-listeners.ts"]},
|
||||
{"chapter": "readline", "symbols": ["readline.Interface", "rl.pause", "rl.resume", "rl.prompt", "rl.setPrompt", "rl.getPrompt", "rl.write", "rl", "'close'", "'error'", "'line'", "'pause'", "'resume'"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", "tests/corpus/5299-readline-stdin.ts"]},
|
||||
{"chapter": "readline", "symbols": ["readline.Interface", "rl.pause", "rl.resume", "rl.prompt", "rl.setPrompt", "rl.getPrompt", "rl.write", "rl", "'close'", "'error'", "'line'", "'pause'", "'resume'"], "status": "partial", "evidence": ["tests/corpus/5295-readline-promises-question.ts", "tests/corpus/5296-readline-lines.ts", "tests/corpus/5297-readline-callback-events.cjs", "tests/corpus/5299-readline-stdin.ts", "tests/corpus/5876-readline-dispose-instanceof.ts", "tests/corpus/5877-readline-question-query.cjs"]},
|
||||
{"chapter": "stream", "symbols": ["stream.addAbortSignal"], "status": "partial", "evidence": ["tests/corpus/5300-stream-add-abort-signal.ts", "tests/corpus/5301-stream-add-abort-signal-errors.cjs"]},
|
||||
{"chapter": "stream", "symbols": ["readable.map", "readable.filter", "readable.forEach", "readable.toArray", "readable.some", "readable.find", "readable.every", "readable.flatMap", "readable.drop", "readable.take", "readable.reduce"], "status": "partial", "evidence": ["tests/corpus/5335-stream-operators-values.ts", "tests/corpus/5336-stream-operators-promise-timing.ts", "tests/corpus/5337-stream-operators-concurrency.ts", "tests/corpus/5338-stream-operators-checks.cjs"]},
|
||||
{"chapter": "stream", "symbols": ["stream.Readable.from"], "status": "partial", "evidence": ["tests/corpus/5340-readable-from-options.ts", "tests/corpus/5341-readable-from-dynamic.cjs", "tests/corpus/5420-readable-from-generators.ts", "tests/corpus/5421-readable-from-generators.cjs", "tests/corpus/5610-readable-from-sets-and-views.ts", "tests/corpus/5611-readable-from-class-iterables.ts", "tests/corpus/5612-readable-from-iterables.cjs"]},
|
||||
@@ -341,14 +341,14 @@
|
||||
{ "chapter": "fs", "signatures": ["`fsPromises.lstat(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2941-fsp-lstat-identity.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`fs.globSync(pattern[, options])`", "`fs.glob(pattern[, options], callback)`", "`fsPromises.glob(pattern[, options])`"], "status": "partial", "evidence": ["tests/corpus/5448-fs-glob-patterns.ts", "tests/corpus/5449-fs-glob-options.ts", "tests/corpus/5450-fs-glob-validation.js", "tests/corpus/5451-fs-promises-glob.ts", "tests/corpus/5452-fs-glob-callback.ts", "tests/fixtures/node-types/fs-glob.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`fs.cpSync(src, dest[, options])`", "`fs.cp(src, dest[, options], callback)`", "`fsPromises.cp(src, dest[, options])`"], "status": "partial", "evidence": ["tests/corpus/5567-fs-cpsync.ts", "tests/corpus/5568-fs-cp-errors.js", "tests/corpus/5569-fs-cp-symlinks.ts", "tests/corpus/5570-fs-cp-async.ts"] },
|
||||
{ "chapter": "process", "signatures": ["`process.send(message[, sendHandle[, options]][, callback])`", "`process.disconnect()`", "`connected` Type: {boolean}", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts"] },
|
||||
{ "chapter": "process", "signatures": ["`process.send(message[, sendHandle[, options]][, callback])`", "`process.disconnect()`", "`connected` Type: {boolean}", "Event: `'message'`", "Event: `'disconnect'`"], "status": "partial", "evidence": ["tests/corpus/2946-child-fork-ipc/main.ts", "tests/corpus/6420-process-send-presence.ts", "tests/corpus/6421-process-send-presence-fork/main.ts"] },
|
||||
{ "chapter": "fs", "symbols": ["fs.Stats", "stats.isBlockDevice", "stats.isCharacterDevice", "stats.isDirectory", "stats.isFIFO", "stats.isFile", "stats.isSocket", "stats.isSymbolicLink"], "status": "partial", "evidence": ["tests/corpus/5541-fs-stats-fields.cjs"] },
|
||||
{ "chapter": "fs", "parent": "Class: `fs.Stats`", "signatures": ["`mode` Type: {number|bigint}", "`nlink` Type: {number|bigint}", "`uid` Type: {number|bigint}", "`gid` Type: {number|bigint}", "`rdev` Type: {number|bigint}", "`size` Type: {number|bigint}", "`blksize` Type: {number|bigint}", "`blocks` Type: {number|bigint}", "`atimeMs` Type: {number|bigint}", "`mtimeMs` Type: {number|bigint}", "`birthtimeMs` Type: {number|bigint}", "`atimeNs` Type: {bigint}", "`mtimeNs` Type: {bigint}", "`ctimeNs` Type: {bigint}", "`birthtimeNs` Type: {bigint}", "`atime` Type: {Date}", "`mtime` Type: {Date}", "`ctime` Type: {Date}", "`birthtime` Type: {Date}"], "status": "partial", "evidence": ["tests/corpus/5541-fs-stats-fields.cjs"] },
|
||||
{ "chapter": "fs", "signatures": ["`dev` Type: {number|bigint}", "`ino` Type: {number|bigint}", "`ctimeMs` Type: {number|bigint}"], "status": "partial", "evidence": ["tests/corpus/2941-fsp-lstat-identity.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`filehandle[Symbol.asyncDispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`fsPromises.realpath(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2940-fsp-realpath.ts"] },
|
||||
{ "chapter": "fs", "signatures": ["`fsPromises.readdir(path[, options])`"], "status": "partial", "evidence": ["tests/corpus/2942-fsp-readdir-dirent.ts"] },
|
||||
{ "chapter": "readline", "signatures": ["`rl[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "readline", "signatures": ["`rl[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts", "tests/corpus/5876-readline-dispose-instanceof.ts"] },
|
||||
{ "chapter": "timers", "signatures": ["`immediate[Symbol.dispose]()`", "`timeout[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/corpus/2844-node-disposable-resources.ts"] },
|
||||
{ "chapter": "stream", "signatures": ["`readable[Symbol.asyncIterator]()`", "`readable.iterator([options])`"], "status": "partial", "evidence": ["tests/corpus/1746-stream-for-await.ts", "tests/corpus/2861-readable-iterator-close.ts"] },
|
||||
{ "chapter": "stream", "signatures": ["`writable.destroy([error])`"], "status": "partial", "evidence": ["tests/corpus/2897-child-stdin-destroy.ts"] },
|
||||
@@ -370,7 +370,7 @@
|
||||
{ "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.fn"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-fn.ts", "tests/fixtures/node-test/cases/mock-fn-default.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"] },
|
||||
{ "chapter": "test", "parent": "Class: `MockTimers`", "symbols": ["timers.enable", "timers.reset", "timers.tick", "timers.runAll", "timers.setTime"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers.ts", "tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/corpus/5813-mock-timers-module.ts", "tests/corpus/5814-mock-timers-date.ts", "tests/harness/node-test.test.ts"] },
|
||||
{ "chapter": "test", "parent": "Class: `MockTimers`", "signatures": ["`timers[Symbol.dispose]()`"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/mock-timers-date.ts", "tests/harness/node-test.test.ts"] },
|
||||
@@ -378,7 +378,7 @@
|
||||
{ "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": ["getTestContext"], "status": "partial", "evidence": ["tests/fixtures/node-test/cases/get-test-context.ts", "tests/fixtures/node-test/cases/get-test-context-cancelled.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/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": "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/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.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"] }
|
||||
@@ -416,7 +416,7 @@
|
||||
{ "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] },
|
||||
{ "symbols": ["net.SocketAddress", "net.BlockList"], "status": "partial", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] },
|
||||
{ "symbols": ["SocketAddress.parse", "BlockList.isBlockList", "blockList.addAddress", "blockList.addAddresses", "blockList.addCIDR", "blockList.addCIDRs", "blockList.addRange", "blockList.addSubnet", "blockList.check", "blockList.clear", "blockList.fromJSON", "blockList.removeAddress", "blockList.removeCIDR", "blockList.removeRange", "blockList.removeSubnet", "blockList.toJSON"], "status": "supported", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] },
|
||||
{ "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts"] },
|
||||
{ "symbols": ["readline.createInterface", "rl.close"], "status": "partial", "evidence": ["tests/corpus/1475-readline-closed-stdin.ts", "tests/corpus/5878-readline-dynamic-streams.mjs"] },
|
||||
{ "symbols": ["test", "it", "test.skip", "test.todo", "test.only", "describe", "suite", "before", "after", "beforeEach", "afterEach", "context.test", "context.skip", "context.todo", "context.assert"], "status": "partial", "evidence": ["tests/harness/node-test.test.ts"] },
|
||||
{ "symbols": ["http.request", "http.get", "http.createServer", "https.request", "https.get", "https.createServer", "request.end", "request.destroy", "request.setTimeout", "request.write", "response.end", "response.getHeader", "response.hasHeader", "response.removeHeader", "response.setHeader", "response.write", "response.writeHead", "headersTimeout", "requestTimeout", "timeout", "keepAliveTimeout", "keepAliveTimeoutBuffer"], "status": "partial", "evidence": ["tests/corpus/1780-http-res-surface.ts", "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts"] }
|
||||
]
|
||||
|
||||
+17
-2
@@ -3372,6 +3372,13 @@ declare module "util" {
|
||||
info: import("node:crypto").BinaryLike,
|
||||
keylen: number,
|
||||
) => Promise<ArrayBuffer>;
|
||||
/* A callback-only function (`obj.close.bind(obj)`): before the targets
|
||||
* whose leading parameters would otherwise capture it. Builtins that take
|
||||
* arguments before their callback never match these. */
|
||||
export function promisify<TResult>(
|
||||
fn: (callback: (err: any, result: TResult) => void) => void,
|
||||
): () => Promise<TResult>;
|
||||
export function promisify(fn: (callback: (err?: any) => void) => void): () => Promise<void>;
|
||||
export function promisify(
|
||||
fn: typeof import("node:crypto").randomFill,
|
||||
): <T extends ArrayBufferView | ArrayBufferLike>(
|
||||
@@ -4006,7 +4013,10 @@ declare module "readline" {
|
||||
meta?: boolean;
|
||||
shift?: boolean;
|
||||
}
|
||||
export interface Interface extends Disposable {
|
||||
export class Interface implements Disposable {
|
||||
/* createInterface creates interfaces; instanceof tells the two
|
||||
* modules' classes apart. */
|
||||
protected constructor();
|
||||
/* terminal says whether the interface edits lines as a terminal;
|
||||
* line and cursor are the terminal's (line is '' otherwise). history
|
||||
* is the history option itself (or the interface's own empty array);
|
||||
@@ -4121,7 +4131,10 @@ declare module "readline/promises" {
|
||||
/* The readline Interface with the promise question: it resolves with
|
||||
* the next line, rejects with an AbortError when options.signal aborts,
|
||||
* and with ERR_USE_AFTER_CLOSE after close(). */
|
||||
export interface Interface extends Disposable {
|
||||
export class Interface implements Disposable {
|
||||
/* createInterface creates interfaces; instanceof tells the two
|
||||
* modules' classes apart. */
|
||||
protected constructor();
|
||||
/* terminal says whether the interface edits lines as a terminal;
|
||||
* line and cursor are the terminal's (line is '' otherwise). history
|
||||
* is the history option itself (or the interface's own empty array);
|
||||
@@ -5238,6 +5251,8 @@ declare module "tls" {
|
||||
getProtocol(): string | null;
|
||||
/* OpenSSL's suite name, the IANA name and the suite's version */
|
||||
getCipher(): CipherNameAndProtocol;
|
||||
/* whether the handshake resumed a cached session */
|
||||
isSessionReused(): boolean;
|
||||
}
|
||||
export interface CipherNameAndProtocol {
|
||||
name: string;
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
import { f64Lit } from "./common.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { emitTruncF64 } from "./trunc.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js";
|
||||
|
||||
/** Supported LLVM targets are little-endian. All memory accesses use align
|
||||
* 1: Buffer fields and DataView windows can start at arbitrary byte offsets. */
|
||||
@@ -79,7 +80,7 @@ export function emitByteNumber(
|
||||
capacity = B.tmp();
|
||||
room = B.tmp();
|
||||
B.line(`${lengthPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver.name}, i64 0, i32 1`);
|
||||
B.line(`${length} = load ${size}, ptr ${lengthPtr}`);
|
||||
B.line(`${length} = load ${size}, ptr ${lengthPtr}${bytesHeaderAlias(host)}`);
|
||||
B.line(`${room} = icmp uge ${size} ${length}, ${spec.width}`);
|
||||
B.line(`${capacity} = sub ${size} ${length}, ${spec.width}`);
|
||||
if (integer) {
|
||||
@@ -221,14 +222,14 @@ export function emitByteNumber(
|
||||
}
|
||||
}
|
||||
const stored = endianBits(host, raw, spec.width, little);
|
||||
B.line(`store i${bits} ${stored}, ptr ${pointer}, align 1`);
|
||||
B.line(`store i${bits} ${stored}, ptr ${pointer}, align 1${bytesElementAlias(host)}`);
|
||||
if (!spec.dataView) {
|
||||
fastResult = B.tmp();
|
||||
B.line(`${fastResult} = fadd double ${offset.name}, ${f64Lit(spec.width)}`);
|
||||
}
|
||||
} else {
|
||||
const loaded = B.tmp();
|
||||
B.line(`${loaded} = load i${bits}, ptr ${pointer}, align 1`);
|
||||
B.line(`${loaded} = load i${bits}, ptr ${pointer}, align 1${bytesElementAlias(host)}`);
|
||||
const raw = endianBits(host, loaded, spec.width, little);
|
||||
fastResult = B.tmp();
|
||||
if (spec.floating) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
/** Alias categories of typed-array storage. A ScrBytes header (length,
|
||||
* data pointer, shared backing) is fixed at construction and only ever
|
||||
* written by runtime code, never by emitted code. Element storage is one
|
||||
* category for every element type: views of different types may share a
|
||||
* buffer. Neither overlaps the other, a native field, array storage or a
|
||||
* module global's slot, so element stores leave header loads and global
|
||||
* receiver slots loop-invariant. Untagged accesses and calls still alias
|
||||
* every category. */
|
||||
export function bytesHeaderAlias(host: LlvmEmitterContext): string {
|
||||
return host.memoryAliasAttachment("bytes:header");
|
||||
}
|
||||
|
||||
export function bytesElementAlias(host: LlvmEmitterContext): string {
|
||||
return host.memoryAliasAttachment("bytes:elements");
|
||||
}
|
||||
|
||||
/** A module global's slot is its own category for whole-slot loads of a
|
||||
* scalar or typed-array binding. Stores stay untagged (and so alias it). */
|
||||
export function globalSlotAlias(host: LlvmEmitterContext, id: string, type: string): string {
|
||||
return type === "bytes" || type === "f64" ? host.memoryAliasAttachment(`global:${id}`) : "";
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrModule,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
} from "../../ir/ir.js";
|
||||
import { analyzeIntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import { matchIntegerCountedForLoop } from "../../ir/integer-loops.js";
|
||||
import { findInitializedByteLoopBindings } from "./byte-windows.js";
|
||||
import { matchBytesLoopVersion, type BytesLoopVersion } from "./bytes-loop-versions.js";
|
||||
import { emitLlvmModule } from "./emitter.js";
|
||||
|
||||
const loc = { file: "versions.ts", start: 0, end: 0 };
|
||||
const f64s: IrType = { kind: "bytes", elem: "f64" };
|
||||
const i32s: IrType = { kind: "bytes", elem: "i32" };
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const ref = (localId: string, type: IrType = F64): IrExpr => ({
|
||||
kind: "varRef",
|
||||
localId,
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
const bin = (op: string, left: IrExpr, right: IrExpr): IrExpr =>
|
||||
({ kind: "bin", op, left, right, type: F64, loc }) as IrExpr;
|
||||
const get = (receiver: IrExpr, index: IrExpr): IrExpr => ({
|
||||
kind: "bytesIntrinsic",
|
||||
method: "get",
|
||||
receiver,
|
||||
args: [index],
|
||||
invalidNaN: true,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const set = (arr: IrExpr, index: IrExpr, value: IrExpr): IrStmt => ({
|
||||
kind: "bytesSet",
|
||||
arr,
|
||||
index,
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
const counted = (
|
||||
body: IrStmt[],
|
||||
limit: IrExpr = ref("n"),
|
||||
counter = "i",
|
||||
): IrStmt & { kind: "for" } => ({
|
||||
kind: "for",
|
||||
init: { kind: "varDecl", localId: counter, init: num(0), loc },
|
||||
cond: { kind: "bin", op: "<", left: ref(counter), right: limit, type: BOOL, loc },
|
||||
update: { kind: "assign", localId: counter, value: bin("+", ref(counter), num(1)), loc },
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
const local = (
|
||||
id: string,
|
||||
type: IrType,
|
||||
mutable = false,
|
||||
extra: Partial<IrLocal> = {},
|
||||
): IrLocal => ({
|
||||
id,
|
||||
name: id,
|
||||
type,
|
||||
mutable,
|
||||
...extra,
|
||||
});
|
||||
|
||||
function fn(body: IrStmt[], extra: IrLocal[] = []): IrFunction {
|
||||
return {
|
||||
name: "kernel",
|
||||
params: [
|
||||
{ localId: "out", name: "out", type: f64s },
|
||||
{ localId: "src", name: "src", type: f64s },
|
||||
{ localId: "table", name: "table", type: i32s },
|
||||
{ localId: "n", name: "n", type: F64 },
|
||||
{ localId: "k", name: "k", type: F64 },
|
||||
],
|
||||
returnType: VOID,
|
||||
locals: [
|
||||
local("out", f64s),
|
||||
local("src", f64s),
|
||||
local("table", i32s),
|
||||
local("n", F64),
|
||||
local("k", F64),
|
||||
local("i", F64, true),
|
||||
local("j", F64, true),
|
||||
...extra,
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function match(
|
||||
f: IrFunction,
|
||||
loop: IrStmt & { kind: "for" },
|
||||
captured: string[] = [],
|
||||
): BytesLoopVersion | null {
|
||||
const ranges = analyzeIntegerRanges(f);
|
||||
const locals = new Map(f.locals.map((l) => [l.id, l]));
|
||||
return matchBytesLoopVersion(loop, matchIntegerCountedForLoop(loop, locals, ranges), {
|
||||
locals,
|
||||
invariantGlobals: new Set(),
|
||||
captured: new Set(captured),
|
||||
ranges,
|
||||
initialized: findInitializedByteLoopBindings(f).get(loop) ?? new Set(),
|
||||
proven: new Set(),
|
||||
});
|
||||
}
|
||||
|
||||
test("a counted loop's affine accesses get one guard and exact operand ranges", () => {
|
||||
const shifted = bin("+", ref("i"), num(1));
|
||||
const loop = counted([set(ref("out", f64s), ref("i"), get(ref("src", f64s), shifted))]);
|
||||
const version = match(fn([loop]), loop)!;
|
||||
expect(version).not.toBeNull();
|
||||
expect(version.bounds.size).toBe(2);
|
||||
expect(version.receivers.map((r) => r.ref.kind === "varRef" && r.ref.localId)).toEqual([
|
||||
"out",
|
||||
"src",
|
||||
]);
|
||||
expect(version.affine.map((c) => [c.scale, c.minOffset, c.maxEnd])).toEqual([
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
]);
|
||||
// `n` is unproven: the guard bounds the counter so `i + 1` stays exact.
|
||||
expect(version.counted?.limitGuarded).toBe(true);
|
||||
expect(version.counterBound).toBe(Number.MAX_SAFE_INTEGER - 1);
|
||||
expect(version.ranges.get(shifted)).toEqual({ min: 1, max: Number.MAX_SAFE_INTEGER });
|
||||
});
|
||||
|
||||
test("statically ranged indices compare one constant with the length", () => {
|
||||
const masked = bin("&", bin("^", ref("i"), ref("k")), num(255));
|
||||
const loop = counted([set(ref("out", f64s), num(0), get(ref("table", i32s), masked))]);
|
||||
const version = match(fn([loop]), loop)!;
|
||||
expect(version.fixed.map((c) => [version.receivers[c.receiver]!.ref, c.end])).toEqual([
|
||||
[ref("out", f64s), 0],
|
||||
[ref("table", i32s), 255],
|
||||
]);
|
||||
expect(version.affine).toEqual([]);
|
||||
expect(version.counted).toBeNull();
|
||||
});
|
||||
|
||||
test("scaled counters and body temps resolve to their receiver and index", () => {
|
||||
const temps = [local("view", f64s), local("at", F64)];
|
||||
const at = bin("+", bin("*", ref("i"), num(4)), num(3));
|
||||
const loop = counted([
|
||||
{ kind: "varDecl", localId: "view", init: ref("src", f64s), loc },
|
||||
{ kind: "varDecl", localId: "at", init: at, loc },
|
||||
set(ref("view", f64s), ref("at"), num(1)),
|
||||
// (The integer counted loop needs one direct counter index.)
|
||||
set(ref("out", f64s), ref("i"), num(0)),
|
||||
]);
|
||||
const version = match(fn([loop], temps), loop)!;
|
||||
expect(version.receivers.map((r) => r.ref)).toEqual([ref("src", f64s), ref("out", f64s)]);
|
||||
expect(version.affine.map((c) => [c.scale, c.minOffset, c.maxEnd])).toEqual([
|
||||
[4, 3, 3],
|
||||
[1, 0, 0],
|
||||
]);
|
||||
expect(version.ranges.has(at)).toBe(true);
|
||||
});
|
||||
|
||||
test("negative factors, rebinding, captures and outer loops are not versioned", () => {
|
||||
// `i * -1` is -0 at zero, so it is not treated as an affine operand.
|
||||
const negative = counted([set(ref("out", f64s), bin("*", ref("i"), num(-1)), num(1))]);
|
||||
expect(match(fn([negative]), negative)).toBeNull();
|
||||
const rebound = counted([
|
||||
set(ref("out", f64s), ref("i"), num(1)),
|
||||
{ kind: "assign", localId: "out", value: ref("src", f64s), loc },
|
||||
]);
|
||||
expect(match(fn([rebound]), rebound)).toBeNull();
|
||||
const plain = counted([set(ref("out", f64s), ref("i"), num(1))]);
|
||||
expect(match(fn([plain]), plain, ["out"])).toBeNull();
|
||||
const inner = counted([set(ref("out", f64s), ref("j"), num(1))], ref("n"), "j");
|
||||
const outer = counted([inner]);
|
||||
expect(match(fn([outer]), outer)).toBeNull();
|
||||
expect(match(fn([outer]), inner)).not.toBeNull();
|
||||
});
|
||||
|
||||
test("emitted loops version the body and tag typed-array memory", () => {
|
||||
const loop = counted([
|
||||
set(ref("out", f64s), ref("i"), bin("*", get(ref("src", f64s), ref("i")), num(2))),
|
||||
]);
|
||||
const kernel = fn([loop]);
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
kernel,
|
||||
],
|
||||
};
|
||||
const llvm = emitLlvmModule(mod);
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(llvm)![0];
|
||||
expect(body).toContain("bytes.version.fast");
|
||||
expect(body).toContain("bytes.version.slow");
|
||||
// The checked copy keeps its per-element tests; the guard reads lengths.
|
||||
expect(body).toContain("bytes.index.invalid");
|
||||
expect(body).toMatch(/store double [^\n]*, align 1, !tbaa !\d+/);
|
||||
expect(body).toMatch(/load double, ptr [^\n]*, align 1, !tbaa !\d+/);
|
||||
expect(body).toMatch(/load i64, ptr [^\n]*, !tbaa !\d+/);
|
||||
expect(body).toMatch(/load ptr, ptr [^\n]*, !tbaa !\d+/);
|
||||
const elements = /store double [^\n]*!tbaa !(\d+)/.exec(body)![1];
|
||||
const header = /load ptr, ptr [^\n]*!tbaa !(\d+)/.exec(body)![1];
|
||||
expect(elements).not.toBe(header);
|
||||
});
|
||||
|
||||
test("Float16Array accesses version in elements and share the element tag", () => {
|
||||
const f16s: IrType = { kind: "bytes", elem: "f16" };
|
||||
const withHalves = (body: IrStmt[]): IrFunction => {
|
||||
const f = fn(body, [local("half", f16s)]);
|
||||
return { ...f, params: [...f.params, { localId: "half", name: "half", type: f16s }] };
|
||||
};
|
||||
// half[i] = src[i + 1]; out[i] = half[i] * 2
|
||||
const loop = counted([
|
||||
set(ref("half", f16s), ref("i"), get(ref("src", f64s), bin("+", ref("i"), num(1)))),
|
||||
set(ref("out", f64s), ref("i"), bin("*", get(ref("half", f16s), ref("i")), num(2))),
|
||||
]);
|
||||
const kernel = withHalves([loop]);
|
||||
const version = match(kernel, loop)!;
|
||||
expect(version.bounds.size).toBe(4);
|
||||
expect(version.receivers.map((r) => r.ref)).toEqual([
|
||||
ref("half", f16s),
|
||||
ref("src", f64s),
|
||||
ref("out", f64s),
|
||||
]);
|
||||
// Element offsets against the element count, whatever the element size.
|
||||
expect(version.affine.map((c) => [c.scale, c.minOffset, c.maxEnd])).toEqual([
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
[1, 0, 0],
|
||||
]);
|
||||
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
kernel,
|
||||
],
|
||||
};
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
const fast = /^bytes\.version\.fast\d+:$[^]*?^bytes\.version\.done\d+:$/m.exec(body)![0];
|
||||
expect(fast).toContain("call double @scr_f16_to_f64");
|
||||
expect(fast).toContain("call zeroext i16 @scr_f16_from_f64");
|
||||
expect(fast).not.toContain("bytes.index");
|
||||
const halfStore = /store i16 [^\n]*, align 1, !tbaa !(\d+)/.exec(fast)![1];
|
||||
const halfLoad = /load i16, ptr [^\n]*, align 1, !tbaa !(\d+)/.exec(fast)![1];
|
||||
const doubleStore = /store double [^\n]*, align 1, !tbaa !(\d+)/.exec(fast)![1];
|
||||
// Views of any element type may share a buffer: one element category.
|
||||
expect(halfStore).toBe(doubleStore);
|
||||
expect(halfLoad).toBe(doubleStore);
|
||||
// The checked copy keeps its per-element tests.
|
||||
expect(body.slice(0, body.indexOf(fast))).toContain("bytes.index.invalid");
|
||||
});
|
||||
|
||||
test("bodies compiled without optimization keep one checked loop", () => {
|
||||
const loop = counted([set(ref("out", f64s), ref("i"), get(ref("src", f64s), ref("i")))]);
|
||||
const boxes = Array.from({ length: 512 }, (_, k) => local(`box${k}`, F64, true, { boxed: true }));
|
||||
const mod: IrModule = {
|
||||
irVersion: 17,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
fn([loop], boxes),
|
||||
],
|
||||
};
|
||||
const body = /^define internal [^\n]*@sc_bf_kernel[^]*?^}/m.exec(emitLlvmModule(mod))![0];
|
||||
expect(body.split("\n", 1)[0]).toContain("optnone");
|
||||
expect(body).not.toContain("bytes.version");
|
||||
expect(body).toContain("bytes.index.invalid");
|
||||
});
|
||||
@@ -0,0 +1,578 @@
|
||||
import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js";
|
||||
import type { IntegerRange, IntegerRanges } from "../../ir/integer-ranges.js";
|
||||
import type { IntegerCountedForLoop } from "../../ir/integer-loops.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { emitCountedLoopLimit, emitCountedLoopSafety } from "./counted-loops.js";
|
||||
import { emitBytesHeaderLength } from "./expr-bytes.js";
|
||||
|
||||
type Node = IrExpr | IrStmt;
|
||||
type VarRef = IrExpr & { kind: "varRef" };
|
||||
|
||||
/** One loop-invariant integer operand of an affine index. */
|
||||
interface Term {
|
||||
expr: IrExpr;
|
||||
/** 1 or -1. */
|
||||
sign: number;
|
||||
}
|
||||
|
||||
interface KeyedTerm extends Term {
|
||||
key: string;
|
||||
}
|
||||
|
||||
/** Every access of `receiver` at a static index range ends below `end + 1`. */
|
||||
interface FixedCheck {
|
||||
receiver: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/** Every access of `receiver` at `scale * counter + Σ terms + offset` has an
|
||||
* offset in [minOffset, maxEnd - width + 1]; `maxEnd` includes the width. */
|
||||
interface AffineCheck {
|
||||
receiver: number;
|
||||
scale: number;
|
||||
terms: KeyedTerm[];
|
||||
minOffset: number;
|
||||
maxEnd: number;
|
||||
}
|
||||
|
||||
export interface BytesLoopVersion {
|
||||
/** Typed-array accesses that are in bounds whenever the guard holds. */
|
||||
bounds: ReadonlySet<Node>;
|
||||
/** Root receiver bindings, read once before the loop. */
|
||||
receivers: { ref: VarRef; global: boolean }[];
|
||||
fixed: FixedCheck[];
|
||||
affine: AffineCheck[];
|
||||
/** The counted loop whose iteration space the affine checks cover. */
|
||||
counted: IntegerCountedForLoop | null;
|
||||
/** The guard also requires every counter value within [-bound, bound]. */
|
||||
counterBound: number | null;
|
||||
/** Integer ranges of affine index operands that hold in the unchecked
|
||||
* loop (where the guard bounds the counter). */
|
||||
ranges: ReadonlyMap<IrExpr, IntegerRange>;
|
||||
}
|
||||
|
||||
export interface BytesLoopFacts {
|
||||
/** The function's own locals (not module globals). */
|
||||
locals: ReadonlyMap<string, IrLocal>;
|
||||
/** Module globals whose slot no call can replace: stable constants and
|
||||
* the private bindings of a guarded initializer. */
|
||||
invariantGlobals: ReadonlySet<string>;
|
||||
captured: ReadonlySet<string>;
|
||||
ranges: IntegerRanges;
|
||||
/** Locals definitely initialized when the loop is entered. */
|
||||
initialized: ReadonlySet<string>;
|
||||
/** Accesses already proved in bounds without a guard. */
|
||||
proven: ReadonlySet<Node>;
|
||||
}
|
||||
|
||||
const MAX_COST = 400;
|
||||
const MAX_FIXED_END = 2 ** 32;
|
||||
const MAX_CHECKS = 8;
|
||||
const MAX_OFFSET = 2 ** 31;
|
||||
const SAFE = Number.MAX_SAFE_INTEGER;
|
||||
const INVARIANT_OPS = new Set(["+", "-", "*", "&", "|", "^", "<<", ">>", ">>>"]);
|
||||
|
||||
/** Loop versioning for typed-array bounds. A typed array's length and data
|
||||
* pointer are fixed at construction (ScrBytes has no resize, transfer or
|
||||
* detach), so an access is in bounds for the whole loop when its receiver
|
||||
* binding and index operands cannot change while it runs. One test before
|
||||
* the loop then covers every iteration:
|
||||
*
|
||||
* - a statically ranged integer index `[lo, hi]` (lo >= 0) needs
|
||||
* `hi < length`, such as `table[x & 0xff]` or a constant-trip counter;
|
||||
* - an index `k * counter + invariant + c` of a counted loop needs its first
|
||||
* and last values in bounds, such as `out[base + i]` for `i < n` or
|
||||
* `view.getUint32(i * 4)`.
|
||||
*
|
||||
* Only small synchronous loops are versioned (nested loops included), and
|
||||
* the emitter bounds how deeply versions nest. The checked loop runs
|
||||
* whenever the guard fails, and keeps every out-of-bounds read, ignored
|
||||
* write and NaN-index behavior. */
|
||||
export function matchBytesLoopVersion(
|
||||
loop: IrStmt & { kind: "for" },
|
||||
counted: IntegerCountedForLoop | null,
|
||||
facts: BytesLoopFacts,
|
||||
): BytesLoopVersion | null {
|
||||
const written = new Set<string>();
|
||||
const temps = new Map<string, IrExpr>();
|
||||
let cost = 0;
|
||||
const scanExpr = (e: IrExpr): boolean => {
|
||||
if (++cost > MAX_COST) return false;
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") written.add(e.localId);
|
||||
return everyExprChild(e, scanExpr, scanStmt);
|
||||
};
|
||||
const scanStmt = (s: IrStmt): boolean => {
|
||||
if (++cost > MAX_COST) return false;
|
||||
switch (s.kind) {
|
||||
case "forOf":
|
||||
written.add(s.localId);
|
||||
break;
|
||||
case "varDecl": {
|
||||
if (temps.has(s.localId) || written.has(s.localId)) temps.delete(s.localId);
|
||||
else if (s.init) temps.set(s.localId, s.init);
|
||||
written.add(s.localId);
|
||||
break;
|
||||
}
|
||||
case "assign":
|
||||
written.add(s.localId);
|
||||
break;
|
||||
case "tryCatch":
|
||||
if (s.catchLocalId) written.add(s.catchLocalId);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return everyStmtChild(s, scanExpr, scanStmt);
|
||||
};
|
||||
// The guard runs before the initializer, so a binding the initializer
|
||||
// writes is not invariant either: `for (let i = (a = b, 0); ...)` must
|
||||
// not keep the bounds the guard proved for the old `a`.
|
||||
if (
|
||||
(loop.init !== null && !scanStmt(loop.init)) ||
|
||||
!loop.body.every(scanStmt) ||
|
||||
(loop.cond !== null && !scanExpr(loop.cond)) ||
|
||||
(loop.update !== null && !scanStmt(loop.update))
|
||||
)
|
||||
return null;
|
||||
// A body temp stands for its initializer only when it is a constant
|
||||
// binding assigned exactly once, by that declaration.
|
||||
const declared = new Map<string, number>();
|
||||
const countDecls = (s: IrStmt): boolean => {
|
||||
if (s.kind === "varDecl") declared.set(s.localId, (declared.get(s.localId) ?? 0) + 1);
|
||||
return everyStmtChild(s, countExprDecls, countDecls);
|
||||
};
|
||||
const countExprDecls = (e: IrExpr): boolean => everyExprChild(e, countExprDecls, countDecls);
|
||||
loop.body.every(countDecls);
|
||||
const temp = (id: string): IrExpr | null => {
|
||||
const local = facts.locals.get(id);
|
||||
const init = temps.get(id);
|
||||
if (
|
||||
!init ||
|
||||
!local ||
|
||||
local.mutable ||
|
||||
local.boxed ||
|
||||
local.tdz ||
|
||||
facts.captured.has(id) ||
|
||||
declared.get(id) !== 1
|
||||
)
|
||||
return null;
|
||||
return init;
|
||||
};
|
||||
const counter = counted?.localId ?? null;
|
||||
// A binding keeps its loop-entry value for the whole loop.
|
||||
const invariant = (id: string): boolean => {
|
||||
if (written.has(id) || id === counter) return false;
|
||||
const local = facts.locals.get(id);
|
||||
if (local)
|
||||
return !local.boxed && !local.tdz && !facts.captured.has(id) && facts.initialized.has(id);
|
||||
return facts.invariantGlobals.has(id);
|
||||
};
|
||||
const resolving = new Set<string>();
|
||||
const root = (e: IrExpr): VarRef | null => {
|
||||
if (e.kind !== "varRef" || e.type.kind !== "bytes") return null;
|
||||
const init = temp(e.localId);
|
||||
if (init) {
|
||||
if (resolving.has(e.localId)) return null;
|
||||
resolving.add(e.localId);
|
||||
const result = root(init);
|
||||
resolving.delete(e.localId);
|
||||
return result;
|
||||
}
|
||||
return invariant(e.localId) ? e : null;
|
||||
};
|
||||
// A pure integer operand evaluated again before the loop.
|
||||
const invariantKey = (e: IrExpr): string | null => {
|
||||
if (!facts.ranges.get(e)) return null;
|
||||
if (e.kind === "numLit") return Number.isSafeInteger(e.value) ? `n${e.value}` : null;
|
||||
if (e.kind === "varRef")
|
||||
return invariant(e.localId) ? `v${e.localId.length}:${e.localId}` : null;
|
||||
if (
|
||||
e.kind === "bin" &&
|
||||
INVARIANT_OPS.has(e.op) &&
|
||||
e.left.type.kind === "f64" &&
|
||||
e.right.type.kind === "f64"
|
||||
) {
|
||||
const left = invariantKey(e.left),
|
||||
right = invariantKey(e.right);
|
||||
return left !== null && right !== null ? `(${e.op} ${left} ${right})` : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
interface Affine {
|
||||
/** The counter's coefficient; 0 when the value is loop-invariant. */
|
||||
counter: number;
|
||||
terms: KeyedTerm[];
|
||||
offset: number;
|
||||
}
|
||||
// Every operand of an affine index is `counter * k + Σ terms + offset`.
|
||||
// Invariant terms need proven ranges. Operands that involve the counter
|
||||
// need none: the guard bounds the counter so that each of them is a safe
|
||||
// integer, which makes the index's double arithmetic exact.
|
||||
const shapes = new Map<IrExpr, Affine | null>();
|
||||
const affine = (e: IrExpr): Affine | null => {
|
||||
if (shapes.has(e)) return shapes.get(e)!;
|
||||
const result = affineShape(e);
|
||||
shapes.set(e, result);
|
||||
return result;
|
||||
};
|
||||
const affineShape = (e: IrExpr): Affine | null => {
|
||||
if (e.kind === "numLit")
|
||||
return Number.isSafeInteger(e.value) && Math.abs(e.value) <= MAX_OFFSET
|
||||
? { counter: 0, terms: [], offset: e.value }
|
||||
: null;
|
||||
if (e.kind === "varRef") {
|
||||
if (e.localId === counter) return { counter: 1, terms: [], offset: 0 };
|
||||
const init = temp(e.localId);
|
||||
if (init) {
|
||||
if (resolving.has(e.localId)) return null;
|
||||
resolving.add(e.localId);
|
||||
const result = affine(init);
|
||||
resolving.delete(e.localId);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (
|
||||
e.kind === "bin" &&
|
||||
(e.op === "+" || e.op === "-") &&
|
||||
e.left.type.kind === "f64" &&
|
||||
e.right.type.kind === "f64"
|
||||
) {
|
||||
const left = affine(e.left),
|
||||
right = affine(e.right);
|
||||
if (left && right && (left.counter === 0 || right.counter === 0)) {
|
||||
const sign = e.op === "+" ? 1 : -1;
|
||||
const offset = left.offset + sign * right.offset;
|
||||
if (Math.abs(offset) > MAX_OFFSET) return null;
|
||||
const terms = [
|
||||
...left.terms,
|
||||
...right.terms.map((t) => ({ expr: t.expr, sign: sign * t.sign, key: t.key })),
|
||||
];
|
||||
const result = { counter: left.counter + sign * right.counter, terms, offset };
|
||||
if (result.counter !== 0 || facts.ranges.get(e)) return result;
|
||||
}
|
||||
}
|
||||
// A positive constant multiple of the counter: `i * 4`, `4 * i`,
|
||||
// `(i + 1) * 8`. (A negative factor turns a zero into -0.)
|
||||
if (e.kind === "bin" && e.op === "*") {
|
||||
const rightLiteral = e.right.kind === "numLit";
|
||||
const scaled = rightLiteral ? e.left : e.right;
|
||||
const literal = rightLiteral ? e.right : e.left;
|
||||
const factor = literal.kind === "numLit" ? literal.value : 0;
|
||||
if (
|
||||
literal.kind === "numLit" &&
|
||||
scaled.type.kind === "f64" &&
|
||||
Number.isSafeInteger(factor) &&
|
||||
factor > 0 &&
|
||||
factor <= MAX_OFFSET
|
||||
) {
|
||||
const inner = affine(scaled);
|
||||
if (inner && inner.counter !== 0 && inner.terms.length === 0) {
|
||||
const counterScale = inner.counter * factor,
|
||||
offset = inner.offset * factor;
|
||||
if (Math.abs(counterScale) <= MAX_OFFSET && Math.abs(offset) <= MAX_OFFSET)
|
||||
return { counter: counterScale, terms: [], offset };
|
||||
}
|
||||
}
|
||||
}
|
||||
const key = invariantKey(e);
|
||||
return key !== null ? { counter: 0, terms: [{ expr: e, sign: 1, key }], offset: 0 } : null;
|
||||
};
|
||||
// The operands of an index that involve the counter.
|
||||
const chain = (e: IrExpr, out: Set<IrExpr>): void => {
|
||||
if (!shapes.get(e)?.counter) return;
|
||||
out.add(e);
|
||||
if (e.kind === "varRef") {
|
||||
const init = temp(e.localId);
|
||||
if (init) chain(init, out);
|
||||
} else if (e.kind === "bin") {
|
||||
chain(e.left, out);
|
||||
chain(e.right, out);
|
||||
}
|
||||
};
|
||||
const termMagnitude = (terms: Term[]): number =>
|
||||
terms.reduce((sum, t) => {
|
||||
const r = facts.ranges.get(t.expr)!;
|
||||
return sum + Math.max(Math.abs(r.min), Math.abs(r.max));
|
||||
}, 0);
|
||||
|
||||
const receivers: { ref: VarRef; global: boolean }[] = [];
|
||||
const receiverIndex = new Map<string, number>();
|
||||
const fixed = new Map<number, { check: FixedCheck; nodes: Node[] }>();
|
||||
const affineChecks = new Map<
|
||||
string,
|
||||
{ check: AffineCheck; nodes: Node[]; operands: Set<IrExpr> }
|
||||
>();
|
||||
const receiverOf = (ref: VarRef): number => {
|
||||
let index = receiverIndex.get(ref.localId);
|
||||
if (index === undefined) {
|
||||
index = receivers.length;
|
||||
receiverIndex.set(ref.localId, index);
|
||||
receivers.push({ ref, global: !facts.locals.has(ref.localId) });
|
||||
}
|
||||
return index;
|
||||
};
|
||||
const access = (node: Node, array: IrExpr, index: IrExpr, width: number): void => {
|
||||
if (facts.proven.has(node)) return;
|
||||
const ref = root(array);
|
||||
if (!ref) return;
|
||||
const shape = counter !== null ? affine(index) : null;
|
||||
if (shape && shape.counter !== 0) {
|
||||
const operands = new Set<IrExpr>();
|
||||
chain(index, operands);
|
||||
const terms = [...shape.terms].sort((a, b) =>
|
||||
a.key < b.key ? -1 : a.key > b.key ? 1 : a.sign - b.sign,
|
||||
);
|
||||
const receiver = receiverOf(ref);
|
||||
const key = `${receiver}|${shape.counter}|${terms.map((t) => `${t.sign}${t.key}`).join(",")}`;
|
||||
const end = shape.offset + width - 1;
|
||||
const group = affineChecks.get(key);
|
||||
if (group) {
|
||||
group.check.minOffset = Math.min(group.check.minOffset, shape.offset);
|
||||
group.check.maxEnd = Math.max(group.check.maxEnd, end);
|
||||
group.nodes.push(node);
|
||||
for (const operand of operands) group.operands.add(operand);
|
||||
} else
|
||||
affineChecks.set(key, {
|
||||
check: { receiver, scale: shape.counter, terms, minOffset: shape.offset, maxEnd: end },
|
||||
nodes: [node],
|
||||
operands,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const range = facts.ranges.get(index);
|
||||
if (!range) return;
|
||||
const end = range.max + width - 1;
|
||||
// Only a length a buffer can have: an outer loop must not pay for a
|
||||
// guard that bounds an inner loop's unbounded counter.
|
||||
if (range.min < 0 || end >= MAX_FIXED_END) return;
|
||||
const receiver = receiverOf(ref);
|
||||
const group = fixed.get(receiver);
|
||||
if (group) {
|
||||
group.check.end = Math.max(group.check.end, end);
|
||||
group.nodes.push(node);
|
||||
} else fixed.set(receiver, { check: { receiver, end }, nodes: [node] });
|
||||
};
|
||||
const visitExpr = (e: IrExpr): boolean => {
|
||||
if (e.kind === "bytesIntrinsic") {
|
||||
const numeric = byteNumberAccess(e);
|
||||
if (numeric) access(e, e.receiver, e.args[numeric.offsetArg]!, numeric.width);
|
||||
else if (e.method === "get" && e.args[0]) access(e, e.receiver, e.args[0], 1);
|
||||
}
|
||||
return everyExprChild(e, visitExpr, visitStmt);
|
||||
};
|
||||
const visitStmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "bytesSet") access(s, s.arr, s.index, 1);
|
||||
return everyStmtChild(s, visitExpr, visitStmt);
|
||||
};
|
||||
loop.body.every(visitStmt);
|
||||
// Bound the counter so every counter operand stays a safe integer.
|
||||
let bound = SAFE;
|
||||
const affineGroups = [...affineChecks.values()].filter((group) => {
|
||||
let limit = SAFE;
|
||||
for (const operand of group.operands) {
|
||||
const shape = shapes.get(operand)!;
|
||||
const room = SAFE - termMagnitude(shape.terms) - Math.abs(shape.offset);
|
||||
limit = Math.min(limit, Math.floor(room / Math.abs(shape.counter)));
|
||||
}
|
||||
if (limit < 0 || !counted) return false;
|
||||
bound = Math.min(bound, limit);
|
||||
return true;
|
||||
});
|
||||
const fixedKept = [...fixed.values()].slice(0, MAX_CHECKS);
|
||||
const affineKept = affineGroups.slice(0, MAX_CHECKS - fixedKept.length);
|
||||
if (fixedKept.length === 0 && affineKept.length === 0) return null;
|
||||
const bounds = new Set<Node>();
|
||||
const used = new Set<number>();
|
||||
const ranges = new Map<IrExpr, IntegerRange>();
|
||||
const counterLow = Math.max(counted?.range.min ?? 0, -bound),
|
||||
counterHigh = Math.min(counted?.range.max ?? 0, bound);
|
||||
for (const group of fixedKept) {
|
||||
used.add(group.check.receiver);
|
||||
for (const node of group.nodes) bounds.add(node);
|
||||
}
|
||||
for (const group of affineKept) {
|
||||
used.add(group.check.receiver);
|
||||
for (const node of group.nodes) bounds.add(node);
|
||||
for (const operand of group.operands) {
|
||||
if (facts.ranges.get(operand)) continue;
|
||||
const shape = shapes.get(operand)!;
|
||||
const a = shape.counter * counterLow,
|
||||
b = shape.counter * counterHigh;
|
||||
let min = Math.min(a, b) + shape.offset,
|
||||
max = Math.max(a, b) + shape.offset;
|
||||
for (const term of shape.terms) {
|
||||
const r = facts.ranges.get(term.expr)!;
|
||||
min += term.sign > 0 ? r.min : -r.max;
|
||||
max += term.sign > 0 ? r.max : -r.min;
|
||||
}
|
||||
if (counterLow <= counterHigh) ranges.set(operand, { min, max });
|
||||
}
|
||||
}
|
||||
// Renumber the receivers the kept checks use.
|
||||
const order = [...used].sort((a, b) => a - b);
|
||||
const renumber = new Map(order.map((old, index) => [old, index]));
|
||||
const needsBound =
|
||||
affineKept.length > 0 &&
|
||||
counted !== null &&
|
||||
(counted.range.min < -bound || counted.range.max > bound);
|
||||
return {
|
||||
bounds,
|
||||
receivers: order.map((index) => receivers[index]!),
|
||||
fixed: fixedKept.map((g) => ({
|
||||
receiver: renumber.get(g.check.receiver)!,
|
||||
end: g.check.end,
|
||||
})),
|
||||
affine: affineKept.map((g) => ({
|
||||
receiver: renumber.get(g.check.receiver)!,
|
||||
scale: g.check.scale,
|
||||
terms: g.check.terms,
|
||||
minOffset: g.check.minOffset,
|
||||
maxEnd: g.check.maxEnd,
|
||||
})),
|
||||
counted: affineKept.length > 0 ? counted : null,
|
||||
counterBound: needsBound ? bound : null,
|
||||
ranges,
|
||||
};
|
||||
}
|
||||
|
||||
/** Branches to `fast` when every access the version proves is in bounds for
|
||||
* every iteration, and to `slow` otherwise (including a counted loop whose
|
||||
* integer form is not established, or a module receiver not yet set). */
|
||||
export function emitBytesLoopVersionGuard(
|
||||
host: LlvmEmitterContext,
|
||||
version: BytesLoopVersion,
|
||||
fast: string,
|
||||
slow: string,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const next = (cond: string): void => {
|
||||
if (cond === "true") return;
|
||||
const ok = B.newLabel("bytes.version.check");
|
||||
B.condBr(cond, ok, slow);
|
||||
B.startBlock(ok);
|
||||
};
|
||||
const counted = version.counted;
|
||||
if (counted?.guarded) next(emitCountedLoopSafety(host, counted));
|
||||
const receivers = version.receivers.map(({ ref, global }) => {
|
||||
const value = host.emitReadReceiver(ref).name;
|
||||
if (global) {
|
||||
const set = B.tmp();
|
||||
B.line(`${set} = icmp ne ptr ${value}, null`);
|
||||
next(set);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
const lengths = receivers.map((receiver) => {
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
if (host.sizeType === "i64") return len;
|
||||
const wide = B.tmp();
|
||||
B.line(`${wide} = zext ${host.sizeType} ${len} to i64`);
|
||||
return wide;
|
||||
});
|
||||
const conditions: string[] = [];
|
||||
for (const check of version.fixed) {
|
||||
const ok = B.tmp();
|
||||
B.line(`${ok} = icmp ugt i64 ${lengths[check.receiver]}, ${check.end}`);
|
||||
conditions.push(ok);
|
||||
}
|
||||
if (counted && version.affine.length > 0) {
|
||||
// The first and last counter values, in ascending order. The last value
|
||||
// of a non-unit step is exact: `start + ⌊(limit ∓ 1 - start) / step⌋ *
|
||||
// step`. A loop that runs no iteration may compute anything here.
|
||||
const startValue = host.emitExpr(counted.start);
|
||||
const startInteger = exactInteger(host, startValue, counted.start);
|
||||
let start: string;
|
||||
if (startInteger) start = widenInteger(host, startInteger);
|
||||
else {
|
||||
// The counted-loop safety test above proved an exact integer.
|
||||
start = B.tmp();
|
||||
B.line(`${start} = fptosi double ${startValue.name} to i64`);
|
||||
}
|
||||
const limit = emitCountedLoopLimit(host, counted);
|
||||
const step = Math.abs(counted.step);
|
||||
const edge = B.tmp();
|
||||
B.line(`${edge} = add i64 ${limit}, ${counted.step > 0 ? -1 : 1}`);
|
||||
let last = edge;
|
||||
if (step !== 1) {
|
||||
const span = B.tmp(),
|
||||
whole = B.tmp(),
|
||||
covered = B.tmp();
|
||||
last = B.tmp();
|
||||
B.line(
|
||||
counted.step > 0
|
||||
? `${span} = sub i64 ${edge}, ${start}`
|
||||
: `${span} = sub i64 ${start}, ${edge}`,
|
||||
);
|
||||
B.line(`${whole} = sdiv i64 ${span}, ${step}`);
|
||||
B.line(`${covered} = mul i64 ${whole}, ${step}`);
|
||||
B.line(`${last} = ${counted.step > 0 ? "add" : "sub"} i64 ${start}, ${covered}`);
|
||||
}
|
||||
const low = counted.step > 0 ? start : last;
|
||||
const high = counted.step > 0 ? last : start;
|
||||
if (version.counterBound !== null) {
|
||||
const above = B.tmp(),
|
||||
below = B.tmp();
|
||||
B.line(`${above} = icmp sge i64 ${low}, ${-version.counterBound}`);
|
||||
B.line(`${below} = icmp sle i64 ${high}, ${version.counterBound}`);
|
||||
conditions.push(above, below);
|
||||
}
|
||||
for (const check of version.affine) {
|
||||
let base = "0";
|
||||
for (const term of check.terms) {
|
||||
const value = host.emitExpr(term.expr);
|
||||
const integer = exactInteger(host, value, term.expr);
|
||||
if (!integer) {
|
||||
// Unreachable: every term has a proven range. Stay checked.
|
||||
B.br(slow);
|
||||
B.startBlock(B.newLabel("bytes.version.dead"));
|
||||
return;
|
||||
}
|
||||
const wide = widenInteger(host, integer);
|
||||
const sum = B.tmp();
|
||||
B.line(`${sum} = ${term.sign > 0 ? "add" : "sub"} i64 ${base}, ${wide}`);
|
||||
base = sum;
|
||||
}
|
||||
// Products wrap only for a loop with no iteration: within the
|
||||
// iteration space every scaled counter is a proven safe integer.
|
||||
const scaledLow = scaleCounter(host, low, check.scale),
|
||||
scaledHigh = scaleCounter(host, high, check.scale);
|
||||
const first = B.tmp(),
|
||||
final = B.tmp(),
|
||||
nonNegative = B.tmp(),
|
||||
below = B.tmp(),
|
||||
both = B.tmp();
|
||||
B.line(`${first} = add i64 ${check.scale > 0 ? scaledLow : scaledHigh}, ${base}`);
|
||||
B.line(`${final} = add i64 ${check.scale > 0 ? scaledHigh : scaledLow}, ${base}`);
|
||||
const firstOffset = B.tmp(),
|
||||
lastEnd = B.tmp();
|
||||
B.line(`${firstOffset} = add i64 ${first}, ${check.minOffset}`);
|
||||
B.line(`${lastEnd} = add i64 ${final}, ${check.maxEnd}`);
|
||||
B.line(`${nonNegative} = icmp sge i64 ${firstOffset}, 0`);
|
||||
// Lengths are at most 2^53 elements: a signed comparison is exact.
|
||||
B.line(`${below} = icmp slt i64 ${lastEnd}, ${lengths[check.receiver]}`);
|
||||
B.line(`${both} = and i1 ${nonNegative}, ${below}`);
|
||||
conditions.push(both);
|
||||
}
|
||||
}
|
||||
let valid = "true";
|
||||
for (const cond of conditions) {
|
||||
if (valid === "true") valid = cond;
|
||||
else {
|
||||
const all = B.tmp();
|
||||
B.line(`${all} = and i1 ${valid}, ${cond}`);
|
||||
valid = all;
|
||||
}
|
||||
}
|
||||
if (valid === "true") B.br(fast);
|
||||
else B.condBr(valid, fast, slow);
|
||||
}
|
||||
|
||||
function scaleCounter(host: LlvmEmitterContext, value: string, scale: number): string {
|
||||
if (scale === 1) return value;
|
||||
const scaled = host.B.tmp();
|
||||
host.B.line(`${scaled} = mul i64 ${value}, ${scale}`);
|
||||
return scaled;
|
||||
}
|
||||
@@ -42,3 +42,54 @@ export function emitCountedLoopLimit(
|
||||
B.line(`${limit} = add nsw i64 ${integer}, ${ascending ? 1 : -1}`);
|
||||
return limit;
|
||||
}
|
||||
|
||||
/** The run-time test of a guarded counted loop: an unproven limit is within
|
||||
* the integer form's finite range and an unproven start is an exact safe
|
||||
* integer. Returns `true` when nothing needs testing. */
|
||||
export function emitCountedLoopSafety(
|
||||
host: LlvmEmitterContext,
|
||||
loop: IntegerCountedForLoop,
|
||||
): string {
|
||||
const B = host.B;
|
||||
let safe = "true";
|
||||
if (loop.limitGuarded) {
|
||||
const bound = host.emitExpr(loop.limit);
|
||||
safe = B.tmp();
|
||||
B.line(
|
||||
`${safe} = fcmp ${loop.step > 0 ? "ole" : "oge"} double ${bound.name}, ${f64Lit(loop.guardLimit)}`,
|
||||
);
|
||||
}
|
||||
if (loop.startGuarded) {
|
||||
// An exact integer of at most 2^53 - 1 in magnitude, and not -0
|
||||
// (the bit patterns of the value and its round trip agree).
|
||||
// NaN and infinities fail the ordered range tests.
|
||||
const start = host.emitExpr(loop.start);
|
||||
const low = B.tmp(),
|
||||
high = B.tmp(),
|
||||
raw = B.tmp(),
|
||||
wide = B.tmp(),
|
||||
back = B.tmp(),
|
||||
bits = B.tmp(),
|
||||
backBits = B.tmp(),
|
||||
exact = B.tmp(),
|
||||
inRange = B.tmp(),
|
||||
ok = B.tmp();
|
||||
B.line(`${low} = fcmp oge double ${start.name}, ${f64Lit(-Number.MAX_SAFE_INTEGER)}`);
|
||||
B.line(`${high} = fcmp ole double ${start.name}, ${f64Lit(Number.MAX_SAFE_INTEGER)}`);
|
||||
B.line(`${inRange} = and i1 ${low}, ${high}`);
|
||||
B.line(`${raw} = fptosi double ${start.name} to i64`);
|
||||
B.line(`${wide} = freeze i64 ${raw}`);
|
||||
B.line(`${back} = sitofp i64 ${wide} to double`);
|
||||
B.line(`${bits} = bitcast double ${start.name} to i64`);
|
||||
B.line(`${backBits} = bitcast double ${back} to i64`);
|
||||
B.line(`${exact} = icmp eq i64 ${bits}, ${backBits}`);
|
||||
B.line(`${ok} = and i1 ${inRange}, ${exact}`);
|
||||
if (safe === "true") safe = ok;
|
||||
else {
|
||||
const both = B.tmp();
|
||||
B.line(`${both} = and i1 ${safe}, ${ok}`);
|
||||
safe = both;
|
||||
}
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
|
||||
@@ -762,7 +762,9 @@ export class LlDyn {
|
||||
B.terminate(`ret i1 true`);
|
||||
B.startBlock(lNext);
|
||||
}
|
||||
if (t.kind !== "dyn") {
|
||||
// A union matches a capsule of another type through its arms below: a
|
||||
// class capsule in a `C | undefined` field matches the `C` arm.
|
||||
if (t.kind !== "dyn" && t.kind !== "union") {
|
||||
const view = this.viewFn(wrapped);
|
||||
this.host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
const kind = this.kindOf(B, "%d");
|
||||
@@ -3263,8 +3265,9 @@ export class LlDyn {
|
||||
}
|
||||
B.startBlock(labels.get(DYN_KIND.BYTES)!);
|
||||
{
|
||||
// Buffer-flavored values coerce utf8; plain Uint8Array joins
|
||||
// its elements.
|
||||
// Buffer-flavored values coerce utf8; typed arrays join their
|
||||
// elements and a DataView answers "[object DataView]" (the
|
||||
// runtime's conversion).
|
||||
const bufp = B.tmp();
|
||||
const bufRaw = B.tmp();
|
||||
const isBuf = B.tmp();
|
||||
@@ -3286,10 +3289,9 @@ export class LlDyn {
|
||||
B.line(`call void @scr_str_release(ptr ${txt})`);
|
||||
B.br(done);
|
||||
B.startBlock(lJoin);
|
||||
const bts = this.payloadOf(B, "%d", "ptr");
|
||||
host.declare(`declare ptr @scr_bytes_join(ptr, ptr)`);
|
||||
host.declare(`declare ptr @scr_dyn_to_string(ptr, ptr)`);
|
||||
const joined = B.tmp();
|
||||
B.line(`${joined} = call ptr @scr_bytes_join(ptr ${bts}, ptr ${host.internLiteral(",")})`);
|
||||
B.line(`${joined} = call ptr @scr_dyn_to_string(ptr %d, ptr null)`);
|
||||
this.putScrStr(B, "%b", joined);
|
||||
B.line(`call void @scr_str_release(ptr ${joined})`);
|
||||
B.br(done);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
import { emitByteNumber } from "./byte-numbers.js";
|
||||
import { integerOrNaNRange } from "./wrapping-sums.js";
|
||||
import { bytesElementAlias, bytesHeaderAlias } from "./bytes-alias.js";
|
||||
|
||||
const BYTES_NUM_KIND: Record<string, { kind: number; le: boolean } | undefined> = {
|
||||
u8: { kind: 0, le: false },
|
||||
@@ -93,10 +94,7 @@ export function emitBytesIndex(
|
||||
B.line(`${narrow} = trunc i64 ${wide} to ${host.sizeType}`);
|
||||
return narrow;
|
||||
}
|
||||
const lenPtr = B.tmp();
|
||||
const len = B.tmp();
|
||||
B.line(`${lenPtr} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
|
||||
B.line(`${len} = load ${host.sizeType}, ptr ${lenPtr}`);
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
if (integer) {
|
||||
const wideIndex = widenInteger(host, integer);
|
||||
const wideLength = host.sizeType === "i64" ? len : B.tmp();
|
||||
@@ -145,11 +143,20 @@ export function emitBytesIndex(
|
||||
return idx;
|
||||
}
|
||||
|
||||
/** The element count of a typed array, in the target's size type. */
|
||||
export function emitBytesHeaderLength(host: LlvmEmitterContext, receiver: string): string {
|
||||
const p = host.B.tmp();
|
||||
const len = host.B.tmp();
|
||||
host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
|
||||
host.B.line(`${len} = load ${host.sizeType}, ptr ${p}${bytesHeaderAlias(host)}`);
|
||||
return len;
|
||||
}
|
||||
|
||||
export function emitBytesData(host: LlvmEmitterContext, receiver: string): string {
|
||||
const p = host.B.tmp();
|
||||
const data = host.B.tmp();
|
||||
host.B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 3`);
|
||||
host.B.line(`${data} = load ptr, ptr ${p}`);
|
||||
host.B.line(`${data} = load ptr, ptr ${p}${bytesHeaderAlias(host)}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -160,10 +167,7 @@ export function emitBytesLength(
|
||||
bytes: boolean,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const p = B.tmp();
|
||||
const len = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 1`);
|
||||
B.line(`${len} = load ${host.sizeType}, ptr ${p}`);
|
||||
const len = emitBytesHeaderLength(host, receiver);
|
||||
const size = BYTES_ELEMENT_SIZE[elem];
|
||||
const count = bytes && size !== 1 ? B.tmp() : len;
|
||||
if (count !== len) B.line(`${count} = shl ${host.sizeType} ${len}, ${Math.log2(size)}`);
|
||||
@@ -187,7 +191,7 @@ function emitSharedBytesSplit(host: LlvmEmitterContext, receiver: string): strin
|
||||
isShared = B.tmp(),
|
||||
expected = B.tmp();
|
||||
B.line(`${slot} = getelementptr inbounds %ScrBytes, ptr ${receiver}, i64 0, i32 9`);
|
||||
B.line(`${storage} = load ptr, ptr ${slot}`);
|
||||
B.line(`${storage} = load ptr, ptr ${slot}${bytesHeaderAlias(host)}`);
|
||||
B.line(`${isShared} = icmp ne ptr ${storage}, null`);
|
||||
B.line(`${expected} = call i1 @llvm.expect.i1(i1 ${isShared}, i1 false)`);
|
||||
const shared = B.newLabel("bytes.shared"),
|
||||
@@ -257,9 +261,15 @@ function emitBytesLoad(
|
||||
inBounds: boolean,
|
||||
invalid: string | undefined,
|
||||
): LlValue {
|
||||
if (host.mod.workers) {
|
||||
const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds);
|
||||
return emitSharedOrLocalLoad(host, elem, receiver, idx);
|
||||
}
|
||||
// The header is immutable, so the data pointer loads beside the length,
|
||||
// ahead of the bounds branch: an unconditional load LICM can hoist.
|
||||
const data = host.emitBytesData(receiver);
|
||||
const idx = emitBytesIndex(host, receiver, index, expr, invalid, inBounds);
|
||||
if (host.mod.workers) return emitSharedOrLocalLoad(host, elem, receiver, idx);
|
||||
return emitBytesLoadAt(host, elem, receiver, idx);
|
||||
return emitBytesLoadAt(host, elem, data, idx);
|
||||
}
|
||||
|
||||
/** Worker programs: the element at a checked index of a view that may be
|
||||
@@ -285,7 +295,7 @@ function emitSharedOrLocalLoad(
|
||||
const sharedDone = B.newLabel("bytes.shared.done");
|
||||
B.br(sharedDone);
|
||||
B.startBlock(local);
|
||||
const own = emitBytesLoadAt(host, elem, receiver, idx);
|
||||
const own = emitBytesLoadAt(host, elem, host.emitBytesData(receiver), idx);
|
||||
const localDone = B.newLabel("bytes.local.done");
|
||||
B.br(localDone);
|
||||
B.startBlock(localDone);
|
||||
@@ -306,15 +316,15 @@ function emitSharedOrLocalLoad(
|
||||
return { name: out, type: F64, uint32: u32 };
|
||||
}
|
||||
|
||||
/** The element at a checked index of a thread-owned view. */
|
||||
/** The element at a checked index of a thread-owned view, given its data
|
||||
* pointer. */
|
||||
function emitBytesLoadAt(
|
||||
host: LlvmEmitterContext,
|
||||
elem: IrBytesElem,
|
||||
receiver: string,
|
||||
data: string,
|
||||
idx: string,
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const data = host.emitBytesData(receiver);
|
||||
const p = B.tmp();
|
||||
if (elem === "f16") {
|
||||
// Half-precision bits widen exactly; the runtime converts them.
|
||||
@@ -322,7 +332,7 @@ function emitBytesLoadAt(
|
||||
const raw = B.tmp();
|
||||
const out = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`${raw} = load i16, ptr ${p}, align 1`);
|
||||
B.line(`${raw} = load i16, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
B.line(`${out} = call double @scr_f16_to_f64(i16 zeroext ${raw})`);
|
||||
return { name: out, type: F64 };
|
||||
}
|
||||
@@ -333,7 +343,7 @@ function emitBytesLoadAt(
|
||||
const wide = B.tmp();
|
||||
const out = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`${raw} = load i${bits}, ptr ${p}, align 1`);
|
||||
B.line(`${raw} = load i${bits}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
B.line(`${wide} = ${signed ? "sext" : "zext"} i${bits} ${raw} to i32`);
|
||||
B.line(`${out} = ${signed ? "sitofp" : "uitofp"} i32 ${wide} to double`);
|
||||
return { name: out, type: F64, uint32: wide };
|
||||
@@ -342,20 +352,20 @@ function emitBytesLoadAt(
|
||||
const raw = B.tmp();
|
||||
const out = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds float, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`${raw} = load float, ptr ${p}, align 1`);
|
||||
B.line(`${raw} = load float, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
B.line(`${out} = fpext float ${raw} to double`);
|
||||
return { name: out, type: F64 };
|
||||
}
|
||||
if (elem === "f64") {
|
||||
const out = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`${out} = load double, ptr ${p}, align 1`);
|
||||
B.line(`${out} = load double, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
return { name: out, type: F64 };
|
||||
}
|
||||
const raw = B.tmp();
|
||||
const out = B.tmp();
|
||||
B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`${raw} = load i32, ptr ${p}, align 1`);
|
||||
B.line(`${raw} = load i32, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
B.line(`${out} = ${elem === "i32" ? "sitofp" : "uitofp"} i32 ${raw} to double`);
|
||||
return { name: out, type: F64, uint32: raw };
|
||||
}
|
||||
@@ -465,6 +475,7 @@ export function emitBytesSet(
|
||||
const B = host.B;
|
||||
// Invalid integer-indexed writes are ignored after evaluating the RHS.
|
||||
const done = B.newLabel("bytes.store.done");
|
||||
const data = host.emitBytesData(receiver);
|
||||
const idx = emitBytesIndex(host, receiver, index, indexExpr, done, inBounds);
|
||||
if (host.mod.workers) {
|
||||
host.declare(`declare void @scr_bytes_shared_set(ptr, ${host.sizeType}, double)`);
|
||||
@@ -487,7 +498,6 @@ export function emitBytesSet(
|
||||
elem === "f16" || elem === "f32" || elem === "f64"
|
||||
? null
|
||||
: host.emitToUint32(value.name, expr, value.uint32);
|
||||
const data = host.emitBytesData(receiver);
|
||||
const p = B.tmp();
|
||||
if (elem === "f16") {
|
||||
// One round-to-nearest-even step from the double, in the runtime.
|
||||
@@ -495,24 +505,24 @@ export function emitBytesSet(
|
||||
const narrowed = B.tmp();
|
||||
B.line(`${narrowed} = call zeroext i16 @scr_f16_from_f64(double ${value.name})`);
|
||||
B.line(`${p} = getelementptr inbounds i16, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store i16 ${narrowed}, ptr ${p}, align 1`);
|
||||
B.line(`store i16 ${narrowed}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
} else if (BYTES_ELEMENT_SIZE[elem] < 4) {
|
||||
const bits = BYTES_ELEMENT_SIZE[elem] * 8;
|
||||
const byte = B.tmp();
|
||||
B.line(`${byte} = trunc i32 ${stored!} to i${bits}`);
|
||||
B.line(`${p} = getelementptr inbounds i${bits}, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store i${bits} ${byte}, ptr ${p}, align 1`);
|
||||
B.line(`store i${bits} ${byte}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
} else if (elem === "f32") {
|
||||
const narrowed = B.tmp();
|
||||
B.line(`${narrowed} = fptrunc double ${value.name} to float`);
|
||||
B.line(`${p} = getelementptr inbounds float, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store float ${narrowed}, ptr ${p}, align 1`);
|
||||
B.line(`store float ${narrowed}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
} else if (elem === "f64") {
|
||||
B.line(`${p} = getelementptr inbounds double, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store double ${value.name}, ptr ${p}, align 1`);
|
||||
B.line(`store double ${value.name}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
} else {
|
||||
B.line(`${p} = getelementptr inbounds i32, ptr ${data}, ${host.sizeType} ${idx}`);
|
||||
B.line(`store i32 ${stored!}, ptr ${p}, align 1`);
|
||||
B.line(`store i32 ${stored!}, ptr ${p}, align 1${bytesElementAlias(host)}`);
|
||||
}
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { type IrType, isRefCounted, isUnitType, typeEquals, typeKey } from "../../ir/ir.js";
|
||||
import {
|
||||
type IrType,
|
||||
UNDEFINED_T,
|
||||
isRefCounted,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { mangleGenResThunk, mangleRecordNew, mangleRecordStruct } from "../mangle.js";
|
||||
import { FN_ATTRS, llFieldType, releaseSym, retainSym, traceArg, vAdapters } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
@@ -68,6 +75,14 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
|
||||
`define internal void @${sym}(ptr %dst, ptr %src) ${FN_ATTRS} { ; race ${key}`,
|
||||
`entry:`,
|
||||
];
|
||||
if (from.kind === "void") {
|
||||
// A Promise<void> entry fulfills with undefined: the result's
|
||||
// undefined arm, whatever the entry's payload slot holds.
|
||||
const tag = tagOf(UNDEFINED_T);
|
||||
d.push(` ${fulfill(host.unitInstanceRef(to.unionId, tag))}`, ` ret void`, `}`, ``);
|
||||
host.resolveThunkDefs.push(...d);
|
||||
return sym;
|
||||
}
|
||||
if (from.kind !== "union") {
|
||||
// One arm wrap, straight off the payload accessors.
|
||||
const tag = tagOf(from);
|
||||
|
||||
@@ -528,7 +528,7 @@ export function emitStableReceiver(
|
||||
if (b.kind !== "boxed") {
|
||||
if (b.kind === "global") host.checkGlobalTdz(receiver.localId);
|
||||
const value = host.B.tmp();
|
||||
host.B.line(`${value} = load ptr, ptr ${b.slot}`);
|
||||
host.B.line(`${value} = load ptr, ptr ${b.slot}${host.slotAlias(receiver.localId)}`);
|
||||
return { name: value, type: receiver.type };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* Focused LLVM expression emission extracted from emitter.ts. */
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { matchStringSelfConcat, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import { isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { F64, isRefCounted, type IrExpr, type IrType } from "../../ir/ir.js";
|
||||
import { mangleRecordClone, mangleRecordNew } from "../mangle.js";
|
||||
import { arrNewCall, elemAccess } from "./shapes.js";
|
||||
import { LlvmUnsupportedError } from "./unsupported.js";
|
||||
@@ -116,7 +116,7 @@ export function emitLiteralExpr(
|
||||
return host.own({ name: host.adoptHeap(v, e.type), type: e.type });
|
||||
}
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = load ${host.llType(b.type)}, ptr ${b.slot}`);
|
||||
B.line(`${t} = load ${host.llType(b.type)}, ptr ${b.slot}${host.slotAlias(e.localId)}`);
|
||||
const integerView = host.integerViews.get(e.localId);
|
||||
if (integerView !== undefined) {
|
||||
// Snapshot both representations before later operands can write
|
||||
@@ -558,6 +558,28 @@ export function regexLiteralTemplate(
|
||||
return `@${re.sym}`;
|
||||
}
|
||||
|
||||
/** A string equality between `s.charAt(i)` and a literal of exactly one
|
||||
* UTF-16 code unit: the charAt call and the unit. */
|
||||
function charAtUnitTest(
|
||||
e: ExprOf<"strEq">,
|
||||
): { charAt: ExprOf<"strIntrinsic">; code: number } | null {
|
||||
let text: string;
|
||||
let other: IrExpr;
|
||||
if (e.right.kind === "strLit") {
|
||||
text = e.right.value;
|
||||
other = e.left;
|
||||
} else if (e.left.kind === "strLit") {
|
||||
text = e.left.value;
|
||||
other = e.right;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
if (text.length !== 1) return null;
|
||||
if (other.kind !== "strIntrinsic" || other.method !== "charAt" || other.args.length !== 1)
|
||||
return null;
|
||||
return { charAt: other, code: text.charCodeAt(0) };
|
||||
}
|
||||
|
||||
export function emitStringExpr(
|
||||
host: LlvmEmitterContext,
|
||||
e: ExprOf<
|
||||
@@ -600,6 +622,26 @@ export function emitStringExpr(
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
case "strEq": {
|
||||
// `s.charAt(i) === "c"` (and `s[i]`, which lowers to charAt) for a
|
||||
// one-unit literal compares the code unit: charCodeAt converts the
|
||||
// index the same way, and an index out of range answers NaN, which
|
||||
// equals no unit, as "" equals no one-unit literal.
|
||||
const unit = charAtUnitTest(e);
|
||||
if (unit !== null) {
|
||||
const code = host.emitExpr({
|
||||
kind: "strIntrinsic",
|
||||
method: "charCodeAt",
|
||||
receiver: unit.charAt.receiver,
|
||||
args: unit.charAt.args,
|
||||
type: F64,
|
||||
loc: unit.charAt.loc,
|
||||
});
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = fcmp ${e.negated ? "une" : "oeq"} double ${code.name}, ${f64Lit(unit.code)}`,
|
||||
);
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
const [l, r] = emitStringInputs(host, [e.left, e.right]);
|
||||
const eq = B.tmp();
|
||||
// A literal side compares its length and bytes inline.
|
||||
|
||||
@@ -5,10 +5,12 @@ import { streamTypedRefEligible, undefinedArmTag } from "../../ir/analysis.js";
|
||||
import {
|
||||
type IrRecordShape,
|
||||
type IrType,
|
||||
CLASS_ATTRIBUTES,
|
||||
DYN_CLASS_PROPERTIES,
|
||||
RUNTIME_STREAM_CLASSES,
|
||||
SYMBOL_T,
|
||||
classDynViewSupported,
|
||||
classFieldAttributesName,
|
||||
isClassOwnEnumerableFieldName,
|
||||
isDirentShape,
|
||||
isDynTypedRefType,
|
||||
@@ -766,6 +768,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
host.declare(`declare ptr @scr_stream_dyn_view(ptr)`);
|
||||
B.line(`${out} = call ptr @scr_stream_dyn_view(ptr %p)`);
|
||||
} else B.line(`${out} = call ptr @scr_dyn_new_obj()`);
|
||||
const withAttributes = meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES);
|
||||
if (meta.def.tracksOwnFields) {
|
||||
host.declare(`declare ptr @scr_dyn_copy_property_descriptors(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
@@ -835,9 +838,27 @@ export function streamTypedRefMaterializeAdapter(
|
||||
const nullable = host.nullableFields.get(t.className, field.name);
|
||||
if (nullable) fieldValue = nullableFieldUnion(host, B, fieldValue, nullable);
|
||||
const boxed = host.streamTypedRefBoxValue(B, field.type, fieldValue);
|
||||
// Attributes copied from the bag do not stop the view taking the value.
|
||||
const setter = withAttributes ? "scr_dyn_obj_set_field" : "scr_dyn_obj_set";
|
||||
if (withAttributes)
|
||||
host.declare(`declare void @scr_dyn_obj_set_field(ptr, ptr, ${host.sizeType}, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`,
|
||||
`call void @${setter}(ptr ${out}, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")}, ptr ${boxed})`,
|
||||
);
|
||||
// A field redefined through Object.defineProperty keeps its attributes.
|
||||
const attributesField = classFieldAttributesName(field.name);
|
||||
if (meta.def.fields.some((f) => f.name === attributesField)) {
|
||||
host.declare(`declare void @scr_dyn_class_field_attributes(ptr, ptr, double)`);
|
||||
const slot = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(
|
||||
`${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${classFieldIndex(meta, attributesField).index}`,
|
||||
);
|
||||
B.line(`${bits} = load double, ptr ${slot}`);
|
||||
B.line(
|
||||
`call void @scr_dyn_class_field_attributes(ptr ${out}, ptr ${host.internLiteral(field.name)}, double ${bits})`,
|
||||
);
|
||||
}
|
||||
if (nullable) {
|
||||
host.declare(`declare void @scr_union_release(ptr)`);
|
||||
B.line(`call void @scr_union_release(ptr ${fieldValue})`);
|
||||
@@ -920,6 +941,17 @@ export function streamTypedRefMaterializeAdapter(
|
||||
B.line(`${copied} = call ptr @scr_dyn_copy_property_descriptors(ptr ${out}, ptr ${bag})`);
|
||||
B.line(`call void @scr_dyn_release_v(ptr ${copied})`);
|
||||
}
|
||||
// A frozen, sealed or non-extensible instance's view says so.
|
||||
if (withAttributes) {
|
||||
host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`);
|
||||
const slot = B.tmp(),
|
||||
bits = B.tmp();
|
||||
B.line(
|
||||
`${slot} = getelementptr inbounds %${classStructSym(t.className)}, ptr %p, i64 0, i32 ${classFieldIndex(meta, CLASS_ATTRIBUTES).index}`,
|
||||
);
|
||||
B.line(`${bits} = load double, ptr ${slot}`);
|
||||
B.line(`call void @scr_dyn_class_view_attributes(ptr ${out}, double ${bits})`);
|
||||
}
|
||||
B.terminate(`ret ptr ${out}`);
|
||||
} else if (t.kind === "record") {
|
||||
const shape = host.recordsById.get(t.shapeId);
|
||||
|
||||
@@ -83,12 +83,19 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
}
|
||||
// A signal that is not an AbortSignal throws ERR_INVALID_ARG_TYPE.
|
||||
if (mode & 8) host.emitPendingCheck();
|
||||
// readline/promises interfaces are instances of that module's class.
|
||||
if (mode & 32) {
|
||||
host.declare(`declare void @scr_rl_mark_promises(double)`);
|
||||
B.line(`call void @scr_rl_mark_promises(double ${t})`);
|
||||
}
|
||||
return { name: t, type: e.type };
|
||||
}
|
||||
if (e.fn === "rl.question") {
|
||||
if (e.fn === "rl.question" || e.fn === "rl.questionDyn") {
|
||||
// The answer callback MOVES into the interface's registry; throws
|
||||
// Node's use-after-close error (the may-throw seed).
|
||||
// Node's use-after-close error (the may-throw seed), and with a
|
||||
// checked-dynamic query that is not a string, ERR_INVALID_ARG_TYPE.
|
||||
host.usesTimers = true;
|
||||
const fn = e.fn === "rl.question" ? "scr_rl_question" : "scr_rl_question_dyn";
|
||||
const cbT = e.args[2]!.type;
|
||||
if (cbT.kind !== "func")
|
||||
throw new InternalCompilerError("llvm emitter bug: rl.question callback not a func");
|
||||
@@ -101,29 +108,37 @@ export function emitIoLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue
|
||||
? "scr_rl_answer_thunk_dyn"
|
||||
: "scr_rl_answer_thunk_str";
|
||||
host.declare(`declare void @${adapter}(ptr, ptr)`);
|
||||
host.declare(`declare void @scr_rl_question(double, ptr, ptr, ptr)`);
|
||||
host.declare(`declare void @${fn}(double, ptr, ptr, ptr)`);
|
||||
B.line(
|
||||
`call void @scr_rl_question(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`,
|
||||
`call void @${fn}(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${args[2]!.name}, ptr @${adapter})`,
|
||||
);
|
||||
host.emitPendingCheck();
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "rl.questionAsync" || e.fn === "rl.questionAsyncSignal") {
|
||||
// The promise question never throws: closed interfaces and aborted
|
||||
// signals reject the answered promise (+1).
|
||||
if (
|
||||
e.fn === "rl.questionAsync" ||
|
||||
e.fn === "rl.questionAsyncSignal" ||
|
||||
e.fn === "rl.questionAsyncDyn" ||
|
||||
e.fn === "rl.questionAsyncSignalDyn"
|
||||
) {
|
||||
// The promise question never throws: closed interfaces, aborted
|
||||
// signals and queries that are not strings reject the answered
|
||||
// promise (+1).
|
||||
host.usesTimers = true;
|
||||
const args = e.args.map((a) => host.emitExpr(a));
|
||||
const dyn = e.fn === "rl.questionAsyncDyn" || e.fn === "rl.questionAsyncSignalDyn";
|
||||
const fn = dyn ? "scr_rl_question_promise_dyn" : "scr_rl_question_promise";
|
||||
let signal = "null";
|
||||
let add = "null";
|
||||
if (e.fn === "rl.questionAsyncSignal") {
|
||||
if (e.fn === "rl.questionAsyncSignal" || e.fn === "rl.questionAsyncSignalDyn") {
|
||||
signal = args[2]!.name;
|
||||
host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`);
|
||||
add = "@scr_events_add_abort_listener";
|
||||
}
|
||||
host.declare(`declare ptr @scr_rl_question_promise(double, ptr, ptr, ptr)`);
|
||||
host.declare(`declare ptr @${fn}(double, ptr, ptr, ptr)`);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @scr_rl_question_promise(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${signal}, ptr ${add})`,
|
||||
`${t} = call ptr @${fn}(double ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${signal}, ptr ${add})`,
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
@@ -1097,6 +1097,30 @@ export function emitPrimitiveLibCall(host: LlvmEmitterContext, e: LibCallExpr):
|
||||
B.line(`${t} = call ptr @scr_str_from_char_code_one(double ${code.name})`);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
if (
|
||||
e.fn === "string.fromCharCode" &&
|
||||
packed.kind === "arrayLit" &&
|
||||
packed.elems.length <= 16 &&
|
||||
!packed.spreads?.length
|
||||
) {
|
||||
// A short argument list passes its codes in a stack array.
|
||||
const codes = packed.elems.map((elem) => host.emitExpr(elem));
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca [${Math.max(codes.length, 1)} x double]`);
|
||||
codes.forEach((code, i) => {
|
||||
const p = B.tmp();
|
||||
B.line(
|
||||
`${p} = getelementptr inbounds [${Math.max(codes.length, 1)} x double], ptr ${slot}, i64 0, i64 ${i}`,
|
||||
);
|
||||
B.line(`store double ${code.name}, ptr ${p}`);
|
||||
});
|
||||
host.declare(`declare ptr @scr_str_from_char_codes(ptr, ${host.sizeType})`);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
`${t} = call ptr @scr_str_from_char_codes(ptr ${slot}, ${host.sizeType} ${codes.length})`,
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
// One packed f64[] (the frontend built it) or one bytes value (the
|
||||
// spread-typed-array form); +1 string.
|
||||
const base =
|
||||
|
||||
@@ -294,9 +294,12 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"process.rusage": "scr_process_rusage",
|
||||
"process.activeResources": "scr_active_resources",
|
||||
"process.exiting": "scr_process_exiting",
|
||||
"process.ipcOpen": "scr_process_ipc_open",
|
||||
"process.exit": "scr_process_exit",
|
||||
"process.setExitCode": "scr_process_set_exit_code",
|
||||
"process.currentExitCode": "scr_process_exit_code_or_zero",
|
||||
"process.setExitCodeTracked": "scr_process_set_exit_code_tracked",
|
||||
"process.hasExitCode": "scr_process_has_exit_code",
|
||||
"process.envSet": "scr_env_set",
|
||||
"process.envUnset": "scr_env_unset",
|
||||
"process.envPairs": "scr_env_pairs",
|
||||
@@ -827,6 +830,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.sameValue": "scr_dyn_same_value",
|
||||
"error.toString": "scr_error_to_string",
|
||||
"error.stack": "scr_error_stack",
|
||||
"error.setStack": "scr_error_set_stack",
|
||||
"error.stackLimitGet": "scr_stack_limit_get",
|
||||
"error.stackLimitSet": "scr_stack_limit_set",
|
||||
"class.name": "scr_classobj_name",
|
||||
@@ -926,6 +930,12 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.objectTag": "scr_dyn_object_tag",
|
||||
"dyn.freeze": "scr_dyn_freeze",
|
||||
"dyn.isFrozen": "scr_dyn_is_frozen",
|
||||
"class.defineField": "scr_class_define_field",
|
||||
"class.definedBits": "scr_class_defined_bits",
|
||||
"class.definedValue": "scr_class_defined_value",
|
||||
"class.descriptorHasValue": "scr_class_descriptor_has_value",
|
||||
"class.throwReadOnly": "scr_class_throw_read_only",
|
||||
"class.throwNotExtensible": "scr_class_throw_not_extensible",
|
||||
"dyn.nativeSetNew": "scr_dyn_native_set_new",
|
||||
"dyn.nativeSetIs": "scr_dyn_native_set_is",
|
||||
"dyn.nativeMapIs": "scr_dyn_native_map_is",
|
||||
@@ -994,6 +1004,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// family have non-generic shapes (adapter/variadic) — special-cased in
|
||||
// emitLibCall.
|
||||
"emitter.new": "scr_emitter_new",
|
||||
"emitter.className": "scr_emitter_class_name",
|
||||
"emitter.ctor": "scr_emitter_init",
|
||||
"emitter.off": "scr_emitter_off",
|
||||
"emitter.offDyn": "scr_emitter_off_dyn",
|
||||
@@ -1039,6 +1050,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"rl.prompt": "scr_rl_prompt",
|
||||
"rl.promptWith": "scr_rl_prompt_with",
|
||||
"rl.setPrompt": "scr_rl_set_prompt",
|
||||
"rl.setPromptDyn": "scr_rl_set_prompt_dyn",
|
||||
"rl.getPrompt": "scr_rl_get_prompt",
|
||||
"rl.write": "scr_rl_write",
|
||||
"rl.writeKey": "scr_rl_write_key",
|
||||
@@ -1054,6 +1066,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"rl.terminal": "scr_rl_terminal",
|
||||
"rl.line": "scr_rl_line",
|
||||
"rl.isTerminal": "scr_rl_is_terminal",
|
||||
"rl.isPromises": "scr_rl_is_promises",
|
||||
"rl.inputFromDyn": "scr_stream_rl_from_dyn",
|
||||
"rl.outputFromDyn": "scr_stream_rl_from_dyn",
|
||||
"rl.cursor": "scr_rl_cursor",
|
||||
"rl.seq": "scr_rl_seq",
|
||||
"rl.moves": "scr_rl_moves",
|
||||
|
||||
@@ -0,0 +1,409 @@
|
||||
/* LlEmitter's receivers: receiver reads, and the analyses that decide when calls, receivers and returns borrow. */
|
||||
import type { LlEmitter } from "./emitter.js";
|
||||
import {
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrType,
|
||||
isRefCounted,
|
||||
} from "../../ir/ir.js";
|
||||
import type { LlValue } from "./expr-context.js";
|
||||
import { guardedValue } from "./borrowed-receivers.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { emitBorrowedArrayRead, emitProjectedArrayRead } from "./local-array-reads.js";
|
||||
import { emitStackMapRead, matchMapRead } from "./map-read-lifetimes.js";
|
||||
import { emitBorrowedResultCall, emitCallExpr } from "./expr-calls.js";
|
||||
import { emitCheckedNarrowReceiver } from "./checked-narrows.js";
|
||||
import { canStackUnion, emitStackUnion } from "./stack-unions.js";
|
||||
import { emitDowncastGuard, emitUnionNarrowGuard } from "./narrow-guards.js";
|
||||
import { emitScalarOptional, isScalarOptionalSource } from "./local-union-storage.js";
|
||||
import { borrowableInputs } from "./borrowed-inputs.js";
|
||||
import { type WalkBorrowHost, analyzeWalks } from "./walk-borrows.js";
|
||||
|
||||
/** Read a receiver whose value is consumed immediately by a field or tag
|
||||
* load. No user code may run between this read and its consumer. Locals
|
||||
* and their projections already have an owner; other expressions keep
|
||||
* their ordinary statement-frame ownership. The caller must retain any
|
||||
* reference result before evaluating another expression. */
|
||||
export function emitReadReceiver(emitter: LlEmitter, e: IrExpr): LlValue {
|
||||
if (!isRefCounted(e.type)) return emitter.emitExpr(e);
|
||||
if (e.kind === "strLit") return { name: emitter.internLiteral(e.value), type: e.type };
|
||||
const guarded = guardedValue(e);
|
||||
if (guarded !== null) {
|
||||
// Throwing guards run in place, exactly as the sequence emits them.
|
||||
for (const s of (e as IrExpr & { kind: "seqExpr" }).stmts) emitter.emitStmt(s);
|
||||
return emitter.emitReadReceiver(guarded);
|
||||
}
|
||||
if (e.kind === "varRef") {
|
||||
emitter.materializeSplitLocal(e.localId);
|
||||
const binding = emitter.binding(e.localId);
|
||||
if (binding.kind === "boxed") {
|
||||
// A projection consumes this pointer before any user code runs.
|
||||
// Keep TDZ checks and caught-value conversion on the owned path.
|
||||
if (!emitter.canBorrowReceiver(e)) return emitter.emitExpr(e);
|
||||
const box = emitter.loadBox(binding.slot);
|
||||
const payload = emitter.B.tmp();
|
||||
const value = emitter.B.tmp();
|
||||
emitter.B.line(`${payload} = getelementptr inbounds %ScrBox, ptr ${box}, i32 0, i32 5`);
|
||||
emitter.B.line(`${value} = load ptr, ptr ${payload}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (binding.kind === "global") emitter.checkGlobalTdz(e.localId);
|
||||
const value = emitter.B.tmp();
|
||||
emitter.B.line(`${value} = load ptr, ptr ${binding.slot}${emitter.slotAlias(e.localId)}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (e.kind === "dynCheck" && e.exact) return emitter.emitExactCapsuleObject(e.value, e.type);
|
||||
if (e.kind === "chainRecv") {
|
||||
// The chain's bind slot holds its receiver unchanged until the body
|
||||
// finishes, owned by the slot itself or by the chain's receiver.
|
||||
const bound = emitter.chainSlots.get(e.id);
|
||||
if (!bound)
|
||||
throw new InternalCompilerError(`llvm emitter bug: chainRecv "${e.id}" outside its chain`);
|
||||
const value = emitter.B.tmp();
|
||||
emitter.B.line(`${value} = load ${emitter.llType(bound.type)}, ptr ${bound.name}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
if (e.kind === "arrayGet" && emitter.canBorrowReceiver(e)) {
|
||||
// The array keeps the element alive until the consumer; a missing
|
||||
// element still traps exactly like the owned read.
|
||||
const slot = emitter.B.slot();
|
||||
emitter.B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
emitBorrowedArrayRead(emitter, e, slot);
|
||||
const value = emitter.B.tmp();
|
||||
emitter.B.line(`${value} = load ptr, ptr ${slot}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
const wrap = emitter.borrowableNullableWrap(e);
|
||||
if (wrap === "unit" && e.kind === "unionWrap")
|
||||
return { name: emitter.unitInstanceRef(e.unionId, e.tag), type: e.type };
|
||||
if (wrap === "ref" && e.kind === "unionWrap")
|
||||
return { name: emitter.emitReadReceiver(e.value).name, type: e.type };
|
||||
const read = matchMapRead(e, emitter.boxedUnionsById);
|
||||
if (read) {
|
||||
const result = emitStackMapRead(emitter, read);
|
||||
if (result.owner) emitter.ownSlot(result.owner.slot, result.owner.type);
|
||||
return result.value;
|
||||
}
|
||||
// A borrowed-return helper (such as an optional element read) lends its
|
||||
// result like any borrowed-return call.
|
||||
if (e.kind === "call" && emitter.borrowedReturns.has(e.callee) && emitter.canBorrowReceiver(e))
|
||||
return emitBorrowedResultCall(emitter, e);
|
||||
const arrayRead = emitter.optionalArrayReads.get(e);
|
||||
if (arrayRead) return emitProjectedArrayRead(emitter, arrayRead);
|
||||
const narrow = emitter.checkedNarrows.get(e);
|
||||
const narrowed = narrow ? emitCheckedNarrowReceiver(emitter, narrow) : null;
|
||||
if (narrowed) return narrowed;
|
||||
if (canStackUnion(e, emitter.boxedUnionsById)) return emitStackUnion(emitter, e).value;
|
||||
if (e.kind === "unionNarrow") {
|
||||
const union = emitter.emitUnionProjection(e.value);
|
||||
emitUnionNarrowGuard(emitter, union.name, e.unionId, e.tag);
|
||||
return { name: emitter.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; a
|
||||
// downcast verifies its target class (narrow-guards.ts).
|
||||
const receiver = emitter.emitReadReceiver(e.value);
|
||||
if (e.kind === "downcast" && e.type.kind === "object")
|
||||
emitDowncastGuard(emitter, 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.
|
||||
if (emitter.nullableFieldGet(e)) return emitter.emitExpr(e);
|
||||
if (e.kind === "recordGet" || e.kind === "fieldGet") {
|
||||
const receiver = emitter.emitReadReceiver(e.obj);
|
||||
if (e.kind === "recordGet") {
|
||||
// An inline union field answers a private stack box.
|
||||
const { ptr, type, inline } = emitter.recordFieldPtr(receiver.name, e.shapeId, e.field);
|
||||
return { name: emitter.loadRecordField(ptr, type, inline), type: e.type };
|
||||
}
|
||||
const { ptr, type, inline } = emitter.classFieldPtr(receiver.name, e.className, e.field);
|
||||
if (emitter.int32Slots.isField(e.className, e.field))
|
||||
return emitter.loadInt32Field(ptr, e.type);
|
||||
return { name: emitter.loadField(ptr, type, inline), type: e.type };
|
||||
}
|
||||
if (
|
||||
e.kind === "ternary" &&
|
||||
emitter.canBorrowReceiver(e.then) &&
|
||||
emitter.canBorrowReceiver(e.else_)
|
||||
) {
|
||||
// Checked projections lower to a ternary with an always-throwing
|
||||
// arm. Keep that check at the use, but borrow the successful arm.
|
||||
const B = emitter.B;
|
||||
const cond = emitter.emitExpr(e.cond);
|
||||
const slot = B.slot();
|
||||
B.entryAllocas.push(`${slot} = alloca ptr`);
|
||||
const yes = B.newLabel("read.then"),
|
||||
no = B.newLabel("read.else"),
|
||||
join = B.newLabel("read.join");
|
||||
B.condBr(cond.name, yes, no);
|
||||
for (const [label, value] of [
|
||||
[yes, e.then],
|
||||
[no, e.else_],
|
||||
] as const) {
|
||||
B.startBlock(label);
|
||||
emitter.frames.push([]);
|
||||
const result = emitter.emitReadReceiver(value);
|
||||
B.line(`store ptr ${result.name}, ptr ${slot}`);
|
||||
emitter.releaseTemps(emitter.frames.pop()!);
|
||||
B.br(join);
|
||||
}
|
||||
B.startBlock(join);
|
||||
const value = B.tmp();
|
||||
B.line(`${value} = load ptr, ptr ${slot}`);
|
||||
return { name: value, type: e.type };
|
||||
}
|
||||
// Scalar optional values and register results answer a private stack
|
||||
// box. A ternary of borrowable arms above keeps lending its arm.
|
||||
if (
|
||||
(e.kind === "optChain" ||
|
||||
e.kind === "nullish" ||
|
||||
e.kind === "ternary" ||
|
||||
e.kind === "arrIntrinsic") &&
|
||||
isScalarOptionalSource(e, emitter.unionSources)
|
||||
)
|
||||
return emitScalarOptional(emitter, e);
|
||||
if (e.kind === "call" && emitter.scalarResultCall(e))
|
||||
return emitCallExpr(emitter, e, false, true);
|
||||
if (e.kind === "call" && emitter.canBorrowReceiver(e)) return emitBorrowedResultCall(emitter, e);
|
||||
return emitter.emitExpr(e);
|
||||
}
|
||||
|
||||
/** A local stable binding keeps its value alive while later arguments
|
||||
* and the callee execute. A proven local union box cannot escape or be
|
||||
* rebound during a call; its independent payload owner keeps it alive.
|
||||
* Other writable bindings need an owned snapshot. */
|
||||
export function canBorrowCallArgument(emitter: LlEmitter, value: IrExpr): boolean {
|
||||
if (value.kind === "strLit") return true;
|
||||
// An exact capsule owns its instance while the capsule's binding holds it.
|
||||
if (value.kind === "dynCheck" && value.exact)
|
||||
return value.value.kind === "varRef" && emitter.canBorrowCallArgument(value.value);
|
||||
// An inline union field has no box to lend.
|
||||
if (emitter.inlineFieldGet(value)) return false;
|
||||
// Class casts reinterpret the same pointer (prefix layouts).
|
||||
if ((value.kind === "upcast" || value.kind === "downcast") && value.value.type.kind === "object")
|
||||
return emitter.canBorrowCallArgument(value.value);
|
||||
if (value.kind === "seqExpr") {
|
||||
const guarded = guardedValue(value);
|
||||
return guarded !== null && emitter.canBorrowCallArgument(guarded);
|
||||
}
|
||||
// A nullable-union wrap reinterprets its payload pointer; an immortal
|
||||
// unit constant needs no owner at all.
|
||||
const wrap = emitter.borrowableNullableWrap(value);
|
||||
if (wrap === "unit") return true;
|
||||
if (wrap === "ref" && value.kind === "unionWrap")
|
||||
return emitter.canBorrowCallArgument(value.value);
|
||||
// Checked casts (`x as C`, `x!` on a nullable) lower to a ternary whose
|
||||
// failing arm always throws; the successful arm projects the same
|
||||
// stable owner. The throwing arm's typed dummy owns nothing.
|
||||
if (value.kind === "unionNarrow") return emitter.canBorrowCallArgument(value.value);
|
||||
if (value.kind === "libCall") return value.fn === "error.nodeThrow";
|
||||
// A final field never changes outside constructors: while its object is
|
||||
// borrowable, so is the field's value. Nullable-pointer fields have no
|
||||
// box to lend and keep their owning path.
|
||||
if (
|
||||
value.kind === "fieldGet" &&
|
||||
emitter.finalFieldBorrows &&
|
||||
isRefCounted(value.type) &&
|
||||
emitter.finalFields.isFinal(value.className, value.field) &&
|
||||
!emitter.nullableFieldGet(value)
|
||||
)
|
||||
return emitter.canBorrowReceiver(value.obj) && emitter.canBorrowCallArgument(value.obj);
|
||||
if (value.kind === "ternary")
|
||||
return (
|
||||
emitter.canBorrowReceiver(value.then) &&
|
||||
emitter.canBorrowReceiver(value.else_) &&
|
||||
emitter.canBorrowCallArgument(value.then) &&
|
||||
emitter.canBorrowCallArgument(value.else_)
|
||||
);
|
||||
// A construction-only field of a stable owner keeps its value until the
|
||||
// owner dies: no later operand or callee can replace and release it.
|
||||
if ((value.kind === "fieldGet" || value.kind === "recordGet") && emitter.immutableRead(value))
|
||||
return emitter.canBorrowCallArgument(value.obj);
|
||||
// An optional chain's bound receiver stays unchanged until its body
|
||||
// finishes (see emitReadReceiver).
|
||||
if (value.kind === "chainRecv") return emitter.chainSlots.has(value.id);
|
||||
if (value.kind !== "varRef") return false;
|
||||
const binding = emitter.binding(value.localId);
|
||||
// A module constant owns its value for the rest of the program.
|
||||
if (binding.kind === "global") return emitter.stableGlobals.has(value.localId);
|
||||
// So does the environment for a capture nothing ever rebinds.
|
||||
if (binding.kind === "boxed")
|
||||
return emitter.stableCaptureIds.has(value.localId) && emitter.canBorrowReceiver(value);
|
||||
return (
|
||||
binding.kind === "local" &&
|
||||
binding.local !== undefined &&
|
||||
(!binding.local.mutable ||
|
||||
emitter.stableCallBindings.has(value.localId) ||
|
||||
emitter.borrowedParameters.has(value.localId) ||
|
||||
emitter.localUnionStorage.has(value.localId)) &&
|
||||
!binding.local.boxed &&
|
||||
!binding.local.tdz
|
||||
);
|
||||
}
|
||||
|
||||
export function canBorrowReceiver(emitter: LlEmitter, e: IrExpr): boolean {
|
||||
switch (e.kind) {
|
||||
case "chainRecv":
|
||||
return emitter.chainSlots.has(e.id);
|
||||
case "varRef": {
|
||||
const binding = emitter.binding(e.localId);
|
||||
return (
|
||||
binding.kind !== "boxed" ||
|
||||
(!binding.local!.tdz &&
|
||||
binding.type.kind === e.type.kind &&
|
||||
e.type.kind !== "caught" &&
|
||||
isRefCounted(e.type))
|
||||
);
|
||||
}
|
||||
case "unionNarrow":
|
||||
// Class casts reinterpret the same pointer (prefix layouts).
|
||||
case "downcast":
|
||||
case "upcast":
|
||||
return emitter.canBorrowReceiver(e.value);
|
||||
// An inline union field has no box to lend: projections read it
|
||||
// through emitReadReceiver's private stack box.
|
||||
case "fieldGet":
|
||||
case "recordGet":
|
||||
return !emitter.inlineFieldGet(e) && emitter.canBorrowReceiver(e.obj);
|
||||
case "ternary":
|
||||
return emitter.canBorrowReceiver(e.then) && emitter.canBorrowReceiver(e.else_);
|
||||
case "seqExpr": {
|
||||
const guarded = guardedValue(e);
|
||||
return guarded !== null && emitter.canBorrowReceiver(guarded);
|
||||
}
|
||||
// The runtime always sets a pending exception; its unreachable
|
||||
// typed dummy is null and owns no receiver.
|
||||
case "libCall":
|
||||
return e.fn === "error.nodeThrow";
|
||||
case "unionWrap": {
|
||||
const wrap = emitter.borrowableNullableWrap(e);
|
||||
return wrap === "unit" || (wrap === "ref" && emitter.canBorrowReceiver(e.value));
|
||||
}
|
||||
// A borrowed-return result stays reachable from its borrowed
|
||||
// arguments until the consumer runs. Each argument is lent without a
|
||||
// snapshot, so neither the callee nor a later argument may replace
|
||||
// what it was read from: a callee that preserves every edge cannot,
|
||||
// and any other one must leave the argument's locations unwritten.
|
||||
case "call": {
|
||||
if (!emitter.borrowedReturns.has(e.callee)) return false;
|
||||
if (!e.args.every((arg) => emitter.referenceEffects.preserves(arg))) return false;
|
||||
const preserving = emitter.referenceEffects.functions.has(e.callee);
|
||||
if (preserving)
|
||||
return e.args.every((arg) => !isRefCounted(arg.type) || emitter.canBorrowReceiver(arg));
|
||||
if (emitter.referenceWrites === null) return false;
|
||||
const lent = borrowableInputs(
|
||||
emitter,
|
||||
e.args,
|
||||
false,
|
||||
emitter.referenceWrites.summary(e.callee),
|
||||
);
|
||||
return e.args.every((arg, i) => !isRefCounted(arg.type) || lent[i] === true);
|
||||
}
|
||||
// A required element read: the (borrowed) array owns the element
|
||||
// while an edge-preserving index computes.
|
||||
case "arrayGet":
|
||||
return (
|
||||
e.arr.type.kind === "array" &&
|
||||
isRefCounted(e.arr.type.elem) &&
|
||||
emitter.canBorrowReceiver(e.arr) &&
|
||||
emitter.referenceEffects.preserves(e.index)
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Functions returning a walk of their borrowed parameters return it
|
||||
* without a reference (+0) from their borrowing body; direct callers
|
||||
* either consume it as a receiver/walk or retain it at once, and the owned
|
||||
* adapter retains before releasing its parameters. Candidates preserve
|
||||
* every heap edge (so the result stays reachable from the arguments),
|
||||
* borrow every reference parameter and return plain pointers. The set
|
||||
* grows to a fixpoint because a call to a borrowed-return function with
|
||||
* walk arguments is itself a walk; walk facts are then recomputed with
|
||||
* the final set, admitting only parameters whose convention borrows. */
|
||||
export function analyzeBorrowedReturns(emitter: LlEmitter): void {
|
||||
const pointer = (type: IrType): boolean => emitter.plainReference(type);
|
||||
const borrowedIds = (fn: IrFunction): Set<string> => {
|
||||
const indexes = emitter.callLifetimes.borrowed.get(fn.name);
|
||||
return new Set(fn.params.filter((_, i) => indexes?.has(i)).map((p) => p.localId));
|
||||
};
|
||||
const hostFor = (fn: IrFunction): WalkBorrowHost => {
|
||||
const allowed = borrowedIds(fn);
|
||||
const host = {
|
||||
pointerLocal: (local: IrLocal) => pointer(local.type),
|
||||
borrowsWithoutOwning: (e: IrExpr) => emitter.borrowsWithoutOwning(e),
|
||||
borrowedReturn: (callee: string) => emitter.borrowedReturns.has(callee),
|
||||
parameterAllowed: (id: string) => allowed.has(id),
|
||||
};
|
||||
return emitter.referenceEffects.functions.has(fn.name)
|
||||
? host
|
||||
: emitter.unwrittenWalkHost(fn, host);
|
||||
};
|
||||
// With ReferenceWrites, a body that writes none of its walks' locations
|
||||
// qualifies like one that preserves every edge.
|
||||
const preserving = [...emitter.fnByName.values()].filter(
|
||||
(fn) => emitter.referenceEffects.functions.has(fn.name) || emitter.referenceWrites !== null,
|
||||
);
|
||||
const candidates = preserving.filter((fn) => {
|
||||
if (!pointer(fn.returnType) || fn.async || fn.generator || fn.captures) return false;
|
||||
// A borrowing vtable slot dispatches to the borrowing body (or its
|
||||
// virtual adapter) directly, and virtual callers own the result.
|
||||
if (emitter.implSlotBorrowed(fn.name).size > 0) return false;
|
||||
const indexes = emitter.callLifetimes.borrowed.get(fn.name);
|
||||
return (
|
||||
indexes !== undefined &&
|
||||
indexes.size > 0 &&
|
||||
fn.params.every((p, i) => !isRefCounted(p.type) || indexes.has(i))
|
||||
);
|
||||
});
|
||||
for (let changed = true; changed;) {
|
||||
changed = false;
|
||||
for (const fn of candidates) {
|
||||
if (emitter.borrowedReturns.has(fn.name)) continue;
|
||||
if (!analyzeWalks(fn, hostFor(fn)).returnsWalk) continue;
|
||||
emitter.borrowedReturns.add(fn.name);
|
||||
// Nested borrowed returns extend the locations a caller depends on.
|
||||
emitter.borrowedReturnKeyCache.clear();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (emitter.borrowedReturns.size === 0) return;
|
||||
for (const fn of preserving) {
|
||||
const walks = analyzeWalks(fn, hostFor(fn)).locals;
|
||||
if (walks.size > 0) emitter.walkBorrowsByFunction.set(fn.name, walks);
|
||||
}
|
||||
}
|
||||
|
||||
export function fieldAliasMetadata(emitter: LlEmitter): string[] {
|
||||
if (emitter.fieldAliasTags.size === 0) return [];
|
||||
const lines = ['!0 = !{!"scriptc native fields"}'];
|
||||
for (const tag of emitter.fieldAliasTags.values()) {
|
||||
lines.push(
|
||||
`!${tag - 1} = !{!"field.${tag}", !0, i64 0}`,
|
||||
`!${tag} = !{!${tag - 1}, !${tag - 1}, i64 0}`,
|
||||
);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** A private stack union source with an independently owned payload. */
|
||||
export function canStackReceiver(emitter: LlEmitter, e: IrExpr): boolean {
|
||||
return emitter.isStackUnionSource(e);
|
||||
}
|
||||
|
||||
/** True for union sources that emitReadReceiver can produce as a private
|
||||
* stack box with an independently owned payload (no heap union). */
|
||||
export function isStackUnionSource(emitter: LlEmitter, e: IrExpr): boolean {
|
||||
if (e.type.kind !== "union") return false;
|
||||
return (
|
||||
canStackUnion(e, emitter.boxedUnionsById) ||
|
||||
matchMapRead(e, emitter.boxedUnionsById) !== null ||
|
||||
emitter.optionalArrayReads.get(e) !== null ||
|
||||
isScalarOptionalSource(e, emitter.unionSources) ||
|
||||
emitter.scalarResultCall(e) ||
|
||||
emitter.inlineFieldGet(e)
|
||||
);
|
||||
}
|
||||
@@ -98,6 +98,218 @@ import { CacheInputsChangedError } from "./session.js";
|
||||
import { ensureRuntimeObjects, stageRuntimeObjects, clearCcCaches } from "./runtime-objects.js";
|
||||
import { localizeLibraryObjects } from "./object-merge.js";
|
||||
|
||||
/** What the runtime-unit table reads: the build options, the target driver,
|
||||
* the units other options imply, and the vendor inputs resolved for this
|
||||
* build. */
|
||||
interface RuntimeUnitContext {
|
||||
opts: CcOptions;
|
||||
driver: ReturnType<typeof resolveCc>;
|
||||
rt: (path: string) => string;
|
||||
rtDir: string;
|
||||
regex: boolean;
|
||||
nativeFetch: boolean;
|
||||
net: boolean;
|
||||
http: boolean;
|
||||
tls: boolean;
|
||||
tlsCa: boolean;
|
||||
cipher: boolean;
|
||||
lreObjects: readonly string[];
|
||||
zlibObjects: readonly string[];
|
||||
tlsArchive: string | null;
|
||||
}
|
||||
|
||||
/** An optional runtime input: runtime sources compiled when `when` holds,
|
||||
* or command-line arguments computed from the build. */
|
||||
interface RuntimeUnit {
|
||||
when: (u: RuntimeUnitContext) => boolean | undefined;
|
||||
sources: readonly string[];
|
||||
args?: (u: RuntimeUnitContext) => readonly string[];
|
||||
}
|
||||
|
||||
const unit = (when: RuntimeUnit["when"], ...sources: string[]): RuntimeUnit => ({ when, sources });
|
||||
const unitArgs = (args: (u: RuntimeUnitContext) => readonly string[]): RuntimeUnit => ({
|
||||
when: () => true,
|
||||
sources: [],
|
||||
args,
|
||||
});
|
||||
|
||||
/** The optional runtime inputs, in link order. The order is part of the
|
||||
* executable: a new unit goes next to the units it references. */
|
||||
const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
unit(
|
||||
(u) => u.opts.workers,
|
||||
"scr_worker.c",
|
||||
"scr_worker_events.c",
|
||||
"scr_mailbox.c",
|
||||
"scr_message.c",
|
||||
),
|
||||
unit((u) => u.opts.copying, "scr_copying.c"),
|
||||
unit((u) => u.opts.fileHandle, "scr_file_handle.c"),
|
||||
// win32 targets compile the libc-shim TU (stpcpy, arc4random_buf,
|
||||
// gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares
|
||||
// them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036,
|
||||
// GetUserNameA), iphlpapi (GetAdaptersAddresses behind
|
||||
// os.networkInterfaces), and ws2_32 (inet_ntop there; the socket
|
||||
// units ride the same import). Never present on the default path, so
|
||||
// the historical line cannot change.
|
||||
unitArgs((u) =>
|
||||
targetPlatform(u.driver) === "win32"
|
||||
? [u.rt(join(u.rtDir, "scr_win.c")), "-ladvapi32", "-liphlpapi", "-lws2_32"]
|
||||
: [],
|
||||
),
|
||||
// musl deliberately has no libc-identification predefine; resolveCc's
|
||||
// SCR_MUSL flag and this target-selected TU travel together.
|
||||
unit((u) => isMuslTarget(u.driver), "scr_musl.c"),
|
||||
// scr_glob.c (path.matchesGlob) compiles its segments through the
|
||||
// regex unit and scr_idna.c (node:url's IDNA) normalizes through it,
|
||||
// so both ride the same switch; section GC drops them from binaries
|
||||
// that never use them.
|
||||
unitArgs((u) =>
|
||||
u.regex
|
||||
? [
|
||||
"-I",
|
||||
vendorEngineDir(),
|
||||
u.rt(join(u.rtDir, "scr_regex.c")),
|
||||
u.rt(join(u.rtDir, "scr_glob.c")),
|
||||
u.rt(join(u.rtDir, "scr_idna.c")),
|
||||
...u.lreObjects,
|
||||
]
|
||||
: [],
|
||||
),
|
||||
unit((u) => u.opts.assert || u.regex || u.opts.symbol, "scr_assert.c"),
|
||||
unit((u) => u.opts.inspect, "scr_inspect.c", "scr_console_native.c"),
|
||||
unit((u) => u.opts.dynInvoke || u.opts.workers || u.nativeFetch, "scr_dyn_invoke.c"),
|
||||
unit((u) => u.opts.dc, "scr_dc.c"),
|
||||
unit((u) => u.opts.dynTier, "scr_js_value.c"),
|
||||
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
|
||||
unit(
|
||||
(u) =>
|
||||
u.opts.dynAsync ||
|
||||
u.opts.dynInvoke ||
|
||||
u.opts.workers ||
|
||||
u.opts.dc ||
|
||||
u.opts.fileHandle ||
|
||||
u.nativeFetch,
|
||||
"scr_async_dyn.c",
|
||||
),
|
||||
// The performance timeline (every perf_hooks user-timing call selects
|
||||
// dynAsync, whose unit carries the timeline's buffer warning).
|
||||
unit((u) => u.opts.dynAsync, "scr_perf.c"),
|
||||
// Blob and File (every blob.* call selects dynAsync, whose unit boxes
|
||||
// their reads as dyn promises).
|
||||
unit((u) => u.opts.dynAsync, "scr_blob.c"),
|
||||
// The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system
|
||||
// libz on the default host-clang build, vendored objects on Zig builds)
|
||||
// also serves the native fetch's gzip decoder — spread exactly once.
|
||||
unitArgs((u) =>
|
||||
u.opts.zlib
|
||||
? isZigDriver(u.driver)
|
||||
? ["-I", vendorZlibDir(), u.rt(join(u.rtDir, "scr_zlib.c")), ...u.zlibObjects]
|
||||
: [u.rt(join(u.rtDir, "scr_zlib.c"))]
|
||||
: u.nativeFetch
|
||||
? isZigDriver(u.driver)
|
||||
? ["-I", vendorZlibDir(), ...u.zlibObjects]
|
||||
: []
|
||||
: [],
|
||||
),
|
||||
unit((u) => u.opts.events, "scr_events.c", "scr_readline.c"),
|
||||
// readline's terminal mode: rl.terminal sets the regex unit it
|
||||
// measures with.
|
||||
unit((u) => u.opts.events && u.regex, "scr_readline_tty.c"),
|
||||
unit((u) => u.opts.emitter || u.opts.workers, "scr_events_emitter.c"),
|
||||
// The checked-dynamic HANDLE support unit (listener gate + runtime
|
||||
// adapter closures): every referencing unit is one of the emitter or
|
||||
// net families (http implies net), so handle-free binaries keep
|
||||
// their exact size class.
|
||||
unit((u) => u.opts.emitter || u.opts.workers || u.net, "scr_dyn_handle.c"),
|
||||
unit((u) => u.opts.symbol, "scr_symbol.c"),
|
||||
unit((u) => u.opts.assert && u.opts.bigint, "scr_bigint_assert.c"),
|
||||
unit((u) => u.opts.qs, "scr_qs.c"),
|
||||
// The legacy url unit references the regex, querystring and dyn-async
|
||||
// units; the url.parse family's IR sets all three.
|
||||
unit((u) => u.regex && u.opts.qs && u.opts.dynAsync, "scr_url_legacy.c"),
|
||||
unit((u) => u.opts.parseArgs, "scr_util.c"),
|
||||
unit((u) => u.opts.parseArgs && u.opts.symbol, "scr_util_compare.c"),
|
||||
unit((u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync, "scr_util_style.c"),
|
||||
unit(
|
||||
(u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex,
|
||||
"scr_util_callable.c",
|
||||
),
|
||||
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
|
||||
// The zlib stream classes reference both the zlib engine and the
|
||||
// stream unit.
|
||||
unit((u) => u.opts.zlib && u.opts.stream, "scr_zlib_stream.c"),
|
||||
unit((u) => u.opts.stream && u.opts.dynAsync, "scr_stream_ops.c"),
|
||||
// FileHandle streams reference both the stream unit and FileHandle.
|
||||
unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"),
|
||||
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
|
||||
// epoll on Linux, WSAPoll on Windows — each TU is empty off its
|
||||
// platform, so all three link whenever a poller-using unit does and
|
||||
// the others cost nothing (ws2_32 rides the unconditional win32 libs
|
||||
// above).
|
||||
unit((u) => u.net || u.opts.dgram, "scr_loop_kqueue.c", "scr_loop_epoll.c", "scr_loop_wsapoll.c"),
|
||||
unit((u) => u.net, "scr_net.c"),
|
||||
unit((u) => u.http, "scr_http.c"),
|
||||
unit((u) => u.opts.http2, "scr_http2.c"),
|
||||
unit((u) => u.opts.dgram, "scr_dgram.c"),
|
||||
unit((u) => u.opts.watch, "scr_watch.c"),
|
||||
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
|
||||
unit((u) => u.opts.nodeTest, "scr_test.c"),
|
||||
unit((u) => u.opts.webcrypto, "scr_webcrypto.c"),
|
||||
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
|
||||
// references its default-set override unconditionally.
|
||||
unit((u) => u.tlsCa, "scr_tls_ca.c"),
|
||||
unitArgs((u) =>
|
||||
u.tlsArchive
|
||||
? [
|
||||
"-I",
|
||||
join(vendorTlsDir(), "include"),
|
||||
...(u.tls ? [u.rt(join(u.rtDir, "scr_tls.c"))] : []),
|
||||
...(u.cipher ? [u.rt(join(u.rtDir, "scr_cipher.c"))] : []),
|
||||
u.tlsArchive,
|
||||
// mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h).
|
||||
// The unconditional win32 libs above do not carry it.
|
||||
// Never present on the default path, so the historical TLS
|
||||
// link line cannot change.
|
||||
...(targetPlatform(u.driver) === "win32" ? ["-lbcrypt"] : []),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
// scr_tls_ca.c enumerates and PEM-encodes Windows system-store entries.
|
||||
// This is independent of the mbedTLS archive: getCACertificates-only
|
||||
// programs need crypt32 too, while TLS programs imply the CA unit.
|
||||
unitArgs((u) => (u.tlsCa && targetPlatform(u.driver) === "win32" ? ["-lcrypt32"] : [])),
|
||||
// Fetch uses the native network, TLS, and HTTP runtime.
|
||||
unit((u) => u.nativeFetch, "scr_fetch.c"),
|
||||
];
|
||||
|
||||
/** The arguments of the optional runtime inputs, in link order. */
|
||||
function runtimeUnitArgs(u: RuntimeUnitContext): string[] {
|
||||
return RUNTIME_UNITS.flatMap((entry) =>
|
||||
entry.args
|
||||
? [...entry.args(u)]
|
||||
: entry.when(u)
|
||||
? entry.sources.map((source) => u.rt(join(u.rtDir, source)))
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
/** The runtime's optional compile-time definitions, in command-line order.
|
||||
* Both the program build and the runtime-object flags use this list. */
|
||||
const RUNTIME_DEFINES: readonly {
|
||||
define: string;
|
||||
when: (opts: CcOptions) => boolean | undefined;
|
||||
}[] = [
|
||||
{ define: "SCR_TEXT_DECODER_LEGACY", when: (opts) => opts.textDecoderLegacy },
|
||||
{ define: "SCR_WARNING_REPLACE", when: (opts) => opts.warningReplace },
|
||||
{ define: "SCR_STDIO_EXTENDED", when: (opts) => opts.stdioExtended },
|
||||
{ define: "SCR_YIN", when: (opts) => opts.youngNursery },
|
||||
];
|
||||
|
||||
function runtimeDefineArgs(opts: CcOptions): string[] {
|
||||
return RUNTIME_DEFINES.filter((d) => d.when(opts)).map((d) => `-D${d.define}`);
|
||||
}
|
||||
|
||||
export async function compileCInternal(
|
||||
opts: CcOptions,
|
||||
cacheWarmOnly: boolean,
|
||||
@@ -444,10 +656,7 @@ export async function compileCInternal(
|
||||
? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
|
||||
: [optimization === "dev" ? "-O0" : "-O2"]),
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []),
|
||||
...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []),
|
||||
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
|
||||
...runtimeDefineArgs(opts),
|
||||
"-fno-math-errno",
|
||||
// The runtime object model uses type-punned C: a hierarchy
|
||||
// upcast is a raw pointer cast, so one object's header (rc, vt) and
|
||||
@@ -465,146 +674,22 @@ export async function compileCInternal(
|
||||
"-I",
|
||||
rtDir,
|
||||
...runtimeSources.map((f) => rt(join(rtDir, f))),
|
||||
...(opts.workers
|
||||
? ["scr_worker.c", "scr_worker_events.c", "scr_mailbox.c", "scr_message.c"].map((f) =>
|
||||
rt(join(rtDir, f)),
|
||||
)
|
||||
: []),
|
||||
...(opts.copying ? [rt(join(rtDir, "scr_copying.c"))] : []),
|
||||
...(opts.fileHandle ? [rt(join(rtDir, "scr_file_handle.c"))] : []),
|
||||
// win32 targets compile the libc-shim TU (stpcpy, arc4random_buf,
|
||||
// gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares
|
||||
// them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036,
|
||||
// GetUserNameA), iphlpapi (GetAdaptersAddresses behind
|
||||
// os.networkInterfaces), and ws2_32 (inet_ntop there; the socket
|
||||
// units ride the same import). Never present on the default path, so
|
||||
// the historical line cannot change.
|
||||
...(targetPlatform(driver) === "win32"
|
||||
? [rt(join(rtDir, "scr_win.c")), "-ladvapi32", "-liphlpapi", "-lws2_32"]
|
||||
: []),
|
||||
// musl deliberately has no libc-identification predefine; resolveCc's
|
||||
// SCR_MUSL flag and this target-selected TU travel together.
|
||||
...(isMuslTarget(driver) ? [rt(join(rtDir, "scr_musl.c"))] : []),
|
||||
// scr_glob.c (path.matchesGlob) compiles its segments through the
|
||||
// regex unit and scr_idna.c (node:url's IDNA) normalizes through it,
|
||||
// so both ride the same switch; section GC drops them from binaries
|
||||
// that never use them.
|
||||
...(regex
|
||||
? [
|
||||
"-I",
|
||||
vendorEngineDir(),
|
||||
rt(join(rtDir, "scr_regex.c")),
|
||||
rt(join(rtDir, "scr_glob.c")),
|
||||
rt(join(rtDir, "scr_idna.c")),
|
||||
...lreObjects,
|
||||
]
|
||||
: []),
|
||||
...(opts.assert || regex || opts.symbol ? [rt(join(rtDir, "scr_assert.c"))] : []),
|
||||
...(opts.inspect
|
||||
? [rt(join(rtDir, "scr_inspect.c")), rt(join(rtDir, "scr_console_native.c"))]
|
||||
: []),
|
||||
...(opts.dynInvoke || opts.workers || nativeFetch ? [rt(join(rtDir, "scr_dyn_invoke.c"))] : []),
|
||||
...(opts.dc ? [rt(join(rtDir, "scr_dc.c"))] : []),
|
||||
...(opts.dynTier === true ? [rt(join(rtDir, "scr_js_value.c"))] : []),
|
||||
...(opts.eventTarget || nativeFetch ? [rt(join(rtDir, "scr_event_target.c"))] : []),
|
||||
...(opts.dynAsync || opts.dynInvoke || opts.workers || opts.dc || opts.fileHandle || nativeFetch
|
||||
? [rt(join(rtDir, "scr_async_dyn.c"))]
|
||||
: []),
|
||||
// The performance timeline (every perf_hooks user-timing call selects
|
||||
// dynAsync, whose unit carries the timeline's buffer warning).
|
||||
...(opts.dynAsync ? [rt(join(rtDir, "scr_perf.c"))] : []),
|
||||
// Blob and File (every blob.* call selects dynAsync, whose unit boxes
|
||||
// their reads as dyn promises).
|
||||
...(opts.dynAsync ? [rt(join(rtDir, "scr_blob.c"))] : []),
|
||||
// The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system
|
||||
// libz on the default host-clang build, vendored objects on Zig builds)
|
||||
// also serves the native fetch's gzip decoder — spread exactly once.
|
||||
...(opts.zlib
|
||||
? isZigDriver(driver)
|
||||
? ["-I", vendorZlibDir(), rt(join(rtDir, "scr_zlib.c")), ...zlibObjects]
|
||||
: [rt(join(rtDir, "scr_zlib.c"))]
|
||||
: nativeFetch
|
||||
? isZigDriver(driver)
|
||||
? ["-I", vendorZlibDir(), ...zlibObjects]
|
||||
: []
|
||||
: []),
|
||||
|
||||
// through the native installation hooks — the emitted main calls its
|
||||
// installer exactly then.
|
||||
...(opts.events ? [rt(join(rtDir, "scr_events.c")), rt(join(rtDir, "scr_readline.c"))] : []),
|
||||
// readline's terminal mode: rl.terminal sets the regex unit it
|
||||
// measures with.
|
||||
...(opts.events && regex ? [rt(join(rtDir, "scr_readline_tty.c"))] : []),
|
||||
...(opts.emitter || opts.workers ? [rt(join(rtDir, "scr_events_emitter.c"))] : []),
|
||||
// The checked-dynamic HANDLE support unit (listener gate + runtime
|
||||
// adapter closures): every referencing unit is one of the emitter or
|
||||
// net families (http implies net), so handle-free binaries keep
|
||||
// their exact size class.
|
||||
...(opts.emitter || opts.workers || net ? [rt(join(rtDir, "scr_dyn_handle.c"))] : []),
|
||||
...(opts.symbol ? [rt(join(rtDir, "scr_symbol.c"))] : []),
|
||||
...(opts.assert && opts.bigint ? [rt(join(rtDir, "scr_bigint_assert.c"))] : []),
|
||||
...(opts.qs ? [rt(join(rtDir, "scr_qs.c"))] : []),
|
||||
// The legacy url unit references the regex, querystring and dyn-async
|
||||
// units; the url.parse family's IR sets all three.
|
||||
...(regex && opts.qs && opts.dynAsync ? [rt(join(rtDir, "scr_url_legacy.c"))] : []),
|
||||
...(opts.parseArgs ? [rt(join(rtDir, "scr_util.c"))] : []),
|
||||
...(opts.parseArgs && opts.symbol ? [rt(join(rtDir, "scr_util_compare.c"))] : []),
|
||||
...(opts.parseArgs && opts.inspect && opts.dynAsync
|
||||
? [rt(join(rtDir, "scr_util_style.c"))]
|
||||
: []),
|
||||
...(opts.parseArgs && opts.inspect && opts.dynAsync && opts.dynInvoke && regex
|
||||
? [rt(join(rtDir, "scr_util_callable.c"))]
|
||||
: []),
|
||||
...(opts.stream ? [rt(join(rtDir, "scr_stream.c")), rt(join(rtDir, "scr_fs_stream.c"))] : []),
|
||||
// The zlib stream classes reference both the zlib engine and the
|
||||
// stream unit.
|
||||
...(opts.zlib && opts.stream ? [rt(join(rtDir, "scr_zlib_stream.c"))] : []),
|
||||
...(opts.stream && opts.dynAsync ? [rt(join(rtDir, "scr_stream_ops.c"))] : []),
|
||||
// FileHandle streams reference both the stream unit and FileHandle.
|
||||
...(opts.stream && opts.fileHandle ? [rt(join(rtDir, "scr_fs_stream_fh.c"))] : []),
|
||||
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
|
||||
// epoll on Linux, WSAPoll on Windows — each TU is empty off its
|
||||
// platform, so all three link whenever a poller-using unit does and
|
||||
// the others cost nothing (ws2_32 rides the unconditional win32 libs
|
||||
// above).
|
||||
...(net || opts.dgram
|
||||
? [
|
||||
rt(join(rtDir, "scr_loop_kqueue.c")),
|
||||
rt(join(rtDir, "scr_loop_epoll.c")),
|
||||
rt(join(rtDir, "scr_loop_wsapoll.c")),
|
||||
]
|
||||
: []),
|
||||
...(net ? [rt(join(rtDir, "scr_net.c"))] : []),
|
||||
...(http ? [rt(join(rtDir, "scr_http.c"))] : []),
|
||||
...((opts.http2 ?? false) ? [rt(join(rtDir, "scr_http2.c"))] : []),
|
||||
...(opts.dgram ? [rt(join(rtDir, "scr_dgram.c"))] : []),
|
||||
...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []),
|
||||
...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []),
|
||||
...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []),
|
||||
...(opts.webcrypto ? [rt(join(rtDir, "scr_webcrypto.c"))] : []),
|
||||
// The CA-store unit rides its own gate OR the tls one: scr_tls.c
|
||||
// references its default-set override unconditionally.
|
||||
...(tlsCa ? [rt(join(rtDir, "scr_tls_ca.c"))] : []),
|
||||
...(tlsArchive
|
||||
? [
|
||||
"-I",
|
||||
join(vendorTlsDir(), "include"),
|
||||
...(tls ? [rt(join(rtDir, "scr_tls.c"))] : []),
|
||||
...(cipher ? [rt(join(rtDir, "scr_cipher.c"))] : []),
|
||||
tlsArchive,
|
||||
// mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h).
|
||||
// The unconditional win32 libs above do not carry it.
|
||||
// Never present on the default path, so the historical TLS
|
||||
// link line cannot change.
|
||||
...(targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
|
||||
]
|
||||
: []),
|
||||
// scr_tls_ca.c enumerates and PEM-encodes Windows system-store entries.
|
||||
// This is independent of the mbedTLS archive: getCACertificates-only
|
||||
// programs need crypt32 too, while TLS programs imply the CA unit.
|
||||
...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []),
|
||||
// Fetch uses the native network, TLS, and HTTP runtime.
|
||||
...(nativeFetch ? [rt(join(rtDir, "scr_fetch.c"))] : []),
|
||||
...runtimeUnitArgs({
|
||||
opts,
|
||||
driver,
|
||||
rt,
|
||||
rtDir,
|
||||
regex,
|
||||
nativeFetch,
|
||||
net,
|
||||
http,
|
||||
tls,
|
||||
tlsCa,
|
||||
cipher,
|
||||
lreObjects,
|
||||
zlibObjects,
|
||||
tlsArchive,
|
||||
}),
|
||||
// A .ll program TU (the LLVM backend) deliberately carries no target
|
||||
// triple (byte-stable output; clang supplies the host/SCRIPTC_TARGET
|
||||
// triple exactly as it does for .c) — silence the -Woverride-module
|
||||
@@ -647,10 +732,7 @@ export async function compileCInternal(
|
||||
? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
|
||||
: [optimization === "dev" ? "-O0" : "-O2"]),
|
||||
...executableSectionFlags.compile,
|
||||
...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
|
||||
...(opts.warningReplace ? ["-DSCR_WARNING_REPLACE"] : []),
|
||||
...(opts.stdioExtended ? ["-DSCR_STDIO_EXTENDED"] : []),
|
||||
...(opts.youngNursery ? ["-DSCR_YIN"] : []),
|
||||
...runtimeDefineArgs(opts),
|
||||
"-fno-math-errno",
|
||||
"-fno-strict-aliasing", // the emitted object model type-puns — see buildArgs
|
||||
"-Wno-deprecated-declarations",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
DYN,
|
||||
F64,
|
||||
STRING,
|
||||
VOID,
|
||||
@@ -49,6 +50,40 @@ test("a reachable stack read is observed", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("parse")]), fn("parse", [stackRead])]))).toBe(true);
|
||||
});
|
||||
|
||||
test("a stack read on a checked-dynamic value is observed", () => {
|
||||
const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
|
||||
const read = (key: string): IrStmt => ({
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
loc,
|
||||
kind: "dynKeyGet",
|
||||
value,
|
||||
key: { loc, kind: "strLit", value: key, type: STRING },
|
||||
type: DYN,
|
||||
},
|
||||
});
|
||||
expect(observesStacks(mod([fn("%main", [read("stack")])]))).toBe(true);
|
||||
expect(observesStacks(mod([fn("%main", [read("message")])]))).toBe(false);
|
||||
});
|
||||
|
||||
test("a computed key reads the stack when the program spells it", () => {
|
||||
const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
|
||||
const key: IrExpr = { loc, kind: "varRef", localId: "k", type: STRING };
|
||||
const computed: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "dynKeyGet", value, key, type: DYN },
|
||||
};
|
||||
const spelled: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "strLit", value: "stack", type: STRING },
|
||||
};
|
||||
expect(observesStacks(mod([fn("%main", [computed, spelled])]))).toBe(true);
|
||||
expect(observesStacks(mod([fn("%main", [computed])]))).toBe(false);
|
||||
});
|
||||
|
||||
test("programs without stack reads are not", () => {
|
||||
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
|
||||
});
|
||||
|
||||
@@ -13,12 +13,34 @@ import { writesPublished } from "./publication.js";
|
||||
/** Lib calls that read a captured stack trace. */
|
||||
const STACK_READS: ReadonlySet<string> = new Set(["error.stack"]);
|
||||
|
||||
function readsStack(expr: IrExpr): boolean {
|
||||
return expr.kind === "libCall" && STACK_READS.has(expr.fn);
|
||||
/** A typed `error.stack` read, or a read on a checked-dynamic value that
|
||||
* can reach an error's own `stack` accessor: the constant key `stack`, or
|
||||
* a computed key when `computedKeys` (the program spells "stack", so a
|
||||
* computed key can name it). A key the program assembles from other
|
||||
* strings or from data it reads keeps the static answer. */
|
||||
function readsStack(expr: IrExpr, computedKeys: boolean): boolean {
|
||||
if (expr.kind === "libCall") return STACK_READS.has(expr.fn);
|
||||
if (expr.kind !== "dynKeyGet") return false;
|
||||
if (expr.key.kind === "strLit") return expr.key.value === "stack";
|
||||
return computedKeys;
|
||||
}
|
||||
|
||||
function containsStackRead(fn: IrFunction): boolean {
|
||||
return !everyStmtList(fn.body, { stmt: () => true, expr: (expr) => !readsStack(expr) });
|
||||
/** Whether the module contains the string literal "stack". */
|
||||
function spellsStack(mod: IrModule): boolean {
|
||||
return mod.functions.some(
|
||||
(fn) =>
|
||||
!everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => !(expr.kind === "strLit" && expr.value === "stack"),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function containsStackRead(fn: IrFunction, computedKeys: boolean): boolean {
|
||||
return !everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (expr) => !readsStack(expr, computedKeys),
|
||||
});
|
||||
}
|
||||
|
||||
/** Visit every expression of `body` that execution can reach: statements
|
||||
@@ -43,7 +65,8 @@ function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void {
|
||||
* and those reads must not cost every call of the program a frame.
|
||||
* Programs without any stack read answer false without the walk. */
|
||||
export function observesStacks(mod: IrModule): boolean {
|
||||
if (!mod.functions.some(containsStackRead)) return false;
|
||||
const computedKeys = spellsStack(mod);
|
||||
if (!mod.functions.some((fn) => containsStackRead(fn, computedKeys))) return false;
|
||||
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
|
||||
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
|
||||
const pending: string[] = [...moduleFunctionReferences(mod)];
|
||||
@@ -67,7 +90,7 @@ export function observesStacks(mod: IrModule): boolean {
|
||||
const fn = byName.get(name);
|
||||
if (fn === undefined) continue;
|
||||
visitLiveExprs(fn.body, (expr) => {
|
||||
if (readsStack(expr)) observed = true;
|
||||
if (readsStack(expr, computedKeys)) observed = true;
|
||||
if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee);
|
||||
const referenced = exprFunctionReference(expr);
|
||||
if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced);
|
||||
|
||||
@@ -27,6 +27,8 @@ test.each([
|
||||
['"default"', '"default;s3:foo;"'],
|
||||
['"default"', '"default\\0"'],
|
||||
["mod", "mod other"],
|
||||
["cb[0]", "cb[1]"],
|
||||
["cb[1]", "cb[10]"],
|
||||
])("retains a semantic difference between %s and %s", (a, b) => {
|
||||
expect(helperTokens(a)).not.toBe(helperTokens(b));
|
||||
});
|
||||
@@ -44,8 +46,12 @@ test.each([
|
||||
"'\\8'",
|
||||
"`template`",
|
||||
"/regexp/",
|
||||
"0",
|
||||
"0x10",
|
||||
"07",
|
||||
"1.5",
|
||||
"1e3",
|
||||
"1n",
|
||||
"1_000",
|
||||
"a / b",
|
||||
"\\u0061",
|
||||
"café",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** Token fingerprints for the fixed bundler helpers we recognize. This is
|
||||
* deliberately not a JavaScript parser: regexes, templates, escaped names,
|
||||
* numbers, and unknown punctuation refuse the helper. None occur in the
|
||||
* canonical helpers. Every accepted token is represented in the result;
|
||||
* comments and whitespace alone may disappear. */
|
||||
* numbers other than plain decimal integers, and unknown punctuation refuse
|
||||
* the helper. None occur in the canonical helpers. Every accepted token is
|
||||
* represented in the result; comments and whitespace alone may disappear. */
|
||||
const operators = [
|
||||
">>>=",
|
||||
"===",
|
||||
@@ -157,6 +157,15 @@ export function helperTokens(text: string): string | null {
|
||||
result += fingerprint("s", token.value);
|
||||
i = token.end;
|
||||
previous = "string";
|
||||
} else if (/[0-9]/.test(ch)) {
|
||||
// Plain decimal integers only (esbuild's `cb[keys(cb)[0]]`): a
|
||||
// legacy octal, fraction, exponent, radix prefix, BigInt suffix or
|
||||
// numeric separator refuses rather than aliasing another value.
|
||||
const start = i++;
|
||||
while (i < text.length && /[0-9]/.test(text.charAt(i))) i++;
|
||||
if (/[A-Za-z0-9_$.\\]/.test(text.charAt(i)) || (ch === "0" && i - start > 1)) return null;
|
||||
previous = text.slice(start, i);
|
||||
result += fingerprint("n", previous);
|
||||
} else if (/[A-Za-z_$]/.test(ch)) {
|
||||
const start = i++;
|
||||
while (i < text.length && /[A-Za-z0-9_$]/.test(text.charAt(i))) i++;
|
||||
|
||||
@@ -164,8 +164,28 @@ export function markFrontendInputsUnstable(): void {
|
||||
(synchronousTracker ?? activeTracker.getStore())?.markUnstable();
|
||||
}
|
||||
|
||||
/** Compiler-synthesized module sources (a bundle's CommonJS chunks split
|
||||
* into sibling modules, npm-static-bundled-chunks.ts). They answer like
|
||||
* regular files but are never recorded: each derives from the content of
|
||||
* a recorded bundle read, which already keys every cache that sees it. */
|
||||
const virtualSources = new Map<string, string>();
|
||||
|
||||
export function setVirtualSource(path: string, text: string): void {
|
||||
virtualSources.set(resolve(path), text);
|
||||
}
|
||||
|
||||
export function isVirtualSource(path: string): boolean {
|
||||
return virtualSources.has(resolve(path));
|
||||
}
|
||||
|
||||
export function clearVirtualSources(): void {
|
||||
virtualSources.clear();
|
||||
}
|
||||
|
||||
export function trackedReadFile(path: string): string | null {
|
||||
path = resolve(path);
|
||||
const virtual = virtualSources.get(path);
|
||||
if (virtual !== undefined) return virtual;
|
||||
try {
|
||||
const text = readFileSync(path, "utf8");
|
||||
record({ op: "file", path, digest: frontendSourceDigest(text) });
|
||||
@@ -178,6 +198,7 @@ export function trackedReadFile(path: string): string | null {
|
||||
|
||||
export function trackedFileExists(path: string): boolean {
|
||||
path = resolve(path);
|
||||
if (virtualSources.has(path)) return true;
|
||||
const kind = pathKind(path);
|
||||
record({ op: "kind", path, kind });
|
||||
return kind === "file";
|
||||
@@ -185,6 +206,7 @@ export function trackedFileExists(path: string): boolean {
|
||||
|
||||
export function trackedDirectoryExists(path: string): boolean {
|
||||
path = resolve(path);
|
||||
if (virtualSources.has(path)) return false;
|
||||
const kind = pathKind(path);
|
||||
record({ op: "kind", path, kind });
|
||||
return kind === "directory";
|
||||
@@ -192,6 +214,7 @@ export function trackedDirectoryExists(path: string): boolean {
|
||||
|
||||
export function trackedExists(path: string): boolean {
|
||||
path = resolve(path);
|
||||
if (virtualSources.has(path)) return true;
|
||||
const kind = pathKind(path);
|
||||
record({ op: "kind", path, kind });
|
||||
return kind !== "missing";
|
||||
@@ -199,6 +222,7 @@ export function trackedExists(path: string): boolean {
|
||||
|
||||
export function trackedRealpath(path: string): string | null {
|
||||
path = resolve(path);
|
||||
if (virtualSources.has(path)) return path;
|
||||
try {
|
||||
const target = systemRealpath(path);
|
||||
record({ op: "realpath", path, target });
|
||||
@@ -223,9 +247,13 @@ export function trackedAccessibleEntries(
|
||||
}
|
||||
files.sort();
|
||||
directories.sort();
|
||||
const answer = { files, directories };
|
||||
record({ op: "entries", path, files, directories });
|
||||
return answer;
|
||||
record({ op: "entries", path, files: [...files], directories });
|
||||
for (const virtual of virtualSources.keys()) {
|
||||
if (dirname(virtual) === path && !files.includes(basename(virtual)))
|
||||
files.push(basename(virtual));
|
||||
}
|
||||
files.sort();
|
||||
return { files, directories };
|
||||
} catch {
|
||||
// Enumeration can fail even while the path remains a directory. Preserve
|
||||
// the failed operation itself so a permission/ACL repair invalidates the
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/* Members a type assertion attaches to record values. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { type IrType, typeEquals } from "../../ir/ir.js";
|
||||
import { hostsAll } from "./expressions/hosted-narrowing.js";
|
||||
|
||||
/** `route as Route & { order: number; internal: boolean }` followed by
|
||||
* `view.internal = true` adds a property to the very object `route`
|
||||
* names: Node has no layouts, so every later read of `route` (its JSON,
|
||||
* its keys, a spread) sees the member. Native records are closed structs,
|
||||
* and the asserted intersection maps to a different struct, so neither a
|
||||
* conversion (a copy that severs the write from `route`) nor an erasure
|
||||
* (a struct without the slot) is faithful.
|
||||
*
|
||||
* This pass finds such assertions before any body lowers and gives each
|
||||
* record arm of the operand's type the asserted members it does not
|
||||
* declare, as optional fields. Values the program builds hold them absent,
|
||||
* which is invisible to every key surface (`in`, Object.keys, JSON,
|
||||
* inspect, spread). The asserted type then refines the stored arms
|
||||
* instead of naming new ones (hosted-narrowing.ts), so the assertion keeps
|
||||
* the value and its writes land in the stored object.
|
||||
*
|
||||
* Only the plain form qualifies: one intersection whose constituents are
|
||||
* the operand's own type and type literals of required property
|
||||
* signatures, over plain data types (object types, aliases and interfaces;
|
||||
* not classes or the standard library's types). Arms that are tuples or
|
||||
* carry an index signature, members whose type does not map, and members
|
||||
* two assertions type differently are left alone: those assertions keep
|
||||
* their exact-shape diagnostics. Assertions whose operand already declares
|
||||
* every member are skipped before any type maps, so programs without the
|
||||
* form intern exactly the types they did before. */
|
||||
export function extendAssertedRecordMembers(
|
||||
lowerer: Lowerer,
|
||||
files: readonly ts.SourceFile[],
|
||||
): void {
|
||||
const additions = new Map<string, Map<string, IrType>>();
|
||||
const conflicts = new Set<string>();
|
||||
const consider = (node: ts.AsExpression | ts.TypeAssertion): void => {
|
||||
let typeNode: ts.TypeNode = node.type;
|
||||
while (ts.isParenthesizedTypeNode(typeNode)) typeNode = typeNode.type;
|
||||
if (!ts.isIntersectionTypeNode(typeNode)) return;
|
||||
const parts = typeNode.types.nodes;
|
||||
const literals = parts.filter((p) => ts.isTypeLiteralNode(p));
|
||||
const bases = parts.filter((p) => !ts.isTypeLiteralNode(p));
|
||||
if (literals.length === 0 || bases.length !== 1) return;
|
||||
const memberNodes: { name: string; type: ts.TypeNode }[] = [];
|
||||
for (const literal of literals) {
|
||||
if (!ts.isTypeLiteralNode(literal)) return;
|
||||
for (const member of literal.members.nodes) {
|
||||
// A required member: the form that writes through the view. An
|
||||
// optional one only reads, and keeps its exact-shape fence.
|
||||
if (!ts.isPropertySignatureDeclaration(member) || !member.type || member.questionToken)
|
||||
return;
|
||||
const name = ts.isIdentifier(member.name)
|
||||
? member.name.text
|
||||
: ts.isStringLiteral(member.name)
|
||||
? member.name.text
|
||||
: null;
|
||||
if (name === null || name.startsWith("%")) return;
|
||||
memberNodes.push({ name, type: member.type });
|
||||
}
|
||||
}
|
||||
// Checker-only gate first: an assertion every constituent of whose
|
||||
// operand type already declares the members (`e as Expr & { kind:
|
||||
// "mul" }`, the refinement idiom) attaches nothing, and mapping its
|
||||
// types here would intern them out of the usual order.
|
||||
const operandTs = lowerer.typeOf(node.expression);
|
||||
const constituents = operandTs.isUnionType() ? ts.constituentTypes(operandTs) : [operandTs];
|
||||
const declares = (t: ts.Type, name: string): boolean =>
|
||||
lowerer.checker.getPropertyOfType(t, name) !== undefined;
|
||||
if (constituents.every((t) => memberNodes.every((m) => declares(t, m.name)))) return;
|
||||
// Plain data types only: object literal types, aliases and interfaces
|
||||
// (not classes, and not the standard library's types).
|
||||
const dataType = (t: ts.Type): boolean => {
|
||||
const symbol = t.getAliasSymbol() ?? t.getSymbol();
|
||||
return (
|
||||
!!symbol &&
|
||||
(symbol.flags & ts.SymbolFlags.Class) === 0 &&
|
||||
symbol.declarations.length > 0 &&
|
||||
symbol.declarations.every(
|
||||
(d) =>
|
||||
!lowerer.isStdlibFile(d.getSourceFile()) &&
|
||||
(ts.isTypeLiteralNode(d) ||
|
||||
ts.isInterfaceDeclaration(d) ||
|
||||
ts.isTypeAliasDeclaration(d)),
|
||||
)
|
||||
);
|
||||
};
|
||||
if (!constituents.every(dataType)) return;
|
||||
const operand = lowerer.mapTypeOf(operandTs);
|
||||
const base = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(bases[0]!));
|
||||
if (!operand || !base || !typeEquals(operand, base)) return;
|
||||
const arms =
|
||||
operand.kind === "union" ? (lowerer.unions.get(operand.unionId)?.arms ?? []) : [operand];
|
||||
if (arms.length === 0) return;
|
||||
const shapeIds: string[] = [];
|
||||
for (const arm of arms) {
|
||||
if (arm.kind !== "record") return;
|
||||
const shape = lowerer.shapes.get(arm.shapeId);
|
||||
if (!shape || shape.tuple || shape.indexValue) return;
|
||||
shapeIds.push(arm.shapeId);
|
||||
}
|
||||
const members: { name: string; type: IrType }[] = [];
|
||||
for (const { name, type } of memberNodes) {
|
||||
if (shapeIds.every((id) => lowerer.shapes.get(id)!.fields.some((f) => f.name === name)))
|
||||
continue;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(type));
|
||||
if (!mapped || mapped.kind === "void") return;
|
||||
const optional = lowerer.withUndefinedArmOf(mapped);
|
||||
if (!optional) return;
|
||||
members.push({ name, type: optional });
|
||||
}
|
||||
for (const shapeId of shapeIds) {
|
||||
const shape = lowerer.shapes.get(shapeId)!;
|
||||
for (const member of members) {
|
||||
if (shape.fields.some((f) => f.name === member.name)) continue;
|
||||
let added = additions.get(shapeId);
|
||||
if (!added) additions.set(shapeId, (added = new Map()));
|
||||
const prior = added.get(member.name);
|
||||
if (prior && !typeEquals(prior, member.type)) conflicts.add(`${shapeId}:${member.name}`);
|
||||
else added.set(member.name, member.type);
|
||||
}
|
||||
}
|
||||
};
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isAsExpression(node) || ts.isTypeAssertion(node)) consider(node);
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
for (const sf of files) {
|
||||
if (sf.isDeclarationFile) continue;
|
||||
// Cheap text gate: the form needs an intersection with a type literal.
|
||||
if (!/&\s*\{/.test(sf.text)) continue;
|
||||
visit(sf);
|
||||
}
|
||||
for (const [shapeId, added] of additions) {
|
||||
const extra = [...added]
|
||||
.filter(([name]) => !conflicts.has(`${shapeId}:${name}`))
|
||||
.map(([name, type]) => ({ name, type }));
|
||||
lowerer.shapes.extendShape(shapeId, extra);
|
||||
}
|
||||
if (additions.size > 0) keepStoredTypeOfAssertedGlobals(lowerer, files);
|
||||
}
|
||||
|
||||
/** A module-level `const view = value as T & { member: X }` binding keeps
|
||||
* the stored type of `value` once the members are attached, exactly as a
|
||||
* local binding does (lowerVarDecl): the assertion keeps the object, and
|
||||
* writes through the binding must reach it. Globals took their checker
|
||||
* (or annotated) type during collection, before the members existed; an
|
||||
* annotation the stored type hosts gives way to it the same way. */
|
||||
function keepStoredTypeOfAssertedGlobals(lowerer: Lowerer, files: readonly ts.SourceFile[]): void {
|
||||
for (const sf of files) {
|
||||
if (sf.isDeclarationFile) continue;
|
||||
for (const statement of sf.statements.nodes) {
|
||||
if (!ts.isVariableStatement(statement)) continue;
|
||||
if ((statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
||||
for (const decl of statement.declarationList.declarations.nodes) {
|
||||
if (!ts.isIdentifier(decl.name) || !decl.initializer) continue;
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (!ts.isAsExpression(init) && !ts.isTypeAssertion(init)) continue;
|
||||
const global = lowerer.globalOf(decl.name);
|
||||
const stored = lowerer.mapTypeOf(lowerer.typeOf(init.expression));
|
||||
if (!global || !stored || (stored.kind !== "union" && stored.kind !== "record")) continue;
|
||||
if (hostsAll(lowerer, stored, global.type)) global.type = stored;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { lowerEventsEmitterHelper, lowerEventsOnce } from "./lower-event-emitter
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { lowerTimersMemberCall } from "./lower-timers.js";
|
||||
import { lowerStreamModuleCall } from "./lower-stream.js";
|
||||
import { lowerCallbackPromisify } from "./builtins/promisify-callback.js";
|
||||
import { lowerNodeModuleCall } from "./builtins/module-resolution.js";
|
||||
import { builtinModuleFnOf, builtinFenceHintOf } from "./surfaces.js";
|
||||
|
||||
@@ -87,6 +88,10 @@ export function lowerBuiltinCall(
|
||||
if (testServed) return testServed;
|
||||
const utilServed = lowerer.lowerUtilModuleCall(expr, bi, loc);
|
||||
if (utilServed) return utilServed;
|
||||
if (bi.module === "util" && bi.member === "promisify") {
|
||||
const promisified = lowerCallbackPromisify(lowerer, expr, loc);
|
||||
if (promisified) return promisified;
|
||||
}
|
||||
const streamServed = lowerStreamModuleCall(lowerer, expr, bi, loc);
|
||||
if (streamServed) return streamServed;
|
||||
const fsTs = lowerer.lowerFsToUnixTimestampCall(expr, bi, loc);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { isJsSourceFile } from "../../program.js";
|
||||
import { isJsSourceFile, locOf } 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";
|
||||
import { callbackPromisifyShape } from "./promisify-callback.js";
|
||||
|
||||
/** Compile-time util.promisify projections. The immutable result binding
|
||||
* owns no runtime slot: calls rewrite to the target's promise lowering,
|
||||
@@ -164,11 +165,14 @@ export function registerPromisifiedBuiltinDecl(
|
||||
: target?.module === "dns" && isPromisifiedDnsMember(target.member)
|
||||
? { kind: "promisified-dns" as const, member: target.member }
|
||||
: null;
|
||||
// A typed callback-last function value: the call lowers to a native
|
||||
// promisified function, and the binding holds it like any closure.
|
||||
if (!projection && callbackPromisifyShape(lowerer, e, locOf(e)) !== null) return false;
|
||||
if (!projection) {
|
||||
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, 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",
|
||||
"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, the fs callback functions with an fs/promises counterpart, and typed callback-last functions whose callback takes (error?: Error | null, value?) are the supported targets",
|
||||
);
|
||||
}
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(nameNode);
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { BOOL, CAUGHT, DYN, F64, type IrExpr, type IrType, STRING } from "../../../ir/ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
CAUGHT,
|
||||
DYN,
|
||||
DYN_CLASS_PROPERTIES,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrType,
|
||||
STRING,
|
||||
} from "../../../ir/ir.js";
|
||||
|
||||
const ASSERTION_ERROR_PROPERTIES: ReadonlySet<string> = new Set([
|
||||
"actual",
|
||||
@@ -126,7 +135,13 @@ export function lowerErrorCodeProperty(
|
||||
declared,
|
||||
);
|
||||
}
|
||||
const systemField = ["errno", "syscall", "path", "dest"].includes(expr.name.text);
|
||||
// A program class under the receiver's class can carry its own `code`
|
||||
// (a declared field or a property added through an untyped reference),
|
||||
// which Node reads instead of the errno code: read through the error's
|
||||
// dynamic view, which answers both.
|
||||
const systemField =
|
||||
["errno", "syscall", "path", "dest"].includes(expr.name.text) ||
|
||||
(expr.name.text === "code" && programClassMayOwnCode(lowerer, recvT.className));
|
||||
if (
|
||||
!systemField &&
|
||||
expr.name.text !== "code" &&
|
||||
@@ -162,7 +177,9 @@ export function lowerErrorCodeProperty(
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING),
|
||||
expr.name.text === "code"
|
||||
? lowerer.envValueType()
|
||||
: lowerer.withUndefinedArm(expr.name.text === "errno" ? F64 : STRING),
|
||||
);
|
||||
}
|
||||
return {
|
||||
@@ -184,6 +201,19 @@ export function lowerErrorCodeProperty(
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether a class declared in the program, `className` or one of its
|
||||
* subclasses, has its own `code` field or a dynamic property bag. The
|
||||
* runtime error classes keep `code` in the errno slot. */
|
||||
function programClassMayOwnCode(lowerer: Lowerer, className: string): boolean {
|
||||
for (const [name, info] of lowerer.classes) {
|
||||
if (name.startsWith("%")) continue;
|
||||
if (name !== className && !lowerer.isSubclassOf(name, className)) continue;
|
||||
if (info.def.fields.some((f) => f.name === "code" || f.name === DYN_CLASS_PROPERTIES))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether values of `type` are errors: class instances whose class is
|
||||
* %Error or extends it. A subclass of a class that does not extend Error
|
||||
* cannot be an error either, so the answer depends on the static class
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { BOOL, type IrExpr, type IrType, STRING, type SrcLoc } from "../../../ir/ir.js";
|
||||
import {
|
||||
BOOL,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
} from "../../../ir/ir.js";
|
||||
import { nodeThrowExpr } from "../../../ir/build.js";
|
||||
import { processHasIpc } from "./process.js";
|
||||
|
||||
/** Method calls on ChildProcess receivers: `child.on("exit"|"error", cb)`
|
||||
* registers a listener with the event loop's child registry. The event
|
||||
@@ -86,8 +96,54 @@ export function ipcSendCallback(lowerer: Lowerer, node: ts.Expression): IrExpr {
|
||||
return cb;
|
||||
}
|
||||
|
||||
/** process.send in a program without fork targets. This binary never opens
|
||||
* a channel then, so only a Node.js parent's NODE_CHANNEL_FD can define
|
||||
* process.send, and that channel has no lowering: a call fences when it runs.
|
||||
* Without any channel the calls behave as Node's do on an undefined member:
|
||||
* `process.send?.(...)` skips its arguments and answers undefined, and
|
||||
* `process.send(...)` evaluates them and throws a TypeError. The message is
|
||||
* never sent, so its type is not checked. */
|
||||
function lowerChannelLessSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const fence = (type: IrType): IrExpr =>
|
||||
nodeThrowExpr(
|
||||
0,
|
||||
"SC2020",
|
||||
"process.send over an IPC channel opened by a Node.js parent (NODE_CHANNEL_FD) is not supported yet",
|
||||
type,
|
||||
loc,
|
||||
);
|
||||
const hasIpc = processHasIpc(lowerer, loc);
|
||||
if (call.questionDotToken) {
|
||||
const type = lowerer.mapTypeOf(lowerer.typeOf(call)) ?? BOOL;
|
||||
const absent: IrExpr =
|
||||
type.kind === "bool"
|
||||
? { kind: "boolLit", value: false, type: BOOL, loc }
|
||||
: lowerer.coerceToExpected(
|
||||
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
||||
type,
|
||||
);
|
||||
return { kind: "ternary", cond: hasIpc, then: fence(type), else_: absent, type, loc };
|
||||
}
|
||||
const stmts: IrStmt[] = call.arguments.nodes.map((arg) => ({
|
||||
kind: "exprStmt",
|
||||
expr: lowerer.lowerExpr(ts.isSpreadElement(arg) ? arg.expression : arg),
|
||||
loc: locOf(arg),
|
||||
}));
|
||||
const result: IrExpr = {
|
||||
kind: "ternary",
|
||||
cond: hasIpc,
|
||||
then: fence(BOOL),
|
||||
else_: nodeThrowExpr(1, "", "process.send is not a function", BOOL, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
return stmts.length === 0 ? result : { kind: "seqExpr", stmts, result, type: BOOL, loc };
|
||||
}
|
||||
|
||||
export function lowerProcessIpcSend(lowerer: Lowerer, call: ts.CallExpression): IrExpr {
|
||||
const loc = locOf(call);
|
||||
if (lowerer.forkTargets.length === 0) return lowerChannelLessSend(lowerer, call);
|
||||
if (
|
||||
call.arguments.nodes.length < 1 ||
|
||||
call.arguments.nodes.length > 2 ||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
type IrExpr,
|
||||
type IrLibFn,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
STRING,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
@@ -30,6 +31,144 @@ import { canonicalBuiltinModule } from "../../builtin-modules.js";
|
||||
import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js";
|
||||
import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js";
|
||||
|
||||
/** Whether the process has an IPC channel, Node's test for defining
|
||||
* process.send and process.disconnect: a channel this fork child opened
|
||||
* (programs with fork targets only), or a non-empty NODE_CHANNEL_FD from a
|
||||
* Node.js parent, the variable Node itself tests at startup. Built from the
|
||||
* existing environment read, so the presence test links no runtime code. */
|
||||
export function processHasIpc(lowerer: Lowerer, loc: SrcLoc): IrExpr {
|
||||
const envT = lowerer.envValueType();
|
||||
const unionId = (envT as IrType & { kind: "union" }).unionId;
|
||||
const stringTag = lowerer.armTag(unionId, STRING);
|
||||
const value = lowerer.declareHiddenLocal("%channelFd", envT);
|
||||
const fromNode: IrExpr = {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: value.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "process.envGet",
|
||||
args: [strLit("NODE_CHANNEL_FD", loc)],
|
||||
type: envT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: {
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag: stringTag,
|
||||
negated: false,
|
||||
value: varRef(value.id, envT, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
right: {
|
||||
kind: "toBool",
|
||||
operand: {
|
||||
kind: "unionNarrow",
|
||||
unionId,
|
||||
tag: stringTag,
|
||||
value: varRef(value.id, envT, loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
if (lowerer.forkTargets.length === 0) return fromNode;
|
||||
return {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: { kind: "libCall", fn: "process.ipcOpen", args: [], type: BOOL, loc },
|
||||
right: fromNode,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** True when the value of `node` is consumed only by ToBoolean: a
|
||||
* condition, the operand of `!`, `Boolean(node)`, or an operand of `&&` /
|
||||
* `||` whose own value is consumed that way (or discarded, for the left
|
||||
* operand). */
|
||||
export function isTruthinessOnlyRead(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
let child: ts.Node = node;
|
||||
let parent = node.parent;
|
||||
while (parent !== undefined && ts.isParenthesizedExpression(parent)) {
|
||||
child = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (parent === undefined) return false;
|
||||
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;
|
||||
if (ts.isPrefixUnaryExpression(parent)) return parent.operator === ts.SyntaxKind.ExclamationToken;
|
||||
if (ts.isCallExpression(parent)) {
|
||||
return (
|
||||
!parent.questionDotToken &&
|
||||
parent.arguments.nodes.length === 1 &&
|
||||
parent.arguments.nodes[0] === child &&
|
||||
lowerer.isStdlibGlobal(parent.expression, "Boolean")
|
||||
);
|
||||
}
|
||||
if (ts.isBinaryExpression(parent)) {
|
||||
const op = parent.operatorToken.kind;
|
||||
if (op !== ts.SyntaxKind.AmpersandAmpersandToken && op !== ts.SyntaxKind.BarBarToken)
|
||||
return false;
|
||||
if (isTruthinessOnlyRead(lowerer, parent)) return true;
|
||||
let outer: ts.Node | undefined = parent.parent;
|
||||
while (outer !== undefined && ts.isParenthesizedExpression(outer)) outer = outer.parent;
|
||||
return parent.left === child && outer !== undefined && ts.isExpressionStatement(outer);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether a program source reads process.exitCode. Such a program
|
||||
* records its exit-code assignments (process.setExitCodeTracked), so a
|
||||
* read can tell an unset code from 0; other programs keep the untracked
|
||||
* write. Computed once per lowering pass. */
|
||||
export function programReadsExitCode(lowerer: Lowerer): boolean {
|
||||
if (lowerer.readsExitCode !== null) return lowerer.readsExitCode;
|
||||
let found = false;
|
||||
const walk = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
node.name.text === "exitCode" &&
|
||||
!(
|
||||
ts.isBinaryExpression(node.parent) &&
|
||||
node.parent.left === node &&
|
||||
node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
||||
) &&
|
||||
lowerer.stdlibGlobalMember(node, "process") === "exitCode"
|
||||
) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(node, walk);
|
||||
};
|
||||
for (const sf of lowerer.program.getImplementationSourceFiles()) {
|
||||
if (sf.isDeclarationFile) continue;
|
||||
walk(sf);
|
||||
if (found) break;
|
||||
}
|
||||
lowerer.readsExitCode = found;
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The pinned Node release the runtime reports as process.versions.node
|
||||
* (packages/runtime/src/scr_lib.c SCR_NODE_COMPAT_VERSION, kept equal to
|
||||
* internal/compatibility/node-v24.json by test/node-pin.test.ts). */
|
||||
@@ -374,9 +513,45 @@ export function lowerProcessProperty(
|
||||
const member = lowerer.stdlibGlobalMember(expr, "process");
|
||||
if (member === null) return null;
|
||||
const loc = locOf(expr);
|
||||
// `if (process.send)`: Node defines process.send and process.disconnect
|
||||
// only when the process has an IPC channel. A read whose value only
|
||||
// feeds a truthiness test answers that presence; other value uses keep
|
||||
// the member fence.
|
||||
if (
|
||||
(member === "send" || member === "disconnect") &&
|
||||
!expr.questionDotToken &&
|
||||
isTruthinessOnlyRead(lowerer, expr)
|
||||
) {
|
||||
return processHasIpc(lowerer, loc);
|
||||
}
|
||||
if (member === "versions") {
|
||||
return { kind: "libCall", fn: "process.versions", args: [], type: DYN, loc };
|
||||
}
|
||||
// process.exitCode: undefined until something assigns it, then the
|
||||
// assigned code. A read the checker narrowed to number follows an
|
||||
// assignment, so the code is set.
|
||||
if (member === "exitCode" && !expr.questionDotToken) {
|
||||
const current: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "process.currentExitCode",
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(expr))?.kind === "f64") return current;
|
||||
const type: IrType = { kind: "union", unionId: lowerer.unions.intern([F64, UNDEFINED_T]) };
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: { kind: "libCall", fn: "process.hasExitCode", args: [], type: BOOL, loc },
|
||||
then: lowerer.coerceToExpected(current, type),
|
||||
else_: lowerer.coerceToExpected(
|
||||
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
|
||||
type,
|
||||
),
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// process.features as a value (`"sea" in process.features`): an object
|
||||
// holding none of the flags, consistent with the member reads above.
|
||||
if (member === "features") {
|
||||
@@ -1575,10 +1750,36 @@ export function lowerProcessMethodCall(
|
||||
}
|
||||
if (member === "exit") {
|
||||
const arg = call.arguments.nodes[0];
|
||||
const code: IrExpr =
|
||||
let code: IrExpr =
|
||||
arg !== undefined
|
||||
? lowerer.lowerExprExpecting(arg, F64)
|
||||
: { kind: "libCall", fn: "process.currentExitCode", args: [], type: F64, loc };
|
||||
// process.exit(code) assigns process.exitCode before the 'exit'
|
||||
// listeners run. Only a program that reads the property can observe
|
||||
// that, so only such a program records the assignment.
|
||||
if (arg !== undefined && programReadsExitCode(lowerer)) {
|
||||
const binding = lowerer.declareHiddenLocal("%exitCode", F64);
|
||||
code = {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: binding.id, init: code, loc },
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "process.setExitCodeTracked",
|
||||
args: [varRef(binding.id, F64, loc)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: varRef(binding.id, F64, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return { kind: "libCall", fn: "process.exit", args: [code], type: VOID, loc };
|
||||
}
|
||||
return null; // process.argv(...) etc. are tsc errors before lowering
|
||||
@@ -1594,6 +1795,16 @@ export function lowerProcessOptionalMethodCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
): IrExpr | null {
|
||||
// `process.send!(message)`: the asserted direct call of the optional
|
||||
// member (the @types/node spelling under strict null checks).
|
||||
if (
|
||||
!expr.questionDotToken &&
|
||||
ts.isNonNullExpression(expr.expression) &&
|
||||
ts.isPropertyAccessExpression(expr.expression.expression) &&
|
||||
!expr.expression.expression.questionDotToken &&
|
||||
lowerer.stdlibGlobalMember(expr.expression.expression, "process") === "send"
|
||||
)
|
||||
return lowerProcessIpcSend(lowerer, expr);
|
||||
if (!expr.questionDotToken) return null;
|
||||
if (!ts.isPropertyAccessExpression(expr.expression)) return null;
|
||||
const member = lowerer.stdlibGlobalMember(expr.expression, "process");
|
||||
|
||||
@@ -65,6 +65,9 @@ export function lowerPromiseStaticCall(
|
||||
const compatible = (inner: IrType): boolean => {
|
||||
if (typeEquals(inner, resultT.inner)) return true;
|
||||
if (!resultArms) return false;
|
||||
// A Promise<void> entry fulfills with undefined, which the result
|
||||
// holds when the checker folded `void` into its undefined arm.
|
||||
if (inner.kind === "void") return resultArms.some((a) => a.kind === "undefinedT");
|
||||
if (inner.kind === "union") {
|
||||
const arms = lowerer.unions.get(inner.unionId)?.arms ?? [];
|
||||
return arms.every((a) => resultArms.some((b) => typeEquals(a, b)));
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
BOOL,
|
||||
type IrExpr,
|
||||
type IrLocal,
|
||||
type IrParam,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
VOID,
|
||||
funcOf,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../../ir/ir.js";
|
||||
|
||||
const ERROR_T: IrType = { kind: "object", className: "%Error" };
|
||||
|
||||
/** The parts of `util.promisify(fn)` for a typed callback-last function:
|
||||
* `fn(...args, (err, value) => void)` becomes `(...args) => Promise<T>`. */
|
||||
export interface CallbackShape {
|
||||
fnT: IrType & { kind: "func" };
|
||||
resultT: IrType & { kind: "func" };
|
||||
args: readonly IrType[];
|
||||
callbackT: IrType & { kind: "func" };
|
||||
/** The parameter's declared type: callbackT, or `callbackT | undefined`
|
||||
* for an optional callback. */
|
||||
callbackParamT: IrType;
|
||||
inner: IrType;
|
||||
/** The error parameter's union and the tag of its Error arm, or null when
|
||||
* the callback declares no error parameter. */
|
||||
error: { type: IrType; errorTag: number; unitTags: number[] } | null;
|
||||
}
|
||||
|
||||
function callbackShape(
|
||||
lowerer: Lowerer,
|
||||
fnT: IrType | null,
|
||||
resultT: IrType | null,
|
||||
loc: SrcLoc,
|
||||
): CallbackShape | null {
|
||||
if (fnT?.kind !== "func" || resultT?.kind !== "func") return null;
|
||||
if (fnT.rest || fnT.restAbi || fnT.argumentsAll || resultT.rest || resultT.restAbi) return null;
|
||||
const callbackParamT = fnT.params[fnT.params.length - 1];
|
||||
// An optional callback (`close(callback?: (err?: Error) => void)`) takes
|
||||
// its function arm; the call passes the callback into the union.
|
||||
const callbackT =
|
||||
callbackParamT?.kind === "union"
|
||||
? (() => {
|
||||
const arms = lowerer.unions.get(callbackParamT.unionId)?.arms ?? [];
|
||||
const fns = arms.filter((arm) => arm.kind === "func");
|
||||
return fns.length === 1 &&
|
||||
arms.every((arm) => arm.kind === "func" || arm.kind === "undefinedT")
|
||||
? fns[0]
|
||||
: undefined;
|
||||
})()
|
||||
: callbackParamT;
|
||||
if (callbackT?.kind !== "func" || callbackT.ret.kind !== "void") return null;
|
||||
if (callbackT.rest || callbackT.restAbi || callbackT.params.length > 2) return null;
|
||||
const args = fnT.params.slice(0, -1);
|
||||
if (
|
||||
resultT.ret.kind !== "promise" ||
|
||||
resultT.params.length !== args.length ||
|
||||
!resultT.params.every((type, i) => typeEquals(type, args[i]!))
|
||||
)
|
||||
return null;
|
||||
const inner = resultT.ret.inner;
|
||||
let error: CallbackShape["error"] = null;
|
||||
const errorT = callbackT.params[0];
|
||||
if (errorT !== undefined) {
|
||||
if (errorT.kind !== "union") return null;
|
||||
const arms = lowerer.unions.get(errorT.unionId)?.arms ?? [];
|
||||
const errorTags = arms.flatMap((arm, tag) =>
|
||||
arm.kind === "object" &&
|
||||
(arm.className === "%Error" || lowerer.isSubclassOf(arm.className, "%Error"))
|
||||
? [tag]
|
||||
: [],
|
||||
);
|
||||
const unitTags = arms.flatMap((arm, tag) => (isUnitType(arm) ? [tag] : []));
|
||||
if (errorTags.length !== 1 || errorTags.length + unitTags.length !== arms.length) return null;
|
||||
error = { type: errorT, errorTag: errorTags[0]!, unitTags };
|
||||
}
|
||||
if (inner.kind !== "void") {
|
||||
const valueT = callbackT.params[1];
|
||||
if (valueT === undefined) return null;
|
||||
const probe: IrExpr = { kind: "varRef", localId: "%probe", type: valueT, loc };
|
||||
if (!typeEquals(lowerer.coerceToExpected(probe, inner).type, inner)) return null;
|
||||
}
|
||||
return { fnT, resultT, args, callbackT, callbackParamT: callbackParamT!, inner, error };
|
||||
}
|
||||
|
||||
/** The callback shape of a `promisify(fn)` call, or null. */
|
||||
export function callbackPromisifyShape(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
): CallbackShape | null {
|
||||
if (call.questionDotToken || call.arguments.nodes.length !== 1) return null;
|
||||
const argNode = call.arguments.nodes[0]!;
|
||||
if (ts.isSpreadElement(argNode)) return null;
|
||||
return callbackShape(
|
||||
lowerer,
|
||||
lowerer.mapTypeOf(lowerer.typeOf(argNode)),
|
||||
lowerer.mapTypeOf(lowerer.typeOf(call)),
|
||||
loc,
|
||||
);
|
||||
}
|
||||
|
||||
function captureParam(localId: string, type: IrType): IrParam {
|
||||
return { localId, name: localId, type };
|
||||
}
|
||||
|
||||
function boxedLocal(localId: string, type: IrType): IrLocal {
|
||||
return { id: localId, name: localId, type, mutable: false, boxed: true };
|
||||
}
|
||||
|
||||
function plainLocal(localId: string, type: IrType): IrLocal {
|
||||
return { id: localId, name: localId, type, mutable: false };
|
||||
}
|
||||
|
||||
/** `util.promisify(fn)` over a typed callback-last function value (a bound
|
||||
* method, a function declaration, a closure): a native function that calls
|
||||
* `fn` with the arguments and a callback, and settles its promise from the
|
||||
* callback like Node's wrapper: a truthy error rejects, otherwise the first
|
||||
* value fulfills. A synchronous throw rejects (the executor's rule). Null
|
||||
* when the argument or result does not have that typed shape. */
|
||||
export function lowerCallbackPromisify(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const shape = callbackPromisifyShape(lowerer, call, loc);
|
||||
if (shape === null) return null;
|
||||
const argNode = call.arguments.nodes[0]!;
|
||||
let fn = lowerer.lowerExpr(argNode);
|
||||
if (!typeEquals(fn.type, shape.fnT)) fn = lowerer.coerceToExpected(fn, shape.fnT);
|
||||
if (!typeEquals(fn.type, shape.fnT)) return null;
|
||||
return {
|
||||
kind: "call",
|
||||
callee: promisifyMaker(lowerer, shape, loc),
|
||||
args: [fn],
|
||||
type: shape.resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Interns the helper family for one (function, result) type pair and
|
||||
* answers the maker: `(fn) => closure(...args) => Promise<T>`. */
|
||||
function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): string {
|
||||
const key = `promisify:${typeKey(shape.fnT)}=>${typeKey(shape.resultT)}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const base = `%promisify.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, base);
|
||||
const { fnT, resultT, args, callbackT, callbackParamT, inner, error } = shape;
|
||||
const promiseT = resultT.ret;
|
||||
const resolveT = funcOf(inner.kind === "void" ? [] : [inner], VOID);
|
||||
const rejectT = funcOf([ERROR_T], VOID);
|
||||
const executorT = funcOf([resolveT, rejectT], VOID);
|
||||
const argIds = args.map((_, i) => `a${i}.0`);
|
||||
|
||||
// The callback: (err, value) => err ? reject(err) : resolve(value).
|
||||
const cbParams: IrParam[] = [];
|
||||
callbackT.params.forEach((type, i) =>
|
||||
cbParams.push(captureParam(i === 0 ? "err.0" : "value.0", type)),
|
||||
);
|
||||
const cbBody: IrStmt[] = [];
|
||||
if (error !== null) {
|
||||
const err = varRef("err.0", error.type, loc);
|
||||
let truthy: IrExpr | null = null;
|
||||
for (const tag of error.unitTags) {
|
||||
const part: IrExpr = {
|
||||
kind: "unionIsTag",
|
||||
unionId: (error.type as IrType & { kind: "union" }).unionId,
|
||||
tag,
|
||||
negated: true,
|
||||
value: err,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
truthy =
|
||||
truthy === null
|
||||
? part
|
||||
: { kind: "logical", op: "&&", left: truthy, right: part, type: BOOL, loc };
|
||||
}
|
||||
const errorArm = lowerer.unions.get((error.type as IrType & { kind: "union" }).unionId)!.arms[
|
||||
error.errorTag
|
||||
]!;
|
||||
let reason: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId: (error.type as IrType & { kind: "union" }).unionId,
|
||||
tag: error.errorTag,
|
||||
value: err,
|
||||
type: errorArm,
|
||||
loc,
|
||||
};
|
||||
if (!typeEquals(errorArm, ERROR_T))
|
||||
reason = { kind: "upcast", value: reason, type: ERROR_T, loc };
|
||||
const reject: IrStmt[] = [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "callValue",
|
||||
callee: varRef("rej.0", rejectT, loc),
|
||||
args: [reason],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
];
|
||||
cbBody.push(
|
||||
truthy === null ? reject[0]! : { kind: "if", cond: truthy, then: reject, else_: null, loc },
|
||||
);
|
||||
if (truthy === null) cbBody.push(reject[1]!);
|
||||
}
|
||||
const value =
|
||||
inner.kind === "void"
|
||||
? []
|
||||
: [lowerer.coerceToExpected(varRef("value.0", callbackT.params[1]!, loc), inner)];
|
||||
cbBody.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "callValue",
|
||||
callee: varRef("res.0", resolveT, loc),
|
||||
args: value,
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name: `${base}.cb`,
|
||||
params: cbParams,
|
||||
returnType: VOID,
|
||||
captures: [captureParam("res.0", resolveT), captureParam("rej.0", rejectT)],
|
||||
locals: [
|
||||
boxedLocal("res.0", resolveT),
|
||||
boxedLocal("rej.0", rejectT),
|
||||
...cbParams.map((p) => plainLocal(p.localId, p.type)),
|
||||
],
|
||||
body: cbBody,
|
||||
loc,
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name: `${base}.mkcb`,
|
||||
params: [captureParam("res.0", resolveT), captureParam("rej.0", rejectT)],
|
||||
returnType: callbackT,
|
||||
locals: [boxedLocal("res.0", resolveT), boxedLocal("rej.0", rejectT)],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "closure",
|
||||
fnName: `${base}.cb`,
|
||||
captures: ["res.0", "rej.0"],
|
||||
type: callbackT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
|
||||
// The executor: (resolve, reject) => fn(...args, callback).
|
||||
const captured: IrParam[] = [captureParam("f.0", fnT)];
|
||||
args.forEach((type, i) => captured.push(captureParam(argIds[i]!, type)));
|
||||
lowerer.liftedFns.push({
|
||||
name: `${base}.exec`,
|
||||
params: [captureParam("res.0", resolveT), captureParam("rej.0", rejectT)],
|
||||
returnType: VOID,
|
||||
captures: captured,
|
||||
locals: [
|
||||
...captured.map((p) => boxedLocal(p.localId, p.type)),
|
||||
plainLocal("res.0", resolveT),
|
||||
plainLocal("rej.0", rejectT),
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "callValue",
|
||||
callee: varRef("f.0", fnT, loc),
|
||||
args: [
|
||||
...args.map((type, i) => varRef(argIds[i]!, type, loc)),
|
||||
lowerer.coerceToExpected(
|
||||
{
|
||||
kind: "call",
|
||||
callee: `${base}.mkcb`,
|
||||
args: [varRef("res.0", resolveT, loc), varRef("rej.0", rejectT, loc)],
|
||||
type: callbackT,
|
||||
loc,
|
||||
},
|
||||
callbackParamT,
|
||||
),
|
||||
],
|
||||
// The function's own return type; the value is discarded.
|
||||
type: fnT.ret,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name: `${base}.mkexec`,
|
||||
params: captured,
|
||||
returnType: executorT,
|
||||
locals: captured.map((p) => boxedLocal(p.localId, p.type)),
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "closure",
|
||||
fnName: `${base}.exec`,
|
||||
captures: captured.map((p) => p.localId),
|
||||
type: executorT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
|
||||
// The promisified function: (...args) => new Promise(executor).
|
||||
const implParams: IrParam[] = [];
|
||||
args.forEach((type, i) => implParams.push(captureParam(argIds[i]!, type)));
|
||||
lowerer.liftedFns.push({
|
||||
name: `${base}.impl`,
|
||||
params: implParams,
|
||||
returnType: promiseT,
|
||||
captures: [captureParam("f.0", fnT)],
|
||||
locals: [boxedLocal("f.0", fnT), ...implParams.map((p) => plainLocal(p.localId, p.type))],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "newPromise",
|
||||
executor: {
|
||||
kind: "call",
|
||||
callee: `${base}.mkexec`,
|
||||
args: [
|
||||
varRef("f.0", fnT, loc),
|
||||
...implParams.map((p) => varRef(p.localId, p.type, loc)),
|
||||
],
|
||||
type: executorT,
|
||||
loc,
|
||||
},
|
||||
type: promiseT,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
lowerer.liftedFns.push({
|
||||
name: base,
|
||||
params: [captureParam("f.0", fnT)],
|
||||
returnType: resultT,
|
||||
locals: [boxedLocal("f.0", fnT)],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "closure", fnName: `${base}.impl`, captures: ["f.0"], type: resultT, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
return base;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { READLINE_DOCUMENTED_OPTIONS, fenceOrDropOptionKey } from "../surfaces.j
|
||||
import { streamSidesOf } from "../lower-stream.js";
|
||||
import type { IrLibFn } from "../../../ir/builtin-signatures.js";
|
||||
import { voidizedCallback } from "../lower-server.js";
|
||||
import { builtinImportOf } from "./module-bindings.js";
|
||||
|
||||
/** The ambient module declaring `d`, or null. */
|
||||
function ambientModuleOf(d: ts.Node): string | null {
|
||||
@@ -270,6 +271,80 @@ function lowerReadlineUiCall(
|
||||
return { kind: "seqExpr", stmts, result: recv, type: F64, loc };
|
||||
}
|
||||
|
||||
/** The module ("readline" or "readline/promises") whose Interface class
|
||||
* `node` names, or null. */
|
||||
function interfaceClassModule(lowerer: Lowerer, node: ts.Expression): string | null {
|
||||
const builtin = ts.isIdentifier(node)
|
||||
? builtinImportOf(lowerer, node)
|
||||
: ts.isPropertyAccessExpression(node) && !node.questionDotToken
|
||||
? lowerer.builtinMemberOf(node)
|
||||
: null;
|
||||
if (builtin?.member !== "Interface") return null;
|
||||
return builtin.module === "readline" || builtin.module === "readline/promises"
|
||||
? builtin.module
|
||||
: null;
|
||||
}
|
||||
|
||||
/** `rl instanceof readline.Interface` and `rl instanceof
|
||||
* readlinePromises.Interface` over a readline Interface: an interface is
|
||||
* an instance of the class of the module that created it (Node's
|
||||
* readline/promises Interface does not extend node:readline's). Null for
|
||||
* other instanceof tests. */
|
||||
export function lowerReadlineInstanceof(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
): IrExpr | null {
|
||||
const module = interfaceClassModule(lowerer, expr.right);
|
||||
if (module === null || !isReadlineTyped(lowerer, expr.left)) return null;
|
||||
const loc = locOf(expr);
|
||||
const recv = lowerer.lowerExprExpecting(expr.left, F64);
|
||||
const isPromises: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "rl.isPromises",
|
||||
args: [recv],
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
if (module === "readline/promises") return isPromises;
|
||||
return { kind: "unary", op: "!", operand: isPromises, type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** `rl[Symbol.dispose]()` on a readline Interface: Node's dispose method
|
||||
* calls close(), which does nothing on a closed interface. Null for other
|
||||
* element-access calls. */
|
||||
export function lowerReadlineDisposeCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
const access = call.expression;
|
||||
if (!ts.isElementAccessExpression(access) || access.questionDotToken || call.questionDotToken) {
|
||||
return null;
|
||||
}
|
||||
let key = access.argumentExpression;
|
||||
while (ts.isParenthesizedExpression(key)) key = key.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(key) ||
|
||||
lowerer.stdlibGlobalMember(key, "Symbol") !== "dispose"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (!isReadlineTyped(lowerer, access.expression)) return null;
|
||||
if (call.arguments.nodes.length > 0) {
|
||||
lowerer.noLowering("readline.Interface[Symbol.dispose] with arguments", call);
|
||||
}
|
||||
const recv = lowerer.lowerExprExpecting(access.expression, F64);
|
||||
return { kind: "libCall", fn: "rl.close", args: [recv], type: VOID, loc: locOf(call) };
|
||||
}
|
||||
|
||||
/** A question query or setPrompt argument: the string, or for a value
|
||||
* that need not be one (JavaScript), the checked-dynamic value, which the
|
||||
* runtime writes the way Node's kWriteToOutput does (dyn: true). */
|
||||
function lowerPromptArg(lowerer: Lowerer, node: ts.Expression): { value: IrExpr; dyn: boolean } {
|
||||
if (lowerer.typeOf(node).flags & ts.TypeFlags.StringLike) {
|
||||
return { value: lowerer.lowerExprExpecting(node, STRING), dyn: false };
|
||||
}
|
||||
const value = lowerer.lowerExpr(node);
|
||||
if (value.type.kind === "string") return { value, dyn: false };
|
||||
return { value: lowerer.coerceInto(node, value, DYN), dyn: true };
|
||||
}
|
||||
|
||||
/** Method calls on readline Interface receivers (both modules):
|
||||
* - question(query, cb) — the callback form; question(query, { signal }?)
|
||||
* — the promise form (readline/promises);
|
||||
@@ -303,18 +378,20 @@ export function lowerReadlineMethodCall(
|
||||
args.length <= 2
|
||||
) {
|
||||
const recv = receiver();
|
||||
const query = lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
const query = lowerPromptArg(lowerer, args[0]!);
|
||||
const signal = questionSignal(lowerer, args[1]);
|
||||
const type = { kind: "promise", inner: STRING } as const;
|
||||
if (signal === null) {
|
||||
return { kind: "libCall", fn: "rl.questionAsync", args: [recv, query], type, loc };
|
||||
const fn = query.dyn ? "rl.questionAsyncDyn" : "rl.questionAsync";
|
||||
return { kind: "libCall", fn, args: [recv, query.value], type, loc };
|
||||
}
|
||||
const sig = lowerer.lowerExprExpecting(signal, DYN);
|
||||
return { kind: "libCall", fn: "rl.questionAsyncSignal", args: [recv, query, sig], type, loc };
|
||||
const fn = query.dyn ? "rl.questionAsyncSignalDyn" : "rl.questionAsyncSignal";
|
||||
return { kind: "libCall", fn, args: [recv, query.value, sig], type, loc };
|
||||
}
|
||||
if (name === "question" && args.length === 2) {
|
||||
const recv = receiver();
|
||||
const query = lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
const query = lowerPromptArg(lowerer, args[0]!);
|
||||
const cb = lowerer.lowerExpr(args[1]!);
|
||||
if (cb.type.kind !== "func" || cb.type.ret.kind !== "void" || cb.type.params.length > 1) {
|
||||
lowerer.unsupported(
|
||||
@@ -331,7 +408,8 @@ export function lowerReadlineMethodCall(
|
||||
`question callbacks whose parameter is not 'string' (got '${lowerer.fmt(param)}')`,
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "rl.question", args: [recv, query, cb], type: VOID, loc };
|
||||
const fn = query.dyn ? "rl.questionDyn" : "rl.question";
|
||||
return { kind: "libCall", fn, args: [recv, query.value, cb], type: VOID, loc };
|
||||
}
|
||||
const simple: Record<string, "rl.close" | "rl.pause" | "rl.resume" | "rl.prompt"> = {
|
||||
close: "rl.close",
|
||||
@@ -366,8 +444,9 @@ export function lowerReadlineMethodCall(
|
||||
}
|
||||
if (name === "setPrompt" && args.length === 1) {
|
||||
const recv = receiver();
|
||||
const prompt = lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
return { kind: "libCall", fn: "rl.setPrompt", args: [recv, prompt], type: VOID, loc };
|
||||
const prompt = lowerPromptArg(lowerer, args[0]!);
|
||||
const fn = prompt.dyn ? "rl.setPromptDyn" : "rl.setPrompt";
|
||||
return { kind: "libCall", fn, args: [recv, prompt.value], type: VOID, loc };
|
||||
}
|
||||
if (name === "getPrompt" && args.length === 0) {
|
||||
return { kind: "libCall", fn: "rl.getPrompt", args: [receiver()], type: STRING, loc };
|
||||
@@ -871,6 +950,25 @@ export function lowerReadlineCursorCall(
|
||||
return { kind: "seqExpr", stmts, result, type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** A checked-dynamic input or output (a JavaScript function's result, an
|
||||
* object's member, an element that may be undefined) as the stream it
|
||||
* holds: the runtime refuses a value that is not a stream with that
|
||||
* side, and answers null for an output of null or undefined. Other
|
||||
* values pass through. */
|
||||
function streamFromDyn(lowerer: Lowerer, node: ts.Expression, output: boolean): IrExpr {
|
||||
let v = lowerer.lowerExpr(node);
|
||||
if (v.type.kind === "union") v = lowerer.coerceInto(node, v, DYN);
|
||||
if (v.type.kind !== "dyn") return v;
|
||||
const loc = locOf(node);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: output ? "rl.outputFromDyn" : "rl.inputFromDyn",
|
||||
args: [v, boolLit(output, loc)],
|
||||
type: { kind: "object", className: output ? "%Writable" : "%Readable" },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The interface's input: process.stdin (null) or a readable stream. */
|
||||
function lowerInput(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
if (
|
||||
@@ -879,7 +977,7 @@ function lowerInput(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const v = lowerer.lowerExpr(node);
|
||||
const v = streamFromDyn(lowerer, node, false);
|
||||
const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined;
|
||||
const sides = streamSidesOf(lowerer, info);
|
||||
if (sides !== "r" && sides !== "rw") {
|
||||
@@ -904,7 +1002,7 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" |
|
||||
return "none";
|
||||
}
|
||||
if (ts.isIdentifier(node) && node.text === "undefined") return "none";
|
||||
const v = lowerer.lowerExpr(node);
|
||||
const v = streamFromDyn(lowerer, node, true);
|
||||
const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined;
|
||||
const sides = streamSidesOf(lowerer, info);
|
||||
if (sides !== "w" && sides !== "rw") {
|
||||
@@ -923,8 +1021,8 @@ function lowerOutput(lowerer: Lowerer, node: ts.Expression): IrExpr | "stdout" |
|
||||
* stream; the output process.stdout, a writable stream, or absent
|
||||
* (prompts are then not written). rl.create's arguments: the mode literal
|
||||
* (1 stream input, 2 stream output, 4 stdout output, 8 signal, 16
|
||||
* prompt), then the input and output streams when present, crlfDelay,
|
||||
* the prompt and the signal when present. An interface that can be a
|
||||
* prompt, 32 node:readline/promises), then the input and output streams
|
||||
* when present, crlfDelay, the prompt and the signal when present. An interface that can be a
|
||||
* terminal one (a `terminal` option that is not false, or none with
|
||||
* process.stdout as the output, which is a terminal when it is a TTY)
|
||||
* goes through rl.terminal with the completer. */
|
||||
@@ -1107,7 +1205,8 @@ export function lowerReadlineCreateCall(
|
||||
(typeof output === "object" ? 2 : 0) |
|
||||
(output === "stdout" ? 4 : 0) |
|
||||
(signal !== null ? 8 : 0) |
|
||||
(prompt !== null ? 16 : 0);
|
||||
(prompt !== null ? 16 : 0) |
|
||||
(promises ? 32 : 0);
|
||||
const args: IrExpr[] = [numLit(mode, loc)];
|
||||
if (input !== null) args.push(input);
|
||||
if (typeof output === "object") args.push(output);
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { type ClassInfo } from "../lower-classes.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { BOOL, STRING, type IrExpr, type IrType, type SrcLoc } from "../../../ir/ir.js";
|
||||
|
||||
/** `x.constructor` on an instance typed as EventEmitter or a stream class
|
||||
* in a TypeScript file: the receiver's runtime ClassInfo, or null. The
|
||||
* runtime records each instance's Node class name (EventEmitter, Readable,
|
||||
* ReadStream, Gzip, Socket, a program subclass's name), which the
|
||||
* lowerings below read with emitter.className. */
|
||||
export function runtimeConstructorReceiver(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { receiver: ts.Expression; info: ClassInfo } | null {
|
||||
let e = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isPropertyAccessExpression(e) || e.questionDotToken) return null;
|
||||
if (e.name.text !== "constructor" || isJsSourceFile(e.getSourceFile())) return null;
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(e.expression));
|
||||
if (mapped?.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(mapped.className);
|
||||
if (!info?.def.runtime || (!info.builtinEmitter && info.builtinStream === undefined)) return null;
|
||||
return { receiver: e.expression, info };
|
||||
}
|
||||
|
||||
/** `x.constructor.name`: the instance's class name. */
|
||||
export function lowerRuntimeConstructorName(
|
||||
lowerer: Lowerer,
|
||||
receiver: ts.Expression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "emitter.className",
|
||||
args: [lowerer.lowerExpr(receiver)],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `x.constructor === C` for EventEmitter or a stream class C: true when
|
||||
* the instance's own class is C, which needs both the vtable (C's own
|
||||
* interval, not a subclass's) and the class name (fs, zlib, crypto, child
|
||||
* process and worker streams share the base classes' vtables). */
|
||||
export function lowerRuntimeConstructorIs(
|
||||
lowerer: Lowerer,
|
||||
receiver: ts.Expression,
|
||||
target: ClassInfo,
|
||||
negated: boolean,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const value = lowerer.lowerExpr(receiver);
|
||||
const local = lowerer.declareHiddenLocal("%constructorOf", value.type);
|
||||
const ref = varRef(local.id, value.type, loc);
|
||||
const test = exactClassTest(lowerer, ref, target, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
|
||||
result: negated ? { kind: "unary", op: "!", operand: test, type: BOOL, loc } : test,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `x.constructor` as a value: the class object of the instance's own class
|
||||
* among EventEmitter and the stream classes, as the receiver's static
|
||||
* class's class value. Instances of the other runtime classes (fs, zlib,
|
||||
* crypto, child process and worker streams) and of program subclasses
|
||||
* report SC1090 at run time. */
|
||||
export function lowerRuntimeConstructor(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
info: ClassInfo,
|
||||
): IrExpr {
|
||||
const loc = locOf(access);
|
||||
const resultT: IrType = { kind: "classval", className: info.def.name };
|
||||
const value = lowerer.lowerExpr(access.expression);
|
||||
const local = lowerer.declareHiddenLocal("%constructorOf", value.type);
|
||||
const ref = varRef(local.id, value.type, loc);
|
||||
let result: IrExpr = refusalCall(lowerer, resultT, loc);
|
||||
for (const candidate of valueClassesUnder(info)) {
|
||||
lowerer.defineClassValueConstruct(candidate);
|
||||
lowerer.defineClassValueConstruct(info);
|
||||
const classRef: IrExpr = {
|
||||
kind: "classRef",
|
||||
className: candidate.def.name,
|
||||
type: { kind: "classval", className: candidate.def.name },
|
||||
loc,
|
||||
};
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: exactClassTest(lowerer, ref, candidate, loc),
|
||||
then: candidate === info ? classRef : { kind: "upcast", value: classRef, type: resultT, loc },
|
||||
else_: result,
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
|
||||
result,
|
||||
type: resultT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `info` and its runtime descendants whose values construct. */
|
||||
function valueClassesUnder(info: ClassInfo): ClassInfo[] {
|
||||
const out: ClassInfo[] = [];
|
||||
const visit = (c: ClassInfo): void => {
|
||||
if (!c.def.runtime) return;
|
||||
if (c.valueCtorParams !== undefined) out.push(c);
|
||||
for (const s of c.subclasses) visit(s);
|
||||
};
|
||||
visit(info);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The instance is of class `target` itself: inside its interval, outside
|
||||
* each direct subclass's (runtime or program), and named as it. */
|
||||
function exactClassTest(lowerer: Lowerer, ref: IrExpr, target: ClassInfo, loc: SrcLoc): IrExpr {
|
||||
let test: IrExpr = {
|
||||
kind: "instanceOf",
|
||||
value: ref,
|
||||
className: target.def.name,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
for (const sub of target.subclasses) {
|
||||
test = {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: test,
|
||||
right: {
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: { kind: "instanceOf", value: ref, className: sub.def.name, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const name: IrExpr = {
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: { kind: "libCall", fn: "emitter.className", args: [ref], type: STRING, loc },
|
||||
right: { kind: "strLit", value: target.def.name.slice(1), type: STRING, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
return { kind: "logical", op: "&&", left: test, right: name, type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** The run-time refusal for an instance whose class has no class value. */
|
||||
function refusalCall(lowerer: Lowerer, type: IrType, loc: SrcLoc): IrExpr {
|
||||
const name = `%constructor.refusal:${type.kind === "classval" ? type.className : ""}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [],
|
||||
locals: [],
|
||||
returnType: type,
|
||||
body: [
|
||||
{
|
||||
kind: "runtimeFence",
|
||||
code: "SC1090",
|
||||
message:
|
||||
"reading 'constructor' of an fs, zlib, crypto, child process or worker stream, or of an instance of a program subclass of EventEmitter or a stream class, is not supported yet",
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: name, args: [], type, loc };
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { isStreamUndefCallExpr } from "./lower-server.js";
|
||||
import { nsPathPrefix } from "./lower-namespaces.js";
|
||||
import { declSymbolOf } from "./lower-modules.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
import { restPackingAdapterFits } from "./coercions/function-adapters.js";
|
||||
import { implicitMonoFile, implicitAnyParamSymbolsOf } from "./generic-functions.js";
|
||||
|
||||
/** How a parameter participates in CALL-SITE COMPLETION (the frontend
|
||||
@@ -46,12 +47,15 @@ export type ParamMode = "required" | "omittable" | "rest" | "dynRest" | "argumen
|
||||
* otherwise. `bodyType` is present exactly for DEFAULTED program params:
|
||||
* the plain T the body sees after the prologue applies the default (see
|
||||
* declareParams). `callDefault` is the compile-time default value for a
|
||||
* table-backed builtin's omittable slot. */
|
||||
* table-backed builtin's omittable slot. `entryNarrow` marks a dyn ABI slot
|
||||
* inherited from a JavaScript base method whose TypeScript override declares
|
||||
* a typed parameter: the prologue narrows the checked value to that type. */
|
||||
export interface ParamShape {
|
||||
type: IrType;
|
||||
mode: ParamMode;
|
||||
bodyType?: IrType;
|
||||
callDefault?: IrExpr;
|
||||
entryNarrow?: IrType;
|
||||
}
|
||||
|
||||
export interface FnSig {
|
||||
@@ -522,6 +526,21 @@ export function requireExactArityValue(
|
||||
// optional/defaulted params spell their `T | undefined` slots (mapType's
|
||||
// completed-signature contract), so omitted trailing args complete with
|
||||
// the undefined arm like any direct call.
|
||||
// A nested function declaration whose only non-required parameter is a
|
||||
// typed rest: its value carries the typed-rest ABI its own type spells,
|
||||
// and every use converts it at its slot (same signature, or the rest
|
||||
// packing adapter for fixed parameters).
|
||||
if (
|
||||
contextual === null &&
|
||||
ts.isFunctionDeclaration(blame) &&
|
||||
blame.name !== undefined &&
|
||||
funcType.kind === "func" &&
|
||||
funcType.restAbi === "typed" &&
|
||||
shapes.every((s) => s.mode === "required" || s.mode === "rest")
|
||||
) {
|
||||
const own = lowerer.mapTypeOf(lowerer.typeOf(blame.name));
|
||||
if (own && typeEquals(own, funcType)) return;
|
||||
}
|
||||
const target = contextual ? lowerer.checker.getContextualType(contextual) : undefined;
|
||||
const mapped = target
|
||||
? lowerer.mapTypeOf(target)
|
||||
@@ -582,6 +601,16 @@ export function requireExactArityValue(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// A typed-rest function in a slot with fixed parameters (`log(...m:
|
||||
// string[])` into `(m: string) => void`): the slot coercion's packing
|
||||
// adapter collects the slot's arguments into the rest array.
|
||||
if (
|
||||
mappedFn?.kind === "func" &&
|
||||
funcType.kind === "func" &&
|
||||
restPackingAdapterFits(lowerer, funcType, mappedFn)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// An uncontextualized npm-static implementation function can still
|
||||
// have a declaration-backed completed ABI: the package's .d.ts
|
||||
// projection supplies the body-facing parameter/return types while
|
||||
@@ -740,7 +769,11 @@ function appendArgumentsParameter(
|
||||
const classMethod =
|
||||
ts.isMethodDeclaration(node) &&
|
||||
(ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent));
|
||||
if (!classMethod && !isNodeEsmFile(node.getSourceFile(), lowerer.program)) {
|
||||
if (
|
||||
!classMethod &&
|
||||
!isNodeEsmFile(node.getSourceFile(), lowerer.program) &&
|
||||
!unmappedArguments(node)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
@@ -751,6 +784,35 @@ function appendArgumentsParameter(
|
||||
shapes.push({ type: DYN, mode });
|
||||
}
|
||||
|
||||
/** Whether `arguments` inside `node` is Node's unmapped object: strict code
|
||||
* (a 'use strict' prologue on the file or an enclosing function, or a class
|
||||
* body) or a non-simple parameter list (defaults, rest, destructuring). Its
|
||||
* elements then never alias the parameters, which is the ES module case. */
|
||||
function unmappedArguments(node: ts.SignatureDeclaration): boolean {
|
||||
if (
|
||||
node.parameters.nodes.some(
|
||||
(p) =>
|
||||
p.initializer !== undefined || p.dotDotDotToken !== undefined || !ts.isIdentifier(p.name),
|
||||
)
|
||||
)
|
||||
return true;
|
||||
const useStrict = (statements: readonly ts.Statement[]): boolean => {
|
||||
for (const statement of statements) {
|
||||
if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression))
|
||||
return false;
|
||||
if (statement.expression.text === "use strict") return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (let n: ts.Node | undefined = node; n; n = n.parent) {
|
||||
if (ts.isClassDeclaration(n) || ts.isClassExpression(n)) return true;
|
||||
if (ts.isFunctionLike(n) && n.body && ts.isBlock(n.body) && useStrict(n.body.statements.nodes))
|
||||
return true;
|
||||
if (ts.isSourceFile(n)) return useStrict(n.statements.nodes);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Signature checks + param shapes + IR func type for any lambda-like
|
||||
* node. The func type's params are the ABI types: optional/default params
|
||||
* become `T | undefined`, and typed rest becomes one packed-array slot.
|
||||
|
||||
@@ -22,13 +22,18 @@ import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import {
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportDiscardReason,
|
||||
cjsExportTargetLiteral,
|
||||
isCjsJsFile,
|
||||
isModuleExportsAccess,
|
||||
isRequireStatement,
|
||||
locOf,
|
||||
npmStaticDepSf7,
|
||||
resolveImport,
|
||||
} from "../program.js";
|
||||
import { cjsWholeExportValueOf, inlineProgramRequireOf } from "./cjs-inline-require.js";
|
||||
import { isCjsModuleGlobal } from "./lower-node-module.js";
|
||||
import { bindingNeverReassigned } from "./binding-analysis.js";
|
||||
import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js";
|
||||
|
||||
/** True for the CommonJS wrapper's `exports` binding (not shadowed by any
|
||||
@@ -428,3 +433,128 @@ export function cjsExportCallIgnoresThis(
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/** The value behind a binding of a CommonJS member export (an `import {
|
||||
* name }` or a top-level `const { name } = require(...)`) written once as
|
||||
* `module.exports.<name> = <value>` or `exports.<name> = <value>`, when the
|
||||
* module exports a value whole and the member is a property added to it
|
||||
* (`exports = module.exports = Client; exports.StatsD = Client`). The
|
||||
* checker gives such a binding a synthesized property symbol, so the
|
||||
* module's statements answer. The value is a binding of the exporting
|
||||
* module or a primitive literal, which the use site reads again. Follows
|
||||
* `module.exports = require("./x")` re-exports. Null for any other binding. */
|
||||
export function cjsMemberAliasValueOf(lowerer: Lowerer, ref: ts.Identifier): ts.Expression | null {
|
||||
const sym = lowerer.checker.getSymbolAtLocation(ref);
|
||||
if (!sym) return null;
|
||||
const decls = sym.declarations;
|
||||
// `import { StatsD } from "pkg"`
|
||||
const specifier = sym.flags & ts.SymbolFlags.Alias ? decls.find(ts.isImportSpecifier) : undefined;
|
||||
if (specifier !== undefined) {
|
||||
const importDecl = specifier.parent?.parent?.parent;
|
||||
if (
|
||||
importDecl === undefined ||
|
||||
!ts.isImportDeclaration(importDecl) ||
|
||||
!ts.isStringLiteral(importDecl.moduleSpecifier)
|
||||
)
|
||||
return null;
|
||||
const spec = importDecl.moduleSpecifier.text;
|
||||
const from = importDecl.getSourceFile();
|
||||
const dep =
|
||||
resolveImport(lowerer.program, from, spec, "import") ??
|
||||
npmStaticDepSf7(lowerer.program, from, spec, "import");
|
||||
if (dep === null) return null;
|
||||
return memberAliasIn(lowerer, dep, (specifier.propertyName ?? specifier.name).text, 0);
|
||||
}
|
||||
// `const { shout } = require("./lib")` at a module's top level
|
||||
const element = decls.find(ts.isBindingElement);
|
||||
if (element === undefined || element.dotDotDotToken || !ts.isIdentifier(element.name))
|
||||
return null;
|
||||
const pattern = element.parent;
|
||||
const declaration = pattern?.parent;
|
||||
if (
|
||||
pattern === undefined ||
|
||||
!ts.isObjectBindingPattern(pattern) ||
|
||||
declaration === undefined ||
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
declaration.initializer === undefined ||
|
||||
(ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) === 0
|
||||
)
|
||||
return null;
|
||||
const key = element.propertyName ?? element.name;
|
||||
if (!ts.isIdentifier(key)) return null;
|
||||
const req = inlineProgramRequireOf(lowerer, declaration.initializer);
|
||||
if (req === null) return null;
|
||||
return memberAliasIn(lowerer, req.dep, key.text, 0);
|
||||
}
|
||||
|
||||
function memberAliasIn(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
name: string,
|
||||
depth: number,
|
||||
): ts.Expression | null {
|
||||
const member = memberWritesIn(lowerer, sf, name, depth);
|
||||
// One write: a member written again would be a runtime slot.
|
||||
return member !== null && member.writes === 1 ? member.value : null;
|
||||
}
|
||||
|
||||
/** The writes of member `name` in `sf` and in the modules it re-exports
|
||||
* through `module.exports = require("./x")`, which share its exports
|
||||
* object. `value` is the last write's value when the use site can read
|
||||
* it again. Null when the module does not export a value whole. */
|
||||
function memberWritesIn(
|
||||
lowerer: Lowerer,
|
||||
sf: ts.SourceFile,
|
||||
name: string,
|
||||
depth: number,
|
||||
): { value: ts.Expression | null; writes: number } | null {
|
||||
if (depth > 32 || sf.isDeclarationFile || !isCjsJsFile(sf, lowerer.program)) return null;
|
||||
const whole = cjsWholeExportValueOf(sf);
|
||||
if (whole === null) return null;
|
||||
let value: ts.Expression | null = null;
|
||||
let writes = 0;
|
||||
const inner = inlineProgramRequireOf(lowerer, whole.rhs);
|
||||
if (inner !== null) {
|
||||
// The re-exported module runs first; this module's own writes follow.
|
||||
const dep = memberWritesIn(lowerer, inner.dep, name, depth + 1);
|
||||
if (dep === null) return null;
|
||||
({ value, writes } = dep);
|
||||
}
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
const cjs = cjsExportAssignmentOf(stmt);
|
||||
if (cjs?.kind !== "member" || !ts.isIdentifier(cjs.name) || cjs.name.text !== name) continue;
|
||||
writes++;
|
||||
if (cjsExportDiscardReason(stmt) !== null) return null;
|
||||
let written: ts.Expression = cjs.value;
|
||||
while (ts.isParenthesizedExpression(written)) written = written.expression;
|
||||
value = rereadableMemberValue(lowerer, stmt, written) ? written : null;
|
||||
}
|
||||
return { value, writes };
|
||||
}
|
||||
|
||||
/** True when reading `value` again at the use site observes what the
|
||||
* member write `stmt` stored: a primitive literal, or a module binding
|
||||
* declared before the write and never reassigned. */
|
||||
function rereadableMemberValue(
|
||||
lowerer: Lowerer,
|
||||
stmt: ts.Statement,
|
||||
value: ts.Expression,
|
||||
): boolean {
|
||||
if (
|
||||
ts.isStringLiteral(value) ||
|
||||
ts.isNumericLiteral(value) ||
|
||||
ts.isNoSubstitutionTemplateLiteral(value) ||
|
||||
value.kind === ts.SyntaxKind.TrueKeyword ||
|
||||
value.kind === ts.SyntaxKind.FalseKeyword
|
||||
)
|
||||
return true;
|
||||
if (!ts.isIdentifier(value)) return false;
|
||||
const sym = lowerer.resolveValueSymbol(value);
|
||||
if (sym === null || sym.declarations.length !== 1) return false;
|
||||
const decl = sym.declarations[0]!;
|
||||
if (decl.getSourceFile() !== stmt.getSourceFile()) return false;
|
||||
// A function declaration is initialized when the module starts; any
|
||||
// other binding must be initialized before the write stores it.
|
||||
if (!ts.isFunctionDeclaration(decl) && decl.end > stmt.pos) return false;
|
||||
return bindingNeverReassigned(lowerer, sym, decl);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,440 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
CLASS_ATTRIBUTES,
|
||||
CLASS_FROZEN,
|
||||
CLASS_NOT_EXTENSIBLE,
|
||||
CLASS_SEALED,
|
||||
DYN,
|
||||
F64,
|
||||
FIELD_NOT_WRITABLE,
|
||||
STRING,
|
||||
VOID,
|
||||
canConvertToDyn,
|
||||
canDynCheckTo,
|
||||
classFieldAttributesName,
|
||||
isClassOwnEnumerableFieldName,
|
||||
isDynTypedRefType,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { numLit, strLit, varRef } from "../../ir/build.js";
|
||||
import { transformStmtList } from "../../ir/traverse.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { type Lowerer, PoisonError } from "./lowerer.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import { classPropertiesHelper } from "./class-dynamic-dispatch.js";
|
||||
import { runtimeRooted } from "./class-descriptors.js";
|
||||
|
||||
/* Object.freeze, Object.seal, Object.preventExtensions and
|
||||
* Object.defineProperty on class instances. Only the hierarchies these calls
|
||||
* name pay for them: such a hierarchy gets a CLASS_ATTRIBUTES integrity word
|
||||
* per instance, an attribute word per redefined field, and a check before
|
||||
* each write to a declared field (guardClassAttributeWrites). The instance's
|
||||
* checked view carries the attributes, so reflection answers from it. */
|
||||
|
||||
type ClassObjectType = Extract<IrType, { kind: "object" }>;
|
||||
|
||||
function attributeClass(lowerer: Lowerer, target: IrExpr): ClassInfo | null {
|
||||
if (!isDynTypedRefType(target.type)) return null;
|
||||
const info = lowerer.classes.get(target.type.className);
|
||||
if (!info || runtimeRooted(info) || info.decl == null) return null;
|
||||
return info;
|
||||
}
|
||||
|
||||
/** Adds a zero-initialized f64 to every class of the hierarchy, at the same
|
||||
* offset, like the property bag. */
|
||||
function addHiddenField(info: ClassInfo, name: string): void {
|
||||
let root = info;
|
||||
while (root.base && !root.base.def.runtime) root = root.base;
|
||||
if (root.fields.has(name)) return;
|
||||
const index = root.def.fields.length;
|
||||
const add = (current: ClassInfo): void => {
|
||||
if (!current.fields.has(name)) {
|
||||
current.fields.set(name, F64);
|
||||
current.def.fields.splice(index, 0, { name, type: F64 });
|
||||
}
|
||||
current.subclasses.forEach(add);
|
||||
};
|
||||
add(root);
|
||||
}
|
||||
|
||||
function hiddenFieldGet(receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrExpr {
|
||||
return { kind: "fieldGet", obj: receiver, className, field, type: F64, loc };
|
||||
}
|
||||
|
||||
/** Object.freeze, Object.seal and Object.preventExtensions of a class
|
||||
* instance: sets the instance's integrity bits, then applies the operation to
|
||||
* the properties added to the instance. Answers the instance. */
|
||||
export function lowerClassIntegrity(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
member: "freeze" | "seal" | "preventExtensions",
|
||||
target: IrExpr,
|
||||
): IrExpr | null {
|
||||
const info = attributeClass(lowerer, target);
|
||||
if (!info) return null;
|
||||
addHiddenField(info, CLASS_ATTRIBUTES);
|
||||
const loc = locOf(call);
|
||||
const type = target.type as ClassObjectType;
|
||||
const bits =
|
||||
member === "freeze"
|
||||
? CLASS_NOT_EXTENSIBLE | CLASS_SEALED | CLASS_FROZEN
|
||||
: member === "seal"
|
||||
? CLASS_NOT_EXTENSIBLE | CLASS_SEALED
|
||||
: CLASS_NOT_EXTENSIBLE;
|
||||
const slot = lowerer.declareHiddenLocal("%integrityReceiver", type);
|
||||
const receiver = (): IrExpr => varRef(slot.id, type, loc);
|
||||
const bag: IrExpr = {
|
||||
kind: "call",
|
||||
callee: classPropertiesHelper(lowerer, loc).name,
|
||||
args: [lowerer.coerceToExpected(receiver(), DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const operation = {
|
||||
freeze: "dyn.freeze",
|
||||
seal: "dyn.seal",
|
||||
preventExtensions: "dyn.preventExtensions",
|
||||
} as const;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: slot.id, init: target, loc },
|
||||
{
|
||||
kind: "fieldSet",
|
||||
obj: receiver(),
|
||||
className: type.className,
|
||||
field: CLASS_ATTRIBUTES,
|
||||
value: {
|
||||
kind: "bin",
|
||||
op: "|",
|
||||
left: hiddenFieldGet(receiver(), type.className, CLASS_ATTRIBUTES, loc),
|
||||
right: numLit(bits, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
attributesChecked: true,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: operation[member], args: [bag], type: DYN, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: receiver(),
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** An object literal with a get or set property. */
|
||||
function literalAccessorDescriptor(node: ts.Expression): boolean {
|
||||
while (ts.isParenthesizedExpression(node)) node = node.expression;
|
||||
return (
|
||||
ts.isObjectLiteralExpression(node) &&
|
||||
node.properties.nodes.some(
|
||||
(property) =>
|
||||
property.name !== undefined &&
|
||||
(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name)) &&
|
||||
(property.name.text === "get" || property.name.text === "set"),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** Object.defineProperty with a string-literal key naming a declared field
|
||||
* of a class instance: a data descriptor changes the field's value and
|
||||
* attributes, validated as Node validates ordinary objects. Answers the
|
||||
* instance, or null when the call is not of that form. An object-literal
|
||||
* accessor descriptor is refused at compile time; other accessor
|
||||
* descriptors throw SC2020 when the call runs. */
|
||||
export function lowerClassFieldDefine(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
target: IrExpr,
|
||||
): IrExpr | null {
|
||||
const info = attributeClass(lowerer, target);
|
||||
const key = call.arguments.nodes[1]!;
|
||||
if (!info || !ts.isStringLiteral(key)) return null;
|
||||
const field = key.text;
|
||||
const fieldType = info.fields.get(field);
|
||||
if (
|
||||
fieldType === undefined ||
|
||||
!isClassOwnEnumerableFieldName(field) ||
|
||||
[...(info.symbolFields?.values() ?? [])].includes(field)
|
||||
)
|
||||
return null;
|
||||
if (literalAccessorDescriptor(call.arguments.nodes[2]!)) {
|
||||
lowerer.pushDiag({
|
||||
code: "SC2020",
|
||||
message: `Object.defineProperty with an accessor descriptor for the declared field '${field}' is not supported yet`,
|
||||
loc: locOf(call),
|
||||
});
|
||||
throw new PoisonError();
|
||||
}
|
||||
const getRecord = (id: string) => lowerer.shapes.get(id);
|
||||
const getUnion = (id: string) => lowerer.unions.get(id);
|
||||
if (
|
||||
!canConvertToDyn(fieldType, getRecord, getUnion) ||
|
||||
!canDynCheckTo(fieldType, getRecord, getUnion)
|
||||
)
|
||||
return null;
|
||||
const attributesField = classFieldAttributesName(field);
|
||||
addHiddenField(info, CLASS_ATTRIBUTES);
|
||||
addHiddenField(info, attributesField);
|
||||
const loc = locOf(call);
|
||||
const type = target.type as ClassObjectType;
|
||||
const receiverSlot = lowerer.declareHiddenLocal("%defineReceiver", type);
|
||||
const descriptorSlot = lowerer.declareHiddenLocal("%defineDescriptor", DYN);
|
||||
const holderSlot = lowerer.declareHiddenLocal("%defineHolder", DYN);
|
||||
const receiver = (): IrExpr => varRef(receiverSlot.id, type, loc);
|
||||
const descriptor = (): IrExpr => varRef(descriptorSlot.id, DYN, loc);
|
||||
const holder = (): IrExpr => varRef(holderSlot.id, DYN, loc);
|
||||
const name = (): IrExpr => strLit(field, loc);
|
||||
const current: IrExpr = {
|
||||
kind: "fieldGet",
|
||||
obj: receiver(),
|
||||
className: type.className,
|
||||
field,
|
||||
type: fieldType,
|
||||
loc,
|
||||
};
|
||||
const defined: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "class.definedValue",
|
||||
args: [holder(), name()],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: receiverSlot.id, init: target, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: descriptorSlot.id,
|
||||
init: lowerer.lowerExprExpecting(call.arguments.nodes[2]!, DYN),
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: holderSlot.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "class.defineField",
|
||||
args: [
|
||||
lowerer.coerceToExpected(current, DYN),
|
||||
hiddenFieldGet(receiver(), type.className, attributesField, loc),
|
||||
hiddenFieldGet(receiver(), type.className, CLASS_ATTRIBUTES, loc),
|
||||
name(),
|
||||
descriptor(),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "libCall",
|
||||
fn: "class.descriptorHasValue",
|
||||
args: [descriptor()],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "fieldSet",
|
||||
obj: receiver(),
|
||||
className: type.className,
|
||||
field,
|
||||
value: lowerer.coerceToExpected(defined, fieldType),
|
||||
attributesChecked: true,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "fieldSet",
|
||||
obj: receiver(),
|
||||
className: type.className,
|
||||
field: attributesField,
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "class.definedBits",
|
||||
args: [holder(), name()],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
attributesChecked: true,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: receiver(),
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function classDisplayName(info: ClassInfo): string {
|
||||
return info.def.jsName ?? info.decl?.name?.text ?? info.def.name.replace(/^%/, "");
|
||||
}
|
||||
|
||||
/** The instance's own class name for an error message: a helper testing the
|
||||
* most derived classes of the hierarchy first. */
|
||||
function instanceClassName(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
receiver: IrExpr,
|
||||
helpers: Map<string, IrFunction>,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
if (info.subclasses.length === 0) return strLit(classDisplayName(info), loc);
|
||||
let helper = helpers.get(info.def.name);
|
||||
if (!helper) {
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const value = varRef("value", type, loc);
|
||||
const descendants = (current: ClassInfo): ClassInfo[] =>
|
||||
current.subclasses.flatMap((child) => [...descendants(child), child]);
|
||||
const body: IrStmt[] = descendants(info).map((child): IrStmt => ({
|
||||
kind: "if",
|
||||
cond: { kind: "instanceOf", value, className: child.def.name, type: BOOL, loc },
|
||||
then: [{ kind: "return", value: strLit(classDisplayName(child), loc), loc }],
|
||||
else_: null,
|
||||
loc,
|
||||
}));
|
||||
body.push({ kind: "return", value: strLit(classDisplayName(info), loc), loc });
|
||||
helper = {
|
||||
name: `%class.instanceName.${helpers.size}`,
|
||||
params: [{ localId: "value", name: "value", type }],
|
||||
returnType: STRING,
|
||||
locals: [{ id: "value", name: "value", type, mutable: false }],
|
||||
body,
|
||||
loc,
|
||||
};
|
||||
helpers.set(info.def.name, helper);
|
||||
}
|
||||
return { kind: "call", callee: helper.name, args: [receiver], type: STRING, loc };
|
||||
}
|
||||
|
||||
/** Checks the integrity word, and the field's attribute word when it has one,
|
||||
* before every write to a declared field of a hierarchy that has them. A
|
||||
* read-only field throws Node's TypeError in strict mode code; a sloppy-mode
|
||||
* write leaves it unchanged. Other classes keep their plain stores. */
|
||||
export function guardClassAttributeWrites(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||
const guarded = new Set(
|
||||
[...lowerer.classes.values()]
|
||||
.filter((info) => info.def.fields.some((field) => field.name === CLASS_ATTRIBUTES))
|
||||
.map((info) => info.def.name),
|
||||
);
|
||||
if (guarded.size === 0) return;
|
||||
const helpers = new Map<string, IrFunction>();
|
||||
for (const fn of functions) {
|
||||
const local = (type: IrType, loc: SrcLoc): Extract<IrExpr, { kind: "varRef" }> => {
|
||||
const id = `%attributeWrite.${fn.locals.length}`;
|
||||
fn.locals.push({ id, name: id, type, mutable: false });
|
||||
return { kind: "varRef", localId: id, type, loc };
|
||||
};
|
||||
fn.body = transformStmtList(fn.body, {
|
||||
stmt: (statement) => {
|
||||
if (
|
||||
statement.kind !== "fieldSet" ||
|
||||
statement.attributesChecked ||
|
||||
!guarded.has(statement.className) ||
|
||||
!isClassOwnEnumerableFieldName(statement.field)
|
||||
)
|
||||
return statement;
|
||||
const info = lowerer.classes.get(statement.className)!;
|
||||
const loc = statement.loc;
|
||||
const receiver = local(statement.obj.type, loc);
|
||||
const value = local(statement.value.type, loc);
|
||||
const masked = (field: string, bit: number): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "&",
|
||||
left: hiddenFieldGet(receiver, statement.className, field, loc),
|
||||
right: numLit(bit, loc),
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const attributesField = classFieldAttributesName(statement.field);
|
||||
let readOnly = masked(
|
||||
CLASS_ATTRIBUTES,
|
||||
statement.define ? CLASS_NOT_EXTENSIBLE : CLASS_FROZEN,
|
||||
);
|
||||
if (!statement.define && info.fields.has(attributesField)) {
|
||||
readOnly = {
|
||||
kind: "bin",
|
||||
op: "|",
|
||||
left: readOnly,
|
||||
right: masked(attributesField, FIELD_NOT_WRITABLE),
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const reject: IrStmt[] = statement.define
|
||||
? [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "class.throwNotExtensible",
|
||||
args: [strLit(statement.field, loc)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]
|
||||
: statement.sloppy
|
||||
? []
|
||||
: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "class.throwReadOnly",
|
||||
args: [
|
||||
strLit(statement.field, loc),
|
||||
instanceClassName(lowerer, info, receiver, helpers, loc),
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: receiver.localId, init: statement.obj, loc },
|
||||
{ kind: "varDecl", localId: value.localId, init: statement.value, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: "!==",
|
||||
left: readOnly,
|
||||
right: numLit(0, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: reject,
|
||||
else_: [{ ...statement, obj: receiver, value, attributesChecked: true }],
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
},
|
||||
expr: (expression) => expression,
|
||||
});
|
||||
}
|
||||
for (const helper of helpers.values()) functions.push(helper);
|
||||
}
|
||||
@@ -23,7 +23,7 @@ function bagOnlyProperty(owner: ClassInfo, name: string): boolean {
|
||||
return !owner.fields.has(name) && owner.subclasses.every((child) => bagOnlyProperty(child, name));
|
||||
}
|
||||
|
||||
function runtimeRooted(info: ClassInfo): boolean {
|
||||
export function runtimeRooted(info: ClassInfo): boolean {
|
||||
for (let current: ClassInfo | null = info; current; current = current.base) {
|
||||
if (
|
||||
current.def.runtime ||
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
observedPrototypeMethod,
|
||||
reflectClassPrototype,
|
||||
} from "./class-prototypes.js";
|
||||
import { perEvaluationPrivateNames } from "./private-brands.js";
|
||||
import {
|
||||
errorPropertyRead,
|
||||
errorPropertyWrite,
|
||||
@@ -2694,6 +2695,7 @@ export class ClassDynamicDispatch {
|
||||
!findGenericMethodOn(lowerer, info, dispatch.name) &&
|
||||
dispatch.name !== "constructor" &&
|
||||
!info.fields.has(dispatch.name) &&
|
||||
!(dispatch.name === "stack" && lowerer.isSubclassOf(info.def.name, "%Error")) &&
|
||||
!findMethodOn(lowerer, info, `get:${dispatch.name}`) &&
|
||||
!findMethodOn(lowerer, info, `set:${dispatch.name}`))
|
||||
)
|
||||
@@ -2885,10 +2887,12 @@ export class ClassDynamicDispatch {
|
||||
for (const [symbol, name] of info.symbolMethods) symbolEntries.push([symbol, name]);
|
||||
for (const [symbol, name] of symbolEntries) {
|
||||
const declaration = symbol.valueDeclaration;
|
||||
// Module-scope consts and CommonJS key-table properties
|
||||
// (classSymbolKeyOfSymbol) both hold their key in a global.
|
||||
if (
|
||||
declaration &&
|
||||
ts.isVariableDeclaration(declaration) &&
|
||||
ts.isIdentifier(declaration.name)
|
||||
((ts.isVariableDeclaration(declaration) && ts.isIdentifier(declaration.name)) ||
|
||||
ts.isPropertyAssignment(declaration))
|
||||
) {
|
||||
const global = lowerer.globalsBySymbol.get(symbol);
|
||||
if (global)
|
||||
@@ -4311,19 +4315,43 @@ export class ClassDynamicDispatch {
|
||||
loc,
|
||||
};
|
||||
if (accessor && info.decl) {
|
||||
const method = classMethodValue(lowerer, info.decl, info, member, loc);
|
||||
if (method) {
|
||||
const call: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.reflectApply",
|
||||
args: [
|
||||
lowerer.coerceToExpected(method, DYN),
|
||||
actualReceiver,
|
||||
{ kind: "dynArrLit", elems: write ? [varRef("p.1", DYN, loc)] : [], type: DYN, loc },
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// An accessor whose private names belong to one evaluation runs as
|
||||
// the instance's own evaluation's prototype accessor.
|
||||
const declarer = accessor.declarer;
|
||||
const prototype = perEvaluationPrivateNames(declarer)
|
||||
? classPrototypeData(lowerer, declarer, loc, upcastTo(lowerer, receiver, declarer.def.name))
|
||||
: null;
|
||||
const method = prototype ? null : classMethodValue(lowerer, info.decl, info, member, loc);
|
||||
if (prototype || method) {
|
||||
const call: IrExpr = prototype
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: write ? "dyn.reflectSet" : "dyn.reflectGet",
|
||||
args: [
|
||||
prototype,
|
||||
lowerer.coerceToExpected(key, DYN),
|
||||
...(write ? [varRef("p.1", DYN, loc)] : []),
|
||||
actualReceiver,
|
||||
],
|
||||
type: write ? BOOL : DYN,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.reflectApply",
|
||||
args: [
|
||||
lowerer.coerceToExpected(method!, DYN),
|
||||
actualReceiver,
|
||||
{
|
||||
kind: "dynArrLit",
|
||||
elems: write ? [varRef("p.1", DYN, loc)] : [],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
{
|
||||
kind: "if",
|
||||
@@ -4498,6 +4526,110 @@ export class ClassDynamicDispatch {
|
||||
},
|
||||
];
|
||||
}
|
||||
// An Error's own `stack`: a string is the native error's stack, which
|
||||
// typed reads share; any other value is kept with the instance's own
|
||||
// properties, which a read checks first. A class whose chain declares
|
||||
// a `stack` accessor answers through it (the accessor code below).
|
||||
if (
|
||||
name === "stack" &&
|
||||
!info.fields.has(name) &&
|
||||
lowerer.isSubclassOf(info.def.name, "%Error") &&
|
||||
!findMethodOn(lowerer, info, "get:stack") &&
|
||||
!findMethodOn(lowerer, info, "set:stack")
|
||||
) {
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
const error = lowerer.upcastTo(receiver, "%Error");
|
||||
if (write) {
|
||||
const helper = classPropertiesHelper(lowerer, loc);
|
||||
this.generated.add(helper);
|
||||
const store: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.keySet",
|
||||
args: [
|
||||
{
|
||||
kind: "call",
|
||||
callee: helper.name,
|
||||
args: [lowerer.coerceToExpected(receiver, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
key,
|
||||
varRef("p.1", DYN, loc),
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
return [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "dynTest",
|
||||
test: "string",
|
||||
value: varRef("p.1", DYN, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "error.setStack",
|
||||
args: [error, lowerer.coerceToExpected(varRef("p.1", DYN, loc), STRING)],
|
||||
type: VOID,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: this.readBagHas(
|
||||
lowerer,
|
||||
"dyn.hasKey",
|
||||
this.readPropertyBag(lowerer, receiver, loc),
|
||||
key,
|
||||
loc,
|
||||
),
|
||||
then: [store],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: [store],
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: null, loc },
|
||||
];
|
||||
}
|
||||
const bag = this.readPropertyBag(lowerer, receiver, loc);
|
||||
return [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc),
|
||||
then: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bagGet",
|
||||
args: [bag, key, lowerer.coerceToExpected(receiver, DYN)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
else_: lowerer.coerceToExpected(
|
||||
{ kind: "libCall", fn: "error.stack", args: [error], type: STRING, loc },
|
||||
DYN,
|
||||
),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
}
|
||||
if ((name === "message" || name === "name") && lowerer.isSubclassOf(info.def.name, "%Error"))
|
||||
return write
|
||||
? [
|
||||
|
||||
@@ -27,6 +27,7 @@ import { funcTypeFromParamShapes, type ParamShape } from "./call-signatures.js";
|
||||
import { erasedGenericInstance, implicitDefaultInstance } from "./generic-functions.js";
|
||||
import { errorToStringMethod } from "./error-methods.js";
|
||||
import { classCallbackValue, isClassCallback } from "./class-callbacks.js";
|
||||
import { perEvaluationPrivateNames } from "./private-brands.js";
|
||||
|
||||
export interface ClassMethodValueSelection {
|
||||
expression: ts.PropertyAccessExpression;
|
||||
@@ -51,18 +52,20 @@ export function lowerClassMethodValue(
|
||||
}
|
||||
const loc = locOf(expr);
|
||||
const receiver = lowerer.lowerExpr(expr.expression);
|
||||
const declared = findMethodOn(lowerer, info, method);
|
||||
// A JS checker may name a class while the value is held in checked
|
||||
// storage. Read its actual property so own replacements and getters
|
||||
// participate, and never pass a dynamic value to a native selector.
|
||||
if (receiver.type.kind === "dyn")
|
||||
// participate, and never pass a dynamic value to a native selector. A
|
||||
// method whose private names belong to one evaluation is that
|
||||
// evaluation's prototype method as well.
|
||||
if (receiver.type.kind === "dyn" || (!!declared && perEvaluationPrivateNames(declared.declarer)))
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: receiver,
|
||||
value: lowerer.coerceToExpected(receiver, DYN),
|
||||
key: { kind: "strLit", value: method, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const declared = findMethodOn(lowerer, info, method);
|
||||
// Abstract declarations have no prototype function. Concrete descendants
|
||||
// provide the extracted value through the same reachability-driven selector.
|
||||
let value = declared?.sig.abstract
|
||||
@@ -537,10 +540,13 @@ export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunctio
|
||||
!(expr.kind === "closure" && expr.captures.includes(thisParam.localId)),
|
||||
});
|
||||
if (usesThis) continue;
|
||||
// A method that never uses this remains callable when detached.
|
||||
// A method that never uses this remains callable when detached. Without
|
||||
// class captures it needs no class environment, so the detached copy
|
||||
// drops the (empty) capture list, which requires a receiver.
|
||||
const name = `%method.receiverless:${target.name}`;
|
||||
const { classCaptures: _classCaptures, ...detached } = target;
|
||||
functions.push({
|
||||
...target,
|
||||
...detached,
|
||||
name,
|
||||
params: target.params.slice(1),
|
||||
locals: target.locals.filter((local) => local.id !== thisParam.localId),
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
type ClassInfo,
|
||||
} from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { newFnCtx } from "./lowerer.js";
|
||||
import { PoisonError, newFnCtx } from "./lowerer.js";
|
||||
import { classMethodValue } from "./class-method-values.js";
|
||||
|
||||
/** Data added to a top-level class prototype has shared identity and remains
|
||||
@@ -376,14 +376,37 @@ export function reflectClassPrototype(
|
||||
const method = ts.isMethodDeclaration(member)
|
||||
? name
|
||||
: `${ts.isGetAccessor(member) ? "get" : "set"}:${name}`;
|
||||
const value = classMethodValue(lowerer, info.decl, info, method, loc);
|
||||
// A member whose value or key has no lowering refuses the reflection
|
||||
// at run time; its own diagnostic must not escape this walk (the
|
||||
// walk runs from emitReachable, after statement fences are placed).
|
||||
let before = lowerer.diags.length;
|
||||
let value: IrExpr | null;
|
||||
try {
|
||||
value = classMethodValue(lowerer, info.decl, info, method, loc);
|
||||
} catch (error) {
|
||||
if (!(error instanceof PoisonError)) throw error;
|
||||
lowerer.diags.splice(before);
|
||||
return refuse(
|
||||
"prototype reflection with a member that has no lowering is not supported yet",
|
||||
);
|
||||
}
|
||||
if (!value || !lowerer.dynConvertible(value.type))
|
||||
return refuse(
|
||||
"prototype reflection with a member that cannot cross a checked value boundary is not supported yet",
|
||||
);
|
||||
const key: IrExpr = ts.isComputedPropertyName(member.name)
|
||||
? lowerer.lowerExpr(member.name.expression)
|
||||
: { kind: "strLit", value: member.name.text, type: STRING, loc };
|
||||
let key: IrExpr;
|
||||
before = lowerer.diags.length;
|
||||
try {
|
||||
key = ts.isComputedPropertyName(member.name)
|
||||
? lowerer.lowerExpr(member.name.expression)
|
||||
: { kind: "strLit", value: member.name.text, type: STRING, loc };
|
||||
} catch (error) {
|
||||
if (!(error instanceof PoisonError)) throw error;
|
||||
lowerer.diags.splice(before);
|
||||
return refuse(
|
||||
"prototype reflection with a computed member key that has no lowering is not supported yet",
|
||||
);
|
||||
}
|
||||
initialization.then.push(
|
||||
define(
|
||||
key,
|
||||
@@ -556,6 +579,15 @@ function prototypeProtectedKeys(
|
||||
const entries: [ts.Symbol, string][] = owner.symbolMethods ? [...owner.symbolMethods] : [];
|
||||
const entry = entries.find(([, field]) => field === name);
|
||||
const declaration = entry ? entry[0].valueDeclaration : undefined;
|
||||
// A CommonJS key-table property holds its key in the table's global.
|
||||
const tableKey =
|
||||
declaration && ts.isPropertyAssignment(declaration)
|
||||
? lowerer.globalsBySymbol.get(entry![0])
|
||||
: undefined;
|
||||
if (tableKey) {
|
||||
keys.push({ kind: "varRef", localId: tableKey.id, type: tableKey.type, loc });
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
!declaration ||
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
|
||||
@@ -111,12 +111,15 @@ export function coercibleValue(lowerer: Lowerer, src: IrType, dst: IrType): bool
|
||||
}
|
||||
|
||||
/** Whether all parameters and the result adapt without a trap-only path.
|
||||
* Rest signatures are excluded; extra destination parameters are ignored. */
|
||||
* Rest signatures are excluded, except a typed-rest source in a slot with
|
||||
* fixed parameters (the rest packing adapter); extra destination
|
||||
* parameters are ignored. */
|
||||
export function cleanFuncAdaptable(
|
||||
lowerer: Lowerer,
|
||||
src: IrType & { kind: "func" },
|
||||
dst: IrType & { kind: "func" },
|
||||
): boolean {
|
||||
if (restPackingAdapterFits(lowerer, src, dst)) return true;
|
||||
if (src.rest === true || dst.rest === true) return false;
|
||||
if (src.params.length > dst.params.length) return false;
|
||||
for (let i = 0; i < src.params.length; i++) {
|
||||
@@ -127,15 +130,145 @@ export function cleanFuncAdaptable(
|
||||
return src.ret.kind === "void" && dst.ret.kind === "dyn";
|
||||
}
|
||||
|
||||
/** Whether a typed-rest function (`(...m: T[]) => R`) can serve a slot
|
||||
* with fixed parameters (`(m: T) => void`): the slot's parameters convert
|
||||
* to the function's fixed parameters, then to the rest element type. */
|
||||
export function restPackingAdapterFits(
|
||||
lowerer: Lowerer,
|
||||
fromT: IrType & { kind: "func" },
|
||||
toT: IrType & { kind: "func" },
|
||||
): boolean {
|
||||
if (fromT.rest !== true || fromT.restAbi !== "typed" || toT.rest === true) return false;
|
||||
const fixed = fromT.params.length - 1;
|
||||
const restT = fromT.params[fixed];
|
||||
if (restT?.kind !== "array" || toT.params.length < fixed) return false;
|
||||
for (let i = 0; i < toT.params.length; i++) {
|
||||
if (!lowerer.coercibleValue(toT.params[i]!, i < fixed ? fromT.params[i]! : restT.elem))
|
||||
return false;
|
||||
}
|
||||
return toT.ret.kind === "void" || lowerer.coercibleValue(fromT.ret, toT.ret);
|
||||
}
|
||||
|
||||
/** The adapter for restPackingAdapterFits: forwards the fixed arguments and
|
||||
* packs the remaining ones into the rest array, as a JavaScript call of the
|
||||
* function with those arguments does. */
|
||||
function restPackingAdapter(
|
||||
lowerer: Lowerer,
|
||||
fromT: IrType & { kind: "func" },
|
||||
toT: IrType & { kind: "func" },
|
||||
loc: SrcLoc,
|
||||
): string {
|
||||
const key = `fnrestadapt:${typeKey(fromT)}:${typeKey(toT)}`;
|
||||
const existing = lowerer.coercions.retags.get(key);
|
||||
if (existing) return existing;
|
||||
const name = `%fn.adapt.${lowerer.coercions.retags.size}`;
|
||||
lowerer.coercions.retags.set(key, name);
|
||||
lowerer.freshClosureAdapters.add(name);
|
||||
const fixed = fromT.params.length - 1;
|
||||
const restT = fromT.params[fixed] as IrType & { kind: "array" };
|
||||
const params: IrParam[] = toT.params.map((t, i) => ({
|
||||
localId: `a.${i}`,
|
||||
name: `a${i}`,
|
||||
type: t,
|
||||
}));
|
||||
const arg = (i: number, want: IrType): IrExpr =>
|
||||
lowerer.coerceToExpected(
|
||||
{ kind: "varRef", localId: `a.${i}`, type: toT.params[i]!, loc },
|
||||
want,
|
||||
);
|
||||
const args: IrExpr[] = [];
|
||||
for (let i = 0; i < fixed; i++) args.push(arg(i, fromT.params[i]!));
|
||||
const packed: IrExpr[] = [];
|
||||
for (let i = fixed; i < toT.params.length; i++) packed.push(arg(i, restT.elem));
|
||||
args.push({ kind: "arrayLit", elems: packed, type: restT, loc });
|
||||
const call: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: { kind: "varRef", localId: "f.0", type: fromT, loc },
|
||||
receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
|
||||
args,
|
||||
type: fromT.ret,
|
||||
loc,
|
||||
};
|
||||
const body: IrStmt[] =
|
||||
toT.ret.kind === "void"
|
||||
? [
|
||||
{ kind: "exprStmt", expr: call, loc },
|
||||
{ kind: "return", value: null, loc },
|
||||
]
|
||||
: [{ kind: "return", value: lowerer.coerceToExpected(call, toT.ret), loc }];
|
||||
lowerer.liftedFns.push(...buildFunctionAdapter(name, fromT, toT, params, [], body, loc));
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Adapt a fixed-parameter function to a checked-dynamic rest signature
|
||||
* without fixed parameters (`() => {}` into mock.fn()'s `(...args: any[])
|
||||
* => undefined`): the adapter receives the hidden argument array and
|
||||
* passes its element i as parameter i, undefined when the caller passed
|
||||
* fewer arguments, as JS does. Null unless every parameter is
|
||||
* checked-dynamic, which holds a missing argument as undefined, and the
|
||||
* result is discarded or converts. */
|
||||
function funcIntoDynRestAdapter(
|
||||
lowerer: Lowerer,
|
||||
fromT: IrType & { kind: "func" },
|
||||
toT: IrType & { kind: "func" },
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
if (fromT.rest === true || fromT.params.some((p) => p.kind !== "dyn")) return null;
|
||||
if (toT.ret.kind !== "void" && !lowerer.coercibleValue(fromT.ret, toT.ret)) return null;
|
||||
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`;
|
||||
const existing = lowerer.coercions.retags.get(key);
|
||||
if (existing) return existing;
|
||||
const name = `%fn.adapt.${lowerer.coercions.retags.size}`;
|
||||
lowerer.coercions.retags.set(key, name);
|
||||
lowerer.freshClosureAdapters.add(name);
|
||||
const params: IrParam[] = [{ localId: "rest.0", name: "args", type: DYN }];
|
||||
const call: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: { kind: "varRef", localId: "f.0", type: fromT, loc },
|
||||
receiver: { kind: "libCall", fn: "dyn.this", args: [], type: DYN, loc },
|
||||
args: fromT.params.map((_, i): IrExpr => ({
|
||||
kind: "dynKeyGet",
|
||||
value: { kind: "varRef", localId: "rest.0", type: DYN, loc },
|
||||
key: { kind: "strLit", value: String(i), type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
})),
|
||||
type: fromT.ret,
|
||||
loc,
|
||||
};
|
||||
let body: IrStmt[];
|
||||
if (toT.ret.kind === "void") {
|
||||
body = [
|
||||
{ kind: "exprStmt", expr: call, loc },
|
||||
{ kind: "return", value: null, loc },
|
||||
];
|
||||
} else {
|
||||
const result = lowerer.coerceToExpected(call, toT.ret);
|
||||
if (!typeEquals(result.type, toT.ret))
|
||||
throw new InternalCompilerError("lowerer bug: probed fn-adapter return stopped coercing");
|
||||
body = [{ kind: "return", value: result, loc }];
|
||||
}
|
||||
lowerer.liftedFns.push(...buildFunctionAdapter(name, fromT, toT, params, [], body, loc));
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Adapt a function value to a destination signature, forwarding and
|
||||
* converting its arguments and result. Unsupported parameter/result pairs
|
||||
* produce checked traps when called; rest signatures remain refused. */
|
||||
* produce checked traps when called. Rest signatures are refused, except a
|
||||
* typed-rest source in a slot with fixed parameters, which the rest
|
||||
* packing adapter serves, and a fixed-parameter function into a
|
||||
* checked-dynamic rest signature without fixed parameters
|
||||
* (funcIntoDynRestAdapter). */
|
||||
export function funcCoerceAdapter(
|
||||
lowerer: Lowerer,
|
||||
fromT: IrType & { kind: "func" },
|
||||
toT: IrType & { kind: "func" },
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
if (restPackingAdapterFits(lowerer, fromT, toT))
|
||||
return restPackingAdapter(lowerer, fromT, toT, loc);
|
||||
if (toT.rest === true && toT.restAbi === undefined && toT.params.length === 0)
|
||||
return funcIntoDynRestAdapter(lowerer, fromT, toT, loc);
|
||||
if (fromT.rest === true || toT.rest === true) return null;
|
||||
if (fromT.params.length > toT.params.length) return null;
|
||||
// Piece dispositions beyond coercibleValue, all CHECKER-APPROVED
|
||||
|
||||
@@ -66,6 +66,10 @@ import { lowerAbstractEquality } from "../abstract-equality.js";
|
||||
import { lowerInExpression } from "./relational.js";
|
||||
import { lowerDynTypeofTest, lowerErrorCodeTypeofTest } from "./typeof.js";
|
||||
import { builtinClassRefOf, sameBuiltinClass } from "../builtins/class-values.js";
|
||||
import {
|
||||
lowerRuntimeConstructorIs,
|
||||
runtimeConstructorReceiver,
|
||||
} from "../builtins/runtime-constructor.js";
|
||||
import { tryLowerIndexedComparison } from "../indexed-comparison.js";
|
||||
import { lowerUnionEquality, tagEqualityMayMissAlias } from "../strict-equality.js";
|
||||
import { withUnitArm } from "../../type-mapper.js";
|
||||
@@ -1322,6 +1326,24 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
lowerUnionTypeofTest(lowerer, expr, loc);
|
||||
if (test) return test;
|
||||
}
|
||||
// `x.constructor === Readable` for an instance of EventEmitter or a
|
||||
// stream class: the instance's own class, compared without a class value.
|
||||
if (
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken
|
||||
) {
|
||||
for (const [side, other] of [
|
||||
[expr.left, expr.right],
|
||||
[expr.right, expr.left],
|
||||
] as const) {
|
||||
const ctor = runtimeConstructorReceiver(lowerer, side);
|
||||
const target = ctor ? builtinClassRefOf(lowerer, other) : null;
|
||||
if (ctor && (target?.kind === "emitter" || target?.kind === "stream")) {
|
||||
const negated = op === ts.SyntaxKind.ExclamationEqualsEqualsToken;
|
||||
return lowerRuntimeConstructorIs(lowerer, ctor.receiver, target.info, negated, loc);
|
||||
}
|
||||
}
|
||||
}
|
||||
// `C === EventEmitter` between two builtin class references: the
|
||||
// classes are compile-time values, so the answer folds.
|
||||
if (
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/* Narrowings that refine record arms without naming a stored arm. */
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { type IrExpr, type IrType, type SrcLoc, typeEquals } from "../../../ir/ir.js";
|
||||
|
||||
/** A checker narrowing can refine the arms of a union of records with an
|
||||
* intersection: a predicate `r is Route & { path: string }`, or the
|
||||
* `"path" in r` narrowing, which intersects arms that do not declare the
|
||||
* name with `Record<"path", unknown>`. Each narrowed arm then maps to a
|
||||
* new record shape that is not an arm of the stored union, although the
|
||||
* value never changed layout. A stored record arm HOSTS a narrowed record
|
||||
* when both name the same members, each narrowed member refined to a
|
||||
* subset of the stored slot type (or to `unknown`); a member an assertion
|
||||
* attached to the stored shape (assertion-members.ts) may be missing from
|
||||
* the narrowed one. A required member the stored arm does not declare
|
||||
* cannot exist on its values: native records are closed, so `in` already
|
||||
* answers false there and the narrowed arm is uninhabited.
|
||||
*
|
||||
* Only unions of two or more record arms without a literal discriminant
|
||||
* qualify: discriminated unions narrow by their discriminant, and a
|
||||
* record with unit arms keeps its own predicate-strengthening reads.
|
||||
*
|
||||
* Returns the stored arms that can hold a value of the narrowed type, or
|
||||
* null when the union does not qualify, when some narrowed arm is neither
|
||||
* a stored arm nor a record, or when no stored arm is excluded (there is
|
||||
* nothing to narrow). */
|
||||
export function hostedArmTags(
|
||||
lowerer: Lowerer,
|
||||
fromId: string,
|
||||
narrowed: IrType,
|
||||
): Set<number> | null {
|
||||
const from = lowerer.unions.get(fromId);
|
||||
if (!from || !recordUnion(lowerer, fromId)) return null;
|
||||
const narrowedArms =
|
||||
narrowed.kind === "union" ? lowerer.unions.get(narrowed.unionId)?.arms : [narrowed];
|
||||
if (!narrowedArms || narrowedArms.length === 0) return null;
|
||||
const allowed = new Set<number>();
|
||||
let refined = false;
|
||||
for (const arm of narrowedArms) {
|
||||
const direct = lowerer.armTag(fromId, arm);
|
||||
if (direct >= 0) {
|
||||
allowed.add(direct);
|
||||
continue;
|
||||
}
|
||||
if (arm.kind !== "record") return null;
|
||||
refined = true;
|
||||
from.arms.forEach((stored, tag) => {
|
||||
if (stored.kind === "record" && recordHosts(lowerer, stored.shapeId, arm.shapeId)) {
|
||||
allowed.add(tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!refined || allowed.size === 0 || allowed.size === from.arms.length) return null;
|
||||
return allowed;
|
||||
}
|
||||
|
||||
/** A union of at least two record arms without a literal discriminant. */
|
||||
function recordUnion(lowerer: Lowerer, unionId: string): boolean {
|
||||
const def = lowerer.unions.get(unionId);
|
||||
if (!def || def.discriminant) return false;
|
||||
return def.arms.filter((arm) => arm.kind === "record").length >= 2;
|
||||
}
|
||||
|
||||
/** Whether every value of `narrowed` (a checker refinement of a value
|
||||
* stored as `storage`) lives in the stored layout: each narrowed arm is a
|
||||
* stored arm or a record a stored record arm hosts (see hostedArmTags),
|
||||
* and at least one needs the hosting. A union storage must qualify as in
|
||||
* hostedArmTags; a single record only when an assertion attached members
|
||||
* to it. A binding initialized by such an assertion keeps the stored type,
|
||||
* so its member writes reach the asserted object. */
|
||||
export function hostsAll(lowerer: Lowerer, storage: IrType, narrowed: IrType): boolean {
|
||||
if (typeEquals(storage, narrowed)) return false;
|
||||
let storedArms: readonly IrType[];
|
||||
if (storage.kind === "union") {
|
||||
if (!recordUnion(lowerer, storage.unionId)) return false;
|
||||
storedArms = lowerer.unions.get(storage.unionId)!.arms;
|
||||
} else if (storage.kind === "record" && lowerer.shapes.attachedNames(storage.shapeId).length) {
|
||||
storedArms = [storage];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
const narrowedArms =
|
||||
narrowed.kind === "union" ? lowerer.unions.get(narrowed.unionId)?.arms : [narrowed];
|
||||
if (!narrowedArms || narrowedArms.length === 0) return false;
|
||||
let refined = false;
|
||||
for (const arm of narrowedArms) {
|
||||
if (storedArms.some((stored) => typeEquals(stored, arm))) continue;
|
||||
if (arm.kind !== "record") return false;
|
||||
const hosted = storedArms.some(
|
||||
(stored) => stored.kind === "record" && recordHosts(lowerer, stored.shapeId, arm.shapeId),
|
||||
);
|
||||
if (!hosted) return false;
|
||||
refined = true;
|
||||
}
|
||||
return refined;
|
||||
}
|
||||
|
||||
function recordHosts(lowerer: Lowerer, storedId: string, narrowedId: string): boolean {
|
||||
const stored = lowerer.shapes.get(storedId);
|
||||
const narrowed = lowerer.shapes.get(narrowedId);
|
||||
if (!stored || !narrowed || stored.tuple || narrowed.tuple) return false;
|
||||
if (stored.indexValue || narrowed.indexValue) return false;
|
||||
const attached = lowerer.shapes.attachedNames(storedId);
|
||||
for (const sf of stored.fields) {
|
||||
if (!narrowed.fields.some((nf) => nf.name === sf.name) && !attached.includes(sf.name))
|
||||
return false;
|
||||
}
|
||||
for (const nf of narrowed.fields) {
|
||||
const sf = stored.fields.find((f) => f.name === nf.name);
|
||||
if (!sf || !slotRefines(lowerer, nf.type, sf.type)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Whether a stored slot of type `stored` can be read as `narrowed`: the
|
||||
* same type, `unknown`, or a subset of the stored union's arms (a read
|
||||
* site narrows from the stored slot exactly as it does for a predicate
|
||||
* strengthening one record). */
|
||||
function slotRefines(lowerer: Lowerer, narrowed: IrType, stored: IrType): boolean {
|
||||
if (typeEquals(narrowed, stored) || narrowed.kind === "dyn") return true;
|
||||
if (stored.kind !== "union") return false;
|
||||
if (narrowed.kind !== "union") return lowerer.armTag(stored.unionId, narrowed) >= 0;
|
||||
const arms = lowerer.unions.get(narrowed.unionId)?.arms;
|
||||
return !!arms && arms.every((arm) => lowerer.armTag(stored.unionId, arm) >= 0);
|
||||
}
|
||||
|
||||
/** The stored view of a hosted narrowing (see hostedArmTags): one stored
|
||||
* record arm extracted with the checked helper, or the sub-union of the
|
||||
* hosting arms re-tagged with the excluded arms as traps. Both keep the
|
||||
* value's stored layout, so member reads and writes reach the same
|
||||
* object. Null when the narrowing is not a hosted one. */
|
||||
export function hostedStorageView(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
narrowed: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (value.type.kind !== "union") return null;
|
||||
const fromId = value.type.unionId;
|
||||
const allowed = hostedArmTags(lowerer, fromId, narrowed);
|
||||
if (!allowed) return null;
|
||||
const from = lowerer.unions.get(fromId)!;
|
||||
if (allowed.size === 1) {
|
||||
const arm = from.arms[[...allowed][0]!]!;
|
||||
const helper = lowerer.narrowedArmHelper(fromId, arm, loc);
|
||||
return helper ? lowerer.checkedNarrowCall(helper, value, arm, loc) : null;
|
||||
}
|
||||
const arms = from.arms.filter((_, tag) => allowed.has(tag));
|
||||
const subId = lowerer.unions.transform(from, arms);
|
||||
const trappable = new Set<number>();
|
||||
from.arms.forEach((_, tag) => {
|
||||
if (!allowed.has(tag)) trappable.add(tag);
|
||||
});
|
||||
const helper = lowerer.unionRetagHelper(fromId, subId, loc, trappable);
|
||||
if (!helper) return null;
|
||||
const type: IrType = { kind: "union", unionId: subId };
|
||||
return { kind: "call", callee: helper, args: [value], type, loc };
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
|
||||
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import * as posix from "node:path/posix";
|
||||
import { type Lowerer, PoisonError } from "../lowerer.js";
|
||||
@@ -351,19 +352,25 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
// MODULE compile-time constants, the containing file's location
|
||||
// (Node's exact values when the same tree runs in place; WASI uses
|
||||
// the equivalent guest-visible path; locals shadowing the names
|
||||
// resolved above). In a file with ESM syntax Node never defines them
|
||||
// (ReferenceError) — fence rather than invent a value there.
|
||||
// resolved above), the same strings as import.meta.dirname/filename.
|
||||
// In a JavaScript ES module Node never defines them, so the read throws
|
||||
// Node's catchable ReferenceError. A .ts/.tsx file keeps them even when
|
||||
// Node's strip-types rule would load it as ESM: TypeScript projects are
|
||||
// commonly built as CommonJS bundles (tsc, esbuild, tsx) where these
|
||||
// globals exist, and @types/node declares them for every file.
|
||||
if (
|
||||
(expr.text === "__dirname" || expr.text === "__filename") &&
|
||||
lowerer.isStdlibGlobal(expr, expr.text)
|
||||
) {
|
||||
const sf = expr.getSourceFile();
|
||||
if (isNodeEsmFile(sf, lowerer.program)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'${expr.text}' in an ES module (Node defines the CJS module globals only in CommonJS modules)`,
|
||||
);
|
||||
if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName)) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "global.undefRead",
|
||||
args: [{ kind: "strLit", value: expr.text, type: STRING, loc }],
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// The WASI runner mounts the host cwd at guest `/` and the host temp
|
||||
// directory at `/tmp`. Bake the matching GUEST spelling so resource
|
||||
@@ -859,6 +866,11 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
lowerer.checker.getSymbolAtLocation(expr),
|
||||
);
|
||||
}
|
||||
// An import of a CommonJS member export whose value is a binding of the
|
||||
// exporting module (`module.exports = Client; module.exports.StatsD =
|
||||
// Client`): the member holds that binding's value.
|
||||
const memberAlias = cjsMemberAliasValueOf(lowerer, expr);
|
||||
if (memberAlias !== null) return lowerer.lowerExpr(memberAlias);
|
||||
// Preflight guarantees no unresolved identifiers; anything else here
|
||||
// is a blocked declaration's binding (the SC2004 cascade) or a
|
||||
// binding form we don't model yet.
|
||||
|
||||
@@ -29,6 +29,7 @@ import { isOptionalProcessStreamProperty } from "../builtins/process.js";
|
||||
import { literalValues } from "../../literal-values.js";
|
||||
import { literalUnionArm } from "../../union-discriminants.js";
|
||||
import { checkedArmExtraction } from "../coercions/unions.js";
|
||||
import { hostedStorageView, hostsAll } from "./hosted-narrowing.js";
|
||||
import { nodeThrowExpr, varRef } from "../../../ir/build.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import {
|
||||
@@ -659,6 +660,12 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
};
|
||||
}
|
||||
}
|
||||
// A narrowing that refines record arms with an intersection keeps the
|
||||
// stored layout: view the value as the stored arms that can hold it.
|
||||
if (narrowed?.kind === "union") {
|
||||
const hosted = hostedStorageView(lowerer, expr, narrowed, expr.loc);
|
||||
if (hosted) return hosted;
|
||||
}
|
||||
// A checker type narrowed to a UNIT arm (the `=== undefined` branch)
|
||||
// also stays union-typed: a unit arm has no payload to extract, and
|
||||
// nothing useful reads such a value anyway. (Standalone undefined maps
|
||||
@@ -668,6 +675,14 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
|
||||
return expr;
|
||||
}
|
||||
const tag = lowerer.armTag(expr.type.unionId, narrowed);
|
||||
if (
|
||||
tag < 0 &&
|
||||
narrowed.kind === "record" &&
|
||||
!lowerer.unions.get(expr.type.unionId)?.discriminant
|
||||
) {
|
||||
const hosted = hostedStorageView(lowerer, expr, narrowed, expr.loc);
|
||||
if (hosted) return hosted;
|
||||
}
|
||||
if (tag < 0) {
|
||||
// The checker may refine fields inside a discriminated record too.
|
||||
// That creates a different mapped shape, but does not change the
|
||||
@@ -926,6 +941,10 @@ export function runtimeOptionalReceiverRead(
|
||||
loc,
|
||||
);
|
||||
if (!helper) {
|
||||
// Intersection-refined record arms read through their stored
|
||||
// layout (the predicate `r is Route & { path: string }`).
|
||||
const hosted = hostedStorageView(lowerer, varRef(local.id, local.type, loc), narrowed, loc);
|
||||
if (hosted) return hosted;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
@@ -946,6 +965,10 @@ export function runtimeOptionalReceiverRead(
|
||||
// path below; mixed storage also needs to validate its concrete class.
|
||||
if (view && (lowerer.unions.get(local.type.unionId)?.arms.length ?? 0) > 2) return view;
|
||||
}
|
||||
if (narrowed.kind === "record" && lowerer.armTag(local.type.unionId, narrowed) < 0) {
|
||||
const hosted = hostedStorageView(lowerer, varRef(local.id, local.type, loc), narrowed, loc);
|
||||
if (hosted) return hosted;
|
||||
}
|
||||
const undefTag = lowerer.armTag(local.type.unionId, UNDEFINED_T);
|
||||
const def = lowerer.unions.get(local.type.unionId);
|
||||
if (!def || undefTag < 0)
|
||||
@@ -1151,6 +1174,18 @@ export function withRuntimeOptionalNarrowed<T>(
|
||||
* memory);
|
||||
* - dyn → anything else (closures, class instances, void) is rejected:
|
||||
* those types cannot be found inside a JSON dyn. */
|
||||
/** A class instance, or a union of class instances and unit arms (a
|
||||
* runtime-optional binding such as `const node = stack.pop()!`). */
|
||||
function classInstanceOrAbsent(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind === "object") return true;
|
||||
if (type.kind !== "union") return false;
|
||||
const arms = lowerer.unions.get(type.unionId)?.arms ?? [];
|
||||
return (
|
||||
arms.some((arm) => arm.kind === "object") &&
|
||||
arms.every((arm) => arm.kind === "object" || isUnitType(arm))
|
||||
);
|
||||
}
|
||||
|
||||
/** `e as T` and the old-style assertion `<T>e` — one node shape (both
|
||||
* carry `.type` and `.expression`), one lowering. */
|
||||
export function lowerAsExpression(
|
||||
@@ -1228,8 +1263,10 @@ export function lowerAsExpression(
|
||||
if (target?.kind === "dyn" && (targetTs0.flags & ts.TypeFlags.Object) !== 0) {
|
||||
// A class instance asserted to a class-backed protocol view (a type
|
||||
// class instances flow into): the same identity-keeping box an
|
||||
// implicit flow into the view uses.
|
||||
if (inner.type.kind === "object" && lowerer.classImplementedProtocol(targetTs0))
|
||||
// implicit flow into the view uses. A runtime-optional binding of a
|
||||
// class (`const node = stack.pop()!`) boxes the same way, and its
|
||||
// undefined arm stays undefined, as in Node.
|
||||
if (classInstanceOrAbsent(lowerer, inner.type) && lowerer.classImplementedProtocol(targetTs0))
|
||||
return lowerer.coerceInto(expr, inner, DYN);
|
||||
const sourceShape =
|
||||
inner.type.kind === "record" ? lowerer.shapes.get(inner.type.shapeId) : undefined;
|
||||
@@ -1286,6 +1323,12 @@ export function lowerAsExpression(
|
||||
) {
|
||||
return lowerer.coerceInto(expr, inner, target);
|
||||
}
|
||||
// `job as Job & { startedAt: number }` on a record: the asserted
|
||||
// record refines the stored one, so the assertion keeps the object
|
||||
// (a width copy would sever writes through the asserted view).
|
||||
if (inner.type.kind === "record" && target && hostsAll(lowerer, inner.type, target)) {
|
||||
return inner;
|
||||
}
|
||||
// Static assertions normally erase, but record layouts are
|
||||
// monomorphic: a consumer selected from the asserted shape must see
|
||||
// that shape physically. Reuse the ordinary slot coercion so plain
|
||||
@@ -1312,6 +1355,12 @@ export function lowerAsExpression(
|
||||
return lowerer.checkedNarrowCall(helper, inner, target, locOf(expr));
|
||||
}
|
||||
}
|
||||
// An intersection refining the stored record arms keeps the value
|
||||
// (hosted-narrowing.ts): the stored arms that can hold it.
|
||||
if (target && (target.kind === "union" || target.kind === "record")) {
|
||||
const hosted = hostedStorageView(lowerer, inner, target, locOf(expr));
|
||||
if (hosted) return hosted;
|
||||
}
|
||||
}
|
||||
// `node.parent as Sub | undefined`: a union target whose class arm is
|
||||
// narrower than the stored class gets the same checked class view as
|
||||
|
||||
@@ -214,6 +214,37 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
|
||||
!nullish && !and && element.kind === "f64"
|
||||
? arrayNumberRead(receiver, key, elementLoc)
|
||||
: arrayValueRead(lowerer, receiver, key, element, elementLoc);
|
||||
} else if (
|
||||
receiver.type.kind === "record" &&
|
||||
key.type.kind === "string" &&
|
||||
pureIndexShapeOf(lowerer, receiver.type.shapeId) !== null
|
||||
) {
|
||||
// `timings[phase] ??= v` over a pure index-signature record
|
||||
// (`Record<string, number>`): the keyed read answers the value or
|
||||
// undefined for a missing key, and the write inserts or replaces
|
||||
// the key, exactly the two halves `o[k]` and `o[k] = v` lower to.
|
||||
const shapeId = receiver.type.shapeId;
|
||||
const indexValue = pureIndexShapeOf(lowerer, shapeId)!;
|
||||
key = save(key, "%shortCircuitKey");
|
||||
const readType =
|
||||
indexValue.kind === "dyn" ? indexValue : lowerer.withUndefinedArmOf(indexValue);
|
||||
if (!readType) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
target,
|
||||
"short-circuit assignment through this indexed receiver",
|
||||
);
|
||||
}
|
||||
storage = indexValue;
|
||||
read = { kind: "recordKeyGet", obj: receiver, shapeId, key, type: readType, loc };
|
||||
write = (value) => ({
|
||||
kind: "recordKeySet",
|
||||
obj: receiver,
|
||||
shapeId,
|
||||
key,
|
||||
value: lowerer.coerceToExpected(value, indexValue),
|
||||
loc,
|
||||
});
|
||||
} else {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -265,3 +296,11 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
|
||||
}
|
||||
return finish({ kind: "nullish", left: read, right, type: read.type, loc });
|
||||
}
|
||||
|
||||
/** The value type of a pure index-signature record shape (no declared
|
||||
* fields, not a tuple), or null. */
|
||||
function pureIndexShapeOf(lowerer: Lowerer, shapeId: string): IrType | null {
|
||||
const shape = lowerer.shapes.get(shapeId);
|
||||
if (!shape || shape.tuple || shape.fields.length > 0 || !shape.indexValue) return null;
|
||||
return shape.indexValue;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { localPrivateBrandClass } from "../private-brands.js";
|
||||
import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js";
|
||||
import {
|
||||
ARRAY_METHODS,
|
||||
@@ -76,6 +77,11 @@ import { lifetimeRegionMemberOf } from "../../experimental-import.js";
|
||||
import { lowerInternTableProperty } from "../intern-tables.js";
|
||||
import { lowerThreadSlotsProperty } from "../thread-slots.js";
|
||||
import { builtinClassRefOf, runtimeClassValueOfType } from "../builtins/class-values.js";
|
||||
import {
|
||||
lowerRuntimeConstructor,
|
||||
lowerRuntimeConstructorName,
|
||||
runtimeConstructorReceiver,
|
||||
} from "../builtins/runtime-constructor.js";
|
||||
import { findGenericMethodOn, lowerStaticFieldRead } from "../lower-classes.js";
|
||||
import { lowerClassPrototypeData } from "../class-prototypes.js";
|
||||
import { lowerEnumAccess } from "../lower-enums.js";
|
||||
@@ -290,6 +296,16 @@ export function lowerPropertyAccessExpr(
|
||||
const ref = builtinClassRefOf(lowerer, expr.expression);
|
||||
if (ref !== null) return { kind: "strLit", value: ref.name, type: STRING, loc };
|
||||
}
|
||||
// `x.constructor.name` and `x.constructor` on an instance of
|
||||
// EventEmitter or a stream class (builtins/runtime-constructor.ts).
|
||||
if (expr.name.text === "name" && !expr.questionDotToken) {
|
||||
const ctor = runtimeConstructorReceiver(lowerer, expr.expression);
|
||||
if (ctor) return lowerRuntimeConstructorName(lowerer, ctor.receiver, loc);
|
||||
}
|
||||
{
|
||||
const ctor = runtimeConstructorReceiver(lowerer, expr);
|
||||
if (ctor) return lowerRuntimeConstructor(lowerer, expr, ctor.info);
|
||||
}
|
||||
// `C.name` through a runtime-provided class VALUE (a `typeof Readable`
|
||||
// parameter): the class object's stored name.
|
||||
if (
|
||||
@@ -3205,6 +3221,22 @@ export function fieldTarget(
|
||||
fieldType: F64,
|
||||
};
|
||||
}
|
||||
// `this.#x` in a function-local class's prototype method: the field of
|
||||
// the brand-checked receiver (checkedPrivateReceiver).
|
||||
if (
|
||||
ts.isPrivateIdentifier(access.name) &&
|
||||
access.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
||||
lowerer.resolveThis()?.type.kind === "dyn"
|
||||
) {
|
||||
const owner = localPrivateBrandClass(lowerer, access.name);
|
||||
if (owner) {
|
||||
const ownerType: IrType & { kind: "object" } = {
|
||||
kind: "object",
|
||||
className: owner.info.def.name,
|
||||
};
|
||||
return classFieldTarget(lowerer, access.expression, ownerType, access.name.text);
|
||||
}
|
||||
}
|
||||
const stored = ts.isIdentifier(access.expression)
|
||||
? (lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type)
|
||||
: undefined;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/* instanceof and in, including private-name and runtime-key in. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
checkedPrivateIn,
|
||||
localPrivateBrandClass,
|
||||
primitiveOperand,
|
||||
privateInMessage,
|
||||
} from "../private-brands.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
BOOL,
|
||||
@@ -17,20 +23,22 @@ import {
|
||||
UNDEFINED_T,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../../ir/ir.js";
|
||||
import { refuseWebAssemblyUse } from "../builtins/webassembly.js";
|
||||
import { lowerSocketInstanceOf } from "../lower-server.js";
|
||||
import { builtinClassRefOf } from "../builtins/class-values.js";
|
||||
import { lowerZlibInstanceOf } from "../builtins/compression.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { lowerDisposableStackInstanceOf } from "../builtins/disposable-stack.js";
|
||||
import { lowerBlobInstanceof } from "../builtins/blob.js";
|
||||
import { lowerReadlineInstanceof } from "../builtins/readline.js";
|
||||
import { lowerKeyObjectInstanceof } from "../builtins/key-object.js";
|
||||
import { lowerCryptoKeyInstanceof } from "../builtins/webcrypto.js";
|
||||
import { lowerNetAddressInstanceof } from "../builtins/net-address.js";
|
||||
import { fenceEarlyNsMemberRef, nsMemberIdentOf } from "../lower-namespaces.js";
|
||||
import { maybePromiseInstanceOf } from "../maybe-promise.js";
|
||||
import { isSafeToDiscard } from "./evaluation-safety.js";
|
||||
import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js";
|
||||
import { type ClassInfo, storedClassValueType } from "../lower-classes.js";
|
||||
import { classInstanceOf } from "../class-dynamic-dispatch.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
@@ -172,6 +180,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
|
||||
if (cryptoKey) return cryptoKey;
|
||||
const netAddress = lowerNetAddressInstanceof(lowerer, expr);
|
||||
if (netAddress) return netAddress;
|
||||
const readlineInterface = lowerReadlineInstanceof(lowerer, expr);
|
||||
if (readlineInterface) return readlineInterface;
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr.right, "SharedArrayBuffer")) {
|
||||
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
|
||||
@@ -1038,6 +1048,76 @@ function lowerPrivateIn(
|
||||
);
|
||||
}
|
||||
}
|
||||
// A checked value in JavaScript (`#x in this` inside a method, whose
|
||||
// receiver is the actual call-site value): the brand is present exactly
|
||||
// when the value is a class instance at or below the declarer. A typed
|
||||
// reference carries the class it was boxed under: the declarer or a
|
||||
// descendant answers true; an ancestor answers per instance.
|
||||
if (recv.type.kind === "dyn" && recv.kind === "varRef" && isJsSourceFile(expr.getSourceFile())) {
|
||||
// A function-local class has one brand per evaluation.
|
||||
if (info.localClass) {
|
||||
const owner = localPrivateBrandClass(lowerer, priv);
|
||||
if (!owner) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
`'${pname} in …' inside a function-local class that is not a named declaration`,
|
||||
);
|
||||
}
|
||||
return checkedPrivateIn(lowerer, owner.info, owner.decl, pname, recv, loc);
|
||||
}
|
||||
const typedRefIs = (className: string): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.typedRefIs",
|
||||
args: [
|
||||
recv,
|
||||
{ kind: "strLit", value: typeKey({ kind: "object", className }), type: STRING, loc },
|
||||
],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const or = (left: IrExpr | null, right: IrExpr): IrExpr =>
|
||||
left === null ? right : { kind: "logical", op: "||", left, right, type: BOOL, loc };
|
||||
let test: IrExpr | null = null;
|
||||
const below = (c: ClassInfo): void => {
|
||||
test = or(test, typedRefIs(c.def.name));
|
||||
for (const sub of c.subclasses) below(sub);
|
||||
};
|
||||
below(info);
|
||||
for (let base = info.base; base; base = base.base) {
|
||||
const baseType: IrType = { kind: "object", className: base.def.name };
|
||||
test = or(test, {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: typedRefIs(base.def.name),
|
||||
right: {
|
||||
kind: "instanceOf",
|
||||
value: { kind: "dynCheck", value: recv, type: baseType, loc },
|
||||
className: declName,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
// Only objects carry private names: a primitive operand throws Node's
|
||||
// TypeError.
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: primitiveOperand(recv, loc),
|
||||
then: {
|
||||
kind: "libCall",
|
||||
fn: "error.nodeThrow",
|
||||
args: [numLit(1, loc), strLit("", loc), privateInMessage(pname, recv, loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
else_: test!,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (recv.type.kind !== "object") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1349,13 +1429,14 @@ export function lowerInExpression(
|
||||
// arm answers membership STATICALLY (a declared non-optional field is
|
||||
// always present, an undeclared name never is), so the whole test
|
||||
// collapses to runtime TAG tests — tsc's own narrowing then types the
|
||||
// branches (the discriminating-`in` idiom). Optional (undefined-armed)
|
||||
// fields and tuple/index-signature arms answer per VALUE, not per arm
|
||||
// — those unions keep the fence below.
|
||||
// branches (the discriminating-`in` idiom). An arm declaring the key
|
||||
// as an optional (undefined-armed) slot answers per VALUE instead:
|
||||
// its tag test joins that payload's presence test (see
|
||||
// perValueUnionIn). Tuple/index-signature arms keep the fence below.
|
||||
if (recv.type.kind === "union") {
|
||||
const unionId = recv.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms ?? [];
|
||||
const answers: { tag: number; has: boolean }[] = [];
|
||||
const answers: { tag: number; has: boolean | "slot"; arm: IrType }[] = [];
|
||||
let staticAnswers = arms.length > 0;
|
||||
for (const arm of arms) {
|
||||
const tag = lowerer.armTag(unionId, arm);
|
||||
@@ -1372,14 +1453,17 @@ export function lowerInExpression(
|
||||
f.type.kind === "union" &&
|
||||
lowerer.armTag(f.type.unionId, UNDEFINED_T) >= 0
|
||||
) {
|
||||
staticAnswers = false; // an optional slot: presence is per-value
|
||||
break;
|
||||
answers.push({ tag, has: "slot", arm }); // presence is per-value
|
||||
continue;
|
||||
}
|
||||
// Accessor properties are own properties to `in` (Node answers
|
||||
// true without invoking the getter) — either slot present makes
|
||||
// the name a member.
|
||||
const acc = shape.fields.some((x) => x.name === `%get:${key}` || x.name === `%set:${key}`);
|
||||
answers.push({ tag, has: f !== undefined || acc || inherited });
|
||||
answers.push({ tag, has: f !== undefined || acc || inherited, arm });
|
||||
}
|
||||
if (staticAnswers && answers.some((a) => a.has === "slot")) {
|
||||
return perValueUnionIn(lowerer, recv, unionId, key, answers, loc);
|
||||
}
|
||||
if (staticAnswers) {
|
||||
const pureRecv =
|
||||
@@ -1531,6 +1615,75 @@ export function lowerInExpression(
|
||||
);
|
||||
}
|
||||
|
||||
/** `"k" in u` over a union of fixed record shapes where some arm declares
|
||||
* `k` as an optional slot. Arms without the name answer false and arms
|
||||
* with an always-present member answer true, both by tag; an optional
|
||||
* slot answers its payload's own-property presence (an explicit
|
||||
* undefined is present, an omitted or deleted property is not), read
|
||||
* through the checked arm extraction the tag test has just proven. The
|
||||
* receiver evaluates once: a non-repeatable one is stabilized in a
|
||||
* hidden local first. */
|
||||
function perValueUnionIn(
|
||||
lowerer: Lowerer,
|
||||
recv: IrExpr,
|
||||
unionId: string,
|
||||
key: string,
|
||||
answers: readonly { tag: number; has: boolean | "slot"; arm: IrType }[],
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const recvType = recv.type;
|
||||
const stable = isSafeToRepeat(recv) ? null : lowerer.declareHiddenLocal("%inRecv", recvType);
|
||||
const ref = (): IrExpr => (stable ? varRef(stable.id, recvType, loc) : recv);
|
||||
const isTag = (tag: number): IrExpr => ({
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value: ref(),
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
let out: IrExpr | null = null;
|
||||
for (const a of answers) {
|
||||
if (a.has === false) continue;
|
||||
let test = isTag(a.tag);
|
||||
if (a.has === "slot" && a.arm.kind === "record") {
|
||||
test = {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: test,
|
||||
right: {
|
||||
kind: "recordHas",
|
||||
obj: {
|
||||
kind: "unionNarrow",
|
||||
unionId,
|
||||
tag: a.tag,
|
||||
value: ref(),
|
||||
type: a.arm,
|
||||
loc,
|
||||
},
|
||||
shapeId: a.arm.shapeId,
|
||||
field: key,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
out = out ? { kind: "logical", op: "||", left: out, right: test, type: BOOL, loc } : test;
|
||||
}
|
||||
const result: IrExpr = out ?? { kind: "boolLit", value: false, type: BOOL, loc };
|
||||
if (!stable) return result;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: stable.id, init: recv, loc }],
|
||||
result,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The runtime-key `in` (see lowerInExpression): `k in r` where k is a
|
||||
* runtime string and r an index-signature record — an interned
|
||||
* `%rec.haskey.<n>(k, r)` walks the declared names (a string-equality
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerWebAssemblyTypeof } from "../builtins/webassembly.js";
|
||||
import { isNavigatorGlobal } from "../builtins/navigator.js";
|
||||
import { processHasIpc } from "../builtins/process.js";
|
||||
import { lowerPerfHooksTypeof } from "../builtins/performance.js";
|
||||
import { isNodeModuleValue } from "../lower-node-module.js";
|
||||
import { builtinClassRefOf } from "../builtins/class-values.js";
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
typeofAnswer,
|
||||
} from "../lower-exprs.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { isJsSourceFile } from "../../program.js";
|
||||
import { isJsSourceFile, isNodeEsmFile } from "../../program.js";
|
||||
import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js";
|
||||
@@ -38,8 +39,41 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
|
||||
if (webAssembly) return webAssembly;
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "process"))
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
{
|
||||
let operand = expr.expression;
|
||||
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
|
||||
const ipcMember =
|
||||
ts.isPropertyAccessExpression(operand) && !operand.questionDotToken
|
||||
? lowerer.stdlibGlobalMember(operand, "process")
|
||||
: null;
|
||||
if (ipcMember === "send" || ipcMember === "disconnect") {
|
||||
return {
|
||||
kind: "ternary",
|
||||
cond: processHasIpc(lowerer, loc),
|
||||
then: { kind: "strLit", value: "function", type: STRING, loc },
|
||||
else_: { kind: "strLit", value: "undefined", type: STRING, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (lowerer.isStdlibGlobal(expr.expression, "fetch"))
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
// `__dirname` / `__filename` in a JavaScript ES module, where Node never
|
||||
// defines them: typeof answers "undefined" instead of throwing.
|
||||
{
|
||||
let operand = expr.expression;
|
||||
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
|
||||
if (
|
||||
ts.isIdentifier(operand) &&
|
||||
(operand.text === "__dirname" || operand.text === "__filename") &&
|
||||
lowerer.isStdlibGlobal(operand, operand.text)
|
||||
) {
|
||||
const sf = operand.getSourceFile();
|
||||
if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName))
|
||||
return { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
}
|
||||
}
|
||||
if (isNavigatorGlobal(lowerer, expr.expression))
|
||||
return { kind: "strLit", value: "object", type: STRING, loc };
|
||||
const performance = lowerPerfHooksTypeof(lowerer, expr.expression);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* Increment, decrement and prefix unary operators, and compound field updates. */
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { isStrictCode } from "../strict-mode.js";
|
||||
import { isPrivateWriteReceiver } from "../private-brands.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import {
|
||||
type FieldTarget,
|
||||
@@ -524,6 +526,19 @@ export function fieldSetStmt(
|
||||
loc: SrcLoc,
|
||||
blame: ts.Node,
|
||||
): IrStmt {
|
||||
// A write through a brand-checked receiver (checkedPrivateReceiver): the
|
||||
// value evaluates first, then the brand check throws, as in Node.
|
||||
if (isPrivateWriteReceiver(target.obj) && value.kind !== "varRef") {
|
||||
const stored = lowerer.declareHiddenLocal("%privateWriteValue", value.type);
|
||||
return {
|
||||
kind: "block",
|
||||
body: [
|
||||
{ kind: "varDecl", localId: stored.id, init: value, loc },
|
||||
fieldSetStmt(lowerer, target, varRef(stored.id, stored.type, loc), loc, blame),
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
// A member write through an unassigned deferred-init field: the value
|
||||
// still evaluates first, then the write throws Node's TypeError.
|
||||
const deferredSlot =
|
||||
@@ -737,6 +752,7 @@ export function fieldSetStmt(
|
||||
className: target.className,
|
||||
field: target.field,
|
||||
value,
|
||||
...(isStrictCode(blame) ? {} : { sloppy: true as const }),
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -224,12 +224,30 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
const loops: number[] = [];
|
||||
let loopIds = 0;
|
||||
const loopKey = (): string => loops.join("/");
|
||||
/** Bindings initialized by another binding's reference (`const view =
|
||||
* value`, the snapshot a union-receiver write takes), so writes through
|
||||
* either count as writes through the one object. */
|
||||
const aliases = new Map<string, { target: string; fn: string }>();
|
||||
const bindingOf = (obj: IrExpr): string | null => {
|
||||
let o = obj;
|
||||
while (o.kind === "unionNarrow" || o.kind === "downcast") o = o.value;
|
||||
if (o.kind !== "varRef") return null;
|
||||
let id = o.localId;
|
||||
const seen = new Set<string>();
|
||||
for (;;) {
|
||||
const alias = aliases.get(id);
|
||||
if (!alias || alias.fn !== fnName || inits.get(id)?.count !== 1 || seen.has(id)) return id;
|
||||
seen.add(id);
|
||||
id = alias.target;
|
||||
}
|
||||
};
|
||||
const noteInit = (id: string, init: IrExpr): void => {
|
||||
const previous = inits.get(id);
|
||||
if (previous !== undefined) {
|
||||
previous.count++;
|
||||
return;
|
||||
}
|
||||
if (init.kind === "varRef") aliases.set(id, { target: init.localId, fn: fnName });
|
||||
const fresh =
|
||||
init.kind === "recordLit" ||
|
||||
init.kind === "recordClone" ||
|
||||
@@ -315,7 +333,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
|
||||
field: st.field,
|
||||
kind: st.value.kind === "fieldAbsent" ? "delete" : "write",
|
||||
fn: fnName,
|
||||
binding: st.obj.kind === "varRef" ? st.obj.localId : null,
|
||||
binding: bindingOf(st.obj),
|
||||
loop: loopKey(),
|
||||
pos,
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { cjsExportCallIgnoresThis, cjsInFileExportGlobalOf } from "./cjs-exports-object.js";
|
||||
import { lowerGenMethodCall } from "./lower-generators.js";
|
||||
import { lowerClassSymbolMethodCall } from "./symbol-methods.js";
|
||||
import { lowerReadlineDisposeCall } from "./builtins/readline.js";
|
||||
import { errorToStringCall } from "./error-methods.js";
|
||||
import { lowerClassPrototypeAssign, lowerClassPrototypeDescriptors } from "./class-prototypes.js";
|
||||
import {
|
||||
@@ -226,6 +227,7 @@ import {
|
||||
} from "./optional-arguments.js";
|
||||
import { fenceSymbolFieldCopy } from "./symbol-fields.js";
|
||||
import { lowerClassDataDescriptor, lowerClassDescriptorRead } from "./class-descriptors.js";
|
||||
import { lowerClassFieldDefine, lowerClassIntegrity } from "./class-attributes.js";
|
||||
import { classStaticDataFor } from "./class-static-data.js";
|
||||
import { lowerUrlFactory } from "./lower-url.js";
|
||||
import { nullishValueUnitOf } from "./binding-analysis.js";
|
||||
@@ -1683,6 +1685,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
if (timersDispose) return timersDispose;
|
||||
const symbolMethod = lowerClassSymbolMethodCall(lowerer, expr);
|
||||
if (symbolMethod) return symbolMethod;
|
||||
const readlineDispose = lowerReadlineDisposeCall(lowerer, expr);
|
||||
if (readlineDispose) return readlineDispose;
|
||||
lowerer.fenceUnsupportedFetchConstructorMember(expr.expression);
|
||||
lowerer.fenceStaticResponseMember(expr.expression, "call");
|
||||
lowerer.fenceStaticHeadersMember(expr.expression, "call");
|
||||
@@ -6769,6 +6773,99 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** Whether `Object.assign(instance, record)` can copy every field of the
|
||||
* record shape into a field the class declares with the same type: a plain
|
||||
* record (no tuple, index signature or accessor slots) and a compiled
|
||||
* class whose chain has no builtin or runtime-provided storage and no
|
||||
* accessor for the written names. */
|
||||
function classAssignPlan(lowerer: Lowerer, className: string, srcShapeId: string): boolean {
|
||||
const info = lowerer.classes.get(className);
|
||||
const shape = lowerer.shapes.get(srcShapeId);
|
||||
if (!info || !shape || shape.tuple || shape.indexValue || shapeHasAccessorSlots(shape))
|
||||
return false;
|
||||
for (let c: ClassInfo | null = info; c; c = c.base) {
|
||||
if (c.builtinError || c.builtinEmitter || c.builtinStream !== undefined || c.def.runtime)
|
||||
return false;
|
||||
}
|
||||
return shape.fields.every((f) => {
|
||||
const declared = info.fields.get(f.name);
|
||||
return (
|
||||
declared !== undefined &&
|
||||
(typeEquals(declared, f.type) || lowerer.widthLiftPlan(f.type, declared) !== null) &&
|
||||
!lowerer.findMethodOn(info, f.name) &&
|
||||
!lowerer.findMethodOn(info, `get:${f.name}`) &&
|
||||
!lowerer.findMethodOn(info, `set:${f.name}`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** Interned `%obj.assignClass.<n>(o, s)` — Object.assign's per-field
|
||||
* write of a record into a class instance (classAssignPlan's gate):
|
||||
* optional source fields write only when present, and the instance
|
||||
* returns. */
|
||||
function classAssignHelper(
|
||||
lowerer: Lowerer,
|
||||
className: string,
|
||||
srcShapeId: string,
|
||||
loc: SrcLoc,
|
||||
): string | null {
|
||||
if (!classAssignPlan(lowerer, className, srcShapeId)) return null;
|
||||
const key = `obj.assignClass:${className}:${srcShapeId}`;
|
||||
const existing = lowerer.arrHofHelpers.get(key);
|
||||
if (existing) return existing;
|
||||
const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, helper);
|
||||
const sShape = lowerer.shapes.get(srcShapeId)!;
|
||||
const oT: IrType = { kind: "object", className };
|
||||
const sT: IrType = { kind: "record", shapeId: srcShapeId };
|
||||
const oRef: IrExpr = { kind: "varRef", localId: "o.0", type: oT, loc };
|
||||
const sRef: IrExpr = { kind: "varRef", localId: "s.0", type: sT, loc };
|
||||
const body: IrStmt[] = [];
|
||||
const info = lowerer.classes.get(className)!;
|
||||
for (const f of sShape.fields) {
|
||||
const declared = info.fields.get(f.name)!;
|
||||
const read: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj: sRef,
|
||||
shapeId: srcShapeId,
|
||||
field: f.name,
|
||||
type: f.type,
|
||||
loc,
|
||||
};
|
||||
const lift = typeEquals(declared, f.type) ? null : lowerer.widthLiftPlan(f.type, declared);
|
||||
const set: IrStmt = {
|
||||
kind: "fieldSet",
|
||||
obj: oRef,
|
||||
className,
|
||||
field: f.name,
|
||||
value: lift === null ? read : lowerer.applyWidthLift(lift, read, declared, loc),
|
||||
loc,
|
||||
};
|
||||
const present = lowerer.recordFieldPresent(sRef, srcShapeId, f.name, loc);
|
||||
body.push(
|
||||
present.kind === "recordHas"
|
||||
? { kind: "if", cond: present, then: [set], else_: null, loc }
|
||||
: set,
|
||||
);
|
||||
}
|
||||
body.push({ kind: "return", value: oRef, loc });
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [
|
||||
{ localId: "o.0", name: "o", type: oT },
|
||||
{ localId: "s.0", name: "s", type: sT },
|
||||
],
|
||||
returnType: oT,
|
||||
locals: [
|
||||
{ id: "o.0", name: "o", type: oT, mutable: true },
|
||||
{ id: "s.0", name: "s", type: sT, mutable: true },
|
||||
],
|
||||
body,
|
||||
loc,
|
||||
});
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** Interned `%obj.assign.<n>(t, s)` — Object.assign's per-field copy
|
||||
* over signature-free records (every source field lands on a same-named,
|
||||
* same-typed target field — the caller's gate): optional source fields
|
||||
@@ -7410,6 +7507,28 @@ function lowerObjectStaticCall(
|
||||
return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc };
|
||||
}
|
||||
}
|
||||
// A CLASS-INSTANCE target and a plain record source whose fields are
|
||||
// all fields the class declares with the same types
|
||||
// (`Object.assign(this, { retries: 3 })`): the per-field write into
|
||||
// the instance, returning it — the instance mutates in place.
|
||||
if (tProbe?.type.kind === "object" && sProbe?.type.kind === "record") {
|
||||
const helper = classAssignPlan(lowerer, tProbe.type.className, sProbe.type.shapeId);
|
||||
if (helper) {
|
||||
const loc = locOf(call);
|
||||
const target = lowerer.lowerExpr(call.arguments.nodes[0]!);
|
||||
const source = lowerer.lowerExpr(call.arguments.nodes[1]!);
|
||||
if (target.type.kind === "object" && source.type.kind === "record") {
|
||||
const name = classAssignHelper(
|
||||
lowerer,
|
||||
target.type.className,
|
||||
source.type.shapeId,
|
||||
loc,
|
||||
);
|
||||
if (name)
|
||||
return { kind: "call", callee: name, args: [target, source], type: target.type, loc };
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tProbe?.type.kind === "record" && sProbe?.type.kind === "record") {
|
||||
const tShape = lowerer.shapes.get(tProbe.type.shapeId);
|
||||
const sShape = lowerer.shapes.get(sProbe.type.shapeId);
|
||||
@@ -7500,6 +7619,25 @@ function lowerObjectStaticCall(
|
||||
const srcNode = spread ? argNode.expression : argNode;
|
||||
const rawSource = lowerer.lowerExpr(srcNode);
|
||||
fenceSymbolFieldCopy(lowerer, srcNode, rawSource.type, spread);
|
||||
// A class instance copied into a fresh object
|
||||
// (`Object.assign({}, instance)`): the result is a plain
|
||||
// object holding the instance's own fields, never an
|
||||
// instance, so a result the checker types as the class has
|
||||
// no representation (the checked cast would throw).
|
||||
if (
|
||||
rawSource.type.kind === "object" &&
|
||||
freshLiteralTarget &&
|
||||
!ts.isExpressionStatement(call.parent)
|
||||
) {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (resultT !== null && resultT.kind === "object") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
srcNode,
|
||||
`Object.assign copying a '${lowerer.fmt(rawSource.type)}' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const src = lowerer.coerceToExpected(rawSource, DYN);
|
||||
if (src.type.kind !== "dyn") {
|
||||
ok = false;
|
||||
@@ -7580,6 +7718,8 @@ function lowerObjectStaticCall(
|
||||
if (target && declaredTarget && isDynTypedRefType(declaredTarget))
|
||||
target = lowerer.coerceToExpected(target, declaredTarget);
|
||||
if (target && member === "defineProperty") {
|
||||
const field = lowerClassFieldDefine(lowerer, call, target);
|
||||
if (field) return field;
|
||||
const native = lowerClassDataDescriptor(lowerer, call, member, target);
|
||||
if (native) return native;
|
||||
}
|
||||
@@ -7722,6 +7862,19 @@ function lowerObjectStaticCall(
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (member === "preventExtensions" || member === "seal") {
|
||||
const integrity = lowerClassIntegrity(lowerer, call, member, value);
|
||||
if (integrity) return integrity;
|
||||
} else if (isDynTypedRefType(value.type)) {
|
||||
// The instance's checked view carries its integrity state.
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: member === "isSealed" ? "dyn.isSealed" : "dyn.isExtensible",
|
||||
args: [lowerer.coerceToExpected(value, DYN)],
|
||||
type: BOOL,
|
||||
loc: locOf(call),
|
||||
};
|
||||
}
|
||||
if (value.type.kind === "dyn") {
|
||||
const operation = {
|
||||
preventExtensions: "dyn.preventExtensions",
|
||||
@@ -7792,6 +7945,8 @@ function lowerObjectStaticCall(
|
||||
)
|
||||
)
|
||||
return value;
|
||||
const integrity = lowerClassIntegrity(lowerer, call, "freeze", value);
|
||||
if (integrity) return integrity;
|
||||
lowerer.noLowering(
|
||||
"Object.freeze of a possibly-aliased value",
|
||||
call,
|
||||
|
||||
@@ -88,7 +88,6 @@ import {
|
||||
isCjsJsFile,
|
||||
isJsSourceFile,
|
||||
isModuleExportsAccess,
|
||||
isNodeTypesPath,
|
||||
locOf,
|
||||
} from "../program.js";
|
||||
import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js";
|
||||
@@ -1046,9 +1045,11 @@ export function builtinStreamInfoOf(
|
||||
if (rec.lib === symbol.name) irName = name;
|
||||
}
|
||||
if (!irName) return null;
|
||||
// Both the bundled declarations and @types/node declare the classes
|
||||
// inside `declare module "stream"` (@types/node nests them in the module's
|
||||
// `internal` namespace).
|
||||
const declared = symbol.declarations.some((d) => {
|
||||
if (!ts.isClassDeclaration(d)) return false;
|
||||
if (isNodeTypesPath(d.getSourceFile().fileName)) return false;
|
||||
let node: ts.Node | undefined = d.parent;
|
||||
while (node) {
|
||||
if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
|
||||
@@ -3368,6 +3369,56 @@ export function collectClassShapeInner(
|
||||
.map((p): ParamShape => ({ type: p.type, mode: "omittable" })),
|
||||
);
|
||||
}
|
||||
// A TypeScript override of a JavaScript method (`class T extends
|
||||
// Agent` over an npm-static package) keeps the base's checked-value
|
||||
// ABI: each untyped base slot stays dyn, and the override's typed
|
||||
// parameter narrows from it at entry with a checked conversion. An
|
||||
// untyped base return takes the override's value boxed. Calls on
|
||||
// a statically typed receiver see the same ABI.
|
||||
if (
|
||||
overridden &&
|
||||
!isJsSourceFile(member.getSourceFile()) &&
|
||||
overridden.declarer.decl !== null &&
|
||||
isJsSourceFile(overridden.declarer.decl.getSourceFile()) &&
|
||||
member.asteriskToken === undefined &&
|
||||
!overridden.sig.async &&
|
||||
!overridden.sig.gen &&
|
||||
shapes.length <= overridden.sig.params.length &&
|
||||
shapes.every((p, i) => {
|
||||
const inherited = overridden.sig.params[i]!;
|
||||
return (
|
||||
(p.mode === "required" || p.mode === "omittable") &&
|
||||
p.bodyType === undefined &&
|
||||
(inherited.mode === "required" || inherited.mode === "omittable") &&
|
||||
(typeEquals(p.type, inherited.type) ||
|
||||
(inherited.type.kind === "dyn" && lowerer.dynConvertible(p.type)))
|
||||
);
|
||||
}) &&
|
||||
overridden.sig.params
|
||||
.slice(shapes.length)
|
||||
.every(
|
||||
(p) => p.type.kind === "dyn" && (p.mode === "required" || p.mode === "omittable"),
|
||||
)
|
||||
) {
|
||||
for (let i = 0; i < shapes.length; i++) {
|
||||
const own = shapes[i]!;
|
||||
if (overridden.sig.params[i]!.type.kind === "dyn" && own.type.kind !== "dyn")
|
||||
shapes[i] = { type: DYN, mode: own.mode, entryNarrow: own.type };
|
||||
}
|
||||
shapes.push(
|
||||
...overridden.sig.params
|
||||
.slice(shapes.length)
|
||||
.map((p): ParamShape => ({ type: p.type, mode: "omittable" })),
|
||||
);
|
||||
if (
|
||||
overridden.sig.ret.kind === "dyn" &&
|
||||
ft.ret.kind !== "dyn" &&
|
||||
ft.ret.kind !== "void" &&
|
||||
lowerer.dynConvertible(ft.ret)
|
||||
)
|
||||
ft.ret = DYN;
|
||||
ft.params = shapes.map((shape) => shape.type);
|
||||
}
|
||||
if (
|
||||
overridden &&
|
||||
member.asteriskToken === undefined &&
|
||||
@@ -5412,6 +5463,75 @@ function namedEvaluationName(expr: ts.ClassLikeDeclaration): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
/** Each evaluation of a function-local class creates fresh private names,
|
||||
* while the compiled class shares one layout across evaluations. A
|
||||
* JavaScript class's prototype methods are closures per evaluation, so the
|
||||
* names agree when every private member is an instance field and every use
|
||||
* is on `this` (privateNamesOnlyOnThis): native methods only run on their
|
||||
* own evaluation's instances, and the closures, which receive any `this`,
|
||||
* check the receiver's evaluation against the class's own binding
|
||||
* (private-brands.ts), so the class must be a named declaration (undici's
|
||||
* webidl FastIterableIterator). The checker rejects writes to that binding. */
|
||||
function localPrivateFieldsAgree(expr: ts.ClassLikeDeclaration): boolean {
|
||||
if (!isJsSourceFile(expr.getSourceFile()) || !ts.isClassDeclaration(expr) || !expr.name)
|
||||
return false;
|
||||
if (
|
||||
expr.members.nodes.some(
|
||||
(member) =>
|
||||
member.name !== undefined &&
|
||||
ts.isPrivateIdentifier(member.name) &&
|
||||
!ts.isPropertyDeclaration(member),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
return privateNamesOnlyOnThis(expr);
|
||||
}
|
||||
|
||||
/** Every private name is used only on `this` inside the class's own instance
|
||||
* members (`this.#x`, `#x in this`), outside nested classes and non-arrow
|
||||
* functions, and the class has no static members that could hand instances
|
||||
* across evaluations. */
|
||||
function privateNamesOnlyOnThis(expr: ts.ClassLikeDeclaration): boolean {
|
||||
let ok = true;
|
||||
for (const member of expr.members.nodes) {
|
||||
if (ts.getModifiers(member)?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) return false;
|
||||
ts.walkPreorder(member, (node) => {
|
||||
if (!ok) return "stop";
|
||||
// A nested class or non-arrow function rebinds `this`: any private
|
||||
// name inside it disqualifies.
|
||||
if (
|
||||
node !== member &&
|
||||
(ts.isClassDeclaration(node) ||
|
||||
ts.isClassExpression(node) ||
|
||||
ts.isFunctionExpression(node) ||
|
||||
ts.isFunctionDeclaration(node))
|
||||
) {
|
||||
ts.walkPreorder(node, (inner) => {
|
||||
if (ts.isPrivateIdentifier(inner)) ok = false;
|
||||
return ok ? undefined : "stop";
|
||||
});
|
||||
return "skip";
|
||||
}
|
||||
if (ts.isPrivateIdentifier(node)) {
|
||||
const parent = node.parent;
|
||||
const onThis =
|
||||
parent === member ||
|
||||
(ts.isPropertyAccessExpression(parent) &&
|
||||
parent.name === node &&
|
||||
parent.expression.kind === ts.SyntaxKind.ThisKeyword) ||
|
||||
(ts.isBinaryExpression(parent) &&
|
||||
parent.left === node &&
|
||||
parent.operatorToken.kind === ts.SyntaxKind.InKeyword &&
|
||||
parent.right.kind === ts.SyntaxKind.ThisKeyword);
|
||||
if (!onThis) ok = false;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
if (!ok) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Collect a class expression once per enclosing specialization. Function-local
|
||||
* evaluations allocate fresh constructor objects; top-level expressions use
|
||||
* immortal templates. Static initialization remains limited to whole top-level
|
||||
@@ -5449,7 +5569,8 @@ export function lowerClassExpressionInfo(
|
||||
}
|
||||
if (
|
||||
local &&
|
||||
expr.members.nodes.some((member) => member.name && ts.isPrivateIdentifier(member.name))
|
||||
expr.members.nodes.some((member) => member.name && ts.isPrivateIdentifier(member.name)) &&
|
||||
!localPrivateFieldsAgree(expr)
|
||||
) {
|
||||
lowerer.unsupported("SC1090", expr, "function-local classes with private brands");
|
||||
}
|
||||
@@ -7942,6 +8063,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
|
||||
className: info.def.name,
|
||||
field: f.name,
|
||||
value: dynUndefinedExpr(loc),
|
||||
define: true,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
@@ -7957,6 +8079,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
|
||||
className: info.def.name,
|
||||
field: f.name,
|
||||
value,
|
||||
define: true,
|
||||
loc: locOf(f.initializer),
|
||||
});
|
||||
} catch (e) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* ToBoolean/ToString coercion helpers, and field/element reads and writes
|
||||
* (FieldTarget). */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { checkedPrivateReceiver, localPrivateBrandClass } from "./private-brands.js";
|
||||
import { constituentTypes } from "../ts7/checker.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { lowerMaybePromiseAwait, maybePromiseUnionOf } from "./maybe-promise.js";
|
||||
@@ -458,6 +459,20 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const local = lowerer.resolveThis();
|
||||
if (local) {
|
||||
const value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc };
|
||||
// `this.#x` in a function-local class's prototype method, whose `this`
|
||||
// is the call-site value: the field lives on the native instance of
|
||||
// this evaluation (checkedPrivateReceiver).
|
||||
const access = expr.parent;
|
||||
if (
|
||||
local.type.kind === "dyn" &&
|
||||
ts.isPropertyAccessExpression(access) &&
|
||||
access.expression === expr &&
|
||||
ts.isPrivateIdentifier(access.name)
|
||||
) {
|
||||
const owner = localPrivateBrandClass(lowerer, access.name);
|
||||
if (owner)
|
||||
return checkedPrivateReceiver(lowerer, owner.info, owner.decl, access, value, loc);
|
||||
}
|
||||
const initialized = lowerer.ctx.superInitialized;
|
||||
if (initialized)
|
||||
return {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
type SrcLoc,
|
||||
canConvertToDyn,
|
||||
canDynCheckTo,
|
||||
CLASS_FIELD_ATTRIBUTES_PREFIX,
|
||||
classDynViewSupported,
|
||||
DYN_CLASS_PROPERTIES,
|
||||
isClassOwnEnumerableFieldName,
|
||||
@@ -500,12 +501,16 @@ function classInspectsThroughView(lowerer: Lowerer, info: ClassInfo | undefined)
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a class holds properties added outside its declared layout, which
|
||||
* only its checked-dynamic view can list. Decided after lowering, when the
|
||||
/** Whether a class holds properties added outside its declared layout, or
|
||||
* fields Object.defineProperty can make non-enumerable, which only its
|
||||
* checked-dynamic view lists as Node does. Decided after lowering, when the
|
||||
* class has its final fields. */
|
||||
function classInspectsAddedProperties(lowerer: Lowerer, info: ClassInfo): boolean {
|
||||
return (
|
||||
info.def.fields.some((field) => field.name === DYN_CLASS_PROPERTIES) &&
|
||||
info.def.fields.some(
|
||||
(field) =>
|
||||
field.name === DYN_CLASS_PROPERTIES || field.name.startsWith(CLASS_FIELD_ATTRIBUTES_PREFIX),
|
||||
) &&
|
||||
classDynViewSupported(
|
||||
info.def.fields,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
@@ -2011,6 +2016,47 @@ function formatOArg(lowerer: Lowerer, node: ts.Expression, depth: number, loc: S
|
||||
return inspectExpr(lowerer, value.type, value, numLit(0, loc), numLit(depth, loc), loc);
|
||||
}
|
||||
|
||||
/** %d and %i over a checked-dynamic argument, as Node's formatWithOptions
|
||||
* reads it: a bigint prints as `${n}n`, a symbol as NaN, and any other
|
||||
* value as `number` (Number(arg) for %d, parseInt(String(arg)) for %i)
|
||||
* formatted. The argument evaluates once. */
|
||||
function formatDynInteger(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
number: (held: IrExpr) => IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const held = lowerer.declareHiddenLocal("%formatArg", DYN);
|
||||
const ref = (): IrExpr => varRef(held.id, DYN, loc);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: held.id, init: value, loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "bigint", value: ref(), type: BOOL, loc },
|
||||
then: {
|
||||
kind: "strConcat",
|
||||
left: { kind: "libCall", fn: "dyn.toStringCoerce", args: [ref()], type: STRING, loc },
|
||||
right: strLit("n", loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
else_: {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test: "symbol", value: ref(), type: BOOL, loc },
|
||||
then: strLit("NaN", loc),
|
||||
else_: { kind: "libCall", fn: "insp.f64", args: [number(ref())], type: STRING, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** %d and %i over the argument types Node formats without a number
|
||||
* conversion: a bigint prints as `${n}n` (formatBigInt) and a symbol as NaN;
|
||||
* null is Number(null) (0) under %d and parseInt("null") (NaN) under %i, and
|
||||
@@ -2141,19 +2187,20 @@ export function lowerFormatCall(
|
||||
};
|
||||
}
|
||||
// A checked-dynamic argument (`console.log('... actual %d.',
|
||||
// context.actual)` — test/common's exit report): VALIDATE it as a
|
||||
// number (dynCheck) and format. A non-number dyn value throws the
|
||||
// catchable TypeError where Node would print its ToNumber (NaN
|
||||
// for objects) — it throws instead of returning a wrong result
|
||||
// (SEMANTICS.md).
|
||||
// context.actual)` — test/common's exit report): Number(arg).
|
||||
if (value.type.kind === "dyn") {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "insp.f64",
|
||||
args: [{ kind: "dynCheck", value, type: F64, loc }],
|
||||
type: STRING,
|
||||
return formatDynInteger(
|
||||
lowerer,
|
||||
value,
|
||||
(held) => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.numberConstructor",
|
||||
args: [held],
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
loc,
|
||||
};
|
||||
);
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`util.format %d of '${lowerer.fmt(value.type)}' values`,
|
||||
@@ -2185,6 +2232,24 @@ export function lowerFormatCall(
|
||||
};
|
||||
return { kind: "libCall", fn: "insp.f64", args: [parsed], type: STRING, loc };
|
||||
}
|
||||
// parseInt(String(arg)) over a checked-dynamic argument.
|
||||
if (value.type.kind === "dyn") {
|
||||
return formatDynInteger(
|
||||
lowerer,
|
||||
value,
|
||||
(held) => ({
|
||||
kind: "libCall",
|
||||
fn: "num.parseInt",
|
||||
args: [
|
||||
{ kind: "libCall", fn: "dyn.toStringCoerce", args: [held], type: STRING, loc },
|
||||
numLit(0, loc),
|
||||
],
|
||||
type: F64,
|
||||
loc,
|
||||
}),
|
||||
loc,
|
||||
);
|
||||
}
|
||||
lowerer.noLowering(`util.format %i of '${lowerer.fmt(value.type)}' values`, node);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,12 @@ function mockWrapperFactory(
|
||||
const { fn, call, calls, once, onceEntry, result, args } = mockContextFields(lowerer, ctxType)!;
|
||||
const impl = `${name}.impl`;
|
||||
const ctx = () => varRef("ctx.0", ctxType, loc);
|
||||
// A checked-dynamic rest function (mock.fn()'s default type) takes its
|
||||
// arguments as one trailing dyn array, recorded as the call's arguments
|
||||
// and spread into the implementation.
|
||||
const dynRest = fn.rest === true && fn.restAbi === undefined;
|
||||
const params = fn.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type }));
|
||||
if (dynRest) params.push({ localId: "rest.0", name: "args", type: DYN });
|
||||
const paramRefs = () => params.map((p) => varRef(p.localId, p.type, loc));
|
||||
const argumentsValue = (): IrExpr =>
|
||||
fn.rest
|
||||
@@ -219,13 +224,23 @@ function mockWrapperFactory(
|
||||
loc,
|
||||
};
|
||||
};
|
||||
const invoke: IrExpr = {
|
||||
kind: "callValue",
|
||||
callee: varRef("f.0", fn, loc),
|
||||
args: paramRefs(),
|
||||
type: fn.ret,
|
||||
loc,
|
||||
};
|
||||
const invoke: IrExpr = dynRest
|
||||
? {
|
||||
kind: "dynCall",
|
||||
callee: { kind: "dynFrom", value: varRef("f.0", fn, loc), type: DYN, loc },
|
||||
calleeName: "implementation",
|
||||
args: paramRefs(),
|
||||
spreads: [{ arg: 0, what: "args" }],
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "callValue",
|
||||
callee: varRef("f.0", fn, loc),
|
||||
args: paramRefs(),
|
||||
type: fn.ret,
|
||||
loc,
|
||||
};
|
||||
const tryBody: IrStmt[] = voidReturn
|
||||
? [
|
||||
{ kind: "exprStmt", expr: invoke, loc },
|
||||
@@ -629,7 +644,37 @@ function requireUndefinedResultArm(
|
||||
}
|
||||
}
|
||||
|
||||
/** mock.fn(original, implementation?) and t.mock.fn(...). */
|
||||
/** `%mock.noop.N`: mock.fn()'s original, Node's `function () {}` — it
|
||||
* returns undefined. Null when the function type's return type cannot hold
|
||||
* undefined. */
|
||||
function mockNoopOriginal(
|
||||
lowerer: Lowerer,
|
||||
ctxType: IrType,
|
||||
fn: FuncType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const voidReturn = fn.ret.kind === "void" || fn.ret.kind === "undefinedT";
|
||||
const undefinedValue = voidReturn ? null : lowerer.wrappedUndefined(fn.ret, loc);
|
||||
if (!voidReturn && !undefinedValue) return null;
|
||||
const name = liftOnce(lowerer, ctxType, "noop", (name) => {
|
||||
const params = fn.params.map((type, i) => ({ localId: `p${i}.0`, name: `p${i}`, type }));
|
||||
if (fn.rest === true && fn.restAbi === undefined)
|
||||
params.push({ localId: "rest.0", name: "args", type: DYN });
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params,
|
||||
returnType: fn.ret,
|
||||
locals: params.map((p) => local(p.localId, p.name, p.type)),
|
||||
body: [{ kind: "return", value: undefinedValue, loc }],
|
||||
loc,
|
||||
});
|
||||
});
|
||||
return { kind: "closure", fnName: name, captures: [], type: fn, loc };
|
||||
}
|
||||
|
||||
/** mock.fn(original?, implementation?) and t.mock.fn(...). Without an
|
||||
* original, the mock's original and implementation are a function that
|
||||
* returns undefined, as in Node. */
|
||||
export function lowerMockFn(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -658,11 +703,19 @@ export function lowerMockFn(
|
||||
const { fn } = fields;
|
||||
requireUndefinedResultArm(lowerer, fields, call, what);
|
||||
const args = call.arguments.nodes;
|
||||
if (args.length === 0 || args.length > 2 || args.some(ts.isSpreadElement)) {
|
||||
if (args.length > 2 || args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`${what} with ${args.length} arguments`,
|
||||
call,
|
||||
"the supported forms are mock.fn(original) and mock.fn(original, implementation); mock.fn() and mock options have no lowering",
|
||||
"the supported forms are mock.fn(), mock.fn(original) and mock.fn(original, implementation); mock options have no lowering",
|
||||
);
|
||||
}
|
||||
const noop = args.length === 0 ? mockNoopOriginal(lowerer, ctxType!, fn, loc) : null;
|
||||
if (args.length === 0 && !noop) {
|
||||
lowerer.noLowering(
|
||||
`${what}() of a function returning '${lowerer.fmt(fn.ret)}'`,
|
||||
call,
|
||||
"mock.fn() without an original returns undefined: pass an original, or use a function type whose return type includes undefined",
|
||||
);
|
||||
}
|
||||
const stmts: IrStmt[] = [];
|
||||
@@ -670,7 +723,7 @@ export function lowerMockFn(
|
||||
stmts.push({
|
||||
kind: "varDecl",
|
||||
localId: originalLocal.id,
|
||||
init: lowerer.lowerExprExpecting(args[0]!, fn),
|
||||
init: noop ?? lowerer.lowerExprExpecting(args[0]!, fn),
|
||||
loc,
|
||||
});
|
||||
const implementation =
|
||||
|
||||
@@ -2476,6 +2476,20 @@ function lowerNetSocketMethodCall(
|
||||
? value
|
||||
: lowerer.coerceInto(call, value, declared);
|
||||
}
|
||||
if (name === "isSessionReused" && args.length === 0) {
|
||||
// TLSSocket.isSessionReused(): the client never offers a cached
|
||||
// session (no session resumption, a documented TLS divergence), so
|
||||
// no handshake resumes one. A plain socket asserted to be a
|
||||
// TLSSocket answers false too, where Node has no such method.
|
||||
const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: receiver, loc }],
|
||||
result: boolLit(false, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (name === "resetAndDestroy") {
|
||||
// An RST close; a connecting socket resets once it connects. Answers
|
||||
// the socket, Node's chaining.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { declaredMemberOf } from "./expressions/property-access.js";
|
||||
import { hostsAll } from "./expressions/hosted-narrowing.js";
|
||||
import {
|
||||
matchAllRowsReadByElements,
|
||||
storedMatchAllRowsReadByElements,
|
||||
@@ -16,7 +17,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 { processDataMemberValue, programReadsExitCode } from "./builtins/process.js";
|
||||
import { requireInitFlagStmt } from "./require-initialization.js";
|
||||
import { isJsonRequireDecl } from "./json-require.js";
|
||||
import { bindingSource } from "../binding-source.js";
|
||||
@@ -87,11 +88,11 @@ import {
|
||||
isCjsJsFile,
|
||||
isEsModuleStamp,
|
||||
isJsSourceFile,
|
||||
isNodeEsmFile,
|
||||
locOf,
|
||||
requireEarlyReference,
|
||||
requireSpecOf,
|
||||
} from "../program.js";
|
||||
import { isStrictCode } from "./strict-mode.js";
|
||||
import {
|
||||
COMPOUND_ASSIGN_OPS,
|
||||
type CompoundOp,
|
||||
@@ -1477,6 +1478,18 @@ function lowerLabeled(lowerer: Lowerer, stmt: ts.LabeledStatement): IrStmt | IrS
|
||||
return { kind: "block", body, labels, loc: locOf(stmt) };
|
||||
}
|
||||
|
||||
// Value kinds a `throw` converts to a checked-dynamic value, so the catch
|
||||
// binding sees the thrown object rather than a type-erased reference.
|
||||
const THROWN_AS_DYN: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"array",
|
||||
"union",
|
||||
"map",
|
||||
"set",
|
||||
"bytes",
|
||||
"func",
|
||||
"regex",
|
||||
]);
|
||||
|
||||
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
|
||||
if (ts.isClassDeclaration(stmt)) {
|
||||
if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations");
|
||||
@@ -1655,14 +1668,12 @@ export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt
|
||||
value = lowerer.coerceToExpected(value, DYN);
|
||||
} else if (value.type.kind === "record" && lowerer.dynConvertible(value.type)) {
|
||||
value = { kind: "dynFrom", value, liveRef: true, type: DYN, loc: locOf(stmt.expression) };
|
||||
} else if (
|
||||
(value.type.kind === "array" || value.type.kind === "union") &&
|
||||
lowerer.dynConvertible(value.type)
|
||||
) {
|
||||
// Arrays, and unions over records and arrays, keep their identity
|
||||
// and contents the same way: the catch binding, `Array.isArray`,
|
||||
} else if (THROWN_AS_DYN.has(value.type.kind) && lowerer.dynConvertible(value.type)) {
|
||||
// Arrays, Maps, Sets, typed arrays, Buffers, functions, regexes and
|
||||
// unions over them keep their identity and contents the same way:
|
||||
// the catch binding, `typeof`, `instanceof`, `Array.isArray`,
|
||||
// inspection and typed views read the thrown object.
|
||||
value = lowerer.coerceToExpected(value, DYN);
|
||||
value = lowerer.coerceInto(stmt.expression, value, DYN);
|
||||
}
|
||||
if (value.type.kind === "date") {
|
||||
lowerer.unsupported(
|
||||
@@ -5472,6 +5483,23 @@ export function lowerVarDecl(
|
||||
settledType = init.type;
|
||||
}
|
||||
}
|
||||
// `const view = route as Route & { order: number }`: the asserted type
|
||||
// refines the record arms the value is stored in (the assertion keeps
|
||||
// that value, see hosted-narrowing.ts). The binding keeps the stored
|
||||
// type, so writes through it reach the asserted object. An annotation
|
||||
// the stored type hosts (`const view: Route & { order: number } = ...`)
|
||||
// keeps it too: coercing into the annotated layout would copy the
|
||||
// value and read members the writes have not created yet.
|
||||
if (
|
||||
!isLet &&
|
||||
decl.initializer &&
|
||||
isAssertionInitializer(decl.initializer) &&
|
||||
(settledType.kind === "union" || settledType.kind === "record") &&
|
||||
(init.type.kind === "union" || init.type.kind === "record") &&
|
||||
hostsAll(lowerer, init.type, settledType)
|
||||
) {
|
||||
settledType = init.type;
|
||||
}
|
||||
// An exact class behind a generic interface keeps its native
|
||||
// representation so generic calls can specialize against its body.
|
||||
if (
|
||||
@@ -6149,35 +6177,6 @@ export function lowerTry(lowerer: Lowerer, stmt: ts.TryStatement): IrStmt {
|
||||
};
|
||||
}
|
||||
|
||||
function hasUseStrictDirective(statements: readonly ts.Statement[]): boolean {
|
||||
for (const statement of statements) {
|
||||
if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression))
|
||||
return false;
|
||||
if (statement.expression.text === "use strict") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isStrictDelete(node: ts.DeleteExpression): boolean {
|
||||
for (let parent: ts.Node | undefined = node.parent; parent; parent = parent.parent) {
|
||||
if (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) return true;
|
||||
if (
|
||||
ts.isFunctionLike(parent) &&
|
||||
parent.body &&
|
||||
ts.isBlock(parent.body) &&
|
||||
hasUseStrictDirective(parent.body.statements.nodes)
|
||||
)
|
||||
return true;
|
||||
if (ts.isSourceFile(parent)) {
|
||||
return (
|
||||
hasUseStrictDirective(parent.statements.nodes) ||
|
||||
(!isCjsJsFile(parent) && (isNodeEsmFile(parent) || ts.isExternalModule(parent)))
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Statement-position `delete`: process.env keys → process.envUnset
|
||||
* (unsetenv), pure `Record<string, T>` keys → recordKeyDelete (the
|
||||
* overflow Map delete), declared OPTIONAL fields → the field's ABSENT
|
||||
@@ -6293,7 +6292,7 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt
|
||||
obj = lowerer.coerceToExpected(obj, DYN);
|
||||
}
|
||||
if (obj.type.kind === "dyn") {
|
||||
const strict = { kind: "boolLit", value: isStrictDelete(expr), type: BOOL, loc } as const;
|
||||
const strict = { kind: "boolLit", value: isStrictCode(expr), type: BOOL, loc } as const;
|
||||
if (ts.isElementAccessExpression(target)) {
|
||||
const key = lowerer.lowerExprExpecting(target.argumentExpression, DYN);
|
||||
return {
|
||||
@@ -6456,7 +6455,10 @@ function lowerCjsExportStatement(
|
||||
// storage — collectGlobals skipped it; importers re-resolve through
|
||||
// cjsMemberExportClassSymbol): the statement lowers to nothing.
|
||||
if (lowerer.cjsMemberExportClassSymbol(cjs.expr) !== null) {
|
||||
return { kind: "block", body: [], loc };
|
||||
// An inline require still evaluates its module here.
|
||||
const req = inlineProgramRequireOf(lowerer, cjs.value);
|
||||
const init = req !== null ? lowerer.requireInitStmt(req.spec, req.call) : null;
|
||||
return init ?? { kind: "block", body: [], loc };
|
||||
}
|
||||
// The tsc VOID-INIT PREAMBLE (`exports.leaf = void 0;` with the real
|
||||
// `exports.leaf = leaf;` further down — every tsc-emitted dist file
|
||||
@@ -6917,12 +6919,13 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
if (
|
||||
ts.isBinaryExpression(expr) &&
|
||||
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
isCjsExportsIdentifier(lowerer, expr.left)
|
||||
isCjsExportsIdentifier(lowerer, expr.left) &&
|
||||
cjsExportAssignmentOf(stmtNode)?.kind !== "table"
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.left,
|
||||
"rebinding 'exports' (exports = module.exports = value): members attached through 'exports' afterwards are properties of that value, which the static export table does not model — write module.exports.<name> = … and module.exports = value",
|
||||
"rebinding 'exports' without module.exports (exports = value): members attached through 'exports' afterwards are properties of that value, which module.exports does not hold — write exports = module.exports = value",
|
||||
);
|
||||
}
|
||||
const cjs = cjsExportAssignmentOf(stmtNode);
|
||||
@@ -7275,9 +7278,12 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
) {
|
||||
const loc = locOf(expr);
|
||||
const value = lowerer.lowerExprExpecting(expr.right, F64);
|
||||
const fn = programReadsExitCode(lowerer)
|
||||
? "process.setExitCodeTracked"
|
||||
: "process.setExitCode";
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "process.setExitCode", args: [value], type: VOID, loc },
|
||||
expr: { kind: "libCall", fn, args: [value], type: VOID, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
@@ -11224,3 +11230,9 @@ export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclarat
|
||||
requireEarlyReference(lowerer.program, decl) === null
|
||||
);
|
||||
}
|
||||
|
||||
function isAssertionInitializer(node: ts.Expression): boolean {
|
||||
let e = node;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return ts.isAsExpression(e) || ts.isTypeAssertion(e);
|
||||
}
|
||||
|
||||
@@ -999,6 +999,15 @@ export function lowerStreamNew(lowerer: Lowerer, expr: ts.NewExpression, info: C
|
||||
};
|
||||
}
|
||||
|
||||
/** True when a value of Node's argument type `passed` widens implicitly
|
||||
* into the declared parameter type `declared` (one of its union arms, or a
|
||||
* sub-union of it). */
|
||||
function widensInto(lowerer: Lowerer, passed: IrType, declared: IrType, loc: SrcLoc): boolean {
|
||||
if (declared.kind !== "union") return false;
|
||||
const probe: IrExpr = { kind: "varRef", localId: "%probe", type: passed, loc };
|
||||
return typeEquals(lowerer.coerceToExpected(probe, declared).type, declared);
|
||||
}
|
||||
|
||||
/* ── user subclasses (`extends Readable`, the underscore-override form) ── */
|
||||
|
||||
/** The wrapper closure binding one overridden underscore method as the
|
||||
@@ -1075,10 +1084,17 @@ function streamMethodWrapper(
|
||||
);
|
||||
}
|
||||
} else if (!typeEquals(got.type, want)) {
|
||||
// A declared supertype of what Node passes (`chunk: Buffer | string`)
|
||||
// is sound: the wrapper takes Node's value and widens it into the
|
||||
// declared parameter.
|
||||
if (widensInto(lowerer, want, got.type, loc)) {
|
||||
paramTypes.push(want);
|
||||
continue;
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`a '${methodName}' method whose parameter ${i} is '${lowerer.fmt(got.type)}' where Node passes '${lowerer.fmt(want)}'`,
|
||||
blame,
|
||||
"annotate the parameter with the Node signature's type (chunks are Buffers here)",
|
||||
"annotate the parameter with the Node signature's type or a supertype of it (chunks are Buffers here)",
|
||||
);
|
||||
}
|
||||
paramTypes.push(got.type);
|
||||
@@ -1094,7 +1110,11 @@ function streamMethodWrapper(
|
||||
for (let i = 0; i < paramTypes.length; i++) {
|
||||
const local = lowerer.declareHiddenLocal(`a${i}`, paramTypes[i]!);
|
||||
params.push({ localId: local.id, name: local.name, type: paramTypes[i]! });
|
||||
argRefs.push({ kind: "varRef", localId: local.id, type: paramTypes[i]!, loc });
|
||||
const arg: IrExpr = { kind: "varRef", localId: local.id, type: paramTypes[i]!, loc };
|
||||
const declaredType = declared[i]!.type;
|
||||
argRefs.push(
|
||||
typeEquals(arg.type, declaredType) ? arg : lowerer.coerceToExpected(arg, declaredType),
|
||||
);
|
||||
}
|
||||
const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisType, loc };
|
||||
let call: IrExpr;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { extendAssertedRecordMembers } from "./assertion-members.js";
|
||||
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
|
||||
import {
|
||||
lifetimeRegionType,
|
||||
@@ -116,6 +117,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
import { defaultAfterUndefined, lowerStaticallyUndefinedArgument } from "./optional-arguments.js";
|
||||
import { type ProtocolDispatch } from "./class-protocol-dispatch.js";
|
||||
import { ClassDynamicDispatch, trackClassFieldCreation } from "./class-dynamic-dispatch.js";
|
||||
import { guardClassAttributeWrites } from "./class-attributes.js";
|
||||
import { ClassProtocols } from "./class-protocols.js";
|
||||
import { earlyReadVarSymbols } from "./var-hoisting.js";
|
||||
import { recordTextCodecClass } from "../../ir/ir.js";
|
||||
@@ -129,7 +131,8 @@ import {
|
||||
} 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";
|
||||
import { finalizePrivateBrands } from "./private-brands.js";
|
||||
import { type ClassSymbolKey, tableKeyFieldType } from "./symbol-fields.js";
|
||||
import type { HttpClientFnBinding } from "./lower-server.js";
|
||||
export type { WidthLift } from "./width-lift.js";
|
||||
export { newFnCtx, type FnCtx } from "./function-context.js";
|
||||
@@ -299,6 +302,7 @@ import {
|
||||
import { prepareCjsModuleGraph, usesCjsTopLevelThis } from "./lower-node-module.js";
|
||||
import {
|
||||
cjsNamespaceModule,
|
||||
cjsWholeExportValueOf,
|
||||
inlineNamespaceRequireOf,
|
||||
inlineProgramRequireOf,
|
||||
} from "./cjs-inline-require.js";
|
||||
@@ -1961,8 +1965,13 @@ export class Lowerer {
|
||||
* their symbol/node keys; no module-level cache should retain a pass. */
|
||||
readonly classSymbolKeys = new Map<ts.Symbol, ClassSymbolKey | null>();
|
||||
readonly registeredClassSymbols = new Map<string, ts.Symbol>();
|
||||
/** Symbol-key identities by description, in resolution order (the slot
|
||||
* ordinals of key-table keys — symbolKeyOf). */
|
||||
readonly symbolFieldIdentities = new Map<string, ts.Symbol[]>();
|
||||
readonly httpClientFnBindings = new Map<ts.Symbol, HttpClientFnBinding>();
|
||||
streamPropMutatedSyms: Set<ts.Symbol> | null = null;
|
||||
/** Whether some source reads process.exitCode (processReadsExitCode). */
|
||||
readsExitCode: boolean | null = null;
|
||||
requestInitPropMutatedSyms: Set<ts.Symbol> | null = null;
|
||||
emitterEventTable: Map<string, EventSig> | null = null;
|
||||
emitterComputedPatterns: ComputedEventPattern[] = [];
|
||||
@@ -2115,6 +2124,51 @@ export class Lowerer {
|
||||
return fallback ?? null;
|
||||
}
|
||||
|
||||
/** A TypeScript object literal handed to a JavaScript checked-value
|
||||
* parameter whose inferred type is open (`constructor({ factory, ...options
|
||||
* } = {})` — the rest element admits any property): the literal names a
|
||||
* property the contextual JS type does not declare, so it has no record
|
||||
* layout there and is built as the plain object the JS side receives. */
|
||||
private excessJsContextualProperty(
|
||||
literal: ts.ObjectLiteralExpression,
|
||||
contextual: readonly ts.Type[],
|
||||
): boolean {
|
||||
const objects = contextual.filter(
|
||||
(part) => !(part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)),
|
||||
);
|
||||
if (objects.length === 0) return false;
|
||||
// Only a destructuring pattern with a rest element is open; other
|
||||
// inferred JS types keep their record lowering.
|
||||
const fromJs = objects.every((part) => {
|
||||
const sym = part.getSymbol();
|
||||
return (
|
||||
sym !== undefined &&
|
||||
sym.declarations.some((d) => {
|
||||
if (!isJsSourceFile(d.getSourceFile())) return false;
|
||||
// The pattern itself, or the `= {}` default of a pattern parameter.
|
||||
const pattern =
|
||||
ts.isObjectLiteralExpression(d) &&
|
||||
d.parent !== undefined &&
|
||||
ts.isParameter(d.parent) &&
|
||||
d.parent.initializer === d
|
||||
? d.parent.name
|
||||
: d;
|
||||
return (
|
||||
ts.isObjectBindingPattern(pattern) &&
|
||||
pattern.elements.nodes.some((element) => element.dotDotDotToken !== undefined)
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
if (!fromJs) return false;
|
||||
return literal.properties.nodes.some((prop) => {
|
||||
if (!prop.name || !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)))
|
||||
return false;
|
||||
const name = prop.name.text;
|
||||
return objects.every((part) => !this.checker.getPropertyOfType(part, name));
|
||||
});
|
||||
}
|
||||
|
||||
/** 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
|
||||
@@ -3719,13 +3773,18 @@ export class Lowerer {
|
||||
? d.parent
|
||||
: null;
|
||||
if (!assign || assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null;
|
||||
if (!ts.isIdentifier(assign.right)) return null;
|
||||
const stmt = assign.parent;
|
||||
if (!stmt || !ts.isExpressionStatement(stmt) || !ts.isSourceFile(stmt.parent)) return null;
|
||||
if (!isJsSourceFile(stmt.parent)) return null;
|
||||
const cjs = cjsExportAssignmentOf(stmt);
|
||||
if (cjs?.kind !== "member" || cjs.expr !== assign) return null;
|
||||
if (cjsExportDiscardReason(stmt) !== null) return null;
|
||||
// `module.exports.C = require('./c')` over a module whose whole export
|
||||
// is a class identifier: the same alias plumbing; the statement keeps
|
||||
// the require's module evaluation at its position.
|
||||
const required = this.memberExportRequiredClass(assign.right);
|
||||
if (required !== undefined) return required;
|
||||
if (!ts.isIdentifier(assign.right)) return null;
|
||||
let value = this.checker.getSymbolAtLocation(assign.right);
|
||||
if (!value) return null;
|
||||
if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value);
|
||||
@@ -3733,6 +3792,22 @@ export class Lowerer {
|
||||
return isClass ? value : null;
|
||||
}
|
||||
|
||||
/** For an inline program `require("./c")`: the class symbol its module's
|
||||
* kept `module.exports = <class identifier>` names, or null when the
|
||||
* whole export is something else. Undefined when `expr` is not such a
|
||||
* require. */
|
||||
memberExportRequiredClass(expr: ts.Expression): ts.Symbol | null | undefined {
|
||||
const req = inlineProgramRequireOf(this, expr);
|
||||
if (req === null) return undefined;
|
||||
const whole = cjsWholeExportValueOf(req.dep);
|
||||
if (whole === null || !ts.isIdentifier(whole.rhs)) return null;
|
||||
let value = this.checker.getSymbolAtLocation(whole.rhs);
|
||||
if (!value) return null;
|
||||
if (value.flags & ts.SymbolFlags.Alias) value = this.checker.getAliasedSymbol(value);
|
||||
const isClass = value.declarations.some((decl) => ts.isClassDeclaration(decl));
|
||||
return isClass ? value : null;
|
||||
}
|
||||
|
||||
/** The CommonJS JS module a DEFAULT-import binding's declaration loads
|
||||
* (`import d from "./lib.cjs"`), or null: Node's ESM-CJS interop binds
|
||||
* the default to module.exports — exactly a require binding — so those
|
||||
@@ -6710,6 +6785,10 @@ export class Lowerer {
|
||||
}
|
||||
this.collectProgram(parts);
|
||||
this.speculativeTypeQueries(() => {
|
||||
extendAssertedRecordMembers(
|
||||
this,
|
||||
parts.map((fp) => fp.sf),
|
||||
);
|
||||
this.analyzeRuntimeOptionalArrayReads(parts);
|
||||
this.analyzeDateJsonResults(parts);
|
||||
this.analyzeGuardedRecordReadGlobals(parts);
|
||||
@@ -6856,7 +6935,9 @@ export class Lowerer {
|
||||
finishModule(functions: IrFunction[]): LowerResult {
|
||||
if (this.diags.length === 0) fuseArrayPipelines(this, functions);
|
||||
finalizeClassMethodValues(this, functions);
|
||||
finalizePrivateBrands(this, functions);
|
||||
trackClassFieldCreation(this, functions);
|
||||
guardClassAttributeWrites(this, functions);
|
||||
|
||||
// Globals typed by a class that never REGISTERED (a JS class whose
|
||||
// collection fenced — Symbol-keyed fields, an unsupported base): the
|
||||
@@ -7116,6 +7197,10 @@ export class Lowerer {
|
||||
};
|
||||
this.collectProgram(parts);
|
||||
this.speculativeTypeQueries(() => {
|
||||
extendAssertedRecordMembers(
|
||||
this,
|
||||
parts.map((fp) => fp.sf),
|
||||
);
|
||||
this.analyzeRuntimeOptionalArrayReads(parts);
|
||||
this.analyzeDateJsonResults(parts);
|
||||
this.analyzeGuardedRecordReadGlobals(parts);
|
||||
@@ -8037,6 +8122,17 @@ export class Lowerer {
|
||||
return this.checker.getUnknownType();
|
||||
}
|
||||
}
|
||||
// A symbol-keyed field read through a CommonJS key-table key reads as
|
||||
// the field's layout type (tableKeyFieldType).
|
||||
if (ts.isElementAccessExpression(node) && isJsSourceFile(node.getSourceFile())) {
|
||||
const field = tableKeyFieldType(this, node);
|
||||
if (field) {
|
||||
if (field.kind === "f64") return this.checker.getNumberType();
|
||||
if (field.kind === "string") return this.checker.getStringType();
|
||||
if (field.kind === "bool") return this.checker.getBooleanType();
|
||||
return this.checker.getUnknownType();
|
||||
}
|
||||
}
|
||||
// A checked JS method receives its actual call-site receiver. Its
|
||||
// declaration's inferred class type must not turn property reads back
|
||||
// into native field loads when the method is borrowed by another object.
|
||||
@@ -9675,7 +9771,8 @@ export class Lowerer {
|
||||
builtinOption("SpawnOptions") ||
|
||||
builtinOption("UrlObject") ||
|
||||
builtinOption("URLFormatOptions") ||
|
||||
!this.mapTypeOf(this.typeOf(x))
|
||||
!this.mapTypeOf(this.typeOf(x)) ||
|
||||
this.excessJsContextualProperty(x, parts)
|
||||
) {
|
||||
return lowerDynObjectLiteral(this, x);
|
||||
}
|
||||
@@ -10962,6 +11059,21 @@ export class Lowerer {
|
||||
return;
|
||||
}
|
||||
const name = (decl.name as ts.Identifier).text;
|
||||
if (shape.entryNarrow) {
|
||||
// A typed override parameter over an inherited checked-value slot:
|
||||
// the body local narrows from the dyn argument at entry.
|
||||
const loc = locOf(decl);
|
||||
const slot = this.declareHiddenLocal(name, shape.type);
|
||||
params.push({ localId: slot.id, name, type: shape.type });
|
||||
const narrowed = this.coerceInto(
|
||||
decl.name,
|
||||
{ kind: "varRef", localId: slot.id, type: shape.type, loc },
|
||||
shape.entryNarrow,
|
||||
);
|
||||
const body = this.declareLocal(decl.name, name, shape.entryNarrow, true);
|
||||
prologue.push({ kind: "varDecl", localId: body.id, init: narrowed, loc });
|
||||
return;
|
||||
}
|
||||
if (shape.mode === "omittable" && shape.bodyType && decl.initializer) {
|
||||
const abi = shape.type;
|
||||
if (abi.kind === "dyn") {
|
||||
|
||||
@@ -25,24 +25,228 @@ function exportedFunction(decl: ts.Node): ts.Node | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether a function's own body reads `this` (arrows share it; other
|
||||
* functions and classes have their own). */
|
||||
function readsOwnThis(fn: ts.Node): boolean {
|
||||
let found = false;
|
||||
/** Whether `node` is `f.apply(this, ...)` or `f.call(this, ...)`, which
|
||||
* passes the receiver on to `f`. */
|
||||
function forwardsThis(node: ts.Node): node is ts.CallExpression & {
|
||||
expression: ts.PropertyAccessExpression;
|
||||
} {
|
||||
if (!ts.isCallExpression(node)) return false;
|
||||
const callee = node.expression;
|
||||
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name)) return false;
|
||||
if (callee.name.text !== "apply" && callee.name.text !== "call") return false;
|
||||
return node.arguments.nodes[0]?.kind === ts.SyntaxKind.ThisKeyword;
|
||||
}
|
||||
|
||||
/* The arguments a call gave a function's parameters, while the analysis
|
||||
* follows the call into the function: each with the bindings of its own
|
||||
* scope. */
|
||||
type Bindings = ReadonlyMap<ts.Symbol, { expr: ts.Expression; bindings: Bindings }>;
|
||||
const noBindings: Bindings = new Map();
|
||||
|
||||
// How many calls and functions deep the analysis follows a forwarded
|
||||
// `this`: enough for a wrapper over a function factory, as Effect's
|
||||
// `dual(3, body).apply(this, arguments)`.
|
||||
const maxDepth = 6;
|
||||
|
||||
/** Whether `fn`'s own body uses `this` only to pass it on with `.apply` or
|
||||
* `.call` to functions that ignore it (and so also when it doesn't use
|
||||
* `this` at all). */
|
||||
function ignoresOwnThis(lowerer: Lowerer, fn: ts.Node, bindings: Bindings, depth: number): boolean {
|
||||
let ignores = true;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (!ignores) return;
|
||||
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
found = true;
|
||||
ignores = false;
|
||||
return;
|
||||
}
|
||||
if ((ts.isFunctionLike(node) && !ts.isArrowFunction(node)) || ts.isClassLikeDeclaration(node))
|
||||
return;
|
||||
if (forwardsThis(node)) {
|
||||
if (!ignoresThis(lowerer, node.expression.expression, bindings, depth + 1)) {
|
||||
ignores = false;
|
||||
return;
|
||||
}
|
||||
visit(node.expression.expression);
|
||||
for (const arg of node.arguments.nodes.slice(1)) visit(arg);
|
||||
return;
|
||||
}
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
fn.forEachChild(visit);
|
||||
return ignores;
|
||||
}
|
||||
|
||||
/** The function `name` provably holds: a function declaration, a const
|
||||
* initialized with a function, or an import of one. */
|
||||
function functionOfName(
|
||||
lowerer: Lowerer,
|
||||
name: ts.Identifier,
|
||||
bindings: Bindings,
|
||||
): { fn: ts.Node | null; bound?: { expr: ts.Expression; bindings: Bindings } } {
|
||||
let symbol = lowerer.checker.getSymbolAtLocation(name);
|
||||
if (symbol === undefined) return { fn: null };
|
||||
const bound = bindings.get(symbol);
|
||||
if (bound) return { fn: null, bound };
|
||||
if ((symbol.flags & ts.SymbolFlags.Alias) !== 0)
|
||||
symbol = lowerer.checker.getAliasedSymbol(symbol);
|
||||
const decls = symbol.declarations;
|
||||
if (decls.length !== 1) return { fn: null };
|
||||
const decl = decls[0]!;
|
||||
if (ts.isFunctionDeclaration(decl)) return { fn: decl };
|
||||
if (
|
||||
ts.isVariableDeclaration(decl) &&
|
||||
decl.initializer &&
|
||||
ts.isVariableDeclarationList(decl.parent) &&
|
||||
(decl.parent.flags & ts.NodeFlags.Const) !== 0
|
||||
) {
|
||||
let init: ts.Expression = decl.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) return { fn: init };
|
||||
}
|
||||
return { fn: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `fn` (with its nested functions) may change what its parameter
|
||||
* `param` holds: an assignment, update, destructuring or for-in/for-of
|
||||
* target that writes it, a declaration of the same name, a write to an
|
||||
* element of `arguments` (which aliases parameters outside strict mode), or
|
||||
* a call of `eval`.
|
||||
*/
|
||||
function mayRebind(lowerer: Lowerer, fn: ts.Node, param: ts.Identifier): boolean {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param);
|
||||
const refersTo = (node: ts.Node): boolean =>
|
||||
ts.isIdentifier(node) &&
|
||||
(node.text === param.text || lowerer.checker.getSymbolAtLocation(node) === symbol);
|
||||
// Whether writing `target` writes the parameter: the name itself, or the
|
||||
// name inside a destructuring pattern.
|
||||
const writes = (target: ts.Node): boolean => {
|
||||
while (ts.isParenthesizedExpression(target)) target = target.expression;
|
||||
if (refersTo(target)) return true;
|
||||
if (
|
||||
ts.isElementAccessExpression(target) &&
|
||||
ts.isIdentifier(target.expression) &&
|
||||
target.expression.text === "arguments"
|
||||
)
|
||||
return true;
|
||||
if (ts.isArrayLiteralExpression(target)) return target.elements.nodes.some(writes);
|
||||
if (ts.isObjectLiteralExpression(target))
|
||||
return target.properties.nodes.some((p) =>
|
||||
ts.isShorthandPropertyAssignment(p)
|
||||
? refersTo(p.name)
|
||||
: ts.isPropertyAssignment(p)
|
||||
? writes(p.initializer)
|
||||
: ts.isSpreadAssignment(p) && writes(p.expression),
|
||||
);
|
||||
if (ts.isSpreadElement(target)) return writes(target.expression);
|
||||
// A default in a pattern: `[body = other] = values`.
|
||||
if (ts.isBinaryExpression(target) && target.operatorToken.kind === ts.SyntaxKind.EqualsToken)
|
||||
return writes(target.left);
|
||||
return false;
|
||||
};
|
||||
const declaresName = (node: ts.Node): boolean => {
|
||||
const name = (node as { name?: ts.Node }).name;
|
||||
return name !== undefined && ts.isIdentifier(name) && name.text === param.text;
|
||||
};
|
||||
let found = false;
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (found) return;
|
||||
if (
|
||||
(ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
|
||||
writes(node.left)) ||
|
||||
((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
||||
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
node.operator === ts.SyntaxKind.MinusMinusToken) &&
|
||||
writes(node.operand)) ||
|
||||
((ts.isForOfStatement(node) || ts.isForInStatement(node)) &&
|
||||
!ts.isVariableDeclarationList(node.initializer) &&
|
||||
writes(node.initializer)) ||
|
||||
((ts.isVariableDeclaration(node) ||
|
||||
ts.isBindingElement(node) ||
|
||||
ts.isParameter(node) ||
|
||||
ts.isFunctionDeclaration(node) ||
|
||||
ts.isClassDeclaration(node)) &&
|
||||
declaresName(node)) ||
|
||||
(ts.isCallExpression(node) &&
|
||||
ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === "eval")
|
||||
) {
|
||||
found = true;
|
||||
return;
|
||||
}
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
// The function's own parameters declare the name; their defaults can write it.
|
||||
const params = (fn as { parameters?: ts.NodeArray<ts.ParameterDeclaration> }).parameters;
|
||||
for (const p of params?.nodes ?? []) if (p.initializer) visit(p.initializer);
|
||||
const body = (fn as { body?: ts.Node }).body;
|
||||
if (body !== undefined) visit(body);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The expressions `fn` returns from its own body. */
|
||||
function returnedExpressions(fn: ts.Node): ts.Expression[] | null {
|
||||
const body = (fn as { body?: ts.Node }).body;
|
||||
if (body === undefined) return null;
|
||||
if (!ts.isBlock(body)) return [body as ts.Expression];
|
||||
const result: ts.Expression[] = [];
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return;
|
||||
if (ts.isReturnStatement(node) && node.expression) result.push(node.expression);
|
||||
node.forEachChild(visit);
|
||||
};
|
||||
body.forEachChild(visit);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Whether the function `expr` evaluates to provably ignores the `this`
|
||||
* it is called with. */
|
||||
function ignoresThis(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
bindings: Bindings,
|
||||
depth: number,
|
||||
): boolean {
|
||||
if (depth > maxDepth) return false;
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (ts.isArrowFunction(expr)) return true;
|
||||
if (ts.isFunctionExpression(expr)) return ignoresOwnThis(lowerer, expr, bindings, depth);
|
||||
if (ts.isIdentifier(expr)) {
|
||||
const { fn, bound } = functionOfName(lowerer, expr, bindings);
|
||||
if (bound) return ignoresThis(lowerer, bound.expr, bound.bindings, depth + 1);
|
||||
if (fn === null) return false;
|
||||
return ts.isArrowFunction(fn) || ignoresOwnThis(lowerer, fn, noBindings, depth + 1);
|
||||
}
|
||||
// A call of a known function: what it returns, with its parameters bound
|
||||
// to this call's arguments.
|
||||
if (ts.isCallExpression(expr) && ts.isIdentifier(expr.expression)) {
|
||||
const { fn } = functionOfName(lowerer, expr.expression, bindings);
|
||||
if (fn === null) return false;
|
||||
const params = (fn as { parameters?: ts.NodeArray<ts.ParameterDeclaration> }).parameters;
|
||||
const inner = new Map(bindings);
|
||||
for (const [i, param] of (params?.nodes ?? []).entries()) {
|
||||
if (!ts.isIdentifier(param.name) || param.dotDotDotToken) return false;
|
||||
const arg = expr.arguments.nodes[i];
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
|
||||
if (arg === undefined || ts.isSpreadElement(arg) || symbol === undefined) return false;
|
||||
// The parameter holds the argument for as long as the function runs.
|
||||
if (mayRebind(lowerer, fn, param.name)) return false;
|
||||
inner.set(symbol, { expr: arg, bindings });
|
||||
}
|
||||
const returned = returnedExpressions(fn);
|
||||
return returned !== null && returned.every((r) => ignoresThis(lowerer, r, inner, depth + 1));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether calling `fn` with another `this` than Node's can change what it
|
||||
* does: it reads `this`, or passes it on to a function that may. */
|
||||
function readsOwnThis(lowerer: Lowerer, fn: ts.Node): boolean {
|
||||
return !ignoresOwnThis(lowerer, fn, noBindings, 0);
|
||||
}
|
||||
|
||||
export function refuseModuleMemberThis(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -54,7 +258,7 @@ export function refuseModuleMemberThis(
|
||||
if (symbol === undefined) return;
|
||||
for (const decl of symbol.declarations) {
|
||||
const fn = exportedFunction(decl);
|
||||
if (fn === null || !readsOwnThis(fn)) continue;
|
||||
if (fn === null || !readsOwnThis(lowerer, fn)) continue;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
RUNTIME_ERROR_CLASSES,
|
||||
STRING,
|
||||
typeKey,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { helperLoc, strLit, varRef } from "../../ir/build.js";
|
||||
import { isJsSourceFile } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
|
||||
/** How a brand helper uses its receiver: `in` answers `#x in v`, `read` and
|
||||
* `write` return the receiver as the declaring class for a field access. */
|
||||
type BrandUse = "in" | "read" | "write";
|
||||
|
||||
const PREFIX = "%private.";
|
||||
|
||||
/** A function-local JavaScript class with private names. Its prototype
|
||||
* methods and accessors exist once per evaluation and check their receiver's
|
||||
* evaluation, so a method or accessor taken from an instance is the function
|
||||
* of that instance's own evaluation. */
|
||||
export function perEvaluationPrivateNames(info: ClassInfo): boolean {
|
||||
const declaration = info.localClass ? info.decl : null;
|
||||
return (
|
||||
!!declaration &&
|
||||
isJsSourceFile(declaration.getSourceFile()) &&
|
||||
declaration.members.nodes.some(
|
||||
(member) => member.name !== undefined && ts.isPrivateIdentifier(member.name),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** The function-local class declaring a private name, when its private names
|
||||
* need a per-evaluation brand check: the nearest enclosing class that
|
||||
* declares the name, as JavaScript scoping resolves it. */
|
||||
export function localPrivateBrandClass(
|
||||
lowerer: Lowerer,
|
||||
priv: ts.PrivateIdentifier,
|
||||
): { info: ClassInfo; decl: ts.ClassDeclaration & { name: ts.Identifier } } | null {
|
||||
for (let node: ts.Node | undefined = priv.parent; node; node = node.parent) {
|
||||
if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) continue;
|
||||
const declares = node.members.nodes.some((member) => {
|
||||
const name = (member as { name?: ts.PropertyName }).name;
|
||||
return name !== undefined && ts.isPrivateIdentifier(name) && name.text === priv.text;
|
||||
});
|
||||
if (!declares) continue;
|
||||
if (!ts.isClassDeclaration(node) || !node.name) return null;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node.name);
|
||||
const info =
|
||||
lowerer.currentClass?.decl === node
|
||||
? lowerer.currentClass
|
||||
: symbol
|
||||
? lowerer.classBySymbol.get(symbol)
|
||||
: undefined;
|
||||
if (!info?.localClass) return null;
|
||||
return { info, decl: node as ts.ClassDeclaration & { name: ts.Identifier } };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Each evaluation of a function-local class creates fresh private names,
|
||||
* while one compiled class serves every evaluation. Its JavaScript prototype
|
||||
* methods are closures per evaluation whose `this` is a checked value, so a
|
||||
* private name on `this` is present exactly when the receiver is an instance
|
||||
* of the class (or a subclass) whose `%classEnvironment` field holds the
|
||||
* class value of the closure's own evaluation. The class's own binding names
|
||||
* that value (lowerClassExpressionInfo accepts only named declarations, whose
|
||||
* binding the checker keeps from being assigned). */
|
||||
export function checkedPrivateReceiver(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
decl: ts.ClassDeclaration & { name: ts.Identifier },
|
||||
access: ts.PropertyAccessExpression,
|
||||
receiver: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const use: BrandUse = isPrivateWriteTarget(access) ? "write" : "read";
|
||||
return brandCall(lowerer, info, decl, use, access.name.text, receiver, loc);
|
||||
}
|
||||
|
||||
/** Whether a checked value is a primitive: `#x in v` throws for those
|
||||
* before any brand lookup. */
|
||||
export function primitiveOperand(value: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return (["nullish", "number", "string", "boolean", "bigint", "symbol"] as const)
|
||||
.map((test): IrExpr => ({ kind: "dynTest", test, value, type: BOOL, loc }))
|
||||
.reduce((left, right) => ({ kind: "logical", op: "||", left, right, type: BOOL, loc }));
|
||||
}
|
||||
|
||||
/** Node's TypeError message for `#x in v` on a primitive, which spells the
|
||||
* value as String(v) does. */
|
||||
export function privateInMessage(name: string, value: IrExpr, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "strConcat",
|
||||
left: strLit(`Cannot use 'in' operator to search for '${name}' in `, loc),
|
||||
right: { kind: "libCall", fn: "dyn.stringConstructor", args: [value], type: STRING, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A receiver checkedPrivateReceiver built for a field write. */
|
||||
export function isPrivateWriteReceiver(expr: IrExpr): boolean {
|
||||
return expr.kind === "call" && expr.callee.startsWith(`${PREFIX}write:`);
|
||||
}
|
||||
|
||||
/** `#x in v` for a checked value inside a function-local class: the brand
|
||||
* test above, after the TypeError a primitive operand throws. */
|
||||
export function checkedPrivateIn(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
decl: ts.ClassDeclaration & { name: ts.Identifier },
|
||||
name: string,
|
||||
receiver: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
return brandCall(lowerer, info, decl, "in", name, receiver, loc);
|
||||
}
|
||||
|
||||
function brandCall(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
decl: ts.ClassDeclaration & { name: ts.Identifier },
|
||||
use: BrandUse,
|
||||
name: string,
|
||||
receiver: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const className = info.def.name;
|
||||
const classType: IrType = { kind: "classval", className };
|
||||
const resultType: IrType = use === "in" ? BOOL : { kind: "object", className };
|
||||
const helper = `${PREFIX}${use}:${className}:${name}`;
|
||||
if (!lowerer.valueHelpers.has(helper)) {
|
||||
const at = helperLoc(loc);
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [
|
||||
{ localId: "value.0", name: "value", type: DYN },
|
||||
{ localId: "class.0", name: "class", type: classType },
|
||||
],
|
||||
returnType: resultType,
|
||||
locals: [
|
||||
{ id: "value.0", name: "value", type: DYN, mutable: false },
|
||||
{ id: "class.0", name: "class", type: classType, mutable: false },
|
||||
],
|
||||
// finalizePrivateBrands fills the body once every class is collected,
|
||||
// including subclasses declared after this use.
|
||||
body: [],
|
||||
loc: at,
|
||||
});
|
||||
lowerer.valueHelpers.set(helper, helper);
|
||||
}
|
||||
const evaluation = lowerer.classValueRef(info, decl.name);
|
||||
return { kind: "call", callee: helper, args: [receiver, evaluation], type: resultType, loc };
|
||||
}
|
||||
|
||||
/** A plain assignment, a destructuring target, or a for-in/of target writes
|
||||
* the field. Compound assignments and updates read it first, and Node
|
||||
* reports the failed read. */
|
||||
function isPrivateWriteTarget(access: ts.PropertyAccessExpression): boolean {
|
||||
let node: ts.Node = access;
|
||||
for (;;) {
|
||||
const parent = node.parent;
|
||||
if (ts.isParenthesizedExpression(parent)) {
|
||||
node = parent;
|
||||
continue;
|
||||
}
|
||||
if (ts.isBinaryExpression(parent))
|
||||
return parent.left === node && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken;
|
||||
if (ts.isForOfStatement(parent) || ts.isForInStatement(parent))
|
||||
return parent.initializer === node;
|
||||
// Inside a destructuring pattern: keep walking to the assignment.
|
||||
if (
|
||||
ts.isArrayLiteralExpression(parent) ||
|
||||
ts.isSpreadElement(parent) ||
|
||||
(ts.isPropertyAssignment(parent) && parent.initializer === node) ||
|
||||
ts.isObjectLiteralExpression(parent) ||
|
||||
ts.isSpreadAssignment(parent)
|
||||
) {
|
||||
node = parent;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Fill the brand helpers: one branch per class a checked reference to an
|
||||
* instance can be boxed under (the declarer, its subclasses, and its
|
||||
* ancestors when the instance is at or below the declarer). */
|
||||
export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||
for (const fn of functions) {
|
||||
if (!fn.name.startsWith(PREFIX) || fn.body.length > 0) continue;
|
||||
// `%private.<use>:<class>:<#name>`; class names may contain colons.
|
||||
const spec = fn.name.slice(PREFIX.length);
|
||||
const use = spec.slice(0, spec.indexOf(":")) as BrandUse;
|
||||
const className = spec.slice(use.length + 1, spec.lastIndexOf(":"));
|
||||
const name = spec.slice(spec.lastIndexOf(":") + 1);
|
||||
const loc = fn.loc;
|
||||
const value = varRef("value.0", DYN, loc);
|
||||
const evaluation = varRef("class.0", { kind: "classval", className }, loc);
|
||||
const ownerType: IrType = { kind: "object", className };
|
||||
const body: IrStmt[] = [];
|
||||
let matches = 0;
|
||||
for (const candidate of lowerer.classes.values()) {
|
||||
const candidateName = candidate.def.name;
|
||||
if (RUNTIME_ERROR_CLASSES.has(candidateName)) continue;
|
||||
const below = candidateName === className || lowerer.isSubclassOf(candidateName, className);
|
||||
const above = lowerer.isSubclassOf(className, candidateName);
|
||||
if (!below && !above) continue;
|
||||
const candidateType: IrType = { kind: "object", className: candidateName };
|
||||
const checked: IrExpr = { kind: "dynCheck", value, type: candidateType, loc };
|
||||
const local = `receiver.${matches++}`;
|
||||
fn.locals.push({ id: local, name: "receiver", type: ownerType, mutable: false });
|
||||
const receiver = varRef(local, ownerType, loc);
|
||||
const sameEvaluation: IrExpr = {
|
||||
kind: "bin",
|
||||
op: "===",
|
||||
left: {
|
||||
kind: "fieldGet",
|
||||
obj: receiver,
|
||||
className,
|
||||
field: `%classEnvironment:${className}`,
|
||||
type: evaluation.type,
|
||||
loc,
|
||||
},
|
||||
right: evaluation,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const match = (typed: IrExpr): IrStmt[] => [
|
||||
{ kind: "varDecl", localId: local, init: typed, loc },
|
||||
{
|
||||
kind: "if",
|
||||
cond: sameEvaluation,
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: use === "in" ? { kind: "boolLit", value: true, type: BOOL, loc } : receiver,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const then: IrStmt[] = below
|
||||
? match(
|
||||
candidateName === className
|
||||
? checked
|
||||
: { kind: "upcast", value: checked, type: ownerType, loc },
|
||||
)
|
||||
: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "instanceOf", value: checked, className, type: BOOL, loc },
|
||||
then: match({ kind: "downcast", value: checked, type: ownerType, loc }),
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.typedRefIs",
|
||||
args: [value, strLit(typeKey(candidateType), loc)],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then,
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const typeError = (message: IrExpr): IrStmt => ({
|
||||
kind: "throw",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "error.new",
|
||||
args: [message],
|
||||
type: { kind: "object", className: "%TypeError" },
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
if (use === "in") {
|
||||
body.push(
|
||||
{
|
||||
kind: "if",
|
||||
cond: primitiveOperand(value, loc),
|
||||
then: [typeError(privateInMessage(name, value, loc))],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc },
|
||||
);
|
||||
} else {
|
||||
const verb = use === "read" ? "reading" : "setting";
|
||||
const action = use === "read" ? "Cannot read properties" : "Cannot set properties";
|
||||
for (const unit of ["undefined", "null"] as const) {
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: unit, value, type: BOOL, loc },
|
||||
then: [typeError(strLit(`${action} of ${unit} (${verb} '${name}')`, loc))],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
body.push(
|
||||
typeError(
|
||||
strLit(
|
||||
use === "read"
|
||||
? `Cannot read private member ${name} from an object whose class did not declare it`
|
||||
: `Cannot write private member ${name} to an object whose class did not declare it`,
|
||||
loc,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
fn.body = body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isCjsJsFile, isNodeEsmFile } from "../program.js";
|
||||
|
||||
function hasUseStrictDirective(statements: readonly ts.Statement[]): boolean {
|
||||
for (const statement of statements) {
|
||||
if (!ts.isExpressionStatement(statement) || !ts.isStringLiteral(statement.expression))
|
||||
return false;
|
||||
if (statement.expression.text === "use strict") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Whether `node` runs in strict mode code: inside a class body, a function
|
||||
* with a "use strict" directive, a module, or a script that starts with
|
||||
* "use strict". CommonJS JavaScript is sloppy otherwise. */
|
||||
export function isStrictCode(node: ts.Node): boolean {
|
||||
for (let parent: ts.Node | undefined = node.parent; parent; parent = parent.parent) {
|
||||
if (ts.isClassDeclaration(parent) || ts.isClassExpression(parent)) return true;
|
||||
if (
|
||||
ts.isFunctionLike(parent) &&
|
||||
parent.body &&
|
||||
ts.isBlock(parent.body) &&
|
||||
hasUseStrictDirective(parent.body.statements.nodes)
|
||||
)
|
||||
return true;
|
||||
if (ts.isSourceFile(parent)) {
|
||||
return (
|
||||
hasUseStrictDirective(parent.statements.nodes) ||
|
||||
(!isCjsJsFile(parent) && (isNodeEsmFile(parent) || ts.isExternalModule(parent)))
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -2656,8 +2656,8 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
|
||||
id: "node-builtin.process.exitCode",
|
||||
kind: "node-builtin",
|
||||
name: "process.exitCode",
|
||||
fns: ["process.setExitCode"],
|
||||
note: "numeric writes in statement position set the implicit exit status; process.exit() reads it",
|
||||
fns: ["process.setExitCode", "process.setExitCodeTracked", "process.hasExitCode"],
|
||||
note: "numeric writes in statement position set the implicit exit status; reads answer the assigned code or undefined; process.exit() reads it",
|
||||
},
|
||||
// ── the tls CA store (lowerTlsCaCall / lowerTlsRootCertificates): the
|
||||
// host's trust anchors, read and replaced. Dedicated paths, and
|
||||
@@ -2969,7 +2969,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, the crypto callback functions, dns.lookup, dns.lookupService 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, plus typed callback-last function values whose callback takes (error?: Error | null, value?); call the bound function directly",
|
||||
},
|
||||
"stream/consumers": {
|
||||
blob:
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { bindingEarlyUse7 } from "../program.js";
|
||||
import {
|
||||
bindingEarlyUse7,
|
||||
cjsExportDiscardReason,
|
||||
isCjsExportTableLiteral,
|
||||
isModuleExportsAccess,
|
||||
} from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import type { IrType } from "../../ir/ir.js";
|
||||
|
||||
@@ -39,7 +44,7 @@ function constantSymbolDescription(
|
||||
}
|
||||
|
||||
export function symbolFieldDisplayName(field: string): string {
|
||||
return field.slice(FIELD_PREFIX.length);
|
||||
return field.slice(FIELD_PREFIX.length).replace(/^\d+:/, "");
|
||||
}
|
||||
|
||||
/** CopyDataProperties includes symbols, unlike the string-keyed view used
|
||||
@@ -96,6 +101,11 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS
|
||||
cache.set(sym, null);
|
||||
const decls = sym.declarations;
|
||||
const decl = sym.valueDeclaration;
|
||||
if (decls.length === 1 && decl && ts.isPropertyAssignment(decl)) {
|
||||
const key = cjsTableSymbolKeyOf(lowerer, sym, decl);
|
||||
if (key) cache.set(sym, key);
|
||||
return key;
|
||||
}
|
||||
if (decls.length !== 1 || !decl || !ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name))
|
||||
return null;
|
||||
const list = decl.parent;
|
||||
@@ -124,21 +134,9 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS
|
||||
cache.set(sym, alias);
|
||||
return alias;
|
||||
}
|
||||
if (!init || !ts.isCallExpression(init) || init.questionDotToken) return null;
|
||||
const callee = init.expression;
|
||||
const registered =
|
||||
ts.isPropertyAccessExpression(callee) && !callee.questionDotToken && callee.name.text === "for";
|
||||
const root = registered ? callee.expression : callee;
|
||||
if (!ts.isIdentifier(root) || !lowerer.isStdlibGlobal(root, "Symbol")) return null;
|
||||
const arg =
|
||||
init.arguments.nodes.length === 0
|
||||
? null
|
||||
: init.arguments.nodes.length === 1
|
||||
? init.arguments.nodes[0]!
|
||||
: undefined;
|
||||
if (arg === undefined || (registered && arg === null)) return null;
|
||||
const description = arg === null ? "" : constantSymbolDescription(lowerer, arg);
|
||||
if (description === null) return null;
|
||||
const call = init ? literalSymbolCallOf(lowerer, init) : null;
|
||||
if (!call) return null;
|
||||
const { registered, description } = call;
|
||||
// Ordinary Symbol() is a fresh identity per evaluation, so only the
|
||||
// module-scope declaration above can name one fixed layout slot.
|
||||
const sf = decl.getSourceFile();
|
||||
@@ -184,6 +182,18 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS
|
||||
) !== null
|
||||
)
|
||||
return null;
|
||||
const key = symbolKeyOf(lowerer, sym, registered, description);
|
||||
cache.set(sym, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
function symbolKeyOf(
|
||||
lowerer: Lowerer,
|
||||
sym: ts.Symbol,
|
||||
registered: boolean,
|
||||
description: string,
|
||||
table = false,
|
||||
): ClassSymbolKey {
|
||||
// A registry name denotes one symbol even when several source bindings
|
||||
// initialize it. Intern those names into a representative checker symbol;
|
||||
// the layout table then has one identity-keyed domain for both forms.
|
||||
@@ -193,16 +203,175 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS
|
||||
if (existing) identity = existing;
|
||||
else lowerer.registeredClassSymbols.set(description, sym);
|
||||
}
|
||||
const key: ClassSymbolKey = {
|
||||
// Distinct identities can share a description (undici's core and mock
|
||||
// key tables both hold a `Symbol('close')`, and MockClient inherits one
|
||||
// and declares the other). A key-table key whose description an earlier
|
||||
// identity already holds takes an ordinal that keeps its slot apart; the
|
||||
// printable name drops it. Const keys keep their plain names.
|
||||
let identities = lowerer.symbolFieldIdentities.get(description);
|
||||
if (!identities) lowerer.symbolFieldIdentities.set(description, (identities = []));
|
||||
let ordinal = identities.indexOf(identity);
|
||||
if (ordinal < 0) ordinal = identities.push(identity) - 1;
|
||||
return {
|
||||
sym,
|
||||
identity,
|
||||
// Symbol slots are not string properties. The reserved prefix keeps
|
||||
// them out of the native class's string-keyed dynamic view, including
|
||||
// Object.keys/JSON serialization and string-property writeback.
|
||||
fieldName: `${FIELD_PREFIX}Symbol(${description})`,
|
||||
fieldName: `${FIELD_PREFIX}${table && ordinal > 0 ? `${ordinal}:` : ""}Symbol(${description})`,
|
||||
};
|
||||
cache.set(sym, key);
|
||||
return key;
|
||||
}
|
||||
|
||||
/** `Symbol(<constant>)` or `Symbol.for(<constant>)` on the standard global. */
|
||||
function literalSymbolCallOf(
|
||||
lowerer: Lowerer,
|
||||
init: ts.Expression,
|
||||
): { registered: boolean; description: string } | null {
|
||||
if (!ts.isCallExpression(init) || init.questionDotToken) return null;
|
||||
const callee = init.expression;
|
||||
const registered =
|
||||
ts.isPropertyAccessExpression(callee) && !callee.questionDotToken && callee.name.text === "for";
|
||||
const root = registered ? callee.expression : callee;
|
||||
if (!ts.isIdentifier(root) || !lowerer.isStdlibGlobal(root, "Symbol")) return null;
|
||||
const arg =
|
||||
init.arguments.nodes.length === 0
|
||||
? null
|
||||
: init.arguments.nodes.length === 1
|
||||
? init.arguments.nodes[0]!
|
||||
: undefined;
|
||||
if (arg === undefined || (registered && arg === null)) return null;
|
||||
const description = arg === null ? "" : constantSymbolDescription(lowerer, arg);
|
||||
return description === null ? null : { registered, description };
|
||||
}
|
||||
|
||||
/** A key table property: `kX: Symbol('x')` in a JS module's effective
|
||||
* top-level `module.exports = { ... }` literal (undici's core/symbols.js).
|
||||
* The module evaluates once, so the property names one symbol identity for
|
||||
* every requirer, as a module-scope const does. Requirers can observe the
|
||||
* table only after its assignment completes, provided nothing before it
|
||||
* can run code (a require there could close a cycle and publish the
|
||||
* initial empty exports object instead). Any write to the property, or
|
||||
* replacement through `exports`/`module.exports`, keeps it unstable. */
|
||||
function cjsTableSymbolKeyOf(
|
||||
lowerer: Lowerer,
|
||||
sym: ts.Symbol,
|
||||
decl: ts.PropertyAssignment,
|
||||
): ClassSymbolKey | null {
|
||||
const table = decl.parent;
|
||||
if (!ts.isObjectLiteralExpression(table) || !ts.isIdentifier(decl.name)) return null;
|
||||
const assign = table.parent;
|
||||
if (
|
||||
!ts.isBinaryExpression(assign) ||
|
||||
assign.right !== table ||
|
||||
!isCjsExportTableLiteral(table) ||
|
||||
!ts.isExpressionStatement(assign.parent)
|
||||
)
|
||||
return null;
|
||||
const stmt = assign.parent;
|
||||
const sf = stmt.getSourceFile();
|
||||
if (cjsExportDiscardReason(stmt) !== null) return null;
|
||||
const call = literalSymbolCallOf(lowerer, decl.initializer);
|
||||
if (!call) return null;
|
||||
const inert = (expr: ts.Expression | undefined): boolean => {
|
||||
if (!expr) return true;
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
return (
|
||||
ts.isIdentifier(expr) ||
|
||||
ts.isArrowFunction(expr) ||
|
||||
ts.isFunctionExpression(expr) ||
|
||||
ts.isStringLiteralLike(expr) ||
|
||||
ts.isNumericLiteral(expr) ||
|
||||
expr.kind === ts.SyntaxKind.TrueKeyword ||
|
||||
expr.kind === ts.SyntaxKind.FalseKeyword ||
|
||||
expr.kind === ts.SyntaxKind.NullKeyword ||
|
||||
literalSymbolCallOf(lowerer, expr) !== null
|
||||
);
|
||||
};
|
||||
const statements = sf.statements.nodes;
|
||||
const prefixInert = statements
|
||||
.slice(0, statements.indexOf(stmt))
|
||||
.every(
|
||||
(statement) =>
|
||||
(ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) ||
|
||||
ts.isFunctionDeclaration(statement) ||
|
||||
(ts.isVariableStatement(statement) &&
|
||||
statement.declarationList.declarations.nodes.every(
|
||||
(d) => ts.isIdentifier(d.name) && inert(d.initializer),
|
||||
)),
|
||||
);
|
||||
if (!prefixInert) return null;
|
||||
for (const property of table.properties.nodes) {
|
||||
if (property === decl) break;
|
||||
if (ts.isShorthandPropertyAssignment(property)) continue;
|
||||
if (
|
||||
!ts.isPropertyAssignment(property) ||
|
||||
ts.isComputedPropertyName(property.name) ||
|
||||
!inert(property.initializer)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
// Requirers can reach the property through a checker-synthesized symbol
|
||||
// (the export= type's member); the declaration's own symbol is the one
|
||||
// identity every requirer and the table's export global share.
|
||||
const canonical = lowerer.checker.getSymbolAtLocation(decl.name) ?? sym;
|
||||
if (tablePropertyWritten(lowerer, new Set([sym, canonical]), decl.name.text, sf)) return null;
|
||||
return { ...symbolKeyOf(lowerer, canonical, call.registered, call.description, true), sym };
|
||||
}
|
||||
|
||||
function tablePropertyWritten(
|
||||
lowerer: Lowerer,
|
||||
symbols: ReadonlySet<ts.Symbol>,
|
||||
name: string,
|
||||
home: ts.SourceFile,
|
||||
): boolean {
|
||||
let written = false;
|
||||
const targets = (node: ts.Node): boolean => {
|
||||
if (ts.isPropertyAccessExpression(node)) {
|
||||
if (node.name.text !== name) return false;
|
||||
const recv = node.expression;
|
||||
if (
|
||||
node.getSourceFile() === home &&
|
||||
((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv))
|
||||
)
|
||||
return true;
|
||||
const target = lowerer.checker.getSymbolAtLocation(node.name);
|
||||
return target !== undefined && symbols.has(target);
|
||||
}
|
||||
if (ts.isElementAccessExpression(node)) {
|
||||
const key = node.argumentExpression;
|
||||
if (!ts.isStringLiteralLike(key) || key.text !== name) return false;
|
||||
const target = lowerer.checker.getSymbolAtLocation(node);
|
||||
return target !== undefined && symbols.has(target);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
for (const sf of lowerer.program.getSourceFiles()) {
|
||||
if (written) break;
|
||||
if (sf.isDeclarationFile || !sf.text.includes(name)) continue;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (written) return "stop";
|
||||
let target: ts.Node | undefined;
|
||||
if (
|
||||
ts.isBinaryExpression(node) &&
|
||||
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
target = node.left;
|
||||
else if (ts.isDeleteExpression(node)) target = node.expression;
|
||||
else if (
|
||||
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
||||
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
||||
node.operator === ts.SyntaxKind.MinusMinusToken)
|
||||
)
|
||||
target = node.operand;
|
||||
if (target && targets(target)) {
|
||||
written = true;
|
||||
return "stop";
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
function bindingWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): boolean {
|
||||
@@ -245,3 +414,29 @@ function bindingWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): bo
|
||||
});
|
||||
return written;
|
||||
}
|
||||
|
||||
/** The layout type of a symbol-keyed field read through a CommonJS
|
||||
* key-table key (`this[kCount]` with `kCount` from a `module.exports = {
|
||||
* kCount: Symbol('count') }` table), or null. The checker types such keys
|
||||
* as plain `symbol`, so it answers the read with the union of every
|
||||
* symbol-keyed property of the class; the field's own type is the read's
|
||||
* type. Keys declared as consts keep their unique-symbol checker types. */
|
||||
export function tableKeyFieldType(
|
||||
lowerer: Lowerer,
|
||||
node: ts.ElementAccessExpression,
|
||||
): IrType | null {
|
||||
const key = node.argumentExpression;
|
||||
if (!ts.isIdentifier(key) && !ts.isPropertyAccessExpression(key)) return null;
|
||||
const keyType = lowerer.checker.getTypeAtLocation(key);
|
||||
if (!(keyType.flags & ts.TypeFlags.ESSymbol) || keyType.flags & ts.TypeFlags.UniqueESSymbol)
|
||||
return null;
|
||||
const resolved = classSymbolKeyOf(lowerer, key);
|
||||
if (!resolved) return null;
|
||||
const decl = resolved.identity.valueDeclaration;
|
||||
if (!decl || !ts.isPropertyAssignment(decl)) return null;
|
||||
const receiver = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(node.expression));
|
||||
if (receiver?.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(receiver.className);
|
||||
const field = info?.symbolFields?.get(resolved.identity);
|
||||
return field === undefined ? null : (info!.fields.get(field) ?? null);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
threadSlotsMemberOf,
|
||||
} from "../experimental-import.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { withUndefinedArm } from "../type-mapper.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { locOf } from "../program.js";
|
||||
@@ -238,10 +239,16 @@ export function lowerThreadSlotsCall(lowerer: Lowerer, expr: ts.CallExpression):
|
||||
const args = [name, receiver, key];
|
||||
switch (member) {
|
||||
case "get": {
|
||||
// The slots hold values in the layout of the type the constructor registered (`family.value`,
|
||||
// which `set` stores with). The receiver's annotation can name a structurally wider value
|
||||
// type, so the value read is converted to the call's type like any value flowing into it.
|
||||
const resultTs = lowerer.typeOf(expr);
|
||||
const result = lowerer.mapTypeOf(resultTs);
|
||||
if (result === null) lowerer.badType(expr, resultTs);
|
||||
return { kind: "intrinsic", name: "slots.get", args, type: result!, loc };
|
||||
const stored = withUndefinedArm(family.value, lowerer.unions);
|
||||
if (stored === null) lowerer.badType(expr, resultTs);
|
||||
const read: IrExpr = { kind: "intrinsic", name: "slots.get", args, type: stored!, loc };
|
||||
return typeKey(stored!) === typeKey(result!) ? read : lowerer.coerceInto(expr, read, result!);
|
||||
}
|
||||
case "has":
|
||||
case "delete":
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
* object; empty for mock.fn
|
||||
*
|
||||
* A MockFunctionCall<F> is { arguments, error, result, this }: the
|
||||
* arguments as a tuple of F's parameters, the result as F's return type or
|
||||
* undefined (a checked-dynamic value for functions returning void or
|
||||
* undefined, since a record field cannot have a unit type), and error and
|
||||
* this as checked-dynamic values. */
|
||||
* arguments as a tuple of F's parameters (a checked-dynamic array for
|
||||
* mock.fn()'s `(...args: any[]) => undefined`), the result as F's return
|
||||
* type or undefined (a checked-dynamic value for functions returning void
|
||||
* or undefined, since a record field cannot have a unit type), and error
|
||||
* and this as checked-dynamic values. */
|
||||
import { DYN, F64, NULL_T, UNDEFINED_T, VOID, typeKey, type IrType } from "../ir/ir.js";
|
||||
import type { TypeMapperCtx } from "./type-mapper.js";
|
||||
|
||||
@@ -34,11 +35,17 @@ function orUndefined(t: IrType, reg: MockShapeRegistries): IrType {
|
||||
}
|
||||
|
||||
/** The arguments type of a call record: a tuple of the parameters, or the
|
||||
* rest array itself for a function whose only parameter is a rest array.
|
||||
* Null for other rest shapes. A function without parameters records the
|
||||
* rest array itself for a function whose only parameter is a rest array,
|
||||
* or a checked-dynamic array for a function returning nothing whose only
|
||||
* parameter is a checked-dynamic rest (mock.fn()'s default, `(...args:
|
||||
* any[]) => undefined`). Null for other rest shapes. A function without parameters records the
|
||||
* empty tuple, which maps to an array of the unit-only union, as
|
||||
* type-mapper.ts maps `[]`. */
|
||||
function mockArgumentsType(fn: IrType & { kind: "func" }, reg: MockShapeRegistries): IrType | null {
|
||||
if (fn.rest && fn.restAbi === undefined) {
|
||||
const returnsNothing = fn.ret.kind === "void" || fn.ret.kind === "undefinedT";
|
||||
return fn.params.length === 0 && returnsNothing ? DYN : null;
|
||||
}
|
||||
if (fn.rest) {
|
||||
if (fn.params.length !== 1 || fn.params[0]!.kind !== "array") return null;
|
||||
return fn.params[0]!;
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { nodeFrontendServices } from "./services-node.js";
|
||||
import { rewriteBundlerCjsExports } from "./npm-static-rewrite.js";
|
||||
import type { BundledChunkSplit } from "./npm-static-bundled-chunks.js";
|
||||
|
||||
const keyedHelper = `var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
};
|
||||
`;
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function split(source: string, file = "/pkg/dist/index.js") {
|
||||
return nodeFrontendServices().splitCjsChunks(source, file);
|
||||
}
|
||||
|
||||
function run(file: string) {
|
||||
const result = spawnSync(process.execPath, [file], { encoding: "utf8" });
|
||||
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
||||
}
|
||||
|
||||
/** Runs the original bundle and its split modules side by side. */
|
||||
function expectSameBehavior(source: string): BundledChunkSplit {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-chunks-"));
|
||||
dirs.push(dir);
|
||||
const original = join(dir, "original.js");
|
||||
writeFileSync(original, source);
|
||||
const bundle = join(dir, "index.js");
|
||||
const result = split(source, bundle);
|
||||
expect(result).not.toBeNull();
|
||||
expect(result).not.toHaveProperty("degrade");
|
||||
const answer = result as BundledChunkSplit;
|
||||
writeFileSync(bundle, answer.text);
|
||||
for (const chunk of answer.chunks) writeFileSync(chunk.path, chunk.source);
|
||||
// Respelled calls and blanked declarations keep the line structure.
|
||||
expect(answer.text.split("\n")).toHaveLength(source.split("\n").length);
|
||||
const oracle = run(original);
|
||||
expect(oracle.status).toBe(0);
|
||||
expect(run(bundle)).toEqual(oracle);
|
||||
return answer;
|
||||
}
|
||||
|
||||
test("chunks run once, at their first require, with shared module instances", () => {
|
||||
const answer = expectSameBehavior(`"use strict";
|
||||
${keyedHelper}
|
||||
var require_b = __commonJS({
|
||||
"b.js"(exports2, module2) {
|
||||
console.log("b init");
|
||||
module2.exports = { n: 1 };
|
||||
}
|
||||
});
|
||||
var require_a = __commonJS({
|
||||
"a.js"(exports2) {
|
||||
console.log("a init");
|
||||
var b = require_b();
|
||||
exports2.value = b.n + 1;
|
||||
exports2.keys = Object.keys({ exports2 });
|
||||
}
|
||||
});
|
||||
console.log("start");
|
||||
console.log(require_a().value, require_a().value, require_b().n, require_a().keys);
|
||||
`);
|
||||
expect(answer.chunks.map((c) => basename(c.path))).toEqual([
|
||||
"index.js.require_b.cjs",
|
||||
"index.js.require_a.cjs",
|
||||
]);
|
||||
expect(answer.text).not.toContain("require_a()");
|
||||
});
|
||||
|
||||
test("the arrow helper form and minified spellings split the same way", () => {
|
||||
expectSameBehavior(
|
||||
`var __commonJS=(cb,mod)=>()=>(mod||cb((mod={exports:{}}).exports,mod),mod.exports);` +
|
||||
`var require_x=__commonJS((exports,module)=>{module.exports={v:"x"}});` +
|
||||
`console.log(require_x().v,require_x()===require_x());`,
|
||||
);
|
||||
});
|
||||
|
||||
test("esbuild 0.11's Object.keys spelling is recognized", () => {
|
||||
expectSameBehavior(`var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[Object.keys(cb)[0]])((mod = {exports: {}}).exports, mod), mod.exports;
|
||||
};
|
||||
var require_y = __commonJS({
|
||||
"y.js"(exports) {
|
||||
exports.y = 3;
|
||||
}
|
||||
});
|
||||
console.log(require_y().y);
|
||||
`);
|
||||
});
|
||||
|
||||
test("pure helpers a chunk reads travel with it", () => {
|
||||
const answer = expectSameBehavior(`${keyedHelper}var __defProp = Object.defineProperty;
|
||||
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
||||
var require_named = __commonJS({
|
||||
"named.js"(exports2, module2) {
|
||||
module2.exports = __name(function () {}, "renamed");
|
||||
}
|
||||
});
|
||||
console.log(require_named().name);
|
||||
`);
|
||||
const chunk = answer.chunks[0]!.source;
|
||||
expect(chunk).toContain("var __name =");
|
||||
expect(chunk).toContain("var __defProp = Object.defineProperty;");
|
||||
});
|
||||
|
||||
test("a helper travels with a chunk whose nested scopes declare its name", () => {
|
||||
const answer = expectSameBehavior(`${keyedHelper}function label(n) {
|
||||
return "label " + n;
|
||||
}
|
||||
function wrap(n) {
|
||||
function inner() {
|
||||
var label = "inner";
|
||||
return label;
|
||||
}
|
||||
return label(n) + " " + inner();
|
||||
}
|
||||
var require_e = __commonJS({
|
||||
"e.js"(exports2, module2) {
|
||||
function nested() {
|
||||
var label = "nested";
|
||||
return label;
|
||||
}
|
||||
var seen = [];
|
||||
for (let label of ["loop"]) seen.push(label);
|
||||
try {
|
||||
throw "caught";
|
||||
} catch (label) {
|
||||
seen.push(label);
|
||||
}
|
||||
{
|
||||
const label = "block";
|
||||
seen.push(label);
|
||||
}
|
||||
module2.exports = { outer: label(1), indirect: (0, label)(3), wrapped: wrap(2), nested: nested(), seen };
|
||||
}
|
||||
});
|
||||
var e = require_e();
|
||||
console.log(e.outer, e.indirect, e.wrapped, e.nested, e.seen.join(","));
|
||||
`);
|
||||
const chunk = answer.chunks[0]!.source;
|
||||
expect(chunk).toContain("function label(n)");
|
||||
expect(chunk).toContain("function wrap(n)");
|
||||
});
|
||||
|
||||
test("a named function expression that only calls itself travels with a chunk", () => {
|
||||
const answer = expectSameBehavior(`${keyedHelper}var fact = function self(n) {
|
||||
return n <= 1 ? 1 : n * self(n - 1);
|
||||
};
|
||||
var kind = function self(self) {
|
||||
return typeof self;
|
||||
};
|
||||
var outer = function self() {
|
||||
var self = 2;
|
||||
return self;
|
||||
};
|
||||
var require_g = __commonJS({
|
||||
"g.js"(exports2, module2) {
|
||||
module2.exports = [fact(5), kind(1), outer()];
|
||||
}
|
||||
});
|
||||
console.log(require_g().join(","));
|
||||
`);
|
||||
const chunk = answer.chunks[0]!.source;
|
||||
expect(chunk).toContain("var fact = function self(n)");
|
||||
});
|
||||
|
||||
test("a helper a parameter default reads travels with a chunk whose body declares its name", () => {
|
||||
const answer = expectSameBehavior(`${keyedHelper}var ten = function () {
|
||||
return 10;
|
||||
};
|
||||
var add = function (n, base = ten()) {
|
||||
var ten = 1;
|
||||
return n + base + ten;
|
||||
};
|
||||
var require_g = __commonJS({
|
||||
"g.js"(exports2, module2) {
|
||||
module2.exports = add(1);
|
||||
}
|
||||
});
|
||||
console.log(require_g());
|
||||
`);
|
||||
const chunk = answer.chunks[0]!.source;
|
||||
expect(chunk).toContain("var ten = function ()");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["exports it", `function tally() {}`, `module2.exports = { tally };`],
|
||||
["reads its properties", `function tally() {}`, `exports2.calls = tally.calls;`],
|
||||
["constructs it", `function tally() {}`, `exports2.made = new tally();`],
|
||||
["stores an arrow helper", `var tally = () => 1;`, `exports2.fn = tally;`],
|
||||
[
|
||||
"calls a function that reads its own name as a value",
|
||||
`var tally = function self() { self.n = (self.n || 0) + 1; return self.n; };`,
|
||||
`exports2.count = tally();`,
|
||||
],
|
||||
[
|
||||
"calls a function whose parameter default reads its own name",
|
||||
`var tally = function self(box = self) { box.n = (box.n || 0) + 1; return box.n; };`,
|
||||
`exports2.count = tally();`,
|
||||
],
|
||||
[
|
||||
"calls a function whose parameter default reads its own name past a body var of that name",
|
||||
`var tally = function self(box = self) { var self = 0; box.n = (box.n || 0) + 1; return box.n + self; };`,
|
||||
`exports2.count = tally();`,
|
||||
],
|
||||
])("a chunk that %s keeps the package refused", (_, declaration, statement) => {
|
||||
const result = split(`${keyedHelper}${declaration}
|
||||
var require_f = __commonJS({
|
||||
"f.js"(exports2, module2) {
|
||||
${statement}
|
||||
}
|
||||
});
|
||||
require_f();
|
||||
`);
|
||||
expect(result).toEqual({
|
||||
degrade: expect.stringContaining(
|
||||
"uses the bundle's top-level function 'tally' as a value",
|
||||
) as string,
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
["reads its factory's receiver", `console.log(this);`],
|
||||
["reads its factory's arguments object", `console.log(arguments.length);`],
|
||||
["returns from its top level", `if (exports2) return;`],
|
||||
["uses its module object beyond module.exports", `console.log(module2.id);`],
|
||||
["reads __filename", `console.log(__filename);`],
|
||||
["rebinds its module parameters", `function f(module2) { return module2; }`],
|
||||
["reads the bundle's top-level binding 'state'", `console.log(state);`],
|
||||
])("a chunk that %s keeps the package refused", (reason, statement) => {
|
||||
const result = split(`${keyedHelper}var state = { n: 1 };
|
||||
var require_c = __commonJS({
|
||||
"c.js"(exports2, module2) {
|
||||
${statement}
|
||||
}
|
||||
});
|
||||
require_c();
|
||||
`);
|
||||
expect(result).toEqual({ degrade: expect.stringContaining(reason) as string });
|
||||
});
|
||||
|
||||
test("a factory used other than as a direct call keeps the package refused", () => {
|
||||
const result =
|
||||
split(`${keyedHelper}var require_d = __commonJS({ "d.js"(exports2) { exports2.d = 1; } });
|
||||
var load = require_d;
|
||||
load();
|
||||
`);
|
||||
expect(result).toEqual({
|
||||
degrade: expect.stringContaining("used other than as a direct call") as string,
|
||||
});
|
||||
});
|
||||
|
||||
test("an unrecognized helper body keeps the package refused", () => {
|
||||
const result =
|
||||
split(`var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod);
|
||||
var require_e = __commonJS((exports) => { exports.e = 1; });
|
||||
require_e();
|
||||
`);
|
||||
expect(result).toEqual({ degrade: expect.stringContaining("__commonJS helper") as string });
|
||||
});
|
||||
|
||||
test("files without chunks and ES-module bundles are left alone", () => {
|
||||
expect(split("exports.x = 1;\n")).toBeNull();
|
||||
expect(
|
||||
split(`${keyedHelper}var require_f = __commonJS({ "f.js"(e) { e.f = 1; } });
|
||||
export default require_f();
|
||||
`),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("an identifier-minified bundle names its lost helper names", () => {
|
||||
// esbuild --minify output: the helper shapes survive, their names do not.
|
||||
const result = split(
|
||||
`var c=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports);` +
|
||||
`var x=c((t,m)=>{m.exports={v:1}});console.log(x().v);`,
|
||||
);
|
||||
expect(result).toEqual({ degrade: expect.stringContaining("identifier-minified") as string });
|
||||
});
|
||||
|
||||
test("an ES-module bundle of CommonJS chunks names the chunk interop", () => {
|
||||
const result = rewriteBundlerCjsExports(
|
||||
`${keyedHelper}var __toESM = (mod) => mod;
|
||||
var require_g = __commonJS({ "g.js"(exports) { exports.g = 1; } });
|
||||
var import_g = __toESM(require_g());
|
||||
export const g = import_g.g;
|
||||
`,
|
||||
"/pkg/dist/index.js",
|
||||
);
|
||||
expect(result).toEqual({
|
||||
degrade: expect.stringContaining("bundled CommonJS modules") as string,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,858 @@
|
||||
/* Bundled CommonJS chunks: esbuild wraps every CommonJS module it bundles
|
||||
* in a lazily evaluated factory,
|
||||
*
|
||||
* var require_x = __commonJS({ "src/x.js"(exports, module) { BODY } });
|
||||
*
|
||||
* and each `require_x()` runs BODY once (later calls answer the cached
|
||||
* module.exports, a call during BODY answers the partial one). That is
|
||||
* exactly a CommonJS module that the bundle requires at the call's
|
||||
* position, so each recognized chunk becomes its own module: a virtual
|
||||
* sibling file `<bundle>.<binding>.cjs` holding BODY, its module
|
||||
* parameters respelled as the CommonJS globals, and every `require_x()`
|
||||
* call becomes `require("./<bundle>.<binding>.cjs")`. The program then
|
||||
* compiles chunks with the ordinary CommonJS machinery: evaluation at the
|
||||
* require's position, the run-once module cache, export tables, and the
|
||||
* __toESM interop erasure.
|
||||
*
|
||||
* Only shapes whose meaning is verified move: the helper must be one of
|
||||
* the recognized __commonJS spellings, every reference to a factory
|
||||
* binding must be a direct call, and BODY must not observe the
|
||||
* difference between a factory and a module (its `this`, `arguments`,
|
||||
* `__filename`, the module object beyond `module.exports`, a top-level
|
||||
* return, or a binding of the bundle other than the pure helpers copied
|
||||
* beside it). Anything else answers a degrade reason. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import { helperTokens } from "./helper-tokens.js";
|
||||
|
||||
export interface BundledChunkSplit {
|
||||
/** The bundle with chunk declarations blanked and calls respelled. */
|
||||
text: string;
|
||||
/** Virtual sibling modules, in declaration order. */
|
||||
chunks: { path: string; source: string }[];
|
||||
}
|
||||
|
||||
interface Edit {
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const CHUNK_SHAPE_DEGRADE =
|
||||
"its shipped JS bundles CommonJS modules through the __commonJS helper in a shape the " +
|
||||
"recognizer cannot verify";
|
||||
|
||||
/** The helper a counter-suffixed bundler name spells (`__commonJS2`). */
|
||||
function helperBase(name: string): string {
|
||||
return /^__[A-Za-z]+\d+$/.test(name) ? name.replace(/\d+$/, "") : name;
|
||||
}
|
||||
|
||||
function unparen(e: ts.Expression): ts.Expression {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** True for an identifier occurrence that reads or writes a binding (as
|
||||
* opposed to a property name, a label, or a declaration's own name).
|
||||
* Shorthand property values count: `{ x }` reads x. */
|
||||
function isReference(id: ts.Identifier): boolean {
|
||||
const p = id.parent;
|
||||
if (p === undefined) return false;
|
||||
if (ts.isPropertyAccessExpression(p)) return p.expression === id;
|
||||
if (ts.isPropertyAssignment(p)) return p.initializer === id;
|
||||
if (
|
||||
ts.isMethodDeclaration(p) ||
|
||||
ts.isPropertyDeclaration(p) ||
|
||||
ts.isGetAccessorDeclaration(p) ||
|
||||
ts.isSetAccessorDeclaration(p)
|
||||
)
|
||||
return p.name !== id;
|
||||
if (ts.isBindingElement(p)) return p.initializer === id;
|
||||
if (ts.isVariableDeclaration(p) || ts.isParameter(p)) return p.initializer === id;
|
||||
if (
|
||||
ts.isFunctionDeclaration(p) ||
|
||||
ts.isFunctionExpression(p) ||
|
||||
ts.isClassDeclaration(p) ||
|
||||
ts.isClassExpression(p)
|
||||
)
|
||||
return p.name !== id;
|
||||
if (ts.isLabeledStatement(p)) return p.label !== id;
|
||||
if (ts.isBreakStatement(p) || ts.isContinueStatement(p)) return false;
|
||||
if (ts.isMetaProperty(p)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Names a subtree declares anywhere inside it: variables (destructuring
|
||||
* included), parameters, function and class names, catch bindings. */
|
||||
function declaredNames(root: ts.Node): Set<string> {
|
||||
const names = new Set<string>();
|
||||
// Binding patterns nest BindingElements, which the walk visits too.
|
||||
ts.walkPreorder(root, (n) => {
|
||||
if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode)
|
||||
return "skip";
|
||||
if (ts.isVariableDeclaration(n) || ts.isParameter(n) || ts.isBindingElement(n)) {
|
||||
const name = n.name;
|
||||
if (name !== undefined && ts.isIdentifier(name)) names.add(name.text);
|
||||
} else if (
|
||||
(ts.isFunctionDeclaration(n) ||
|
||||
ts.isFunctionExpression(n) ||
|
||||
ts.isClassDeclaration(n) ||
|
||||
ts.isClassExpression(n)) &&
|
||||
n.name !== undefined
|
||||
)
|
||||
names.add(n.name.text);
|
||||
return undefined;
|
||||
});
|
||||
return names;
|
||||
}
|
||||
|
||||
/** Adds the identifiers a binding name (an identifier or a destructuring
|
||||
* pattern) declares. */
|
||||
function addBindingNames(name: ts.BindingName, out: Set<string>): void {
|
||||
if (ts.isIdentifier(name)) {
|
||||
out.add(name.text);
|
||||
return;
|
||||
}
|
||||
for (const element of name.elements.nodes) {
|
||||
if (!ts.isOmittedExpression(element) && element.name !== undefined)
|
||||
addBindingNames(element.name, out);
|
||||
}
|
||||
}
|
||||
|
||||
function isFunctionScope(n: ts.Node): n is ts.SignatureDeclaration {
|
||||
return (
|
||||
ts.isFunctionDeclaration(n) ||
|
||||
ts.isFunctionExpression(n) ||
|
||||
ts.isArrowFunction(n) ||
|
||||
ts.isMethodDeclaration(n) ||
|
||||
ts.isGetAccessorDeclaration(n) ||
|
||||
ts.isSetAccessorDeclaration(n) ||
|
||||
ts.isConstructorDeclaration(n)
|
||||
);
|
||||
}
|
||||
|
||||
/** The `var` names a function body declares, without entering nested
|
||||
* functions. Function declarations in nested blocks count as block-scoped
|
||||
* here; treating one as free only adds an unused helper copy. */
|
||||
function varScopedNames(body: ts.Node): Set<string> {
|
||||
const out = new Set<string>();
|
||||
ts.walkPreorder(body, (n) => {
|
||||
if (n !== body && (isFunctionScope(n) || ts.isClassDeclaration(n) || ts.isClassExpression(n)))
|
||||
return "skip";
|
||||
if (ts.isVariableDeclarationList(n) && (n.flags & ts.NodeFlags.BlockScoped) === 0) {
|
||||
for (const d of n.declarations.nodes) addBindingNames(d.name, out);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The names a function binds for references in its parameter list
|
||||
* (default values, computed keys of destructuring patterns): its
|
||||
* parameters and a function expression's own name. Parameter initializers
|
||||
* run in their own scope, so the body's `var`s are not visible there. */
|
||||
function parameterScopeNames(scope: ts.SignatureDeclaration): Set<string> {
|
||||
const out = new Set<string>();
|
||||
for (const param of scope.parameters.nodes) addBindingNames(param.name, out);
|
||||
if (ts.isFunctionExpression(scope) && scope.name !== undefined) out.add(scope.name.text);
|
||||
return out;
|
||||
}
|
||||
|
||||
const NO_NAMES: ReadonlySet<string> = new Set();
|
||||
|
||||
/** True when `node` lies inside `ancestor` (or is it). */
|
||||
function within(node: ts.Node, ancestor: ts.Node): boolean {
|
||||
for (let n: ts.Node | undefined = node; n !== undefined; n = n.parent)
|
||||
if (n === ancestor) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The names one scope node binds for the references inside it. */
|
||||
function scopeNames(scope: ts.Node): Set<string> {
|
||||
const out = new Set<string>();
|
||||
if (isFunctionScope(scope)) {
|
||||
for (const param of scope.parameters.nodes) addBindingNames(param.name, out);
|
||||
if (ts.isFunctionExpression(scope) && scope.name !== undefined) out.add(scope.name.text);
|
||||
if (scope.body !== undefined) for (const name of varScopedNames(scope.body)) out.add(name);
|
||||
} else if (ts.isBlock(scope) || ts.isSourceFile(scope) || ts.isCaseBlock(scope)) {
|
||||
const statements = ts.isCaseBlock(scope)
|
||||
? scope.clauses.nodes.flatMap((clause) => clause.statements.nodes)
|
||||
: scope.statements.nodes;
|
||||
for (const stmt of statements) {
|
||||
if ((ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) && stmt.name)
|
||||
out.add(stmt.name.text);
|
||||
else if (
|
||||
ts.isVariableStatement(stmt) &&
|
||||
(stmt.declarationList.flags & ts.NodeFlags.BlockScoped) !== 0
|
||||
) {
|
||||
for (const d of stmt.declarationList.declarations.nodes) addBindingNames(d.name, out);
|
||||
}
|
||||
}
|
||||
} else if (ts.isCatchClause(scope) && scope.variableDeclaration !== undefined) {
|
||||
addBindingNames(scope.variableDeclaration.name, out);
|
||||
} else if (
|
||||
(ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) &&
|
||||
scope.initializer !== undefined &&
|
||||
ts.isVariableDeclarationList(scope.initializer) &&
|
||||
(scope.initializer.flags & ts.NodeFlags.BlockScoped) !== 0
|
||||
) {
|
||||
for (const d of scope.initializer.declarations.nodes) addBindingNames(d.name, out);
|
||||
} else if (
|
||||
(ts.isClassDeclaration(scope) || ts.isClassExpression(scope)) &&
|
||||
scope.name !== undefined
|
||||
) {
|
||||
out.add(scope.name.text);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Resolves references against one bundle's scopes, keeping each scope's
|
||||
* names for the bundle's split. */
|
||||
class BundleScopes {
|
||||
private readonly names = new Map<ts.Node, Set<string>>();
|
||||
private readonly parameterNames = new Map<ts.Node, Set<string>>();
|
||||
|
||||
private of(scope: ts.Node): Set<string> {
|
||||
let names = this.names.get(scope);
|
||||
if (names === undefined) {
|
||||
names = scopeNames(scope);
|
||||
this.names.set(scope, names);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** The names `scope` binds for a reference reached through its child
|
||||
* `from`: a function's parameter list sees its parameters and own name
|
||||
* but not its body's `var`s, and a computed method name sees none of
|
||||
* them. */
|
||||
private visible(scope: ts.Node, from: ts.Node): ReadonlySet<string> {
|
||||
if (!isFunctionScope(scope) || from === scope.body) return this.of(scope);
|
||||
if (from === scope.name) return NO_NAMES;
|
||||
let names = this.parameterNames.get(scope);
|
||||
if (names === undefined) {
|
||||
names = parameterScopeNames(scope);
|
||||
this.parameterNames.set(scope, names);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/** True when a scope between the reference and `root` (inclusive)
|
||||
* binds its name. */
|
||||
boundWithin(id: ts.Identifier, root: ts.Node): boolean {
|
||||
for (let from: ts.Node = id, n = id.parent; n !== undefined; from = n, n = n.parent) {
|
||||
if (this.visible(n, from).has(id.text)) return true;
|
||||
if (n === root) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True when a scope inside `root` (exclusive) binds the reference's
|
||||
* name. */
|
||||
boundInside(id: ts.Identifier, root: ts.Node): boolean {
|
||||
for (
|
||||
let from: ts.Node = id, n = id.parent;
|
||||
n !== undefined && n !== root;
|
||||
from = n, n = n.parent
|
||||
) {
|
||||
if (this.visible(n, from).has(id.text)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True when the reference resolves to a top-level binding of `sf`. */
|
||||
topLevel(id: ts.Identifier, sf: ts.SourceFile): boolean {
|
||||
for (
|
||||
let from: ts.Node = id, n = id.parent;
|
||||
n !== undefined && n !== sf;
|
||||
from = n, n = n.parent
|
||||
) {
|
||||
if (this.visible(n, from).has(id.text)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Every binding reference inside a subtree. */
|
||||
function references(root: ts.Node): ts.Identifier[] {
|
||||
const out: ts.Identifier[] = [];
|
||||
ts.walkPreorder(root, (n) => {
|
||||
if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode)
|
||||
return "skip";
|
||||
if (ts.isIdentifier(n) && isReference(n)) out.push(n);
|
||||
return undefined;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** True when the reference is written (assignment target, update operand,
|
||||
* loop target, delete operand). */
|
||||
function isWritten(id: ts.Identifier): boolean {
|
||||
let child: ts.Node = id;
|
||||
for (let p = child.parent; p !== undefined; child = p, p = p.parent) {
|
||||
if (ts.isBinaryExpression(p)) {
|
||||
return (
|
||||
p.left === child &&
|
||||
p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
p.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
||||
);
|
||||
}
|
||||
if (ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) {
|
||||
return (
|
||||
p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken
|
||||
);
|
||||
}
|
||||
if (ts.isForInStatement(p) || ts.isForOfStatement(p)) return p.initializer === child;
|
||||
if (ts.isDeleteExpression(p)) return true;
|
||||
if (
|
||||
!(
|
||||
ts.isParenthesizedExpression(p) ||
|
||||
ts.isArrayLiteralExpression(p) ||
|
||||
ts.isObjectLiteralExpression(p) ||
|
||||
ts.isShorthandPropertyAssignment(p) ||
|
||||
ts.isPropertyAssignment(p) ||
|
||||
ts.isSpreadElement(p)
|
||||
)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The recognized __commonJS helper forms, as token templates over the
|
||||
* helper's own parameter names (CB, MOD) and the own-key reader (KEYS:
|
||||
* esbuild's `__getOwnPropNames` alias, or `Object.keys` in esbuild 0.11):
|
||||
* keyed: `(CB, MOD) => function [name]() { return <keyed>; }` — the
|
||||
* factory is the only method of an object literal;
|
||||
* arrow: `(CB, MOD) => () => (<arrow>)` — the factory itself. */
|
||||
const KEYED_RETURN =
|
||||
"MOD || (0, CB[KEYS(CB)[0]])((MOD = { exports: {} }).exports, MOD), MOD.exports";
|
||||
const ARROW_BODY = "(MOD || CB((MOD = { exports: {} }).exports, MOD), MOD.exports)";
|
||||
|
||||
function template(text: string, cb: string, mod: string, keys: string): string | null {
|
||||
return helperTokens(
|
||||
text
|
||||
.replace(/\bCB\b/g, cb)
|
||||
.replace(/\bMOD\b/g, mod)
|
||||
.replace(/\bKEYS\b/g, keys),
|
||||
);
|
||||
}
|
||||
|
||||
type HelperForm = "keyed" | "arrow";
|
||||
|
||||
/** The helper's form, or null when its initializer is not recognized.
|
||||
* `ownNames` answers whether an identifier names the verified
|
||||
* Object.getOwnPropertyNames alias. */
|
||||
function helperForm(
|
||||
init: ts.Expression,
|
||||
sf: ts.SourceFile,
|
||||
ownNames: (name: string) => boolean,
|
||||
): HelperForm | null {
|
||||
const fn = unparen(init);
|
||||
if (!ts.isArrowFunction(fn) || fn.parameters.nodes.length !== 2) return null;
|
||||
const params = fn.parameters.nodes.map((p) =>
|
||||
ts.isIdentifier(p.name) && p.initializer === undefined && p.dotDotDotToken === undefined
|
||||
? p.name.text
|
||||
: null,
|
||||
);
|
||||
const [cb, mod] = params;
|
||||
if (cb === null || mod === null || cb === undefined || mod === undefined || cb === mod)
|
||||
return null;
|
||||
if (ts.isBlock(fn.body)) return null;
|
||||
const body = unparen(fn.body);
|
||||
if (ts.isFunctionExpression(body)) {
|
||||
if (body.parameters.nodes.length !== 0 || body.asteriskToken || body.modifiers?.nodes.length)
|
||||
return null;
|
||||
const stmts = body.body.statements.nodes;
|
||||
const ret = stmts[0];
|
||||
if (stmts.length !== 1 || ret === undefined || !ts.isReturnStatement(ret) || !ret.expression)
|
||||
return null;
|
||||
const actual = helperTokens(ret.expression.getText(sf));
|
||||
if (actual === null) return null;
|
||||
// The own-key reader: esbuild's alias or the 0.11 spelling.
|
||||
const keysRef = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(ret.expression.getText(sf));
|
||||
const keys = keysRef?.[1];
|
||||
if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null;
|
||||
return actual === template(KEYED_RETURN, cb, mod, keys) ? "keyed" : null;
|
||||
}
|
||||
if (ts.isArrowFunction(body) && body.parameters.nodes.length === 0 && !ts.isBlock(body.body)) {
|
||||
const actual = helperTokens(body.body.getText(sf));
|
||||
return actual !== null && actual === template(ARROW_BODY, cb, mod, "") ? "arrow" : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A top-level declaration the virtual chunk can copy verbatim: a pure
|
||||
* helper whose evaluation runs nothing (a function, or a member read of
|
||||
* Object such as `Object.create` / `Object.prototype.hasOwnProperty`). */
|
||||
function copyableInitializer(init: ts.Expression | undefined): boolean {
|
||||
if (init === undefined) return false;
|
||||
const e = unparen(init);
|
||||
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) return true;
|
||||
let walk: ts.Expression = e;
|
||||
let depth = 0;
|
||||
while (ts.isPropertyAccessExpression(walk) && ts.isIdentifier(walk.name)) {
|
||||
walk = walk.expression;
|
||||
depth++;
|
||||
}
|
||||
return depth > 0 && ts.isIdentifier(walk) && walk.text === "Object";
|
||||
}
|
||||
|
||||
/** A top-level declaration whose value is a function. */
|
||||
function isFunctionValued(node: ts.VariableDeclaration | ts.FunctionDeclaration): boolean {
|
||||
if (ts.isFunctionDeclaration(node)) return true;
|
||||
const init = node.initializer && unparen(node.initializer);
|
||||
return init !== undefined && (ts.isArrowFunction(init) || ts.isFunctionExpression(init));
|
||||
}
|
||||
|
||||
/** True when a named function expression initializer (`var f = function
|
||||
* self() {}`) uses its own name only as a callee: `self` is the copied
|
||||
* function, so it is as observable as the top-level binding. */
|
||||
function selfCalledOnly(
|
||||
node: ts.VariableDeclaration | ts.FunctionDeclaration,
|
||||
scopes: BundleScopes,
|
||||
): boolean {
|
||||
if (!ts.isVariableDeclaration(node) || node.initializer === undefined) return true;
|
||||
const fn = unparen(node.initializer);
|
||||
if (!ts.isFunctionExpression(fn) || fn.name === undefined || fn.body === undefined) return true;
|
||||
const self = fn.name.text;
|
||||
// A parameter of the same name hides the function's own name everywhere
|
||||
// in it; a `var` of the same name hides it only in the body, since the
|
||||
// parameter initializers do not see the body's `var`s.
|
||||
if (fn.parameters.nodes.some((p) => bindsName(p.name, self))) return true;
|
||||
const bodyVar = varScopedNames(fn.body).has(self);
|
||||
const body = fn.body;
|
||||
return references(fn).every(
|
||||
(r) =>
|
||||
r.text !== self || scopes.boundInside(r, fn) || (bodyVar && within(r, body)) || isCallee(r),
|
||||
);
|
||||
}
|
||||
|
||||
/** True when a binding name (an identifier or a destructuring pattern)
|
||||
* declares `name`. */
|
||||
function bindsName(binding: ts.BindingName, name: string): boolean {
|
||||
const names = new Set<string>();
|
||||
addBindingNames(binding, names);
|
||||
return names.has(name);
|
||||
}
|
||||
|
||||
/** True when a reference is the function a call invokes: `f(...)`, or the
|
||||
* `(0, f)(...)` form that drops the receiver. */
|
||||
function isCallee(id: ts.Identifier): boolean {
|
||||
let callee: ts.Node = id;
|
||||
const p = id.parent;
|
||||
if (
|
||||
p !== undefined &&
|
||||
ts.isBinaryExpression(p) &&
|
||||
p.operatorToken.kind === ts.SyntaxKind.CommaToken &&
|
||||
p.right === id &&
|
||||
p.parent !== undefined &&
|
||||
ts.isParenthesizedExpression(p.parent)
|
||||
)
|
||||
callee = p.parent;
|
||||
while (callee.parent !== undefined && ts.isParenthesizedExpression(callee.parent))
|
||||
callee = callee.parent;
|
||||
const call = callee.parent;
|
||||
return call !== undefined && ts.isCallExpression(call) && call.expression === callee;
|
||||
}
|
||||
|
||||
/** One recognized chunk declaration. */
|
||||
interface Chunk {
|
||||
name: string;
|
||||
stmt: ts.VariableStatement;
|
||||
params: string[];
|
||||
body: ts.Block;
|
||||
path: string;
|
||||
spec: string;
|
||||
}
|
||||
|
||||
interface TopLevel {
|
||||
stmt: ts.Statement;
|
||||
/** The declarator (or function declaration) itself. */
|
||||
node: ts.VariableDeclaration | ts.FunctionDeclaration;
|
||||
}
|
||||
|
||||
/** True when the file opens with a "use strict" directive prologue. */
|
||||
function strictPrologue(stmts: readonly ts.Statement[]): boolean {
|
||||
for (const s of stmts) {
|
||||
if (!ts.isExpressionStatement(s) || !ts.isStringLiteral(s.expression)) return false;
|
||||
if (s.expression.text === "use strict") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A cheap text probe: the helper's name, or its minified body's
|
||||
* `(m={exports:{}}).exports,m` fragment. */
|
||||
export function mayBundleChunks(source: string): boolean {
|
||||
return (
|
||||
source.includes("__commonJS") ||
|
||||
/\{\s*exports\s*:\s*\{\s*\}\s*\}\s*\)\s*\.exports\s*,/.test(source)
|
||||
);
|
||||
}
|
||||
|
||||
/** Splits a CommonJS bundle's __commonJS chunks into virtual modules (see
|
||||
* the header). Null: no chunks (or not a CommonJS bundle) — serve the file
|
||||
* unchanged. */
|
||||
export function splitBundledChunks(
|
||||
sf: ts.SourceFile,
|
||||
filePath: string,
|
||||
): BundledChunkSplit | { degrade: string } | null {
|
||||
const source = sf.text;
|
||||
if (!mayBundleChunks(source)) return null;
|
||||
// ES-module bundles have no require: their chunks keep the existing
|
||||
// treatment (the Bun factory recognizer, or the ESM interop refusal).
|
||||
if (!filePath.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
|
||||
|
||||
const top = new Map<string, TopLevel>();
|
||||
const duplicate = new Set<string>();
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
const add = (name: string, node: TopLevel["node"]): void => {
|
||||
if (top.has(name)) duplicate.add(name);
|
||||
else top.set(name, { stmt, node });
|
||||
};
|
||||
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined) add(stmt.name.text, stmt);
|
||||
else if (ts.isVariableStatement(stmt)) {
|
||||
for (const d of stmt.declarationList.declarations.nodes) {
|
||||
if (ts.isIdentifier(d.name)) add(d.name.text, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
const helperNames = [...top.keys()].filter((n) => helperBase(n) === "__commonJS");
|
||||
|
||||
// Every name a recognizer relies on is written nowhere and declared
|
||||
// once at the top level; `Object` itself is never rebound.
|
||||
const allRefs = references(sf);
|
||||
const written = new Set<string>();
|
||||
for (const r of allRefs) if (isWritten(r)) written.add(r.text);
|
||||
const declaredAnywhere = declaredNames(sf);
|
||||
if (declaredAnywhere.has("Object") || written.has("Object"))
|
||||
return { degrade: CHUNK_SHAPE_DEGRADE };
|
||||
const stable = (name: string): boolean => !duplicate.has(name) && !written.has(name);
|
||||
// Names whose every reference in the bundle is a callee: `f(...)` or the
|
||||
// `(0, f)(...)` form.
|
||||
const scopes = new BundleScopes();
|
||||
const valueUsed = new Set<string>();
|
||||
for (const r of allRefs) if (!isCallee(r) && scopes.topLevel(r, sf)) valueUsed.add(r.text);
|
||||
const calledOnly = (name: string): boolean => !valueUsed.has(name);
|
||||
const ownNames = (name: string): boolean => {
|
||||
const t = top.get(name);
|
||||
if (t === undefined || !stable(name) || !ts.isVariableDeclaration(t.node)) return false;
|
||||
const init = t.node.initializer;
|
||||
return (
|
||||
init !== undefined &&
|
||||
helperTokens(init.getText(sf)) === helperTokens("Object.getOwnPropertyNames")
|
||||
);
|
||||
};
|
||||
if (helperNames.length === 0) {
|
||||
// The helper's shape under another name: an identifier-minified bundle,
|
||||
// whose other interop helpers lost their names as well.
|
||||
for (const [name, t] of top) {
|
||||
const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined;
|
||||
if (init !== undefined && stable(name) && helperForm(init, sf, ownNames) !== null)
|
||||
return {
|
||||
degrade:
|
||||
"its shipped JS is an identifier-minified bundle whose bundler-interop helpers " +
|
||||
"(__commonJS and the rest) have lost the names the recognizers verify",
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const forms = new Map<string, HelperForm>();
|
||||
for (const name of helperNames) {
|
||||
const t = top.get(name)!;
|
||||
const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined;
|
||||
const form = init !== undefined && stable(name) ? helperForm(init, sf, ownNames) : null;
|
||||
if (form === null) return { degrade: CHUNK_SHAPE_DEGRADE };
|
||||
forms.set(name, form);
|
||||
}
|
||||
|
||||
// The chunk declarations: `var NAME = HELPER(factory);`, one declarator.
|
||||
const chunks = new Map<string, Chunk>();
|
||||
const base = filePath.replace(/^.*[\\/]/, "");
|
||||
for (const [name, t] of top) {
|
||||
if (!ts.isVariableDeclaration(t.node) || t.node.initializer === undefined) continue;
|
||||
const call = unparen(t.node.initializer);
|
||||
if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) continue;
|
||||
const form = forms.get(call.expression.text);
|
||||
if (form === undefined) continue;
|
||||
const stmt = t.stmt;
|
||||
if (
|
||||
!ts.isVariableStatement(stmt) ||
|
||||
stmt.declarationList.declarations.nodes.length !== 1 ||
|
||||
call.arguments.nodes.length !== 1 ||
|
||||
!stable(name)
|
||||
)
|
||||
return { degrade: CHUNK_SHAPE_DEGRADE };
|
||||
let factory: ts.Node = call.arguments.nodes[0]!;
|
||||
if (form === "keyed") {
|
||||
if (!ts.isObjectLiteralExpression(factory) || factory.properties.nodes.length !== 1)
|
||||
return { degrade: CHUNK_SHAPE_DEGRADE };
|
||||
factory = factory.properties.nodes[0]!;
|
||||
if (!ts.isMethodDeclaration(factory) || ts.isComputedPropertyName(factory.name))
|
||||
return { degrade: CHUNK_SHAPE_DEGRADE };
|
||||
} else {
|
||||
factory = unparen(factory as ts.Expression);
|
||||
if (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory))
|
||||
return { degrade: CHUNK_SHAPE_DEGRADE };
|
||||
}
|
||||
const fn = factory as ts.MethodDeclaration | ts.ArrowFunction | ts.FunctionExpression;
|
||||
if (
|
||||
fn.body === undefined ||
|
||||
!ts.isBlock(fn.body) ||
|
||||
fn.asteriskToken !== undefined ||
|
||||
(fn.modifiers?.nodes.length ?? 0) > 0 ||
|
||||
fn.parameters.nodes.length > 2
|
||||
)
|
||||
return { degrade: CHUNK_SHAPE_DEGRADE };
|
||||
const params: string[] = [];
|
||||
for (const p of fn.parameters.nodes) {
|
||||
if (!ts.isIdentifier(p.name) || p.initializer !== undefined || p.dotDotDotToken)
|
||||
return { degrade: CHUNK_SHAPE_DEGRADE };
|
||||
params.push(p.name.text);
|
||||
}
|
||||
const file = `${base}.${name}.cjs`;
|
||||
chunks.set(name, {
|
||||
name,
|
||||
stmt,
|
||||
params,
|
||||
body: fn.body,
|
||||
path: filePath.slice(0, filePath.length - base.length) + file,
|
||||
spec: `./${file}`,
|
||||
});
|
||||
}
|
||||
if (chunks.size === 0) return null;
|
||||
|
||||
// Every factory reference is a direct zero-argument call. Calls inside a
|
||||
// chunk body belong to that chunk's text, the rest to the bundle's.
|
||||
const callEdits: Edit[] = [];
|
||||
for (const r of allRefs) {
|
||||
const chunk = chunks.get(r.text);
|
||||
if (chunk === undefined) continue;
|
||||
const call = r.parent;
|
||||
if (
|
||||
call === undefined ||
|
||||
!ts.isCallExpression(call) ||
|
||||
call.expression !== r ||
|
||||
call.arguments.nodes.length !== 0 ||
|
||||
call.questionDotToken !== undefined
|
||||
)
|
||||
return {
|
||||
degrade: `its bundled CommonJS module factory '${r.text}' is used other than as a direct call`,
|
||||
};
|
||||
callEdits.push({
|
||||
start: call.getStart(sf),
|
||||
end: call.getEnd(),
|
||||
text: `require(${JSON.stringify(chunk.spec)})`,
|
||||
});
|
||||
}
|
||||
for (const name of chunks.keys()) {
|
||||
if (declaredAnywhere.has(name) && countDeclarations(sf, name) > 1)
|
||||
return { degrade: CHUNK_SHAPE_DEGRADE };
|
||||
}
|
||||
|
||||
const bundleStrict = strictPrologue(sf.statements.nodes);
|
||||
const out: BundledChunkSplit["chunks"] = [];
|
||||
const bundleEdits: Edit[] = [...callEdits];
|
||||
for (const chunk of chunks.values()) {
|
||||
const result = chunkSource(
|
||||
sf,
|
||||
chunk,
|
||||
chunks,
|
||||
top,
|
||||
stable,
|
||||
calledOnly,
|
||||
scopes,
|
||||
callEdits,
|
||||
bundleStrict,
|
||||
);
|
||||
if (typeof result !== "string") return result;
|
||||
out.push({ path: chunk.path, source: result });
|
||||
bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" });
|
||||
}
|
||||
return { text: applyEdits(source, 0, source.length, bundleEdits), chunks: out };
|
||||
}
|
||||
|
||||
function countDeclarations(sf: ts.SourceFile, name: string): number {
|
||||
let n = 0;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
(ts.isVariableDeclaration(node) ||
|
||||
ts.isParameter(node) ||
|
||||
ts.isBindingElement(node) ||
|
||||
ts.isFunctionDeclaration(node) ||
|
||||
ts.isFunctionExpression(node) ||
|
||||
ts.isClassDeclaration(node) ||
|
||||
ts.isClassExpression(node)) &&
|
||||
node.name !== undefined &&
|
||||
ts.isIdentifier(node.name) &&
|
||||
node.name.text === name
|
||||
)
|
||||
n++;
|
||||
return undefined;
|
||||
});
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Applies non-overlapping edits inside [from, to): replaced spans keep
|
||||
* their newlines (blanked text stays line-aligned); edits nested in a
|
||||
* blanked span vanish with it. */
|
||||
function applyEdits(source: string, from: number, to: number, edits: Edit[]): string {
|
||||
const inside = edits
|
||||
.filter((e) => e.start >= from && e.end <= to)
|
||||
.sort((a, b) => a.start - b.start || b.end - a.end);
|
||||
let text = "";
|
||||
let at = from;
|
||||
for (const e of inside) {
|
||||
if (e.start < at) continue; // nested in an earlier edit
|
||||
text += source.slice(at, e.start);
|
||||
const replaced = source.slice(e.start, e.end);
|
||||
// A respelled span keeps its line breaks after the replacement.
|
||||
text +=
|
||||
e.text === "" ? replaced.replace(/[^\r\n]/g, " ") : e.text + replaced.replace(/[^\n]/g, "");
|
||||
at = e.end;
|
||||
}
|
||||
return text + source.slice(at, to);
|
||||
}
|
||||
|
||||
/** The virtual module text for one chunk, or a degrade reason. */
|
||||
function chunkSource(
|
||||
sf: ts.SourceFile,
|
||||
chunk: Chunk,
|
||||
chunks: ReadonlyMap<string, Chunk>,
|
||||
top: ReadonlyMap<string, TopLevel>,
|
||||
stable: (name: string) => boolean,
|
||||
calledOnly: (name: string) => boolean,
|
||||
scopes: BundleScopes,
|
||||
callEdits: readonly Edit[],
|
||||
bundleStrict: boolean,
|
||||
): string | { degrade: string } {
|
||||
const why = (reason: string): { degrade: string } => ({
|
||||
degrade: `its bundled CommonJS module '${chunk.name}' ${reason}`,
|
||||
});
|
||||
const [exportsParam, moduleParam] = chunk.params;
|
||||
const body = chunk.body;
|
||||
const local = declaredNames(body);
|
||||
for (const p of chunk.params) {
|
||||
if (
|
||||
local.has(p) ||
|
||||
(p !== "exports" && local.has("exports")) ||
|
||||
(p !== "module" && local.has("module"))
|
||||
)
|
||||
return why(
|
||||
"rebinds its module parameters inside its body (an identifier-minified bundle the recognizer cannot respell)",
|
||||
);
|
||||
}
|
||||
|
||||
/* The factory's own frame must stay unobservable: its receiver, its
|
||||
* arguments object, and a top-level return have no module equivalent. */
|
||||
let frameUse: string | null = null;
|
||||
/** The innermost frame boundary between a node and the chunk body. */
|
||||
const frameOf = (n: ts.Node): "factory" | "arrow" | "function" => {
|
||||
let arrow = false;
|
||||
for (let p = n.parent; p !== undefined && p !== body; p = p.parent) {
|
||||
if (
|
||||
ts.isFunctionDeclaration(p) ||
|
||||
ts.isFunctionExpression(p) ||
|
||||
ts.isMethodDeclaration(p) ||
|
||||
ts.isGetAccessorDeclaration(p) ||
|
||||
ts.isSetAccessorDeclaration(p) ||
|
||||
ts.isConstructorDeclaration(p) ||
|
||||
ts.isClassDeclaration(p) ||
|
||||
ts.isClassExpression(p)
|
||||
)
|
||||
return "function";
|
||||
if (ts.isArrowFunction(p)) arrow = true;
|
||||
}
|
||||
return arrow ? "arrow" : "factory";
|
||||
};
|
||||
ts.walkPreorder(body, (n) => {
|
||||
if (frameUse !== null) return "stop";
|
||||
if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode)
|
||||
return "skip";
|
||||
let use: string | null = null;
|
||||
if (n.kind === ts.SyntaxKind.ThisKeyword) use = "reads its factory's receiver (`this`)";
|
||||
else if (n.kind === ts.SyntaxKind.SuperKeyword) use = "reads `super` at its top level";
|
||||
else if (ts.isIdentifier(n) && n.text === "arguments" && isReference(n))
|
||||
use = "reads its factory's arguments object";
|
||||
else if (ts.isMetaProperty(n)) use = "reads a meta property at its top level";
|
||||
else if (ts.isReturnStatement(n)) {
|
||||
if (frameOf(n) === "factory") frameUse = "returns from its top level";
|
||||
return undefined;
|
||||
}
|
||||
if (use !== null && frameOf(n) !== "function") frameUse = use;
|
||||
return undefined;
|
||||
});
|
||||
if (frameUse !== null) return why(frameUse);
|
||||
|
||||
const edits: Edit[] = callEdits.filter((e) => e.start >= body.pos && e.end <= body.end);
|
||||
const copied = new Set<string>();
|
||||
const copyQueue: string[] = [];
|
||||
for (const r of references(body)) {
|
||||
const name = r.text;
|
||||
if (chunks.has(name)) continue; // a call edit already respells it
|
||||
if (name === exportsParam || name === moduleParam) {
|
||||
if (name === moduleParam) {
|
||||
const p = r.parent;
|
||||
if (!ts.isPropertyAccessExpression(p) || p.expression !== r || p.name.text !== "exports")
|
||||
return why("uses its module object beyond module.exports");
|
||||
}
|
||||
const global = name === exportsParam ? "exports" : "module";
|
||||
if (name !== global) {
|
||||
const p = r.parent;
|
||||
edits.push(
|
||||
ts.isShorthandPropertyAssignment(p)
|
||||
? { start: r.getStart(sf), end: r.getEnd(), text: `${name}: ${global}` }
|
||||
: { start: r.getStart(sf), end: r.getEnd(), text: global },
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// The factory function itself binds the parameters and `var`s.
|
||||
if (scopes.boundWithin(r, body.parent ?? body)) continue;
|
||||
if (name === "exports" || name === "module")
|
||||
return why(`reads the bundle's own '${name}' binding`);
|
||||
if (name === "__filename") return why("reads __filename, which names the bundle file");
|
||||
if (top.has(name)) {
|
||||
if (!copied.has(name)) {
|
||||
copied.add(name);
|
||||
copyQueue.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The pure helpers BODY reads, closed over their own top-level reads.
|
||||
const header: string[] = [];
|
||||
for (let i = 0; i < copyQueue.length; i++) {
|
||||
const name = copyQueue[i]!;
|
||||
const t = top.get(name)!;
|
||||
const node = t.node;
|
||||
const pure =
|
||||
stable(name) &&
|
||||
(ts.isFunctionDeclaration(node) ||
|
||||
(ts.isVariableDeclaration(node) && copyableInitializer(node.initializer)));
|
||||
if (!pure || chunks.has(name))
|
||||
return why(`reads the bundle's top-level binding '${name}', which it cannot carry along`);
|
||||
// Each chunk gets its own copy, so a function's identity and its
|
||||
// properties must stay unobservable: the bundle only ever calls it.
|
||||
if (isFunctionValued(node) && (!calledOnly(name) || !selfCalledOnly(node, scopes)))
|
||||
return why(
|
||||
`uses the bundle's top-level function '${name}' as a value, which its chunks cannot share`,
|
||||
);
|
||||
for (const r of references(node)) {
|
||||
if (scopes.boundWithin(r, node) || !top.has(r.text) || copied.has(r.text)) continue;
|
||||
copied.add(r.text);
|
||||
copyQueue.push(r.text);
|
||||
}
|
||||
}
|
||||
// Declaration order of the bundle: a helper reads only earlier ones.
|
||||
const ordered = [...copied].sort(
|
||||
(a, b) => top.get(a)!.node.getStart(sf) - top.get(b)!.node.getStart(sf),
|
||||
);
|
||||
for (const name of ordered) {
|
||||
const node = top.get(name)!.node;
|
||||
header.push(ts.isFunctionDeclaration(node) ? node.getText(sf) : `var ${node.getText(sf)};`);
|
||||
}
|
||||
|
||||
const bodyStrict = strictPrologue(body.statements.nodes);
|
||||
const text = applyEdits(sf.text, body.getStart(sf) + 1, body.getEnd() - 1, edits);
|
||||
const prologue = bundleStrict || bodyStrict ? '"use strict";\n' : "";
|
||||
return prologue + (header.length > 0 ? header.join("\n") + "\n" : "") + text + "\n";
|
||||
}
|
||||
@@ -2,7 +2,12 @@
|
||||
import { dirname, join, resolve as resolvePath } from "node:path";
|
||||
import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js";
|
||||
import type { Ts7SourceParser } from "./ts7/source-parser.js";
|
||||
import { trackedDirectoryExists, trackedFileExists, trackedReadFile } from "./input-tracker.js";
|
||||
import {
|
||||
isVirtualSource,
|
||||
trackedDirectoryExists,
|
||||
trackedFileExists,
|
||||
trackedReadFile,
|
||||
} from "./input-tracker.js";
|
||||
import { resolveExports } from "./resolve.js";
|
||||
import { packageNameOfSpecifier } from "./workspace-registry.js";
|
||||
import { isBundlerCjsCandidate, rewriteBundlerCjsSyntax } from "./npm-static-rewrite-syntax.js";
|
||||
@@ -134,7 +139,7 @@ function requireTargetEsModuleStamped(
|
||||
} catch {
|
||||
/* unlexable — keep probing */
|
||||
}
|
||||
if (src.includes("__toCommonJS(")) return true;
|
||||
if (/__toCommonJS\d*\(/.test(src)) return true;
|
||||
const fwd = /module\.exports\s*=\s*require\(\s*["']([^"']+)["']\s*\)/.exec(src);
|
||||
if (fwd !== null) return requireTargetEsModuleStamped(parser, file, fwd[1]!, depth + 1);
|
||||
return false;
|
||||
@@ -163,9 +168,12 @@ export function rewriteBundlerCjsWithParser(
|
||||
parser: Ts7SourceParser,
|
||||
source: string,
|
||||
filePath: string,
|
||||
liveExportTable?: (filePath: string) => void,
|
||||
): string | { degrade: string } | null {
|
||||
if (!isBundlerCjsCandidate(source)) return null;
|
||||
return rewriteBundlerCjsSyntax(parser.parse(filePath, source, "js"), filePath, {
|
||||
liveExportTable,
|
||||
requireOnly: isVirtualSource,
|
||||
requireTargetEsModuleStamped: (from, spec) => requireTargetEsModuleStamped(parser, from, spec),
|
||||
starTargetNames: (from, spec) => {
|
||||
const target = resolveRelativeCjs(from, spec);
|
||||
|
||||
@@ -12,6 +12,70 @@ import { rewriteBundledFunctionImports } from "./npm-static-bundled-cjs.js";
|
||||
export interface CjsRewriteHost {
|
||||
requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean;
|
||||
starTargetNames(fromFile: string, specifier: string): Set<string>;
|
||||
/** Told when the rewritten file's tail export table stands for a live
|
||||
* getter table the original installed before it could run another
|
||||
* module's code (liveExportTableFirst): a module of a require cycle that
|
||||
* captured the export object early observes exactly that table. */
|
||||
liveExportTable?: ((filePath: string) => void) | undefined;
|
||||
/** True for a module only ever loaded through require() (a bundle's
|
||||
* CommonJS chunk split into its own module): no ES-module importer
|
||||
* exists, so Node's lexer-visible name set bounds nothing and every
|
||||
* name the export plumbing defines is part of its surface. */
|
||||
requireOnly?: ((filePath: string) => boolean) | undefined;
|
||||
}
|
||||
|
||||
/** True when esbuild's export object (`module.exports = __toCommonJS(T)`
|
||||
* over a getter table `__export(T, {...})`) is installed before anything
|
||||
* that can run another module's code, and nothing else writes the export
|
||||
* object. Every statement before the installation is then a declaration,
|
||||
* a directive, or the getter-table call (whose getters run only when
|
||||
* read), and variable initializers evaluate no calls outside function
|
||||
* bodies. The getters read live bindings, so the object a cycle partner
|
||||
* captures early answers every later read exactly like the final table. */
|
||||
function liveExportTableFirst(
|
||||
sf: ts.SourceFile,
|
||||
installAt: number,
|
||||
target: string,
|
||||
exportTables: readonly { target: string }[],
|
||||
): boolean {
|
||||
if (!exportTables.some((t) => t.target === target)) return false;
|
||||
let callsOutsideFunctions = false;
|
||||
const noCalls = (root: ts.Node): boolean => {
|
||||
callsOutsideFunctions = false;
|
||||
ts.walkPreorder(root, (n) => {
|
||||
if (callsOutsideFunctions) return "stop";
|
||||
if (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))
|
||||
return "skip";
|
||||
if (ts.isCallExpression(n) || ts.isNewExpression(n) || ts.isTaggedTemplateExpression(n))
|
||||
callsOutsideFunctions = true;
|
||||
return undefined;
|
||||
});
|
||||
return !callsOutsideFunctions;
|
||||
};
|
||||
let sawTable = false;
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (stmt.getStart(sf) >= installAt) return sawTable;
|
||||
if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) continue;
|
||||
if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) continue;
|
||||
if (ts.isVariableStatement(stmt)) {
|
||||
if (
|
||||
stmt.declarationList.declarations.nodes.every(
|
||||
(d) => ts.isIdentifier(d.name) && (d.initializer === undefined || noCalls(d.initializer)),
|
||||
)
|
||||
)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
if (ts.isExpressionStatement(stmt) && ts.isCallExpression(stmt.expression)) {
|
||||
const table = exportCallOf(stmt.expression);
|
||||
if (table !== null && table.target === target) {
|
||||
sawTable = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isBundlerCjsCandidate(source: string): boolean {
|
||||
@@ -24,16 +88,31 @@ export function isBundlerCjsCandidate(source: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** The helper a bundler-emitted name spells. esbuild suffixes a helper
|
||||
* that a bundled module declares again with a counter (a bundled esbuild
|
||||
* dist inside another bundle carries `__export2`, `__toCommonJS2`, …); the
|
||||
* recognizers below verify each call's shape, so the counter carries no
|
||||
* meaning beyond the name collision it avoided. */
|
||||
export function bundlerHelperBase(name: string): string {
|
||||
return /^__[A-Za-z]+\d+$/.test(name) ? name.replace(/\d+$/, "") : name;
|
||||
}
|
||||
|
||||
/** The last NAME of a callee: bare identifier or any member chain's final
|
||||
* name (`tslib_1.__exportStar` → "__exportStar"). */
|
||||
* name (`tslib_1.__exportStar` → "__exportStar"), helper counters dropped. */
|
||||
function calleeNameOf(call: ts.CallExpression): string | null {
|
||||
const callee = call.expression;
|
||||
if (ts.isIdentifier(callee)) return callee.text;
|
||||
if (ts.isIdentifier(callee)) return bundlerHelperBase(callee.text);
|
||||
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name))
|
||||
return callee.name.text;
|
||||
return bundlerHelperBase(callee.name.text);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** True for an identifier spelling the named helper (counter-suffixed
|
||||
* spellings included). */
|
||||
function isHelperIdent(node: ts.Node, base: string): node is ts.Identifier {
|
||||
return ts.isIdentifier(node) && bundlerHelperBase(node.text) === base;
|
||||
}
|
||||
|
||||
/** An entry the canonical table can carry. */
|
||||
interface EntrySpec {
|
||||
/** The export name (may need string-key spelling). */
|
||||
@@ -194,13 +273,8 @@ interface ToEsmPlan {
|
||||
* __copyProps(…, mod) call copying onto a conditional that tests
|
||||
* isNodeMode/__esModule and defines "default" with value mod. Comments and
|
||||
* formatting are free; the SHAPE is the contract. */
|
||||
function recognizedToEsmDecl(stmt: ts.Statement): boolean {
|
||||
if (!ts.isVariableStatement(stmt)) return false;
|
||||
const decls = stmt.declarationList.declarations.nodes;
|
||||
if (decls.length !== 1) return false;
|
||||
const d = decls[0]!;
|
||||
if (!ts.isIdentifier(d.name) || d.name.text !== "__toESM" || d.initializer === undefined)
|
||||
return false;
|
||||
function recognizedToEsmDecl(d: ts.VariableDeclaration): boolean {
|
||||
if (!isHelperIdent(d.name, "__toESM") || d.initializer === undefined) return false;
|
||||
let init: ts.Expression = d.initializer;
|
||||
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
||||
if (!ts.isArrowFunction(init)) return false;
|
||||
@@ -218,12 +292,7 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean {
|
||||
expr = expr.right;
|
||||
}
|
||||
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
|
||||
if (
|
||||
!ts.isCallExpression(expr) ||
|
||||
!ts.isIdentifier(expr.expression) ||
|
||||
expr.expression.text !== "__copyProps"
|
||||
)
|
||||
return false;
|
||||
if (!ts.isCallExpression(expr) || !isHelperIdent(expr.expression, "__copyProps")) return false;
|
||||
if (expr.arguments.nodes.length < 2) return false;
|
||||
const from = expr.arguments.nodes[1]!;
|
||||
if (!ts.isIdentifier(from) || from.text !== mod) return false;
|
||||
@@ -234,12 +303,7 @@ function recognizedToEsmDecl(stmt: ts.Statement): boolean {
|
||||
if (!condText.includes(isNodeMode) || !condText.includes("__esModule")) return false;
|
||||
let stamp: ts.Expression = to.whenTrue;
|
||||
while (ts.isParenthesizedExpression(stamp)) stamp = stamp.expression;
|
||||
if (
|
||||
!ts.isCallExpression(stamp) ||
|
||||
!ts.isIdentifier(stamp.expression) ||
|
||||
stamp.expression.text !== "__defProp"
|
||||
)
|
||||
return false;
|
||||
if (!ts.isCallExpression(stamp) || !isHelperIdent(stamp.expression, "__defProp")) return false;
|
||||
if (stamp.arguments.nodes.length !== 3) return false;
|
||||
const nameArg = stamp.arguments.nodes[1]!;
|
||||
if (!ts.isStringLiteral(nameArg) || nameArg.text !== "default") return false;
|
||||
@@ -278,19 +342,23 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH
|
||||
degrade: reason,
|
||||
});
|
||||
|
||||
const declStmts = sf.statements.nodes.filter(
|
||||
(s) =>
|
||||
ts.isVariableStatement(s) &&
|
||||
s.declarationList.declarations.nodes.some(
|
||||
(d) => ts.isIdentifier(d.name) && d.name.text === "__toESM",
|
||||
),
|
||||
);
|
||||
// Top-level declarators of the helper, any statement shape (minified
|
||||
// dists share one `var` among several helpers), by spelling.
|
||||
const decls = new Map<string, ts.VariableDeclaration[]>();
|
||||
for (const s of sf.statements.nodes) {
|
||||
if (!ts.isVariableStatement(s)) continue;
|
||||
for (const d of s.declarationList.declarations.nodes) {
|
||||
if (!isHelperIdent(d.name, "__toESM")) continue;
|
||||
const list = decls.get(d.name.text);
|
||||
if (list === undefined) decls.set(d.name.text, [d]);
|
||||
else list.push(d);
|
||||
}
|
||||
}
|
||||
const refs: ts.Identifier[] = [];
|
||||
const calls: ts.CallExpression[] = [];
|
||||
const collect = (n: ts.Node): void => {
|
||||
if (ts.isIdentifier(n) && n.text === "__toESM") refs.push(n);
|
||||
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression) && n.expression.text === "__toESM")
|
||||
calls.push(n);
|
||||
if (isHelperIdent(n, "__toESM")) refs.push(n);
|
||||
if (ts.isCallExpression(n) && isHelperIdent(n.expression, "__toESM")) calls.push(n);
|
||||
};
|
||||
ts.walkPreorder(sf, collect);
|
||||
if (calls.length === 0 && refs.length === 0) return plan;
|
||||
@@ -298,15 +366,16 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH
|
||||
// a helper that escapes as a VALUE is outside the recognized shape.
|
||||
const callees = new Set<ts.Node>(calls.map((c) => c.expression));
|
||||
const declNames = new Set<ts.Node>();
|
||||
for (const s of declStmts) {
|
||||
for (const d of (s as ts.VariableStatement).declarationList.declarations.nodes) {
|
||||
if (ts.isIdentifier(d.name) && d.name.text === "__toESM") declNames.add(d.name);
|
||||
}
|
||||
}
|
||||
for (const list of decls.values()) for (const d of list) declNames.add(d.name);
|
||||
if (refs.some((r) => !callees.has(r) && !declNames.has(r))) return fail(TO_ESM_ESCAPE_DEGRADE);
|
||||
if (calls.length === 0) return plan; // declared but never called — dead helper
|
||||
if (declStmts.length !== 1 || !recognizedToEsmDecl(declStmts[0]!))
|
||||
return fail(TO_ESM_SHAPE_DEGRADE);
|
||||
// Every spelling a call uses must be declared exactly once, in the
|
||||
// recognized shape.
|
||||
for (const call of calls) {
|
||||
const list = decls.get((call.expression as ts.Identifier).text);
|
||||
if (list === undefined || list.length !== 1 || !recognizedToEsmDecl(list[0]!))
|
||||
return fail(TO_ESM_SHAPE_DEGRADE);
|
||||
}
|
||||
|
||||
/** Module-scope interop bindings: name → whether `.default` IS the
|
||||
* module (pad the access) rather than a member read of its `default`. */
|
||||
@@ -467,16 +536,20 @@ export function rewriteBundlerCjsSyntax(
|
||||
|
||||
// ESM bundles can contain closed CommonJS function factories. Recognize
|
||||
// those before refusing interop that needs a dynamic module namespace.
|
||||
if (ts.isExternalModule(sf)) {
|
||||
// TypeScript forces every `.cjs` file to be an external module
|
||||
// (isFileForcedToBeModuleByFormat), yet Node always loads one as
|
||||
// CommonJS: the extension, not that flag, decides the format there.
|
||||
if (!filePath.endsWith(".cjs") && ts.isExternalModule(sf)) {
|
||||
const bundled = rewriteBundledFunctionImports(source, sf);
|
||||
if (bundled !== null) return bundled;
|
||||
return source.includes("__toESM(")
|
||||
? {
|
||||
degrade:
|
||||
"its ES-module dist routes an external dependency through the __toESM " +
|
||||
"bundler-interop helper, which cannot compile natively in ESM output",
|
||||
}
|
||||
: null;
|
||||
if (!source.includes("__toESM(")) return null;
|
||||
return {
|
||||
degrade: source.includes("__commonJS")
|
||||
? "its ES-module bundle reaches bundled CommonJS modules through the __commonJS and " +
|
||||
"__toESM bundler-interop helpers, which cannot compile natively in ESM output"
|
||||
: "its ES-module dist routes an external dependency through the __toESM " +
|
||||
"bundler-interop helper, which cannot compile natively in ESM output",
|
||||
};
|
||||
}
|
||||
|
||||
// The __toESM interop pass (see the section header): wrapper call sites
|
||||
@@ -644,6 +717,11 @@ export function rewriteBundlerCjsSyntax(
|
||||
}
|
||||
|
||||
const lexed = cjsLexedExportsOfFile(sf);
|
||||
if (host.requireOnly?.(filePath) === true) {
|
||||
for (const t of exportTables) for (const [name] of t.entries) lexed.exports.add(name);
|
||||
for (const d of definePropEntries) lexed.exports.add(d.name);
|
||||
for (const m of memberStmts) for (const name of m.names) lexed.exports.add(name);
|
||||
}
|
||||
let emitTable = sawBundleShape && (lexed.exports.size > 0 || lexed.reexports.length > 0);
|
||||
|
||||
const starSpecs: string[] = [];
|
||||
@@ -814,21 +892,35 @@ export function rewriteBundlerCjsSyntax(
|
||||
"__reExport",
|
||||
"__toESM",
|
||||
"__toCommonJS",
|
||||
"__commonJS",
|
||||
"__exportStar",
|
||||
"__createBinding",
|
||||
"__setModuleDefault",
|
||||
"__importStar",
|
||||
"__importDefault",
|
||||
]);
|
||||
/* Helper declarators by name. A minified dist shares one `var` among
|
||||
* several helpers (`var __getProtoOf=…,__hasOwnProp=…;`): each
|
||||
* declarator is its own candidate, and the statement pads whole only
|
||||
* once every declarator in it is dead. */
|
||||
const helperDecls = new Map<string, { start: number; end: number }>();
|
||||
const helperStmts: ts.VariableStatement[] = [];
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (!ts.isVariableStatement(stmt)) continue;
|
||||
const decls = stmt.declarationList.declarations.nodes;
|
||||
if (decls.length !== 1) continue;
|
||||
const d = decls[0]!;
|
||||
if (ts.isIdentifier(d.name) && HELPER_NAMES.has(d.name.text)) {
|
||||
helperDecls.set(d.name.text, { start: stmt.getStart(sf), end: stmt.getEnd() });
|
||||
let anyHelper = false;
|
||||
for (const d of decls) {
|
||||
if (!ts.isIdentifier(d.name) || !HELPER_NAMES.has(bundlerHelperBase(d.name.text))) continue;
|
||||
if (helperDecls.has(d.name.text)) continue; // redeclared: keep the first, never sweep twice
|
||||
anyHelper = true;
|
||||
helperDecls.set(
|
||||
d.name.text,
|
||||
decls.length === 1
|
||||
? { start: stmt.getStart(sf), end: stmt.getEnd() }
|
||||
: { start: d.getStart(sf), end: d.getEnd() },
|
||||
);
|
||||
}
|
||||
if (anyHelper && decls.length > 1) helperStmts.push(stmt);
|
||||
}
|
||||
// every identifier occurrence of a helper name outside its own
|
||||
// declarator name node, position-recorded for the span test
|
||||
@@ -862,8 +954,39 @@ export function rewriteBundlerCjsSyntax(
|
||||
}
|
||||
}
|
||||
}
|
||||
// A swept declarator of a shared statement pads with its separating
|
||||
// comma, and the statement pads whole once every declarator is dead:
|
||||
// padding the bare declarator span would leave a dangling comma.
|
||||
const declaratorSpans = new Set<{ start: number; end: number }>();
|
||||
const declaratorPads: { start: number; end: number }[] = [];
|
||||
for (const stmt of helperStmts) {
|
||||
const decls = stmt.declarationList.declarations.nodes;
|
||||
const deadAt = decls.map((d) => {
|
||||
if (!ts.isIdentifier(d.name) || !removed.has(d.name.text)) return false;
|
||||
const span = helperDecls.get(d.name.text)!;
|
||||
if (span.start !== d.getStart(sf)) return false;
|
||||
declaratorSpans.add(span);
|
||||
return true;
|
||||
});
|
||||
if (deadAt.every((x) => x)) {
|
||||
declaratorPads.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
|
||||
continue;
|
||||
}
|
||||
// Dead declarators before the last live one take their trailing
|
||||
// comma; the dead tail after it pads from that declarator's end.
|
||||
const lastLive = deadAt.lastIndexOf(false);
|
||||
for (let i = 0; i < decls.length; i++) {
|
||||
if (!deadAt[i]) continue;
|
||||
const d = decls[i]!;
|
||||
declaratorPads.push(
|
||||
i < lastLive
|
||||
? { start: d.getStart(sf), end: decls[i + 1]!.getStart(sf) }
|
||||
: { start: decls[lastLive]!.getEnd(), end: d.getEnd() },
|
||||
);
|
||||
}
|
||||
}
|
||||
neutralize.length = 0;
|
||||
neutralize.push(...dead);
|
||||
neutralize.push(...dead.filter((span) => !declaratorSpans.has(span)), ...declaratorPads);
|
||||
}
|
||||
|
||||
/* ── emission ─────────────────────────────────────────────────────── */
|
||||
@@ -875,6 +998,15 @@ export function rewriteBundlerCjsSyntax(
|
||||
|
||||
if (!emitTable) return text; // interop pads only — nothing appends
|
||||
|
||||
if (
|
||||
esbuildMainTarget !== null &&
|
||||
tablePos !== null &&
|
||||
memberStmts.length === 0 &&
|
||||
definePropEntries.length === 0 &&
|
||||
liveExportTableFirst(sf, tablePos, esbuildMainTarget, exportTables)
|
||||
)
|
||||
host.liveExportTable?.(filePath);
|
||||
|
||||
const lines: string[] = [""];
|
||||
const hasAnyFallback = [...entries.values()].some((s) => s.kind === "any");
|
||||
if (hasAnyFallback) {
|
||||
|
||||
@@ -13,7 +13,13 @@ import {
|
||||
registerWorkspacePackage,
|
||||
workspacePackageOfPath,
|
||||
} from "./workspace-registry.js";
|
||||
import { trackedExists, trackedReadFile, trackedRealpath } from "./input-tracker.js";
|
||||
import {
|
||||
clearVirtualSources,
|
||||
setVirtualSource,
|
||||
trackedExists,
|
||||
trackedReadFile,
|
||||
trackedRealpath,
|
||||
} from "./input-tracker.js";
|
||||
|
||||
let activePackages: ReadonlySet<string> = new Set();
|
||||
let declarationOverloads: ReadonlyMap<string, NpmStaticDeclarationOverloads> = new Map();
|
||||
@@ -28,12 +34,24 @@ const offenders = new Map<string, string>();
|
||||
* the host probes the same file many times, and the rewrite parses. */
|
||||
const rewriteCache = new Map<string, string | null>();
|
||||
|
||||
/** Rewritten files whose tail export table stands for a live getter table
|
||||
* installed before the file could run another module's code (the rewrite
|
||||
* certifies it; require-cycle admission consults it). */
|
||||
const liveExportFiles = new Set<string>();
|
||||
|
||||
export function npmStaticLiveExportTable(path: string): boolean {
|
||||
return liveExportFiles.has(path.split("\\").join("/"));
|
||||
}
|
||||
|
||||
export function setNpmStaticPackages<T extends Iterable<string>>(packages: T): void {
|
||||
activePackages = new Set(packages);
|
||||
declarationOverloads = new Map();
|
||||
declarationProperties = new Map();
|
||||
offenders.clear();
|
||||
fileDegrades.clear();
|
||||
rewriteCache.clear();
|
||||
liveExportFiles.clear();
|
||||
clearVirtualSources();
|
||||
untypedPkgCache.clear();
|
||||
realpathProbed.clear();
|
||||
}
|
||||
@@ -81,6 +99,21 @@ export function npmStaticOffenders(): ReadonlyMap<string, string> {
|
||||
return offenders;
|
||||
}
|
||||
|
||||
/** Files whose bundler interop the rewrite could not finish this attempt,
|
||||
* with their package and reason. A file refuses its package only when
|
||||
* the compiled program can load it (npmStaticLoadableFiles): a package is
|
||||
* judged by what its imported subpaths reach, not by every file it ships. */
|
||||
const fileDegrades = new Map<string, { pkg: string; reason: string }>();
|
||||
|
||||
export function npmStaticFileDegrades(): ReadonlyMap<string, { pkg: string; reason: string }> {
|
||||
return fileDegrades;
|
||||
}
|
||||
|
||||
function reportFileDegrade(path: string, pkg: string, reason: string): void {
|
||||
const key = path.split("\\").join("/");
|
||||
if (!fileDegrades.has(key)) fileDegrades.set(key, { pkg, reason });
|
||||
}
|
||||
|
||||
/** The DefinitelyTyped name mangling ("@scope/pkg" → "scope__pkg") — the
|
||||
* @types twin that must hide alongside the package's own declarations. */
|
||||
function mangledTypesName(name: string): string {
|
||||
@@ -362,7 +395,18 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
if (hit !== undefined) return hit ?? undefined;
|
||||
let rewritten: string | null = null;
|
||||
try {
|
||||
const original = trackedReadFile(path);
|
||||
let original = trackedReadFile(path);
|
||||
// A bundle's CommonJS chunks move to their own modules first, so
|
||||
// every later pass (and this shadow, for each chunk) sees plain
|
||||
// CommonJS module text.
|
||||
const split = original === null ? null : services.splitCjsChunks(original, path);
|
||||
if (split !== null && "degrade" in split) {
|
||||
reportFileDegrade(path, target.pkg, split.degrade);
|
||||
original = null;
|
||||
} else if (split !== null) {
|
||||
for (const chunk of split.chunks) setVirtualSource(chunk.path, chunk.source);
|
||||
original = split.text;
|
||||
}
|
||||
if (original !== null) {
|
||||
const namepaths = services.jsDocNamepaths(path, original);
|
||||
const source = namepaths ?? original;
|
||||
@@ -385,9 +429,10 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
classFields?.text ??
|
||||
source,
|
||||
path,
|
||||
(live) => liveExportFiles.add(live.split("\\").join("/")),
|
||||
);
|
||||
if (answer !== null && typeof answer === "object") {
|
||||
reportNpmStaticOffender(target.pkg, answer.degrade);
|
||||
reportFileDegrade(path, target.pkg, answer.degrade);
|
||||
} else {
|
||||
rewritten =
|
||||
answer ??
|
||||
@@ -395,7 +440,8 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
propertyProjected?.text ??
|
||||
findWidened?.text ??
|
||||
classFields?.text ??
|
||||
namepaths;
|
||||
namepaths ??
|
||||
(split !== null ? original : null);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -434,6 +480,14 @@ function looksUnminified(source: string): boolean {
|
||||
return source.length / lines.length <= 200;
|
||||
}
|
||||
|
||||
/** A whitespace/syntax-minified bundler dist whose interop helpers keep
|
||||
* their names (`var __toESM=…,__toCommonJS=mod=>…`): the recognizers
|
||||
* verify those shapes and refuse the package precisely when they cannot,
|
||||
* so the minification heuristic need not refuse it up front. */
|
||||
function namedBundlerHelpers(source: string): boolean {
|
||||
return /\b__(?:toCommonJS|commonJS|toESM)\d*\s*=\s*(?:\(|[A-Za-z_$][\w$]*\s*=>)/.test(source);
|
||||
}
|
||||
|
||||
function hasTransformMarkers(source: string): boolean {
|
||||
return TRANSFORM_MARKERS.some((m) => source.includes(m));
|
||||
}
|
||||
@@ -456,7 +510,8 @@ export function npmStaticIneligibleReason(
|
||||
if (jsEntry === null) return "no runtime JS entry resolves";
|
||||
const source = trackedReadFile(jsEntry);
|
||||
if (source === null) return `its runtime entry ${jsEntry} cannot be read`;
|
||||
if (!looksUnminified(source)) return "its shipped JS looks minified";
|
||||
if (!looksUnminified(source) && !namedBundlerHelpers(source))
|
||||
return "its shipped JS looks minified";
|
||||
if (hasTransformMarkers(source)) {
|
||||
return "its shipped JS carries build-transform markers (bundled/transpiled dist)";
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ import {
|
||||
checkPreflight,
|
||||
isNodeTypesPath,
|
||||
locOf,
|
||||
npmStaticLoadableFiles,
|
||||
requiresOf,
|
||||
resolveNpmImport,
|
||||
type LoadResult,
|
||||
} from "./program.js";
|
||||
import {
|
||||
npmStaticFileDegrades,
|
||||
npmStaticIneligibleReason,
|
||||
npmStaticOffenders,
|
||||
npmStaticPackageOfPath,
|
||||
@@ -458,6 +460,15 @@ function loadFrontend(
|
||||
const typecheckRefused = new Set<string>();
|
||||
while (effective.size > 0) {
|
||||
const reasons = new Map<string, string>(npmStaticOffenders());
|
||||
// Bundler interop the rewrite could not finish refuses its package
|
||||
// only where the compiled program can load the file.
|
||||
if (npmStaticFileDegrades().size > 0) {
|
||||
const loadable = npmStaticLoadableFiles(load.program, load.moduleOrder);
|
||||
for (const [file, { pkg, reason }] of npmStaticFileDegrades()) {
|
||||
if (!reasons.has(pkg) && (loadable === null || loadable.has(file)))
|
||||
reasons.set(pkg, reason);
|
||||
}
|
||||
}
|
||||
for (const d of preflight) {
|
||||
const pkg = npmStaticPackageOfPath(d.loc.file);
|
||||
if (pkg !== null && !reasons.has(pkg))
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
isNpmStaticPackage,
|
||||
npmStaticActive,
|
||||
npmStaticFsShadow,
|
||||
npmStaticLiveExportTable,
|
||||
npmStaticPackageOfPath,
|
||||
reportNpmStaticOffender,
|
||||
setNpmStaticDeclarationOverloads,
|
||||
@@ -1438,6 +1439,8 @@ export function requireBindingEarlyUse7(
|
||||
k: number,
|
||||
decl: ts.VariableDeclaration,
|
||||
precedingDecls: readonly ts.VariableDeclaration[],
|
||||
/** A reference of the binding whose early evaluation is known exact. */
|
||||
exactEarlyRead?: (reference: ts.Identifier) => boolean,
|
||||
): ts.Identifier | null {
|
||||
const checker = program.getTypeChecker();
|
||||
const bindings = new Set<ts.Symbol>();
|
||||
@@ -1669,6 +1672,7 @@ export function requireBindingEarlyUse7(
|
||||
const sym = symbolOf(n);
|
||||
if (sym === undefined) return undefined;
|
||||
if (bindings.has(sym)) {
|
||||
if (exactEarlyRead?.(n) === true) return undefined;
|
||||
hit = n;
|
||||
return "stop";
|
||||
}
|
||||
@@ -3119,6 +3123,16 @@ function cjsCycleRefusal7(
|
||||
// anything else can change what a module of the cycle observes.
|
||||
let stray: ts.Node | null = null;
|
||||
const settled = new Set(stmts.slice(0, firstEntry).filter(exportStatement));
|
||||
// A bundler export table the rewrite certified (npmStaticLiveExportTable)
|
||||
// stands for the live getter object the original installed before its
|
||||
// first require: the tail table settles as if it ran there.
|
||||
const tail = stmts[stmts.length - 1];
|
||||
if (
|
||||
tail !== undefined &&
|
||||
npmStaticLiveExportTable(m.fileName) &&
|
||||
cjsExportAssignmentOf(tail)?.kind === "table"
|
||||
)
|
||||
settled.add(tail);
|
||||
ts.walkPreorder(m, (n) => {
|
||||
if (stray !== null) return "stop";
|
||||
if (ts.isStatement(n) && settled.has(n)) return "skip";
|
||||
@@ -3171,7 +3185,9 @@ function cjsCycleRefusal7(
|
||||
if (req.decl === null) continue;
|
||||
const dep = resolveImport7(program, m, req.spec, "require");
|
||||
if (dep === null || !members.has(dep)) continue;
|
||||
const reached = requireBindingEarlyUse7(program, m, stmts.length, req.decl, []);
|
||||
const reached = requireBindingEarlyUse7(program, m, stmts.length, req.decl, [], (id) =>
|
||||
functionExportRead7(id, dep),
|
||||
);
|
||||
if (reached !== null) {
|
||||
return `the top-level code of ${m.fileName} can use its binding '${reached.text}' of a module of the cycle (${lineOf(reached)}) while the cycle initializes`;
|
||||
}
|
||||
@@ -3181,6 +3197,161 @@ function cjsCycleRefusal7(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** True when `use` initializes a top-level const whose every other
|
||||
* reference is a value of an export statement (`module.exports = { … }`
|
||||
* or `exports.n = …`). */
|
||||
function publishedAlias7(use: ts.Node, decl: ts.Node | undefined): boolean {
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
decl.initializer !== use ||
|
||||
!ts.isIdentifier(decl.name)
|
||||
)
|
||||
return false;
|
||||
const list = decl.parent;
|
||||
const stmt = list?.parent;
|
||||
const sf = stmt?.parent;
|
||||
if (
|
||||
list === undefined ||
|
||||
(list.flags & ts.NodeFlags.Const) === 0 ||
|
||||
stmt === undefined ||
|
||||
!ts.isVariableStatement(stmt) ||
|
||||
sf === undefined ||
|
||||
!ts.isSourceFile(sf)
|
||||
)
|
||||
return false;
|
||||
const alias = decl.name;
|
||||
const published = new Set<ts.Node>();
|
||||
for (const st of sf.statements.nodes) {
|
||||
const cjs = cjsExportAssignmentOf(st);
|
||||
if (cjs === null) continue;
|
||||
if (cjs.kind === "member") published.add(cjs.value);
|
||||
else if (cjs.obj !== null) {
|
||||
for (const p of cjs.obj.properties.nodes) {
|
||||
if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name))
|
||||
published.add(p.initializer);
|
||||
else if (ts.isShorthandPropertyAssignment(p)) published.add(p.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
let kept = false;
|
||||
ts.walkPreorder(sf, (n) => {
|
||||
if (kept) return "stop";
|
||||
if (!ts.isIdentifier(n) || n === alias || n.text !== alias.text) return undefined;
|
||||
const p = n.parent;
|
||||
if (
|
||||
p !== undefined &&
|
||||
((ts.isPropertyAccessExpression(p) && p.name === n) ||
|
||||
(ts.isPropertyAssignment(p) && p.name === n))
|
||||
)
|
||||
return undefined;
|
||||
if (!published.has(n)) kept = true;
|
||||
return undefined;
|
||||
});
|
||||
return !kept;
|
||||
}
|
||||
|
||||
/** True for a plain read `binding.name` (not a call, not a write, not a
|
||||
* value that is stored or passed on) of the cycle member `dep` whose
|
||||
* export `name` is a top-level function declaration nothing reassigns.
|
||||
* Every member settles its exports before entering the cycle, so Node
|
||||
* reads that same function whenever the read runs, and the compiled read
|
||||
* resolves to the declaration itself. */
|
||||
function functionExportRead7(id: ts.Identifier, dep: ts.SourceFile): boolean {
|
||||
const access = id.parent;
|
||||
if (!ts.isPropertyAccessExpression(access) || access.expression !== id) return false;
|
||||
// The value the read yields: through parentheses and the right operand
|
||||
// of a comma, so `(0, binding.name)(...)` is the call it spells.
|
||||
let use: ts.Node = access;
|
||||
for (;;) {
|
||||
const p = use.parent;
|
||||
if (ts.isParenthesizedExpression(p)) use = p;
|
||||
else if (
|
||||
ts.isBinaryExpression(p) &&
|
||||
p.operatorToken.kind === ts.SyntaxKind.CommaToken &&
|
||||
p.right === use
|
||||
)
|
||||
use = p;
|
||||
else break;
|
||||
}
|
||||
const parent = use.parent;
|
||||
// Only a use that neither runs the function nor keeps it is a read: a
|
||||
// stored alias (`const f = binding.name`, `{ f: binding.name }`) or an
|
||||
// argument can be called while the cycle initializes. A top-level const
|
||||
// that only publishes it (`module.exports = { name: f }`, the static
|
||||
// rewrite's re-export spelling) runs nothing: a module that reads the
|
||||
// export back checks that read itself.
|
||||
const observes =
|
||||
publishedAlias7(use, parent) ||
|
||||
ts.isExpressionStatement(parent) ||
|
||||
ts.isTypeOfExpression(parent) ||
|
||||
ts.isVoidExpression(parent) ||
|
||||
(ts.isPrefixUnaryExpression(parent) && parent.operator === ts.SyntaxKind.ExclamationToken) ||
|
||||
(ts.isBinaryExpression(parent) &&
|
||||
(parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
||||
parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken ||
|
||||
parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken)) ||
|
||||
((ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) &&
|
||||
parent.expression === use) ||
|
||||
(ts.isConditionalExpression(parent) && parent.condition === use);
|
||||
if (!observes) return false;
|
||||
const name = access.name.text;
|
||||
// The exported value's identifier: a table entry or a member export.
|
||||
const values: ts.Expression[] = [];
|
||||
for (const stmt of dep.statements.nodes) {
|
||||
if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue;
|
||||
const e = stmt.expression;
|
||||
if (e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue;
|
||||
if (isModuleExportsAccess(e.left)) {
|
||||
if (!ts.isObjectLiteralExpression(e.right)) return false;
|
||||
for (const p of e.right.properties.nodes) {
|
||||
if (ts.isSpreadAssignment(p)) return false;
|
||||
if (ts.isShorthandPropertyAssignment(p) && p.name.text === name)
|
||||
values.push(p.name as ts.Identifier);
|
||||
else if (
|
||||
ts.isPropertyAssignment(p) &&
|
||||
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
||||
p.name.text === name
|
||||
)
|
||||
values.push(p.initializer);
|
||||
}
|
||||
} else if (
|
||||
ts.isPropertyAccessExpression(e.left) &&
|
||||
e.left.name.text === name &&
|
||||
((ts.isIdentifier(e.left.expression) && e.left.expression.text === "exports") ||
|
||||
isModuleExportsAccess(e.left.expression))
|
||||
) {
|
||||
values.push(e.right);
|
||||
}
|
||||
}
|
||||
const value = values.length === 1 ? values[0]! : null;
|
||||
if (value === null || !ts.isIdentifier(value)) return false;
|
||||
const fnName = value.text;
|
||||
if (
|
||||
!dep.statements.nodes.some(
|
||||
(st) => ts.isFunctionDeclaration(st) && st.name?.text === fnName && st.body !== undefined,
|
||||
)
|
||||
)
|
||||
return false;
|
||||
let rebound = false;
|
||||
ts.walkPreorder(dep, (n) => {
|
||||
if (rebound) return "stop";
|
||||
if (
|
||||
(ts.isBinaryExpression(n) &&
|
||||
ts.isIdentifier(n.left) &&
|
||||
n.left.text === fnName &&
|
||||
n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
n.operatorToken.kind <= ts.SyntaxKind.LastAssignment) ||
|
||||
(ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.name.text === fnName) ||
|
||||
(ts.isFunctionDeclaration(n) && n.name?.text === fnName && !ts.isSourceFile(n.parent))
|
||||
)
|
||||
rebound = true;
|
||||
return undefined;
|
||||
});
|
||||
return !rebound;
|
||||
}
|
||||
|
||||
/* ── exact cyclic initialization ─────────────────────────────────────────
|
||||
* Cycles that fail the declaration-only rule still evaluate exactly like
|
||||
* Node when their shape is representable: the guarded %init calls already
|
||||
@@ -5930,6 +6101,58 @@ export function orderedImportsOf(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The files a compiled program can load: the module order closed over
|
||||
* every literal require() and import() call it contains (deferred and
|
||||
* nested ones included) and, for opted-in package JavaScript, over every
|
||||
* import and export declaration as well. A package file outside this set
|
||||
* is reachable only through types, so whatever it spells cannot affect
|
||||
* the compiled program. Null when a reachable file loads a computed
|
||||
* specifier: then any file may load. */
|
||||
export function npmStaticLoadableFiles(
|
||||
program: ts.Program,
|
||||
moduleOrder: readonly ts.SourceFile[],
|
||||
): Set<string> | null {
|
||||
const seen = new Set<ts.SourceFile>();
|
||||
const work: ts.SourceFile[] = [];
|
||||
const add = (sf: ts.SourceFile | null | undefined): void => {
|
||||
if (sf === null || sf === undefined || seen.has(sf) || sf.isDeclarationFile) return;
|
||||
seen.add(sf);
|
||||
work.push(sf);
|
||||
};
|
||||
for (const sf of moduleOrder) add(sf);
|
||||
let computed = false;
|
||||
while (work.length > 0 && !computed) {
|
||||
const sf = work.pop()!;
|
||||
const packageJs =
|
||||
npmStaticPackageOfPath(sf.fileName) !== null && isJsSourceFileName(sf.fileName);
|
||||
const follow = (spec: string, kind: "import" | "require"): void => {
|
||||
if (canonicalBuiltinModule(spec) !== null) return;
|
||||
add(resolveImport7(program, sf, spec, kind) ?? npmStaticDepSf7(program, sf, spec, kind));
|
||||
};
|
||||
ts.walkPreorder(sf, (n) => {
|
||||
if (computed) return "stop";
|
||||
if (ts.isCallExpression(n)) {
|
||||
const isImport = n.expression.kind === ts.SyntaxKind.ImportKeyword;
|
||||
const isRequire = ts.isIdentifier(n.expression) && n.expression.text === "require";
|
||||
if (!isImport && !isRequire) return undefined;
|
||||
const arg = n.arguments.nodes[0];
|
||||
if (arg !== undefined && ts.isStringLiteralLike(arg))
|
||||
follow(arg.text, isImport ? "import" : "require");
|
||||
else if (packageJs || isImport) computed = true;
|
||||
} else if (
|
||||
packageJs &&
|
||||
(ts.isImportDeclaration(n) || ts.isExportDeclaration(n)) &&
|
||||
n.moduleSpecifier !== undefined &&
|
||||
ts.isStringLiteral(n.moduleSpecifier)
|
||||
) {
|
||||
follow(n.moduleSpecifier.text, "import");
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
return computed ? null : new Set([...seen].map((sf) => sf.fileName.split("\\").join("/")));
|
||||
}
|
||||
|
||||
/** The program source file a bare specifier reaches when it names an
|
||||
* opted-in --npm-static package (the shipped-JS entry preflight admitted
|
||||
* as a module edge), else null. No offender reporting here — preflight
|
||||
@@ -5967,10 +6190,15 @@ export function isModuleExportsAccess(expr: ts.Expression): expr is ts.PropertyA
|
||||
* - member: `exports.f = <expr>` / `module.exports.f = <expr>`.
|
||||
* Null for everything else. Callers gate on top-level position in a JS
|
||||
* module file — these forms anywhere else keep their generic fences. */
|
||||
export function cjsExportAssignmentOf(
|
||||
stmt: ts.Statement,
|
||||
):
|
||||
| { kind: "table"; obj: ts.ObjectLiteralExpression | null; expr: ts.BinaryExpression }
|
||||
export function cjsExportAssignmentOf(stmt: ts.Statement):
|
||||
| {
|
||||
kind: "table";
|
||||
obj: ts.ObjectLiteralExpression | null;
|
||||
expr: ts.BinaryExpression;
|
||||
/** `exports = module.exports = value`: the `exports` binding names
|
||||
* the new value too, so later `exports.f =` members attach to it. */
|
||||
rebindsExports?: true;
|
||||
}
|
||||
| { kind: "member"; name: ts.MemberName; value: ts.Expression; expr: ts.BinaryExpression }
|
||||
| null {
|
||||
if (!ts.isExpressionStatement(stmt)) return null;
|
||||
@@ -5979,6 +6207,20 @@ export function cjsExportAssignmentOf(
|
||||
if (isModuleExportsAccess(e.left)) {
|
||||
return { kind: "table", obj: ts.isObjectLiteralExpression(e.right) ? e.right : null, expr: e };
|
||||
}
|
||||
if (ts.isIdentifier(e.left) && e.left.text === "exports") {
|
||||
let inner: ts.Expression = e.right;
|
||||
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
||||
if (
|
||||
ts.isBinaryExpression(inner) &&
|
||||
inner.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
isModuleExportsAccess(inner.left) &&
|
||||
// `exports = module.exports = {}` is the fresh-table reset, which the
|
||||
// export walk handles on its own.
|
||||
!ts.isObjectLiteralExpression(inner.right)
|
||||
) {
|
||||
return { kind: "table", obj: null, expr: inner, rebindsExports: true };
|
||||
}
|
||||
}
|
||||
if (ts.isPropertyAccessExpression(e.left) && !e.left.questionDotToken) {
|
||||
const recv = e.left.expression;
|
||||
const isExports = ts.isIdentifier(recv) && recv.text === "exports";
|
||||
@@ -6110,6 +6352,8 @@ export function cjsExportDiscardReason(stmt: ts.Statement): string | null {
|
||||
return "export members attached before a 'module.exports =' assignment (Node replaces the export object — importers never see this member; attach it to module.exports AFTER the table)";
|
||||
}
|
||||
const left = mine.expr.left as ts.PropertyAccessExpression;
|
||||
const last = cjsExportAssignmentOf(stmts[lastTable]!);
|
||||
if (last?.kind === "table" && last.rebindsExports === true) return null;
|
||||
return ts.isIdentifier(left.expression) && left.expression.text === "exports"
|
||||
? "'exports.f =' after a 'module.exports =' assignment ('exports' still references the replaced object — Node discards this member; write module.exports.f instead)"
|
||||
: null;
|
||||
|
||||
@@ -2,6 +2,11 @@ import { Ts7SourceParser, type Ts7SourceKind } from "./ts7/source-parser.js";
|
||||
import { Ts7Host, type Ts7ApiFactory, type Ts7HostOptions } from "./ts7/program-host.js";
|
||||
import { rewriteBundlerCjsWithParser } from "./npm-static-rewrite-host.js";
|
||||
import { isBundlerCjsCandidate } from "./npm-static-rewrite-syntax.js";
|
||||
import {
|
||||
mayBundleChunks,
|
||||
splitBundledChunks,
|
||||
type BundledChunkSplit,
|
||||
} from "./npm-static-bundled-chunks.js";
|
||||
import * as syntax from "./npm-static-declaration-syntax.js";
|
||||
import type { SourceFile } from "./ts7/ast-types.js";
|
||||
|
||||
@@ -55,10 +60,21 @@ export class FrontendServices {
|
||||
this.ensureOpen();
|
||||
return new Ts7Host(this.createApi, { ...options, cwd: options?.cwd ?? this.cwd });
|
||||
}
|
||||
rewriteCjs(source: string, path: string): string | { degrade: string } | null {
|
||||
rewriteCjs(
|
||||
source: string,
|
||||
path: string,
|
||||
liveExportTable?: (path: string) => void,
|
||||
): string | { degrade: string } | null {
|
||||
this.ensureOpen();
|
||||
if (!isBundlerCjsCandidate(source)) return null;
|
||||
return rewriteBundlerCjsWithParser(this.sourceParser(), source, path);
|
||||
return rewriteBundlerCjsWithParser(this.sourceParser(), source, path, liveExportTable);
|
||||
}
|
||||
/** A CommonJS bundle's __commonJS chunks as their own modules (null: no
|
||||
* chunks; a degrade reason: chunks the recognizer cannot move). */
|
||||
splitCjsChunks(source: string, path: string): BundledChunkSplit | { degrade: string } | null {
|
||||
this.ensureOpen();
|
||||
if (!mayBundleChunks(source)) return null;
|
||||
return splitBundledChunks(this.parse(path, source, "js"), path);
|
||||
}
|
||||
nullableClassFields(path: string, source: string): syntax.NpmStaticOverloadRewrite | null {
|
||||
return syntax.applyNpmStaticNullableClassFields(this.parse(path, source, "js"), source);
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { IrRecordShape, IrType, IrUnionDef, IrUnionDiscriminant } from "../
|
||||
import {
|
||||
BYTES_ELEMENT_NAME,
|
||||
arrayOf,
|
||||
isDynTypedRefType,
|
||||
BOOL,
|
||||
bytesOf,
|
||||
canConvertToDyn,
|
||||
@@ -125,6 +126,14 @@ export function jsOpenObjectType(
|
||||
: type;
|
||||
}
|
||||
|
||||
/** The modules whose RequestOptions interface maps to the checked dynamic. */
|
||||
const HTTP_REQUEST_OPTION_MODULES: ReadonlySet<string> = new Set([
|
||||
"http",
|
||||
"node:http",
|
||||
"https",
|
||||
"node:https",
|
||||
]);
|
||||
|
||||
/** The WebCrypto interfaces whose values are checked-dynamic: the crypto
|
||||
* global, SubtleCrypto, CryptoKey handles and the algorithm dictionaries
|
||||
* (scr_webcrypto.c). */
|
||||
@@ -325,7 +334,7 @@ export class ShapeRegistry {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
shape.declaredOrder = declaredOrder;
|
||||
shape.declaredOrder = this.withExtendedNames(shape.id, declaredOrder);
|
||||
this.declaredOrderPriority.set(shape.id, this.currentDeclaredOrderPriority);
|
||||
}
|
||||
|
||||
@@ -341,7 +350,7 @@ export class ShapeRegistry {
|
||||
}
|
||||
const shape = this.byId.get(shapeId);
|
||||
if (!shape) continue;
|
||||
shape.declaredOrder = best.order;
|
||||
shape.declaredOrder = this.withExtendedNames(shapeId, best.order);
|
||||
this.declaredOrderPriority.set(shapeId, best.priority);
|
||||
}
|
||||
for (const finalize of this.derivedDeclaredOrderFinalizers) finalize();
|
||||
@@ -374,7 +383,7 @@ export class ShapeRegistry {
|
||||
// still obeys first-writer-wins.
|
||||
if (shape?.declaredOrder !== originalOrder) return;
|
||||
const next = order();
|
||||
if (next !== undefined) shape.declaredOrder = next;
|
||||
if (next !== undefined) shape.declaredOrder = this.withExtendedNames(shape.id, next);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -482,6 +491,53 @@ export class ShapeRegistry {
|
||||
get(shapeId: string): IrRecordShape | undefined {
|
||||
return this.byId.get(shapeId);
|
||||
}
|
||||
|
||||
/** Names extendShape added to a shape, which every key-order table the
|
||||
* shape adopts lists after its declared names. */
|
||||
private readonly extendedNames = new Map<string, string[]>();
|
||||
|
||||
/** The members extendShape attached to a shape (empty for most). */
|
||||
attachedNames(shapeId: string): readonly string[] {
|
||||
return this.extendedNames.get(shapeId) ?? [];
|
||||
}
|
||||
|
||||
private withExtendedNames(shapeId: string, order: string[]): string[] {
|
||||
const extra = this.extendedNames.get(shapeId);
|
||||
if (!extra) return order;
|
||||
const missing = extra.filter((name) => !order.includes(name));
|
||||
return missing.length === 0 ? order : [...order, ...missing];
|
||||
}
|
||||
|
||||
/** Adds optional fields to an interned shape in place: members a type
|
||||
* assertion attaches to the shape's values (`r as R & { tag: string }`
|
||||
* followed by `view.tag = ...`; see assertion-members.ts). Every value
|
||||
* of the shape holds them absent until a write through such a view, so
|
||||
* the shape keeps its id, both structural keys resolve to it, and its
|
||||
* key-order table lists the new names after the declared ones. Runs
|
||||
* before any body lowers; existing layouts change, so the revision
|
||||
* advances like a closing recursive placeholder. */
|
||||
extendShape(shapeId: string, extra: readonly { name: string; type: IrType }[]): void {
|
||||
const shape = this.byId.get(shapeId);
|
||||
if (!shape || shape.tuple || extra.length === 0) return;
|
||||
const fresh = extra.filter((f) => !shape.fields.some((x) => x.name === f.name));
|
||||
if (fresh.length === 0) return;
|
||||
shape.fields = [...shape.fields, ...fresh].sort((a, b) =>
|
||||
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
||||
);
|
||||
this.extendedNames.set(shapeId, [
|
||||
...(this.extendedNames.get(shapeId) ?? []),
|
||||
...fresh.map((f) => f.name),
|
||||
]);
|
||||
if (shape.declaredOrder)
|
||||
shape.declaredOrder = this.withExtendedNames(shapeId, shape.declaredOrder);
|
||||
const candidates = this.declaredOrderCandidates.get(shapeId);
|
||||
if (candidates) {
|
||||
for (const c of candidates) c.order = this.withExtendedNames(shapeId, c.order);
|
||||
}
|
||||
this.revision++;
|
||||
const key = this.keyOf(shape.fields, false, shape.indexValue);
|
||||
if (!this.byKey.has(key)) this.byKey.set(key, shapeId);
|
||||
}
|
||||
}
|
||||
|
||||
/** The frontend's union interner — mirrors ShapeRegistry. A union's
|
||||
@@ -1543,6 +1599,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
contextResolutions++;
|
||||
return DYN;
|
||||
}
|
||||
// Readonly<C> over a program class (Object.freeze's result type) is the
|
||||
// same instance.
|
||||
const readonlyClass = readonlyClassType(type, ctx);
|
||||
if (readonlyClass !== null) return readonlyClass;
|
||||
if (resolveTypeParam && type.flags & ts.TypeFlags.TypeParameter) {
|
||||
const bound = resolveTypeParam(type);
|
||||
if (bound) {
|
||||
@@ -2038,12 +2098,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// {}` — classNamer spells them "%anon" (unique per file), the same
|
||||
// name their collection registered.
|
||||
!classDecl.getSourceFile().isDeclarationFile &&
|
||||
checker.getConstructSignatures(widened).length === 0
|
||||
checker.getConstructSignatures(widened).length === 0 &&
|
||||
// A TypeScript class declared OUTSIDE the lowered program (reached
|
||||
// through `import type` alone) never registers, and the program can
|
||||
// hold no instance of it: its instance type maps below as the
|
||||
// structural type it describes, like an interface.
|
||||
(ctx.isProgramFile(classDecl.getSourceFile()) || isJsSourceFile(classDecl.getSourceFile()))
|
||||
) {
|
||||
// Declared OUTSIDE the lowered program (reached through the type world
|
||||
// alone — jsdoc `typeof import()` over a never-required module): the
|
||||
// class will never register, so the instance type is unmappable, not
|
||||
// an object type naming a struct that cannot exist.
|
||||
// A JavaScript class outside the lowered program (jsdoc `typeof
|
||||
// import()` over a never-required module) stays unmappable: JSDoc
|
||||
// types are not checked, so its slots fall back to the dynamic value.
|
||||
if (!ctx.isProgramFile(classDecl.getSourceFile())) return null;
|
||||
// The class inside a MIXIN function: per-call-site instantiations
|
||||
// share this one node — the CURRENT instantiation (collecting or
|
||||
@@ -2447,6 +2511,21 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
// spawn consumes a presence-sensitive option bag, including optional
|
||||
// stdio and env values. Keep it checked-dynamic for native normalization.
|
||||
if (isStdlibInterface("SpawnOptions")) return DYN;
|
||||
// http.RequestOptions / https.RequestOptions as a declared type: an
|
||||
// option record whose members include functions, agents and signals.
|
||||
// Values ride checked-dynamic storage, like SpawnOptions.
|
||||
if (
|
||||
psym?.name === "RequestOptions" &&
|
||||
psym.declarations.some((d) => {
|
||||
if (!ts.isInterfaceDeclaration(d) || !ctx.isStdlibFile(d.getSourceFile())) return false;
|
||||
for (let p: ts.Node | undefined = d.parent; p !== undefined; p = p.parent) {
|
||||
if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name))
|
||||
return HTTP_REQUEST_OPTION_MODULES.has(p.name.text);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
)
|
||||
return DYN;
|
||||
|
||||
// exactly like npm-declared types. Static fetch has one deliberately
|
||||
// narrower native representation: Response is an opaque checked-
|
||||
@@ -4320,10 +4399,13 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
|
||||
// params). Declaration files keep their existing paths: library
|
||||
// callback slots such as String#replace's `(substring, ...args: any[])`
|
||||
// have intrinsic lowerings that inspect the unmapped type.
|
||||
// node:test's default mock function type, `(...args: any[]) =>
|
||||
// undefined` (the type parameter default of mock.fn), takes the same
|
||||
// ABI: mock.fn() without an original records whatever its callers pass.
|
||||
if (
|
||||
rest &&
|
||||
decl !== undefined &&
|
||||
!decl.getSourceFile().isDeclarationFile &&
|
||||
(!decl.getSourceFile().isDeclarationFile || isNodeTestDefaultFunctionParam(decl, ctx)) &&
|
||||
(pt?.kind === "dyn" || (!pt && isAnyArrayType(checker.getTypeOfSymbol(p), checker)))
|
||||
) {
|
||||
dynRest = true;
|
||||
@@ -5055,6 +5137,19 @@ function mentionsMappedParams(
|
||||
return false;
|
||||
}
|
||||
|
||||
function readonlyClassType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const alias = type.getAliasSymbol();
|
||||
if (alias?.name !== "Readonly") return null;
|
||||
const args = type.getAliasTypeArguments();
|
||||
if (args.length !== 1 || !args[0]) return null;
|
||||
const isLibAlias = alias.declarations.some(
|
||||
(d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()),
|
||||
);
|
||||
if (!isLibAlias) return null;
|
||||
const mapped = mapType(args[0], ctx);
|
||||
return mapped !== null && isDynTypedRefType(mapped) ? mapped : null;
|
||||
}
|
||||
|
||||
function mapGenericUtilityAlias(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
const { resolveTypeParam, shapes, unions } = ctx;
|
||||
if (!resolveTypeParam) return null;
|
||||
@@ -6516,6 +6611,23 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** A parameter of a function type that is the default of a type parameter
|
||||
* declared in node:test's standard-library declarations (MockTracker.fn's
|
||||
* `F extends Function = (...args: any[]) => undefined`). */
|
||||
function isNodeTestDefaultFunctionParam(decl: ts.Declaration, ctx: TypeMapperCtx): boolean {
|
||||
const fnType = decl.parent;
|
||||
return (
|
||||
ts.isParameter(decl) &&
|
||||
fnType !== undefined &&
|
||||
ts.isFunctionTypeNode(fnType) &&
|
||||
fnType.parent !== undefined &&
|
||||
ts.isTypeParameterDeclaration(fnType.parent) &&
|
||||
fnType.parent.defaultType === fnType &&
|
||||
ctx.isStdlibFile(decl.getSourceFile()) &&
|
||||
isDeclaredInAmbientModule(decl, "test")
|
||||
);
|
||||
}
|
||||
|
||||
/** `any[]` (or `readonly any[]`): an array type whose element is `any`. */
|
||||
export function isAnyArrayType(t: ts.Type, checker: ts.TypeChecker): boolean {
|
||||
if (!checker.isArrayType(t)) return false;
|
||||
|
||||
@@ -346,6 +346,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"process.kill",
|
||||
"process.killNum",
|
||||
"process.setExitCode",
|
||||
"process.setExitCodeTracked",
|
||||
// Node's argument grammar throws ERR_INVALID_ARG_TYPE at the call.
|
||||
"process.emitWarning",
|
||||
// cpuUsage(prev)'s field validation: negative/non-finite prev fields
|
||||
@@ -448,6 +449,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"https.requestFn",
|
||||
"https.requestFnCb",
|
||||
"rl.question",
|
||||
"rl.questionDyn",
|
||||
"rl.checkOptions",
|
||||
"rl.checkCompleter",
|
||||
"readable.fromArr",
|
||||
@@ -470,6 +472,11 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"rl.resume",
|
||||
"rl.prompt",
|
||||
"rl.promptWith",
|
||||
"rl.setPromptDyn",
|
||||
"rl.getPrompt",
|
||||
"rl.cursorPos",
|
||||
"rl.inputFromDyn",
|
||||
"rl.outputFromDyn",
|
||||
"rl.write",
|
||||
"rl.writeKey",
|
||||
"rl.cursor",
|
||||
@@ -591,6 +598,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dyn.mapSeedEntry",
|
||||
"dyn.freeze",
|
||||
"dyn.isFrozen",
|
||||
"class.defineField",
|
||||
"class.throwReadOnly",
|
||||
"class.throwNotExtensible",
|
||||
"dyn.toString",
|
||||
"dyn.valueOf",
|
||||
"string.fromCodePoint",
|
||||
|
||||
@@ -206,6 +206,23 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"dyn.in": { argTypes: [DYN, STRING], result: BOOL },
|
||||
"dyn.freeze": { argTypes: [DYN], result: DYN },
|
||||
"dyn.isFrozen": { argTypes: [DYN], result: BOOL },
|
||||
/** Object.defineProperty on a declared class field (scr_json.c): the
|
||||
* field's current value (borrowed), its FIELD_* attribute word, the
|
||||
* instance's CLASS_* integrity word, the field name and the descriptor.
|
||||
* Validates as Node does (TypeError "Cannot redefine property: f") and
|
||||
* answers a one-property holder of the defined value and attributes (+1),
|
||||
* read back with class.definedBits and class.definedValue. */
|
||||
"class.defineField": { argTypes: [DYN, F64, F64, STRING, DYN], result: DYN },
|
||||
"class.definedBits": { argTypes: [DYN, STRING], result: F64 },
|
||||
"class.definedValue": { argTypes: [DYN, STRING], result: DYN },
|
||||
/** Whether a property descriptor has a `value` member. */
|
||||
"class.descriptorHasValue": { argTypes: [DYN], result: BOOL },
|
||||
/** Throws Node's TypeError for a write to a read-only class field: the
|
||||
* field name and the instance's class name. */
|
||||
"class.throwReadOnly": { argTypes: [STRING, STRING], result: VOID },
|
||||
/** Throws Node's TypeError for a class field initializer on a
|
||||
* non-extensible instance: the field name. */
|
||||
"class.throwNotExtensible": { argTypes: [STRING], result: VOID },
|
||||
"dyn.nativeSetNew": { argTypes: [DYN], result: DYN },
|
||||
"dyn.nativeSetIs": { argTypes: [DYN], result: BOOL },
|
||||
"dyn.nativeMapIs": { argTypes: [DYN], result: BOOL },
|
||||
@@ -2391,6 +2408,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** Rest listeners use the checked function bridge to pack event arguments. */
|
||||
"child.onDyn": { argTypes: [CHILD_T, STRING, DYN], result: VOID },
|
||||
"process.connected": { argTypes: [], result: BOOL },
|
||||
/** Whether this fork child opened its channel; part of process.send's
|
||||
* presence in programs with fork targets. Never throws. */
|
||||
"process.ipcOpen": { argTypes: [], result: BOOL },
|
||||
"process.send": { argTypes: [STRING], result: BOOL },
|
||||
"process.sendCb": { argTypes: [STRING, null], result: BOOL },
|
||||
"process.disconnect": { argTypes: [], result: VOID },
|
||||
@@ -3025,8 +3045,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"process.exit": { argTypes: [F64], result: VOID },
|
||||
/** Numeric process.exitCode write (integer validation, implicit exit status). */
|
||||
"process.setExitCode": { argTypes: [F64], result: VOID },
|
||||
/** The process.exitCode write of a program that also reads the
|
||||
* property: the same validation, and the code counts as assigned. */
|
||||
"process.setExitCodeTracked": { argTypes: [F64], result: VOID },
|
||||
/** The code for process.exit() with no argument, or zero when unset. */
|
||||
"process.currentExitCode": { argTypes: [], result: F64 },
|
||||
/** Whether process.exitCode reads a code rather than undefined. Never throws. */
|
||||
"process.hasExitCode": { argTypes: [], result: BOOL },
|
||||
"process.cwd": { argTypes: [], result: STRING },
|
||||
/** getpid(2) / getuid(2): zero args → f64. POSIX-only target, so both
|
||||
* always answer (the checker's `getuid?` optionality covers Windows —
|
||||
@@ -3231,6 +3256,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** SameValue over checked values, including NaN, signed zero and reference identity. */
|
||||
"dyn.sameValue": { argTypes: [DYN, DYN], result: BOOL },
|
||||
"error.stack": { argTypes: [null], result: STRING },
|
||||
"error.setStack": { argTypes: [null, STRING], result: VOID },
|
||||
"error.stackLimitGet": { argTypes: [], result: F64 },
|
||||
"error.stackLimitSet": { argTypes: [F64], result: VOID },
|
||||
// Receiver (any error-hierarchy object) and the program-dependent
|
||||
@@ -3499,6 +3525,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* throws err when unhandled. count/countFn/names/listeners/getMax/
|
||||
* setMax/setDefaultMax/getDefaultMax are the introspection surface. */
|
||||
"emitter.new": { argTypes: [], result: VOID },
|
||||
/** `x.constructor.name` of an emitter or stream (a borrowed object of
|
||||
* EventEmitter's hierarchy): the +1 class name the instance records. */
|
||||
"emitter.className": { argTypes: [null], result: STRING },
|
||||
"emitter.ctor": { argTypes: [null], result: VOID },
|
||||
"emitter.on": { argTypes: [null, STRING, null, BOOL, BOOL], result: VOID },
|
||||
"emitter.off": { argTypes: [null, STRING, null], result: VOID },
|
||||
@@ -4016,6 +4045,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
argTypes: [F64, STRING, DYN],
|
||||
result: { kind: "promise", inner: STRING },
|
||||
},
|
||||
/** The question forms with a checked-dynamic query (a JavaScript value
|
||||
* that need not be a string). */
|
||||
"rl.questionDyn": { argTypes: [F64, DYN, null], result: VOID },
|
||||
"rl.questionAsyncDyn": { argTypes: [F64, DYN], result: { kind: "promise", inner: STRING } },
|
||||
"rl.questionAsyncSignalDyn": {
|
||||
argTypes: [F64, DYN, DYN],
|
||||
result: { kind: "promise", inner: STRING },
|
||||
},
|
||||
"rl.close": { argTypes: [F64], result: VOID },
|
||||
"rl.pause": { argTypes: [F64], result: VOID },
|
||||
"rl.resume": { argTypes: [F64], result: VOID },
|
||||
@@ -4023,6 +4060,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
/** prompt(preserveCursor): [handle, preserveCursor] */
|
||||
"rl.promptWith": { argTypes: [F64, BOOL], result: VOID },
|
||||
"rl.setPrompt": { argTypes: [F64, STRING], result: VOID },
|
||||
"rl.setPromptDyn": { argTypes: [F64, DYN], result: VOID },
|
||||
"rl.getPrompt": { argTypes: [F64], result: STRING },
|
||||
"rl.write": { argTypes: [F64, STRING], result: VOID },
|
||||
/** write(data, key): [handle, hasData, data, hasKey, hasName, name,
|
||||
@@ -4055,6 +4093,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"rl.terminal": { argTypes: [F64, F64, BOOL, DYN, BOOL], result: VOID },
|
||||
"rl.line": { argTypes: [F64], result: STRING },
|
||||
"rl.isTerminal": { argTypes: [F64], result: BOOL },
|
||||
"rl.isPromises": { argTypes: [F64], result: BOOL },
|
||||
/** createInterface's input or output as a checked-dynamic value:
|
||||
* [value, output] — the stream; refused when it is not one. */
|
||||
"rl.inputFromDyn": { argTypes: [DYN, BOOL], result: { kind: "object", className: "%Readable" } },
|
||||
"rl.outputFromDyn": {
|
||||
argTypes: [DYN, BOOL],
|
||||
result: { kind: "object", className: "%Writable" },
|
||||
},
|
||||
"rl.cursor": { argTypes: [F64], result: F64 },
|
||||
/** readline.cursorTo / moveCursor / clearLine / clearScreenDown: rl.seq
|
||||
* [kind (0-3), the first and second numbers, whether the second is
|
||||
|
||||
@@ -1932,8 +1932,23 @@ export type IrStmt =
|
||||
| { kind: "return"; value: IrExpr | null; awaited?: true; loc: SrcLoc }
|
||||
/** Field write `obj.f = v` — statement-only, like `assign`/`arraySet`.
|
||||
* Evaluation order: obj, then value. The old value is released; ownership
|
||||
* of a refcounted new value MOVES into the object. */
|
||||
| { kind: "fieldSet"; obj: IrExpr; className: string; field: string; value: IrExpr; loc: SrcLoc }
|
||||
* of a refcounted new value MOVES into the object. `sloppy` marks a write
|
||||
* in sloppy-mode code, which skips a read-only field instead of throwing.
|
||||
* `define` marks a class field initializer, which defines the property
|
||||
* (a non-extensible instance rejects it). `attributesChecked` marks a
|
||||
* write that already honors the instance's property attributes
|
||||
* (CLASS_ATTRIBUTES). */
|
||||
| {
|
||||
kind: "fieldSet";
|
||||
obj: IrExpr;
|
||||
className: string;
|
||||
field: string;
|
||||
value: IrExpr;
|
||||
sloppy?: true;
|
||||
define?: true;
|
||||
attributesChecked?: true;
|
||||
loc: SrcLoc;
|
||||
}
|
||||
/** Record field write `r.f = v` — mirrors `fieldSet` exactly (evaluation
|
||||
* order obj then value; old value released; refcounted new value moved
|
||||
* in), with a shape id in place of a class name. */
|
||||
@@ -3891,6 +3906,9 @@ function isJsonSafeAt(
|
||||
const shape = getRecord(t.shapeId);
|
||||
if (!shape) return false;
|
||||
if (recordTextCodecClass(shape) !== null) return false;
|
||||
// Accessor slots are not function-valued properties: Node's
|
||||
// JSON.stringify invokes the getter (see accessorSlotProp).
|
||||
if (shapeHasAccessorSlots(shape)) return false;
|
||||
// The recursive knot: answer true and let the rest of the graph
|
||||
// decide (any unsafe constituent ends the traversal immediately).
|
||||
if (visiting.has(t.shapeId)) return true;
|
||||
@@ -4075,6 +4093,27 @@ export function isClassOwnEnumerableFieldName(name: string): boolean {
|
||||
/** Compiler-owned storage for properties added through untyped references. */
|
||||
export const DYN_CLASS_PROPERTIES = "%dynProperties";
|
||||
|
||||
/** Compiler-owned integrity state of a class instance whose hierarchy is
|
||||
* passed to Object.freeze, Object.seal, Object.preventExtensions or
|
||||
* Object.defineProperty: an f64 holding CLASS_NOT_EXTENSIBLE, CLASS_SEALED
|
||||
* and CLASS_FROZEN bits. Other hierarchies have no such field. */
|
||||
export const CLASS_ATTRIBUTES = "%attributes";
|
||||
export const CLASS_NOT_EXTENSIBLE = 1;
|
||||
export const CLASS_SEALED = 2;
|
||||
export const CLASS_FROZEN = 4;
|
||||
|
||||
/** Compiler-owned attributes of one declared field that Object.defineProperty
|
||||
* changes: an f64 holding FIELD_NOT_WRITABLE, FIELD_NOT_ENUMERABLE and
|
||||
* FIELD_NOT_CONFIGURABLE bits (zero is an ordinary data property). */
|
||||
export const CLASS_FIELD_ATTRIBUTES_PREFIX = "%attr:";
|
||||
export const FIELD_NOT_WRITABLE = 1;
|
||||
export const FIELD_NOT_ENUMERABLE = 2;
|
||||
export const FIELD_NOT_CONFIGURABLE = 4;
|
||||
|
||||
export function classFieldAttributesName(field: string): string {
|
||||
return CLASS_FIELD_ATTRIBUTES_PREFIX + field;
|
||||
}
|
||||
|
||||
/** A class capsule can always preserve its exact native identity. Its
|
||||
* optional property view additionally needs converters in both directions;
|
||||
* fields such as Maps may remain opaque without preventing the round trip. */
|
||||
@@ -4228,6 +4267,9 @@ function canBoxDynCompositeAt(
|
||||
const shape = getRecord(t.shapeId);
|
||||
if (!shape) return false;
|
||||
if (recordTextCodecClass(shape) !== null) return false;
|
||||
// Accessor slots hold the getter and setter closures, not property
|
||||
// values (see accessorSlotProp).
|
||||
if (shapeHasAccessorSlots(shape)) return false;
|
||||
// Recursive shapes answer coinductively, like isJsonSafeType.
|
||||
if (visiting.has(t.shapeId)) return true;
|
||||
visiting.add(t.shapeId);
|
||||
|
||||
@@ -9,59 +9,6 @@ import {
|
||||
} from "./ir.js";
|
||||
import { everyModuleNode } from "./traverse.js";
|
||||
|
||||
export interface RuntimeFeatures {
|
||||
regex: boolean;
|
||||
copying: boolean;
|
||||
legacyTextDecoder: boolean;
|
||||
fileHandle: boolean;
|
||||
fetch: boolean;
|
||||
eventTarget: boolean;
|
||||
processEvents: boolean;
|
||||
/** process.emitWarning is assigned (process.setEmitWarning). Declared
|
||||
* in the literal's order: JSON.stringify of the features record must
|
||||
* match between the Node-run and self-hosted compilers. */
|
||||
warningReplace: boolean;
|
||||
/** stdin setEncoding / string data listeners / stdio 'resize' events. */
|
||||
stdioExtended: boolean;
|
||||
emitter: boolean;
|
||||
stream: boolean;
|
||||
zlib: boolean;
|
||||
dc: boolean;
|
||||
assert: boolean;
|
||||
dynInvoke: boolean;
|
||||
dynAsync: boolean;
|
||||
/** The program has dynamic-tier jsval operations (js.* builtins, the
|
||||
* jsval pass): links the scr_js_* units (two-tier design I3). */
|
||||
dynTier: boolean;
|
||||
/** The program enters AsyncLocalStorage stores (store.run, enterWith,
|
||||
* a diagnostics_channel store binding) or reads the test runner's
|
||||
* context (getTestContext): its timer, immediate, tick and
|
||||
* queueMicrotask scheduling goes through the runtime's *_als entry
|
||||
* points, which carry the active stores into each callback. */
|
||||
asyncContext: boolean;
|
||||
inspect: boolean;
|
||||
childProcess: boolean;
|
||||
net: boolean;
|
||||
symbol: boolean;
|
||||
bigint: boolean;
|
||||
searchParams: boolean;
|
||||
qs: boolean;
|
||||
parseArgs: boolean;
|
||||
fsWatch: boolean;
|
||||
nodeTest: boolean;
|
||||
/** The program calls mock.timers members: its timer calls go through
|
||||
* scr_test.c's mock-aware entry points. */
|
||||
mockTimers: boolean;
|
||||
dgram: boolean;
|
||||
http: boolean;
|
||||
http2: boolean;
|
||||
tls: boolean;
|
||||
tlsCa: boolean;
|
||||
cipher: boolean;
|
||||
/** crypto.subtle importKey/sign/verify/digest: scr_webcrypto.c. */
|
||||
webcrypto: boolean;
|
||||
}
|
||||
|
||||
const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
// DEP0040 rides the dyn-async unit's warning machinery.
|
||||
"punycode.warn",
|
||||
@@ -204,6 +151,8 @@ const STREAM_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"fileHandle.createWriteStream",
|
||||
"fs.createReadStream",
|
||||
"fs.createWriteStream",
|
||||
"rl.inputFromDyn",
|
||||
"rl.outputFromDyn",
|
||||
"sp.finished",
|
||||
"sp.pipeline",
|
||||
"sp.pipelineStages",
|
||||
@@ -262,6 +211,7 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"process.connected",
|
||||
"process.disconnect",
|
||||
"process.forkTarget",
|
||||
"process.ipcOpen",
|
||||
"process.onDisconnect",
|
||||
"process.onMessage",
|
||||
"process.send",
|
||||
@@ -311,9 +261,6 @@ const COPYING_BYTES_METHODS: ReadonlySet<string> = new Set([
|
||||
"with",
|
||||
]);
|
||||
|
||||
const startsWithAny = (fn: string, prefixes: readonly string[]): boolean =>
|
||||
prefixes.some((prefix) => fn.startsWith(prefix));
|
||||
|
||||
/** Inspect types and expressions together in one traversal. This is a fresh
|
||||
* snapshot: callers may mutate an IR module between compilation passes. */
|
||||
/** The checked-dynamic undefined value (dynUndefinedExpr). */
|
||||
@@ -393,60 +340,366 @@ function boxesPromise(t: IrType, tables: TypeTables, seen: Set<string>): boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** The libCall fns served by the OPTIONAL events unit (scr_events.c):
|
||||
* process signal/exit listeners and the piped-stdin surface. */
|
||||
const PROCESS_EVENT_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"process.stdio",
|
||||
"process.onSignal",
|
||||
"process.offSignal",
|
||||
"process.onExit",
|
||||
"process.offExit",
|
||||
"stdin.onData",
|
||||
"stdin.onEnd",
|
||||
"stdin.onError",
|
||||
"stdin.nextChunk",
|
||||
// node:readline rides the stdin unit (scr_readline.c links beside
|
||||
// scr_events.c under the same gate).
|
||||
"rl.create",
|
||||
"rl.createSignal",
|
||||
"rl.question",
|
||||
"rl.questionAsync",
|
||||
"rl.questionAsyncSignal",
|
||||
"rl.questionDyn",
|
||||
"rl.questionAsyncDyn",
|
||||
"rl.questionAsyncSignalDyn",
|
||||
"rl.close",
|
||||
"rl.pause",
|
||||
"rl.resume",
|
||||
"rl.prompt",
|
||||
"rl.promptWith",
|
||||
"rl.setPrompt",
|
||||
"rl.setPromptDyn",
|
||||
"rl.getPrompt",
|
||||
"rl.write",
|
||||
"rl.writeKey",
|
||||
"rl.on",
|
||||
"rl.iterOpen",
|
||||
"rl.iterNext",
|
||||
"rl.iterReturn",
|
||||
"rl.checkOptions",
|
||||
"rl.checkCompleter",
|
||||
"rl.setOptions",
|
||||
"rl.setHistory",
|
||||
"rl.history",
|
||||
"rl.numProp",
|
||||
"rl.terminal",
|
||||
"rl.line",
|
||||
"rl.isTerminal",
|
||||
"rl.isPromises",
|
||||
"rl.cursor",
|
||||
"rl.seq",
|
||||
"rl.moves",
|
||||
"rl.cursorPos",
|
||||
"rl.keypressStdin",
|
||||
"rl.keypressStream",
|
||||
"stdin.onKeypress",
|
||||
"rl.uiOpen",
|
||||
"rl.uiOpenStream",
|
||||
"rl.uiCursorTo",
|
||||
"rl.uiMoveCursor",
|
||||
"rl.uiClearLine",
|
||||
"rl.uiClearScreenDown",
|
||||
"rl.uiRollback",
|
||||
"rl.uiCommit",
|
||||
]);
|
||||
|
||||
/** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the
|
||||
* allowHTTP1 compatibility slice) — they must NOT pull scr_http2.c, so
|
||||
* divergence-57 binaries keep their exact link line. */
|
||||
const HTTP2_LEGACY_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"http2.streamNoop",
|
||||
"http2.streamUndefCall",
|
||||
]);
|
||||
|
||||
/** Names that reach the extended process-stream surface (the
|
||||
* SCR_STDIO_EXTENDED events unit): stdin.setEncoding and 'resize'. */
|
||||
const STDIO_EXTENDED_NAMES: ReadonlySet<string> = new Set(["setEncoding", "resize"]);
|
||||
|
||||
function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): RuntimeFeatures {
|
||||
const features: RuntimeFeatures = {
|
||||
regex: false,
|
||||
copying: false,
|
||||
legacyTextDecoder: false,
|
||||
fileHandle: false,
|
||||
fetch: false,
|
||||
// Subclasses of the runtime EventTarget/Event bases tear down and box
|
||||
// through the EventTarget unit even when no event.* libCall survives.
|
||||
eventTarget: (mod.classes ?? []).some(
|
||||
(c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS,
|
||||
),
|
||||
processEvents: false,
|
||||
warningReplace: false,
|
||||
stdioExtended: false,
|
||||
emitter:
|
||||
mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
|
||||
stream: (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)),
|
||||
zlib: false,
|
||||
dc: false,
|
||||
assert: false,
|
||||
dynInvoke: mod.workers === true,
|
||||
dynAsync:
|
||||
/** The libCall nodes the rules below test. */
|
||||
type LibCallNode = IrExpr & { kind: "libCall" };
|
||||
|
||||
/** What makes a program need one optional runtime unit. The feature is set
|
||||
* when any condition holds: `initial` from the module's declarations, a
|
||||
* libCall named in `fns` or starting with one of `prefixes`, a libCall
|
||||
* `libCall` accepts, another expression node `expr` accepts, or a type
|
||||
* slot of one of the `types` kinds. A libCall or type that needs several
|
||||
* units appears in each unit's rule. */
|
||||
interface RuntimeFeatureRule {
|
||||
initial?: (mod: IrModule) => boolean;
|
||||
fns?: readonly IrLibFn[];
|
||||
prefixes?: readonly string[];
|
||||
libCall?: (node: LibCallNode) => boolean;
|
||||
expr?: (node: IrExpr, tables: () => TypeTables) => boolean;
|
||||
types?: readonly IrType["kind"][];
|
||||
}
|
||||
|
||||
/** Gives every rule in the table one type. */
|
||||
const rule = (r: RuntimeFeatureRule): RuntimeFeatureRule => r;
|
||||
|
||||
/* The HTTP, HTTPS, HTTP/2 and TLS surfaces, which link the net unit. */
|
||||
const NET_PREFIXES: readonly string[] = ["net.", "http.", "tls.", "https.", "http2."];
|
||||
|
||||
/** One rule per runtime feature, sorted by name. A new feature is an
|
||||
* entry here and its line in noRuntimeFeatures; RuntimeFeatures, the scan
|
||||
* and its early exit derive from this table. */
|
||||
const RUNTIME_FEATURE_RULES = {
|
||||
assert: rule({ prefixes: ["assert."] }),
|
||||
/** The program enters AsyncLocalStorage stores (store.run, enterWith,
|
||||
* a diagnostics_channel store binding) or reads the test runner's
|
||||
* context (getTestContext): its timer, immediate, tick and
|
||||
* queueMicrotask scheduling goes through the runtime's *_als entry
|
||||
* points, which carry the active stores into each callback. */
|
||||
asyncContext: rule({ fns: [...ASYNC_CONTEXT_LIB_FNS] }),
|
||||
bigint: rule({ fns: ["process.hrtimeValue"], prefixes: ["bigint."], types: ["bigint"] }),
|
||||
childProcess: rule({
|
||||
fns: [...CHILD_PROCESS_LIB_FNS],
|
||||
prefixes: CHILD_PROCESS_LIB_PREFIXES,
|
||||
types: ["child", "childStream", "childWriter", "spawnRes"],
|
||||
}),
|
||||
/** scr_cipher.c over the mbedTLS archive; DEP0182 reports through the
|
||||
* process-warning unit, and unknown output encodings render through the
|
||||
* inspect unit. */
|
||||
cipher: rule({ fns: ["crypto.cipherNew"] }),
|
||||
copying: rule({
|
||||
expr: (node, _tables) =>
|
||||
(node.kind === "arrIntrinsic" && COPYING_ARR_METHODS.has(node.method)) ||
|
||||
(node.kind === "bytesIntrinsic" && COPYING_BYTES_METHODS.has(node.method)),
|
||||
}),
|
||||
dc: rule({ prefixes: ["dc."] }),
|
||||
/** dns.lookup lives in the dgram unit. */
|
||||
dgram: rule({ prefixes: ["dgram.", "dns."], types: ["dgramSocket"] }),
|
||||
dynAsync: rule({
|
||||
initial: (mod) =>
|
||||
mod.workers === true ||
|
||||
mod.functions.some(
|
||||
(fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn",
|
||||
),
|
||||
dynTier: false,
|
||||
// The legacy url functions and crypto.createCipheriv emit their
|
||||
// deprecation warnings through the dyn-async unit.
|
||||
fns: [
|
||||
...DYN_ASYNC_LIB_FNS,
|
||||
...READABLE_DYN_ASYNC_LIB_FNS,
|
||||
...LEGACY_URL_LIB_FNS,
|
||||
"crypto.cipherNew",
|
||||
],
|
||||
prefixes: READABLE_DYN_ASYNC_PREFIXES,
|
||||
expr: (node, tables) =>
|
||||
(node.kind === "awaitExpr" && node.type.kind === "dyn") ||
|
||||
// Rejection reasons cross as dyn values (allSettled's records, any's
|
||||
// AggregateError list).
|
||||
(node.kind === "intrinsic" &&
|
||||
(node.name === "promise.allSettled" || node.name === "promise.any")) ||
|
||||
(node.kind === "dynFrom" && boxesPromise(node.value.type, tables(), new Set())),
|
||||
}),
|
||||
dynInvoke: rule({
|
||||
initial: (mod) => mod.workers === true,
|
||||
fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS],
|
||||
expr: (node, _tables) => node.kind === "dynInvoke",
|
||||
}),
|
||||
/** The program has dynamic-tier jsval operations (js.* builtins, the
|
||||
* jsval pass): links the scr_js_* units (two-tier design I3). */
|
||||
dynTier: rule({ prefixes: ["js."] }),
|
||||
/** The stream unit emits through the emitter unit, also when the
|
||||
* program constructs no stream (a pipeline of functions). */
|
||||
emitter: rule({
|
||||
initial: (mod) =>
|
||||
mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
|
||||
fns: [...STREAM_LIB_FNS],
|
||||
prefixes: ["emitter.", ...STREAM_LIB_PREFIXES],
|
||||
}),
|
||||
/** Subclasses of the runtime EventTarget/Event bases tear down and box
|
||||
* through the EventTarget unit even when no event.* libCall survives. */
|
||||
eventTarget: rule({
|
||||
initial: (mod) =>
|
||||
(mod.classes ?? []).some(
|
||||
(c) => c.name === RUNTIME_EVENT_TARGET_CLASS || c.name === RUNTIME_EVENT_CLASS,
|
||||
),
|
||||
fns: ["events.addAbortListener", ...ABORT_SIGNAL_LIB_FNS],
|
||||
prefixes: ["event."],
|
||||
}),
|
||||
fetch: rule({ prefixes: ["fetch."] }),
|
||||
fileHandle: rule({ types: ["fileHandle"] }),
|
||||
fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }),
|
||||
http: rule({
|
||||
prefixes: ["http.", "tls.", "https.", "http2."],
|
||||
types: ["httpReq", "httpRes", "httpClientReq", "secureCtx"],
|
||||
}),
|
||||
/** The real h2 surface (scr_http2.c); the legacy http2.* libCalls of the
|
||||
* allowHTTP1 slice are served by scr_http.c and scr_tls.c. */
|
||||
http2: rule({
|
||||
libCall: (node) => node.fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(node.fn),
|
||||
types: ["http2Session", "http2Stream"],
|
||||
}),
|
||||
/** scr_test.c renders thrown strings with the inspect unit's string
|
||||
* formatting; scr_util_callable.c wraps the inspect unit; readline's
|
||||
* terminal unit prints completion errors through it when there is a
|
||||
* completer. */
|
||||
inspect: rule({
|
||||
fns: ["global.native", "util.styleText", "crypto.cipherNew", ...UTIL_CALLABLE_LIB_FNS],
|
||||
prefixes: ["insp.", "console.", "test."],
|
||||
libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!),
|
||||
}),
|
||||
legacyTextDecoder: rule({ fns: [...LEGACY_TEXT_DECODER_LIB_FNS] }),
|
||||
/** The program calls mock.timers members: its timer calls go through
|
||||
* scr_test.c's mock-aware entry points. */
|
||||
mockTimers: rule({ prefixes: ["test.mockTimers", "test.ctxMockTimers"] }),
|
||||
net: rule({
|
||||
prefixes: NET_PREFIXES,
|
||||
types: [
|
||||
"netServer",
|
||||
"netSocket",
|
||||
"http2Session",
|
||||
"http2Stream",
|
||||
"httpReq",
|
||||
"httpRes",
|
||||
"httpClientReq",
|
||||
"secureCtx",
|
||||
],
|
||||
}),
|
||||
nodeTest: rule({ prefixes: ["test."], types: ["testCtx"] }),
|
||||
/** scr_util_callable.c reads options through the parseArgs unit's
|
||||
* descriptors. */
|
||||
parseArgs: rule({ fns: [...PARSE_ARGS_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS] }),
|
||||
processEvents: rule({ fns: [...PROCESS_EVENT_LIB_FNS, "util.styleText"], types: ["procStream"] }),
|
||||
/** scr_url_legacy.c parses and stringifies queries. */
|
||||
qs: rule({ fns: [...QS_LIB_FNS, ...LEGACY_URL_LIB_FNS] }),
|
||||
/** scr_url_legacy.c lowercases and normalizes hostnames with the regex
|
||||
* unit's libunicode, NODE_DEBUG matching in scr_util_callable.c and
|
||||
* readline's terminal unit use the regex unit, and locale case mapping
|
||||
* runs its default conversion. */
|
||||
regex: rule({
|
||||
fns: [
|
||||
...REGEX_LIB_FNS,
|
||||
...LEGACY_URL_LIB_FNS,
|
||||
...UTIL_CALLABLE_LIB_FNS,
|
||||
"rl.terminal",
|
||||
"str.localeCase",
|
||||
],
|
||||
expr: (node, _tables) =>
|
||||
node.kind === "regexLit" ||
|
||||
node.kind === "regexIntrinsic" ||
|
||||
(node.kind === "strIntrinsic" && CASE_MAPPING_STR_METHODS.has(node.method)),
|
||||
types: ["regex"],
|
||||
}),
|
||||
searchParams: rule({ fns: ["url.searchParams"], prefixes: ["sp."], types: ["searchParams"] }),
|
||||
/** stdin setEncoding / string data listeners / stdio 'resize' events. A
|
||||
* typed string 'data' listener takes setEncoding-decoded chunks; the
|
||||
* process stream object's setEncoding and 'resize' members are reached
|
||||
* by name (dynamic member calls and event-name strings). */
|
||||
stdioExtended: rule({
|
||||
libCall: (node) =>
|
||||
node.fn === "stdin.onData" &&
|
||||
node.args[0]?.type.kind === "func" &&
|
||||
node.args[0].type.params[0]?.kind === "string",
|
||||
expr: (node, _tables) =>
|
||||
(node.kind === "dynInvoke" && STDIO_EXTENDED_NAMES.has(node.method)) ||
|
||||
(node.kind === "strLit" && STDIO_EXTENDED_NAMES.has(node.value)),
|
||||
}),
|
||||
stream: rule({
|
||||
initial: (mod) => (mod.classes ?? []).some((c) => RUNTIME_STREAM_CLASSES.has(c.name)),
|
||||
fns: [...STREAM_LIB_FNS],
|
||||
prefixes: STREAM_LIB_PREFIXES,
|
||||
}),
|
||||
/** A Dirent row converted to dyn carries Node's Symbol(type) key
|
||||
* (scr_dyn_dirent_finish, defined in scr_symbol.c). */
|
||||
symbol: rule({
|
||||
initial: (mod) => (mod.records ?? []).some(isDirentShape),
|
||||
fns: [...SYMBOL_LIB_FNS],
|
||||
prefixes: ["sym.", "disposable."],
|
||||
types: ["symbol"],
|
||||
}),
|
||||
tls: rule({ prefixes: ["tls.", "https.", "http2."], types: ["secureCtx"] }),
|
||||
tlsCa: rule({ prefixes: ["tlsca."] }),
|
||||
/** process.emitWarning is assigned (process.setEmitWarning). */
|
||||
warningReplace: rule({ fns: ["process.setEmitWarning"] }),
|
||||
/** crypto.subtle importKey/sign/verify/digest and CryptoKey:
|
||||
* scr_webcrypto.c. */
|
||||
webcrypto: rule({ prefixes: ["webcrypto."] }),
|
||||
zlib: rule({ prefixes: ["zlib."] }),
|
||||
};
|
||||
|
||||
type RuntimeFeatureName = keyof typeof RUNTIME_FEATURE_RULES;
|
||||
|
||||
export type RuntimeFeatures = Record<RuntimeFeatureName, boolean>;
|
||||
|
||||
const RUNTIME_FEATURE_NAMES: RuntimeFeatureName[] = [];
|
||||
for (const name in RUNTIME_FEATURE_RULES) RUNTIME_FEATURE_NAMES.push(name as RuntimeFeatureName);
|
||||
|
||||
/** Every feature off, in the table's order. TypeScript rejects this
|
||||
* literal when it misses a feature or names one without a rule. */
|
||||
function noRuntimeFeatures(): RuntimeFeatures {
|
||||
return {
|
||||
assert: false,
|
||||
asyncContext: false,
|
||||
inspect: false,
|
||||
childProcess: false,
|
||||
net: false,
|
||||
// A Dirent row converted to dyn carries Node's Symbol(type) key
|
||||
// (scr_dyn_dirent_finish, defined in scr_symbol.c).
|
||||
symbol: (mod.records ?? []).some(isDirentShape),
|
||||
bigint: false,
|
||||
searchParams: false,
|
||||
qs: false,
|
||||
parseArgs: false,
|
||||
fsWatch: false,
|
||||
nodeTest: false,
|
||||
mockTimers: false,
|
||||
childProcess: false,
|
||||
cipher: false,
|
||||
copying: false,
|
||||
dc: false,
|
||||
dgram: false,
|
||||
dynAsync: false,
|
||||
dynInvoke: false,
|
||||
dynTier: false,
|
||||
emitter: false,
|
||||
eventTarget: false,
|
||||
fetch: false,
|
||||
fileHandle: false,
|
||||
fsWatch: false,
|
||||
http: false,
|
||||
http2: false,
|
||||
inspect: false,
|
||||
legacyTextDecoder: false,
|
||||
mockTimers: false,
|
||||
net: false,
|
||||
nodeTest: false,
|
||||
parseArgs: false,
|
||||
processEvents: false,
|
||||
qs: false,
|
||||
regex: false,
|
||||
searchParams: false,
|
||||
stdioExtended: false,
|
||||
stream: false,
|
||||
symbol: false,
|
||||
tls: false,
|
||||
tlsCa: false,
|
||||
cipher: false,
|
||||
warningReplace: false,
|
||||
webcrypto: false,
|
||||
zlib: false,
|
||||
};
|
||||
}
|
||||
|
||||
const ruleOf = (name: RuntimeFeatureName): RuntimeFeatureRule => RUNTIME_FEATURE_RULES[name];
|
||||
|
||||
/** The rules indexed for the scan: features by libCall name and by type
|
||||
* kind, and the prefix and predicate conditions in rule order. */
|
||||
const FEATURES_BY_FN = new Map<string, RuntimeFeatureName[]>();
|
||||
const FEATURES_BY_TYPE = new Map<string, RuntimeFeatureName[]>();
|
||||
const FEATURE_PREFIXES: { prefix: string; name: RuntimeFeatureName }[] = [];
|
||||
const FEATURE_LIBCALL_TESTS: RuntimeFeatureName[] = [];
|
||||
const FEATURE_EXPR_TESTS: RuntimeFeatureName[] = [];
|
||||
for (const name of RUNTIME_FEATURE_NAMES) {
|
||||
const rule = ruleOf(name);
|
||||
for (const fn of rule.fns ?? []) {
|
||||
const names = FEATURES_BY_FN.get(fn);
|
||||
if (names) names.push(name);
|
||||
else FEATURES_BY_FN.set(fn, [name]);
|
||||
}
|
||||
for (const kind of rule.types ?? []) {
|
||||
const names = FEATURES_BY_TYPE.get(kind);
|
||||
if (names) names.push(name);
|
||||
else FEATURES_BY_TYPE.set(kind, [name]);
|
||||
}
|
||||
for (const prefix of rule.prefixes ?? []) FEATURE_PREFIXES.push({ prefix, name });
|
||||
if (rule.libCall) FEATURE_LIBCALL_TESTS.push(name);
|
||||
if (rule.expr) FEATURE_EXPR_TESTS.push(name);
|
||||
}
|
||||
|
||||
function scanRuntimeFeatures(mod: IrModule, stopAt?: RuntimeFeatureName): RuntimeFeatures {
|
||||
const features = noRuntimeFeatures();
|
||||
for (const name of RUNTIME_FEATURE_NAMES) {
|
||||
const initial = ruleOf(name).initial;
|
||||
if (initial && initial(mod)) features[name] = true;
|
||||
}
|
||||
const keepGoing = (): boolean => stopAt === undefined || !features[stopAt];
|
||||
let tables: TypeTables | undefined;
|
||||
const typeTables = (): TypeTables =>
|
||||
@@ -458,207 +711,19 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
|
||||
const expr = (node: IrExpr): boolean => {
|
||||
if (node.kind === "libCall") {
|
||||
const fn = node.fn;
|
||||
if (fn.startsWith("js.")) features.dynTier = true;
|
||||
if (REGEX_LIB_FNS.has(fn)) features.regex = true;
|
||||
if (LEGACY_TEXT_DECODER_LIB_FNS.has(fn)) features.legacyTextDecoder = true;
|
||||
if (fn.startsWith("fetch.")) features.fetch = true;
|
||||
if (fn === "process.setEmitWarning") features.warningReplace = true;
|
||||
// A typed string 'data' listener takes setEncoding-decoded chunks.
|
||||
if (
|
||||
fn === "stdin.onData" &&
|
||||
node.args[0]?.type.kind === "func" &&
|
||||
node.args[0].type.params[0]?.kind === "string"
|
||||
)
|
||||
features.stdioExtended = true;
|
||||
if (fn.startsWith("event.") || fn === "events.addAbortListener") features.eventTarget = true;
|
||||
if (ABORT_SIGNAL_LIB_FNS.has(fn)) features.eventTarget = true;
|
||||
if (PROCESS_EVENT_LIB_FNS.has(fn)) features.processEvents = true;
|
||||
// With a completer, readline's terminal unit prints completion errors
|
||||
// through the inspect unit.
|
||||
if (fn === "rl.terminal") {
|
||||
features.regex = true;
|
||||
if (!isDynUndefined(node.args[3]!)) features.inspect = true;
|
||||
}
|
||||
if (fn.startsWith("emitter.")) features.emitter = true;
|
||||
if (STREAM_LIB_FNS.has(fn) || startsWithAny(fn, STREAM_LIB_PREFIXES)) {
|
||||
// the stream unit emits through the emitter unit, also when the
|
||||
// program constructs no stream (a pipeline of functions)
|
||||
features.stream = true;
|
||||
features.emitter = true;
|
||||
}
|
||||
if (fn.startsWith("zlib.")) features.zlib = true;
|
||||
if (READABLE_DYN_ASYNC_LIB_FNS.has(fn) || startsWithAny(fn, READABLE_DYN_ASYNC_PREFIXES))
|
||||
features.dynAsync = true;
|
||||
if (fn.startsWith("dc.")) features.dc = true;
|
||||
if (fn.startsWith("assert.")) features.assert = true;
|
||||
if (DYN_INVOKE_LIB_FNS.has(fn)) features.dynInvoke = true;
|
||||
if (DYN_ASYNC_LIB_FNS.has(fn)) features.dynAsync = true;
|
||||
if (ASYNC_CONTEXT_LIB_FNS.has(fn)) features.asyncContext = true;
|
||||
if (
|
||||
fn.startsWith("insp.") ||
|
||||
fn.startsWith("console.") ||
|
||||
fn === "global.native" ||
|
||||
fn === "util.styleText"
|
||||
)
|
||||
features.inspect = true;
|
||||
if (CHILD_PROCESS_LIB_FNS.has(fn) || startsWithAny(fn, CHILD_PROCESS_LIB_PREFIXES))
|
||||
features.childProcess = true;
|
||||
if (fn.startsWith("net.")) features.net = true;
|
||||
if (fn.startsWith("http.")) {
|
||||
features.net = true;
|
||||
features.http = true;
|
||||
}
|
||||
if (fn.startsWith("tls.") || fn.startsWith("https.") || fn.startsWith("http2.")) {
|
||||
features.net = true;
|
||||
features.http = true;
|
||||
features.tls = true;
|
||||
}
|
||||
if (fn.startsWith("http2.") && !HTTP2_LEGACY_FNS.has(fn)) features.http2 = true;
|
||||
if (fn.startsWith("sym.") || fn.startsWith("disposable.") || SYMBOL_LIB_FNS.has(fn))
|
||||
features.symbol = true;
|
||||
if (fn === "process.hrtimeValue") features.bigint = true;
|
||||
if (fn.startsWith("bigint.")) features.bigint = true;
|
||||
if (fn.startsWith("sp.") || fn === "url.searchParams") features.searchParams = true;
|
||||
if (QS_LIB_FNS.has(fn)) features.qs = true;
|
||||
// scr_url_legacy.c lowercases and normalizes hostnames (the regex
|
||||
// unit's libunicode), parses and stringifies queries, and emits its
|
||||
// deprecation warnings through the dyn-async unit.
|
||||
if (LEGACY_URL_LIB_FNS.has(fn)) {
|
||||
features.regex = true;
|
||||
features.qs = true;
|
||||
features.dynAsync = true;
|
||||
}
|
||||
if (PARSE_ARGS_LIB_FNS.has(fn)) features.parseArgs = true;
|
||||
if (fn === "util.styleText") features.processEvents = true;
|
||||
// scr_util_callable.c: dyn wrappers over the descriptor, inspect,
|
||||
// regex (NODE_DEBUG matching) and terminal-color units.
|
||||
if (UTIL_CALLABLE_LIB_FNS.has(fn)) {
|
||||
features.parseArgs = true;
|
||||
features.inspect = true;
|
||||
features.dynInvoke = true;
|
||||
features.regex = true;
|
||||
}
|
||||
if (fn.startsWith("fs.watch") || fn.startsWith("watcher.")) features.fsWatch = true;
|
||||
// scr_test.c renders thrown strings in the failing-tests section with
|
||||
// the inspect unit's string formatting.
|
||||
if (fn.startsWith("test.")) {
|
||||
features.nodeTest = true;
|
||||
features.inspect = true;
|
||||
if (fn.startsWith("test.mockTimers") || fn.startsWith("test.ctxMockTimers")) {
|
||||
features.mockTimers = true;
|
||||
}
|
||||
}
|
||||
if (fn.startsWith("dgram.") || fn.startsWith("dns.")) features.dgram = true;
|
||||
if (fn.startsWith("tlsca.")) features.tlsCa = true;
|
||||
// Locale case mapping runs the default conversion of the regex unit.
|
||||
if (fn === "str.localeCase") features.regex = true;
|
||||
// scr_cipher.c over the mbedTLS archive; DEP0182 reports through the
|
||||
// process-warning unit, and unknown output encodings render through
|
||||
// the inspect unit.
|
||||
if (fn === "crypto.cipherNew") {
|
||||
features.cipher = true;
|
||||
features.dynAsync = true;
|
||||
features.inspect = true;
|
||||
}
|
||||
// crypto.subtle and CryptoKey: scr_webcrypto.c.
|
||||
if (fn.startsWith("webcrypto.")) features.webcrypto = true;
|
||||
for (const name of FEATURES_BY_FN.get(fn) ?? []) features[name] = true;
|
||||
for (const { prefix, name } of FEATURE_PREFIXES)
|
||||
if (!features[name] && fn.startsWith(prefix)) features[name] = true;
|
||||
for (const name of FEATURE_LIBCALL_TESTS)
|
||||
if (!features[name] && ruleOf(name).libCall!(node)) features[name] = true;
|
||||
} else {
|
||||
switch (node.kind) {
|
||||
case "regexLit":
|
||||
case "regexIntrinsic":
|
||||
features.regex = true;
|
||||
break;
|
||||
case "strIntrinsic":
|
||||
if (CASE_MAPPING_STR_METHODS.has(node.method)) features.regex = true;
|
||||
break;
|
||||
case "arrIntrinsic":
|
||||
if (COPYING_ARR_METHODS.has(node.method)) features.copying = true;
|
||||
break;
|
||||
case "bytesIntrinsic":
|
||||
if (COPYING_BYTES_METHODS.has(node.method)) features.copying = true;
|
||||
break;
|
||||
case "dynInvoke":
|
||||
features.dynInvoke = true;
|
||||
if (STDIO_EXTENDED_NAMES.has(node.method)) features.stdioExtended = true;
|
||||
break;
|
||||
// The process stream object's setEncoding and 'resize' members are
|
||||
// reached by name (dynamic member calls and event-name strings).
|
||||
case "strLit":
|
||||
if (STDIO_EXTENDED_NAMES.has(node.value)) features.stdioExtended = true;
|
||||
break;
|
||||
case "awaitExpr":
|
||||
if (node.type.kind === "dyn") features.dynAsync = true;
|
||||
break;
|
||||
case "intrinsic":
|
||||
// Rejection reasons cross as dyn values (allSettled's records,
|
||||
// any's AggregateError list).
|
||||
if (node.name === "promise.allSettled" || node.name === "promise.any")
|
||||
features.dynAsync = true;
|
||||
break;
|
||||
case "dynFrom":
|
||||
if (!features.dynAsync && boxesPromise(node.value.type, typeTables(), new Set()))
|
||||
features.dynAsync = true;
|
||||
break;
|
||||
}
|
||||
for (const name of FEATURE_EXPR_TESTS)
|
||||
if (!features[name] && ruleOf(name).expr!(node, typeTables)) features[name] = true;
|
||||
}
|
||||
return keepGoing();
|
||||
};
|
||||
const type = (node: IrType): boolean => {
|
||||
switch (node.kind) {
|
||||
case "regex":
|
||||
features.regex = true;
|
||||
break;
|
||||
case "fileHandle":
|
||||
features.fileHandle = true;
|
||||
break;
|
||||
case "procStream":
|
||||
features.processEvents = true;
|
||||
break;
|
||||
case "child":
|
||||
case "childStream":
|
||||
case "childWriter":
|
||||
case "spawnRes":
|
||||
features.childProcess = true;
|
||||
break;
|
||||
case "netServer":
|
||||
case "netSocket":
|
||||
features.net = true;
|
||||
break;
|
||||
case "http2Session":
|
||||
case "http2Stream":
|
||||
features.net = true;
|
||||
features.http2 = true;
|
||||
break;
|
||||
case "httpReq":
|
||||
case "httpRes":
|
||||
case "httpClientReq":
|
||||
features.net = true;
|
||||
features.http = true;
|
||||
break;
|
||||
case "secureCtx":
|
||||
features.net = true;
|
||||
features.http = true;
|
||||
features.tls = true;
|
||||
break;
|
||||
case "symbol":
|
||||
features.symbol = true;
|
||||
break;
|
||||
case "bigint":
|
||||
features.bigint = true;
|
||||
break;
|
||||
case "searchParams":
|
||||
features.searchParams = true;
|
||||
break;
|
||||
case "fsWatcher":
|
||||
features.fsWatch = true;
|
||||
break;
|
||||
case "testCtx":
|
||||
features.nodeTest = true;
|
||||
break;
|
||||
case "dgramSocket":
|
||||
features.dgram = true;
|
||||
break;
|
||||
}
|
||||
for (const name of FEATURES_BY_TYPE.get(node.kind) ?? []) features[name] = true;
|
||||
return keepGoing();
|
||||
};
|
||||
if (keepGoing()) everyModuleNode(mod, { expr, stmt: keepGoing, type });
|
||||
@@ -702,64 +767,6 @@ export function moduleUsesFetch(mod: IrModule): boolean {
|
||||
return scanRuntimeFeatures(mod, "fetch").fetch;
|
||||
}
|
||||
|
||||
/** The libCall fns served by the OPTIONAL events unit (scr_events.c):
|
||||
* process signal/exit listeners and the piped-stdin surface. */
|
||||
const PROCESS_EVENT_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"process.stdio",
|
||||
"process.onSignal",
|
||||
"process.offSignal",
|
||||
"process.onExit",
|
||||
"process.offExit",
|
||||
"stdin.onData",
|
||||
"stdin.onEnd",
|
||||
"stdin.onError",
|
||||
"stdin.nextChunk",
|
||||
// node:readline rides the stdin unit (scr_readline.c links beside
|
||||
// scr_events.c under the same gate).
|
||||
"rl.create",
|
||||
"rl.createSignal",
|
||||
"rl.question",
|
||||
"rl.questionAsync",
|
||||
"rl.questionAsyncSignal",
|
||||
"rl.close",
|
||||
"rl.pause",
|
||||
"rl.resume",
|
||||
"rl.prompt",
|
||||
"rl.promptWith",
|
||||
"rl.setPrompt",
|
||||
"rl.getPrompt",
|
||||
"rl.write",
|
||||
"rl.writeKey",
|
||||
"rl.on",
|
||||
"rl.iterOpen",
|
||||
"rl.iterNext",
|
||||
"rl.iterReturn",
|
||||
"rl.checkOptions",
|
||||
"rl.checkCompleter",
|
||||
"rl.setOptions",
|
||||
"rl.setHistory",
|
||||
"rl.history",
|
||||
"rl.numProp",
|
||||
"rl.terminal",
|
||||
"rl.line",
|
||||
"rl.isTerminal",
|
||||
"rl.cursor",
|
||||
"rl.seq",
|
||||
"rl.moves",
|
||||
"rl.cursorPos",
|
||||
"rl.keypressStdin",
|
||||
"rl.keypressStream",
|
||||
"stdin.onKeypress",
|
||||
"rl.uiOpen",
|
||||
"rl.uiOpenStream",
|
||||
"rl.uiCursorTo",
|
||||
"rl.uiMoveCursor",
|
||||
"rl.uiClearLine",
|
||||
"rl.uiClearScreenDown",
|
||||
"rl.uiRollback",
|
||||
"rl.uiCommit",
|
||||
]);
|
||||
|
||||
/** True when the module uses the process-events surface — the link switch
|
||||
* that pulls scr_events.c into the binary and has the emitted main call
|
||||
* scr_events_install (native-toolchain.ts + emitter; the scr_regex/scr_fetch/scr_zlib
|
||||
@@ -949,14 +956,6 @@ export function moduleUsesHttpServer(mod: IrModule): boolean {
|
||||
return scanRuntimeFeatures(mod, "http").http;
|
||||
}
|
||||
|
||||
/** The legacy http2.* libCalls implemented by scr_http.c/scr_tls.c (the
|
||||
* allowHTTP1 compatibility slice) — they must NOT pull scr_http2.c, so
|
||||
* divergence-57 binaries keep their exact link line. */
|
||||
const HTTP2_LEGACY_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"http2.streamNoop",
|
||||
"http2.streamUndefCall",
|
||||
]);
|
||||
|
||||
/** True when the module uses the REAL h2 surface (scr_http2.c): any core
|
||||
* http2.* libCall, or an h2 handle type left behind by a fenced statement
|
||||
* (its emitted release call needs the unit — the moduleUsesNet story). */
|
||||
|
||||
@@ -42,14 +42,15 @@ export function unshareNodes(mod: IrModule): IrModule {
|
||||
}
|
||||
if (affected.size === 0) return mod;
|
||||
// Rebuilding a function copies every node that has children; a repeated
|
||||
// childless node reads back as a fresh object at each of its sites there.
|
||||
// childless node gets its own shallow copy at each of its sites there.
|
||||
// Both kinds of copy keep sharing the type and location objects.
|
||||
const expr = (node: IrExpr): IrExpr => {
|
||||
const copy = mapExprChildren(node, expr, stmt);
|
||||
return copy !== node || !repeated.has(node) ? copy : reparseExpr(node);
|
||||
return copy !== node || !repeated.has(node) ? copy : copyChildlessExpr(node);
|
||||
};
|
||||
const stmt = (node: IrStmt): IrStmt => {
|
||||
const copy = mapStmtChildren(node, expr, stmt);
|
||||
return copy !== node || !repeated.has(node) ? copy : reparseStmt(node);
|
||||
return copy !== node || !repeated.has(node) ? copy : copyChildlessStmt(node);
|
||||
};
|
||||
return {
|
||||
...mod,
|
||||
@@ -58,3 +59,66 @@ export function unshareNodes(mod: IrModule): IrModule {
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/** A shallow copy of a node that mapExprChildren returns unchanged. Each
|
||||
* case spreads one node kind, so the copy is a plain record copy of that
|
||||
* kind. Any other kind is copied through the encoding. */
|
||||
function copyChildlessExpr(node: IrExpr): IrExpr {
|
||||
switch (node.kind) {
|
||||
case "varRef":
|
||||
return { ...node };
|
||||
case "strLit":
|
||||
return { ...node };
|
||||
case "numLit":
|
||||
return { ...node };
|
||||
case "boolLit":
|
||||
return { ...node };
|
||||
case "unitLit":
|
||||
return { ...node };
|
||||
case "closure":
|
||||
return { ...node };
|
||||
case "moduleNsRef":
|
||||
return { ...node };
|
||||
case "incDec":
|
||||
return { ...node };
|
||||
case "chainRecv":
|
||||
return { ...node };
|
||||
case "regexLit":
|
||||
return { ...node };
|
||||
case "templateStrings":
|
||||
return { ...node };
|
||||
case "setNew":
|
||||
return { ...node };
|
||||
case "mapNew":
|
||||
return { ...node };
|
||||
case "dynObjLit":
|
||||
return { ...node };
|
||||
case "selfRef":
|
||||
return { ...node };
|
||||
case "promiseWithResolvers":
|
||||
return { ...node };
|
||||
case "classRef":
|
||||
return { ...node };
|
||||
case "fieldAbsent":
|
||||
return { ...node };
|
||||
default:
|
||||
return reparseExpr(node);
|
||||
}
|
||||
}
|
||||
|
||||
/** A shallow copy of a statement that mapStmtChildren returns unchanged,
|
||||
* one case per kind as in copyChildlessExpr. */
|
||||
function copyChildlessStmt(node: IrStmt): IrStmt {
|
||||
switch (node.kind) {
|
||||
case "break":
|
||||
return { ...node };
|
||||
case "continue":
|
||||
return { ...node };
|
||||
case "runtimeFence":
|
||||
return { ...node };
|
||||
case "rethrow":
|
||||
return { ...node };
|
||||
default:
|
||||
return reparseStmt(node);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4109,6 +4109,7 @@ function validateFunction(
|
||||
if (checkLibEmitterCall(e, sig.result)) return;
|
||||
if (
|
||||
e.fn === "error.stack" ||
|
||||
e.fn === "error.setStack" ||
|
||||
e.fn === "error.cause" ||
|
||||
e.fn === "error.hasCause" ||
|
||||
e.fn === "error.setCause" ||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user