mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-31 07:23:06 +08:00
Merge main
This commit is contained in:
@@ -498,6 +498,30 @@ jobs:
|
||||
- name: TypeScript 7 full parity sweep
|
||||
run: pnpm test:ts7
|
||||
|
||||
# tsc-ts, the default checker, and tsgo (SCRIPTC_CHECKER=tsgo) serve the same TypeScript 7 API. Every corpus
|
||||
# program must compile to the same LLVM IR, with the same diagnostics, with either checker.
|
||||
checker_identity:
|
||||
needs: [validation_scope, lanes]
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
|
||||
name: test (tsgo and tsc-ts give the same IR)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 11.1.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
cache: pnpm
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build
|
||||
- name: Corpus LLVM IR with tsgo and with tsc-ts (release)
|
||||
run: pnpm test:checker-identity --optimization release
|
||||
- name: Corpus LLVM IR with tsgo and with tsc-ts (dev)
|
||||
run: pnpm test:checker-identity --optimization dev
|
||||
|
||||
llvm_artifacts_linux_x64:
|
||||
needs: [validation_scope, lanes]
|
||||
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
|
||||
@@ -863,7 +887,28 @@ jobs:
|
||||
# Require this check on the stack base. Upper layers must not pass before
|
||||
# full validation, so untested groups cannot merge. Merge bottom-up.
|
||||
test:
|
||||
needs: [validation_scope, lanes, code_quality, native_macos, tests, tests_macos, native_linux, effects, bootstrap_seed, bootstrap, linux_host_clang, ts7_parity, llvm_artifacts_linux_x64, llvm_artifacts_macos_arm64, llvm_artifacts_macos_x64, windows_cli, llvm_artifacts_windows_x64, mobile_runtime_packs]
|
||||
needs:
|
||||
[
|
||||
validation_scope,
|
||||
lanes,
|
||||
code_quality,
|
||||
native_macos,
|
||||
tests,
|
||||
tests_macos,
|
||||
native_linux,
|
||||
effects,
|
||||
bootstrap_seed,
|
||||
bootstrap,
|
||||
linux_host_clang,
|
||||
ts7_parity,
|
||||
checker_identity,
|
||||
llvm_artifacts_linux_x64,
|
||||
llvm_artifacts_macos_arm64,
|
||||
llvm_artifacts_macos_x64,
|
||||
windows_cli,
|
||||
llvm_artifacts_windows_x64,
|
||||
mobile_runtime_packs,
|
||||
]
|
||||
# Run even after failed or cancelled lanes: skipped required jobs allow merging.
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
@@ -887,6 +932,7 @@ jobs:
|
||||
lane bootstrap "${{ needs.bootstrap.result }}" "${{ needs.lanes.outputs.bootstrap }}"
|
||||
lane linux_host_clang "${{ needs.linux_host_clang.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane ts7_parity "${{ needs.ts7_parity.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane checker_identity "${{ needs.checker_identity.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane llvm_artifacts_linux_x64 "${{ needs.llvm_artifacts_linux_x64.result }}" "${{ needs.lanes.outputs.linux }}"
|
||||
lane llvm_artifacts_macos_arm64 "${{ needs.llvm_artifacts_macos_arm64.result }}" "${{ needs.lanes.outputs.macos }}"
|
||||
lane llvm_artifacts_macos_x64 "${{ needs.llvm_artifacts_macos_x64.result }}" "${{ needs.lanes.outputs.macos }}"
|
||||
|
||||
@@ -178,6 +178,9 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
|
||||
|
||||
<dt><code>SCRIPTC_RUNTIME_PACK</code></dt>
|
||||
<dd>A runtime pack directory to use instead of an installed <code>@scriptc/runtime-<target></code> package.</dd>
|
||||
|
||||
<dt><code>SCRIPTC_CHECKER</code></dt>
|
||||
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is <code>tsgo</code>, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.</dd>
|
||||
</dl>
|
||||
|
||||
```console
|
||||
|
||||
@@ -79,7 +79,6 @@ Some code compiles but can behave differently than under Node.js. Coverage repor
|
||||
- `SC6002`: values of one record type are built in different key orders, so the type keeps its declaration order, an object literal's key order differs from it, and the program observes the order through `JSON.stringify`, `Object.keys`, `Object.values`, `Object.entries`, `for...in`, or console output.
|
||||
- `SC6003`: `localeCompare` compares a string literal outside the built-in Latin-script collation.
|
||||
- `SC6004`: a call passes a class instance to a function that casts it to an unrelated class.
|
||||
- `SC6005`: a `Date` string literal uses a form the native parser does not accept.
|
||||
|
||||
## Gating builds and CI
|
||||
|
||||
|
||||
@@ -52,10 +52,14 @@ Type-only imports follow Node.js's type stripping. `import type` and `export typ
|
||||
|
||||
scriptc compiles a package automatically when it:
|
||||
|
||||
- ships its own type declarations, and
|
||||
- ships readable, unminified JavaScript.
|
||||
- ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and
|
||||
- ships readable JavaScript: unminified, or minified by a bundler whose helpers keep their names.
|
||||
|
||||
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. A package whose declarations come from a separate `@types/` package doesn't ship its own; its diagnostic names the package and the `--npm-static` option that compiles it anyway.
|
||||
The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use.
|
||||
|
||||
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself.
|
||||
|
||||
`graceful-fs` is the exception: it compiles as `node:fs` instead of from its JavaScript, with one [documented difference](/docs/limitations#graceful-fs).
|
||||
|
||||
<span id="value-conversion" />
|
||||
<span id="static-package-compilation-experimental" />
|
||||
@@ -63,17 +67,17 @@ A package that doesn't meet these requirements, or that uses something scriptc c
|
||||
|
||||
## Explicit package selection
|
||||
|
||||
Some packages rely on separately installed declarations, such as an `@types/` package, or on types inferred from their JavaScript. To try compiling one of them, name it with `--npm-static`:
|
||||
Some packages your program imports directly ship no declarations, so their types can only be inferred from their JavaScript. To try compiling one of them, name it with `--npm-static`:
|
||||
|
||||
```console
|
||||
$ scriptc build app.ts --npm-static=three -o app >/dev/null
|
||||
```
|
||||
|
||||
Naming a package skips the declaration requirement above, but the package still has to compile. The option selects only the packages it names: a dependency of a named package that doesn't ship its own declarations is still refused. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
|
||||
Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
|
||||
|
||||
## CommonJS packages
|
||||
|
||||
CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes do, and uses the other modules only inside functions that its top-level code can't call. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms.
|
||||
CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes and esbuild's per-file output do, and uses the other modules only inside functions that its top-level code can't call. Bundles built by esbuild compile with their bundled CommonJS modules and lazily initialized ES modules, each evaluated once, at its first use, as under Node.js. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms.
|
||||
|
||||
## Unsupported code inside a package
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/builtin-apis/process-memory.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/v8.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/performance-entries.mdx</include>
|
||||
|
||||
<include>../limitations/builtin-apis/builtin-module-lookup.mdx</include>
|
||||
@@ -212,6 +214,10 @@ title: "Limitations"
|
||||
|
||||
<include>../limitations/console-and-process-streams.mdx</include>
|
||||
|
||||
### graceful-fs
|
||||
|
||||
<include>../limitations/graceful-fs.mdx</include>
|
||||
|
||||
### DNS lookup errors
|
||||
|
||||
<include>../limitations/dns-lookup-errors.mdx</include>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported.
|
||||
Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
- **Date:** Date unions, spread constructor arguments, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported.
|
||||
Zero-argument, single number/string, and local-time year/month field constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
|
||||
JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization.
|
||||
A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`.
|
||||
See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
|
||||
See [Dates and timezones](#dates-and-timezones) for timezone data.
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
- **Globals:** Stored `globalThis` references support identity and probes for absent capabilities.
|
||||
Most builtin members require direct global access; access through a stored global object can raise `SC2020`.
|
||||
`Temporal` is not defined, as in Node.js 24: `typeof Temporal` is `"undefined"`, `globalThis.Temporal` is `undefined`, and any other use throws `ReferenceError: Temporal is not defined`.
|
||||
Programs that name it type-check against TypeScript's `Temporal` declarations.
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion.
|
||||
Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
|
||||
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
|
||||
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
|
||||
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
|
||||
The constructors `Object`, `Function`, `Date`, `RegExp`, `Map`, `Set`, `WeakMap`, `WeakSet`, and `Promise` read as function values; through a stored value only `Object` can be called, and `instanceof` and `new` against them remain unsupported.
|
||||
|
||||
@@ -26,3 +26,4 @@
|
||||
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.
|
||||
Chained listener registration is unsupported.
|
||||
`node:stream/web` exports the same `ReadableStream` class as the global; its other classes are unsupported.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
- **V8 heap statistics:** `node:v8` can be imported.
|
||||
`v8.getHeapStatistics()` returns Node's fields in Node's order, computed from the runtime allocator like `process.memoryUsage()`: `total_heap_size` and `total_physical_size` are the bytes the heap holds, `used_heap_size` the bytes allocated, `heap_size_limit` the machine's memory (the heap has no fixed ceiling), and `total_available_size` the limit minus the used bytes.
|
||||
Fields that describe V8 itself (`total_heap_size_executable`, `malloced_memory`, `peak_malloced_memory`, the global handle sizes, `external_memory`, and `does_zap_garbage`) are `0`, and `number_of_native_contexts` is `1`.
|
||||
The values are approximate and are not V8 heap figures.
|
||||
The other `node:v8` members observe the V8 engine, such as heap snapshots, profiles, flags, and serialization, and throw `SC2020` where they are used.
|
||||
@@ -5,6 +5,10 @@ Some builtins can be stored and passed as functions:
|
||||
- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`.
|
||||
- `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`.
|
||||
|
||||
In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles.
|
||||
Calls through these values bypass `node:test` mock timers.
|
||||
Other builtin globals stored in JavaScript remain opaque identity values.
|
||||
|
||||
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere.
|
||||
`util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
String constructors and `Date.parse` accept ECMAScript date-only forms, date-times with an explicit `Z` or numeric offset, and GMT certificate-validity strings from the supported X509 APIs.
|
||||
Other V8-specific forms and offset-less local date-times produce an Invalid Date or `NaN`; `SC6005` reports string literals in those forms.
|
||||
String constructors and `Date.parse` use V8's date-string grammar: ECMAScript date-time strings and the legacy forms Node.js accepts, such as RFC 1123 HTTP dates (`Mon, 01 Jan 2024 00:00:00 GMT`), `Date#toString` output, and `Jan 2, 2024`.
|
||||
Strings without a timezone, and `new Date(year, monthIndex, ...)` fields, are local time.
|
||||
|
||||
Local calendar getters use the operating system's timezone database.
|
||||
Local calendar getters and local-time conversions use the operating system's timezone database.
|
||||
Historical results can differ from Node.js when timezone data differs.
|
||||
For valid extreme years outside a platform calendar API's range, scriptc uses a calendar-equivalent year in the 400-year Gregorian cycle to obtain the timezone rule.
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
After a failed `dns.lookup` or `dns.lookupService`, Node calls the callback with the error alone, so the address and family parameters are `undefined`.
|
||||
A callback whose address parameter is typed `string` (as Node's own type declarations type it) receives `""` and `NaN` there, or an empty array for `all: true`, because a typed string or number cannot hold `undefined`.
|
||||
An untyped callback receives the error alone, as in Node.
|
||||
The promise forms in `node:dns/promises` (also reached as `dns.promises`) reject with the error and are unaffected.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
The `graceful-fs` package compiles as `node:fs`.
|
||||
Its imports and `require("graceful-fs")` calls resolve to the builtin module, so the package's JavaScript is not compiled, and `gracefulify(fs)` as a statement does nothing.
|
||||
Programs behave as with the package except under file-descriptor exhaustion: graceful-fs queues operations that fail with `EMFILE` or `ENFILE` and retries them when a descriptor closes, while a compiled program reports the error to its caller, as `fs` does.
|
||||
In a compiled program, `require("graceful-fs") === require("fs")` is `true`.
|
||||
@@ -1,4 +1,7 @@
|
||||
`eval`, `new Function`, and other ways of running JavaScript source at runtime are unsupported, in application code and in packages.
|
||||
Rewrite code that depends on them, or replace the package.
|
||||
To compute a value from code at build time instead, use `comptime`.
|
||||
A compiled program embeds no JavaScript engine, so it cannot run JavaScript source at runtime.
|
||||
`eval`, indirect `eval`, `new Function`, and `Function` behave as they do under `node --disallow-code-generation-from-strings`, a deliberate difference from default Node.js: every call throws a catchable `EvalError` with the message `Code generation from strings disallowed for this context`.
|
||||
The arguments are evaluated first, and the Function constructor converts them to strings before it throws, as Node.js does.
|
||||
Packages that test for code generation before using it, such as zod 4, take their interpreted path.
|
||||
Code that needs to evaluate source at runtime must be rewritten; to compute a value from code at build time, use `comptime`.
|
||||
The `vm` module is refused for the same reason.
|
||||
See [npm Dependencies](/docs/dependencies#which-packages-compile) for which packages compile.
|
||||
|
||||
@@ -239,9 +239,9 @@
|
||||
"apiEntries": 51,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"partial": 10,
|
||||
"refused": 0,
|
||||
"not-implemented": 51,
|
||||
"not-implemented": 41,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -809,10 +809,10 @@
|
||||
"apiEntries": 68,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 3,
|
||||
"partial": 4,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 65,
|
||||
"by-design": 64,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
@@ -869,9 +869,9 @@
|
||||
"apiEntries": 89,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 7,
|
||||
"partial": 8,
|
||||
"refused": 1,
|
||||
"not-implemented": 81,
|
||||
"not-implemented": 80,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -16626,7 +16626,7 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"status": "partial",
|
||||
"detail": "Derived from the exact API rows in this chapter.",
|
||||
"verification": "derived"
|
||||
}
|
||||
@@ -16718,9 +16718,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -16741,9 +16741,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -17132,9 +17132,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -17201,9 +17201,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -17316,9 +17316,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -17339,9 +17339,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -17362,9 +17362,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -17730,9 +17730,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -17753,9 +17753,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -75973,9 +75973,9 @@
|
||||
"anchor": "v8getheapstatistics",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "by-design",
|
||||
"detail": "Intentionally outside the static native execution model.",
|
||||
"verification": "architectural-policy"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -81011,9 +81011,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented static async-iteration subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
"nodeVersion": "24.22.0",
|
||||
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
|
||||
"summary": {
|
||||
"taskCount": 3453,
|
||||
"tierItemCount": 3453,
|
||||
"taskCount": 3454,
|
||||
"tierItemCount": 3454,
|
||||
"tiers": {
|
||||
"static": {
|
||||
"audit-partial": 1179,
|
||||
"verify-gap": 2146,
|
||||
"audit-partial": 1190,
|
||||
"verify-gap": 2136,
|
||||
"replace-refusal": 41,
|
||||
"classify": 2,
|
||||
"implement": 85
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"priorities": {
|
||||
"high": 2287,
|
||||
"normal": 694,
|
||||
"normal": 695,
|
||||
"low": 472
|
||||
}
|
||||
},
|
||||
@@ -12300,12 +12300,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:dns",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:dns.dnsPromises.lookup",
|
||||
"tests": [
|
||||
"tests/corpus/dns-promises-lookup.ts",
|
||||
"tests/corpus/dns-promises-untyped.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -12325,12 +12328,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:dns",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:dns.dnsPromises.lookupService",
|
||||
"tests": [
|
||||
"tests/corpus/dns-promises-lookup.ts",
|
||||
"tests/corpus/dns-promises-untyped.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -12750,12 +12756,15 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:dns",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:dns.dnsPromises.getDefaultResultOrder",
|
||||
"tests": [
|
||||
"tests/corpus/dns-promises-lookup.ts",
|
||||
"tests/corpus/dns-promises-untyped.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -12825,12 +12834,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:dns",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:dns.dns.lookup",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -12950,12 +12963,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:dns",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:dns.dns.getServers",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -12975,12 +12992,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:dns",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:dns.dns.lookup",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13000,12 +13021,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:dns",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:dns.dns.lookupService",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13400,12 +13425,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:dns",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:dns.dns.setDefaultResultOrder",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -13425,12 +13454,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:dns",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:dns.dns.getDefaultResultOrder",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -77813,6 +77846,29 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "v8:cefde4a8297c",
|
||||
"chapter": "v8",
|
||||
"label": "v8.getHeapStatistics()",
|
||||
"signature": "`v8.getHeapStatistics()`",
|
||||
"apiSymbol": "v8.getHeapStatistics",
|
||||
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/v8.html#v8getheapstatistics",
|
||||
"nodeStability": null,
|
||||
"priority": "normal",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.v8.getHeapStatistics",
|
||||
"tests": [
|
||||
"tests/corpus/v8-heap-statistics.ts",
|
||||
"tests/corpus/v8-require-optional.cjs"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "webcrypto:22408f834c48",
|
||||
"chapter": "webcrypto",
|
||||
@@ -81965,12 +82021,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:webstreams",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:webstreams.ReadableStream",
|
||||
"tests": [
|
||||
"tests/corpus/stream-web-readable.ts",
|
||||
"tests/corpus/stream-web-require.cjs",
|
||||
"tests/corpus/2862-web-readable-async-iteration.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -251,9 +251,9 @@
|
||||
"apiEntries": 51,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 0,
|
||||
"partial": 10,
|
||||
"refused": 0,
|
||||
"not-implemented": 51,
|
||||
"not-implemented": 41,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -866,10 +866,10 @@
|
||||
"apiEntries": 68,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 3,
|
||||
"partial": 4,
|
||||
"refused": 0,
|
||||
"not-implemented": 0,
|
||||
"by-design": 65,
|
||||
"by-design": 64,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
}
|
||||
@@ -926,9 +926,9 @@
|
||||
"apiEntries": 89,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 7,
|
||||
"partial": 8,
|
||||
"refused": 1,
|
||||
"not-implemented": 81,
|
||||
"not-implemented": 80,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -30720,8 +30720,15 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "derived:descendants"
|
||||
"status": "partial",
|
||||
"evidence": "derived:descendants",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs",
|
||||
"tests/corpus/dns-promises-lookup.ts",
|
||||
"tests/corpus/dns-promises-untyped.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "chapter"
|
||||
},
|
||||
@@ -30835,8 +30842,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:dns"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:dns.dnsPromises.lookup",
|
||||
"tests": [
|
||||
"tests/corpus/dns-promises-lookup.ts",
|
||||
"tests/corpus/dns-promises-untyped.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -30858,8 +30869,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:dns"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:dns.dnsPromises.lookupService",
|
||||
"tests": [
|
||||
"tests/corpus/dns-promises-lookup.ts",
|
||||
"tests/corpus/dns-promises-untyped.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -31249,8 +31264,12 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:dns"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:dns.dnsPromises.getDefaultResultOrder",
|
||||
"tests": [
|
||||
"tests/corpus/dns-promises-lookup.ts",
|
||||
"tests/corpus/dns-promises-untyped.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -31364,8 +31383,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:dns"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:dns.dns.lookup",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -31479,8 +31503,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:dns"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:dns.dns.getServers",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -31502,8 +31531,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:dns"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:dns.dns.lookup",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -31548,8 +31582,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:dns"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:dns.dns.lookupService",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -31916,8 +31955,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:dns"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:dns.dns.setDefaultResultOrder",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -31939,8 +31983,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:dns"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:dns.dns.getDefaultResultOrder",
|
||||
"tests": [
|
||||
"tests/corpus/5648-dns-lookup-forms.ts",
|
||||
"tests/corpus/5649-dns-service-servers.ts",
|
||||
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -104477,6 +104526,8 @@
|
||||
"status": "partial",
|
||||
"evidence": "derived:descendants",
|
||||
"tests": [
|
||||
"tests/corpus/v8-heap-statistics.ts",
|
||||
"tests/corpus/v8-require-optional.cjs",
|
||||
"tests/harness/node-test.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -105599,8 +105650,12 @@
|
||||
"anchor": "v8getheapstatistics",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "by-design",
|
||||
"evidence": "compiler-chapter-policy:v8"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.v8.getHeapStatistics",
|
||||
"tests": [
|
||||
"tests/corpus/v8-heap-statistics.ts",
|
||||
"tests/corpus/v8-require-optional.cjs"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -111212,6 +111267,8 @@
|
||||
"tests/corpus/2630-stream-consumers-errors.ts",
|
||||
"tests/corpus/2862-web-readable-async-iteration.ts",
|
||||
"tests/corpus/4875-stream-consumers-array-buffer.ts",
|
||||
"tests/corpus/stream-web-readable.ts",
|
||||
"tests/corpus/stream-web-require.cjs",
|
||||
"tests/harness/fetch-conformance.test.ts"
|
||||
]
|
||||
},
|
||||
@@ -111480,8 +111537,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:webstreams"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:webstreams.ReadableStream",
|
||||
"tests": [
|
||||
"tests/corpus/stream-web-readable.ts",
|
||||
"tests/corpus/stream-web-require.cjs",
|
||||
"tests/corpus/2862-web-readable-async-iteration.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
"node-builtin.dgram": ["tests/corpus/2597-dgram-send-ladders.cjs"],
|
||||
"node-builtin.diagnostics_channel": ["tests/corpus/2214-dc-bind-store.cjs"],
|
||||
"node-builtin.dns": ["tests/corpus/2597-dgram-send-ladders.cjs"],
|
||||
"node-builtin.dns.promises": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"],
|
||||
"node-builtin.stream.web": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs"],
|
||||
"node-builtin.v8": ["tests/corpus/v8-require-optional.cjs"],
|
||||
"node-builtin.v8.getHeapStatistics": ["tests/corpus/v8-heap-statistics.ts", "tests/corpus/v8-require-optional.cjs"],
|
||||
"node-builtin.events": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"],
|
||||
"node-builtin.fs": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts"],
|
||||
"node-builtin.fs.realpathSync.native": ["tests/corpus/fs-native-realpath.ts"],
|
||||
@@ -381,7 +385,10 @@
|
||||
{ "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"] }
|
||||
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] },
|
||||
{ "chapter": "dns", "symbols": ["dns.lookup", "dns.lookupService", "dns.getServers", "dns.getDefaultResultOrder", "dns.setDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/5648-dns-lookup-forms.ts", "tests/corpus/5649-dns-service-servers.ts", "tests/corpus/5650-dns-lookup-errors-untyped.cjs"] },
|
||||
{ "chapter": "dns", "symbols": ["dnsPromises.lookup", "dnsPromises.lookupService", "dnsPromises.getDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"] },
|
||||
{ "chapter": "webstreams", "symbols": ["ReadableStream"], "status": "partial", "evidence": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs", "tests/corpus/2862-web-readable-async-iteration.ts"] }
|
||||
],
|
||||
"dedicated": [
|
||||
{ "symbols": ["assert.doesNotThrow", "assert.doesNotReject"], "status": "partial", "evidence": ["tests/corpus/node24-no-error-assertions.ts", "tests/corpus/node24-no-error-assertions.cjs", "tests/corpus/node24-no-error-assertions-coercion.js", "tests/fixtures/node-types/no-error-assertions.ts", "tests/harness/project-config.test.ts"] },
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"bench:backend-analysis": "node scripts/bench-backend-analysis.mjs",
|
||||
"test": "vitest run",
|
||||
"test:ts7": "node scripts/test-ts7.mjs",
|
||||
"test:checker-identity": "node scripts/checker-identity.mjs",
|
||||
"generate:ts7-ast": "node scripts/generate-ts7-ast-schema.mjs",
|
||||
"test:fetch-conformance": "vitest run tests/harness/fetch-conformance.test.ts",
|
||||
"test:test262": "tsx tests/test262/run.ts",
|
||||
|
||||
@@ -42,6 +42,7 @@ export function relocateToolchain(manifest, sourceDirectory, destinationDirector
|
||||
const pathFrom = (value) => relative(destinationDirectory, resolve(sourceDirectory, value));
|
||||
for (const name of [
|
||||
"ts7",
|
||||
"tsc_ts",
|
||||
"llvm_package",
|
||||
"runtime_pack",
|
||||
"runtime_sources",
|
||||
@@ -96,6 +97,7 @@ export function installNativeCli(
|
||||
if (process.platform !== "win32") {
|
||||
for (const path of [
|
||||
manifest.ts7,
|
||||
...(manifest.tsc_ts === undefined ? [] : [manifest.tsc_ts]),
|
||||
manifest.comptime,
|
||||
join(manifest.llvm_package, "bin/scriptc-llvm-codegen"),
|
||||
]) {
|
||||
|
||||
@@ -116,6 +116,13 @@ async function tryFastPath(): Promise<number | null> {
|
||||
// the whole compiler graph to rediscover it.
|
||||
const helperRuntimePackTarget = !values.sanitize ? startup.precompiledRuntimePackTarget() : null;
|
||||
const helperObjectRoute = helperRuntimePackTarget !== null;
|
||||
// The full compiler reports a checker it cannot find.
|
||||
let checker: string | undefined;
|
||||
try {
|
||||
checker = startup.checkerCacheIdentity();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const hit = await startup.readRoutedExecutableCache(root, {
|
||||
entryPath: input,
|
||||
outDir,
|
||||
@@ -126,6 +133,7 @@ async function tryFastPath(): Promise<number | null> {
|
||||
...(optimization === "dev" || optimization === "speed" ? { optimization } : {}),
|
||||
...(values.strip ? { strip: true as const } : {}),
|
||||
...(values["detached-exit"] ? { detachedExit: true as const } : {}),
|
||||
...(checker === undefined ? {} : { checker }),
|
||||
npmStatic,
|
||||
ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! },
|
||||
target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${
|
||||
|
||||
@@ -1878,6 +1878,13 @@ declare module "node:fs" {
|
||||
declare module "fs" {
|
||||
export * from "node:fs";
|
||||
}
|
||||
/* graceful-fs — served as node:fs (it monkeypatches fs and retries on
|
||||
* EMFILE/ENFILE; compiled programs call fs directly, without the retry
|
||||
* queue). gracefulify is a no-op statement. The @types/graceful-fs shape. */
|
||||
declare module "graceful-fs" {
|
||||
export * from "node:fs";
|
||||
export function gracefulify<T>(fsModule: T): T;
|
||||
}
|
||||
|
||||
/* fs/promises — the SAME synchronous operations behind already-settled
|
||||
* promises: success fulfills, failure REJECTS with the would-be thrown
|
||||
@@ -6179,10 +6186,79 @@ declare module "dns" {
|
||||
export function getServers(): string[];
|
||||
export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim";
|
||||
export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void;
|
||||
/* The promise forms (node:dns/promises). */
|
||||
export const promises: typeof import("dns/promises");
|
||||
}
|
||||
declare module "node:dns" {
|
||||
export * from "dns";
|
||||
}
|
||||
/* node:dns/promises — lookup and lookupService as promises over the same
|
||||
* resolution as the callback forms: argument errors throw synchronously,
|
||||
* failures reject with the callbacks' errors. */
|
||||
declare module "dns/promises" {
|
||||
import type { LookupAddress, LookupAllOptions, LookupOneOptions, LookupOptions } from "dns";
|
||||
export function lookup(hostname: string, family: number): Promise<LookupAddress>;
|
||||
export function lookup(hostname: string, options: LookupOneOptions): Promise<LookupAddress>;
|
||||
export function lookup(hostname: string, options: LookupAllOptions): Promise<LookupAddress[]>;
|
||||
export function lookup(
|
||||
hostname: string,
|
||||
options: LookupOptions,
|
||||
): Promise<LookupAddress | LookupAddress[]>;
|
||||
export function lookup(hostname: string): Promise<LookupAddress>;
|
||||
export function lookupService(
|
||||
address: string,
|
||||
port: number,
|
||||
): Promise<{ hostname: string; service: string }>;
|
||||
export function getServers(): string[];
|
||||
export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim";
|
||||
export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void;
|
||||
}
|
||||
declare module "node:dns/promises" {
|
||||
export * from "dns/promises";
|
||||
}
|
||||
|
||||
/* node:v8 — importable for the packages that require it at top level.
|
||||
* getHeapStatistics answers allocator-derived figures in Node's shape
|
||||
* (approximate: the runtime heap's total and used bytes, the machine's
|
||||
* memory as the limit; V8-only figures are 0). The other members observe
|
||||
* V8 itself and fence at their use with the by-design reason; they are
|
||||
* declared so programs that type-check under their own tsc reach that
|
||||
* fence. */
|
||||
declare module "v8" {
|
||||
type DoesZapCodeSpaceFlag = 0 | 1;
|
||||
export interface HeapInfo {
|
||||
total_heap_size: number;
|
||||
total_heap_size_executable: number;
|
||||
total_physical_size: number;
|
||||
total_available_size: number;
|
||||
used_heap_size: number;
|
||||
heap_size_limit: number;
|
||||
malloced_memory: number;
|
||||
peak_malloced_memory: number;
|
||||
does_zap_garbage: DoesZapCodeSpaceFlag;
|
||||
number_of_native_contexts: number;
|
||||
number_of_detached_contexts: number;
|
||||
total_global_handles_size: number;
|
||||
used_global_handles_size: number;
|
||||
external_memory: number;
|
||||
}
|
||||
export function getHeapStatistics(): HeapInfo;
|
||||
export function cachedDataVersionTag(): number;
|
||||
export function getHeapSpaceStatistics(): unknown[];
|
||||
export function getHeapCodeStatistics(): unknown;
|
||||
export function getHeapSnapshot(options?: unknown): unknown;
|
||||
export function writeHeapSnapshot(fileName?: string, options?: unknown): string;
|
||||
export function setFlagsFromString(flags: string): void;
|
||||
export function setHeapSnapshotNearHeapLimit(limit: number): void;
|
||||
export function serialize(value: unknown): Buffer;
|
||||
export function deserialize(buffer: Uint8Array): unknown;
|
||||
export function takeCoverage(): void;
|
||||
export function stopCoverage(): void;
|
||||
export function isStringOneByteRepresentation(content: string): boolean;
|
||||
}
|
||||
declare module "node:v8" {
|
||||
export * from "v8";
|
||||
}
|
||||
|
||||
/* node:worker_threads — native workers use statically compiled entry points.
|
||||
* Lowering and runtime diagnostics define the supported options and methods. */
|
||||
@@ -6972,6 +7048,22 @@ declare module "node:stream/consumers" {
|
||||
import streamConsumers = require("stream/consumers");
|
||||
export = streamConsumers;
|
||||
}
|
||||
/* node:stream/web — the module exports the web stream globals themselves
|
||||
* (require("stream/web").ReadableStream === globalThis.ReadableStream).
|
||||
* ReadableStream is the implemented class; its reader and controller are
|
||||
* exported as types. */
|
||||
declare module "stream/web" {
|
||||
export const ReadableStream: typeof globalThis.ReadableStream;
|
||||
export type ReadableStream<T = unknown> = globalThis.ReadableStream<T>;
|
||||
export type ReadableStreamDefaultReader<T = unknown> = globalThis.ReadableStreamDefaultReader<T>;
|
||||
export type ReadableStreamDefaultController<T = unknown> =
|
||||
globalThis.ReadableStreamDefaultController<T>;
|
||||
export type UnderlyingSource<T = unknown> = globalThis.UnderlyingSource<T>;
|
||||
export type QueuingStrategy<T = unknown> = globalThis.QueuingStrategy<T>;
|
||||
}
|
||||
declare module "node:stream/web" {
|
||||
export * from "stream/web";
|
||||
}
|
||||
interface ErrorConstructor {
|
||||
stackTraceLimit: number;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@scriptc/runtime": "workspace:*",
|
||||
"tsc-ts": "0.0.3",
|
||||
"typescript": "7.0.2",
|
||||
"typescript5": "npm:typescript@5.9.3"
|
||||
},
|
||||
|
||||
@@ -37,6 +37,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
|
||||
http: features.http,
|
||||
http2: features.http2,
|
||||
dgram: features.dgram,
|
||||
dnsPromises: features.dnsPromises,
|
||||
watch: features.fsWatch,
|
||||
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
|
||||
nodeTest: features.nodeTest,
|
||||
|
||||
@@ -48,6 +48,7 @@ export async function compilePackedLibrary(
|
||||
http: false,
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -34,6 +34,7 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* ScrDynPath { parent, key, index } — the %ScrDynPath type. */
|
||||
import type { IrRecordShape, IrType, IrUnionDef } from "../../ir/ir.js";
|
||||
import {
|
||||
accessorSlotProp,
|
||||
DYN_HANDLE_KINDS,
|
||||
isDirentShape,
|
||||
isDynTypedRefType,
|
||||
@@ -613,6 +614,18 @@ export class LlDyn {
|
||||
* tracking does not count the read's reference. Returns the value and
|
||||
* the matching release. */
|
||||
private memberRead(B: BlockBuilder, key: string, t: IrType): { value: string; release: string } {
|
||||
// An accessor slot reads the getter or setter of the accessor property
|
||||
// the checked-dynamic copy holds for it (see accessorSlotProp).
|
||||
const slot = accessorSlotProp(key);
|
||||
if (slot !== null) {
|
||||
this.host.declare(`declare ptr @scr_dyn_obj_accessor_fn(ptr, ptr, ${this.S}, i1 zeroext)`);
|
||||
this.host.declare(`declare void @scr_dyn_release(ptr)`);
|
||||
const fn = B.tmp();
|
||||
B.line(
|
||||
`${fn} = call ptr @scr_dyn_obj_accessor_fn(ptr %d, ptr ${this.host.cstr(slot.prop)}, ${this.S} ${Buffer.byteLength(slot.prop, "utf8")}, i1 ${slot.kind === "set" ? "true" : "false"})`,
|
||||
);
|
||||
return { value: fn, release: "scr_dyn_release" };
|
||||
}
|
||||
const held = this.isRecursiveShape(t) || this.holdsRecursiveShape(t);
|
||||
const release = held ? "scr_dyn_release_held" : "scr_dyn_release";
|
||||
this.host.declare(`declare void @${release}(ptr)`);
|
||||
@@ -2687,13 +2700,53 @@ export class LlDyn {
|
||||
// Keys insert in DECLARED order (JS insertion order); internal
|
||||
// '%'-fields follow so a record→dyn→record round trip keeps them.
|
||||
const byName = new Map(shape.fields.map((f) => [f.name, f]));
|
||||
const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
|
||||
// An accessor property is one key whose getter and setter are the
|
||||
// slot closures (see accessorSlotProp), in accessorOrder's place.
|
||||
const order = shape.accessorOrder ?? shape.declaredOrder ?? shape.fields.map((f) => f.name);
|
||||
const inOrder = new Set(order);
|
||||
const dynFields = [
|
||||
...order.map((n) => byName.get(n)).filter((f) => f !== undefined),
|
||||
...shape.fields.filter((f) => !inOrder.has(f.name)),
|
||||
const accessors = new Set(
|
||||
shape.accessorOrder === undefined
|
||||
? []
|
||||
: shape.fields.flatMap((f) => {
|
||||
const slot = accessorSlotProp(f.name);
|
||||
return slot === null ? [] : [slot.prop];
|
||||
}),
|
||||
);
|
||||
type DynField = { name: string; type: IrType } | { accessor: string };
|
||||
const dynFields: DynField[] = [
|
||||
...order.flatMap((n) => {
|
||||
if (accessors.has(n)) return [{ accessor: n } as DynField];
|
||||
const f = byName.get(n);
|
||||
return f === undefined ? [] : [f as DynField];
|
||||
}),
|
||||
...shape.fields.filter(
|
||||
(f) =>
|
||||
!inOrder.has(f.name) && !(accessors.size > 0 && accessorSlotProp(f.name) !== null),
|
||||
),
|
||||
];
|
||||
for (const f of dynFields) {
|
||||
for (const entry of dynFields) {
|
||||
if ("accessor" in entry) {
|
||||
host.declare(
|
||||
`declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`,
|
||||
);
|
||||
const boxed = (kind: "get" | "set"): string => {
|
||||
const slot = byName.get(`%${kind}:${entry.accessor}`);
|
||||
if (slot === undefined) return "null";
|
||||
const fv = loadFieldOf(slot.name, slot.type);
|
||||
const conv = B.tmp();
|
||||
B.line(
|
||||
`${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`,
|
||||
);
|
||||
return conv;
|
||||
};
|
||||
const getter = boxed("get");
|
||||
const setter = boxed("set");
|
||||
B.line(
|
||||
`call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const f = entry;
|
||||
const klen = Buffer.byteLength(f.name, "utf8");
|
||||
const fv = loadFieldOf(f.name, f.type);
|
||||
// An ABSENT optional field contributes no key.
|
||||
|
||||
@@ -55,6 +55,23 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
|
||||
if (sym) return sym;
|
||||
sym = `sc_race_${host.resolveThunks.size}`;
|
||||
host.resolveThunks.set(key, sym);
|
||||
const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : [];
|
||||
if (from.kind === "void" && !toArms.some((a) => a.kind === "undefinedT")) {
|
||||
// A Promise<never> entry (the frontend admits void entries without an
|
||||
// undefined result arm only for those): it can only reject, so its
|
||||
// fulfillment adapter never runs.
|
||||
host.declare(`declare void @scr_trap(ptr)`);
|
||||
const msg = host.cstr("scriptc: a Promise<never> race entry fulfilled\n");
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal void @${sym}(ptr %dst, ptr %src) ${FN_ATTRS} { ; race ${key}`,
|
||||
`entry:`,
|
||||
` call void @scr_trap(ptr ${msg})`,
|
||||
` unreachable`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return sym;
|
||||
}
|
||||
if (to.kind !== "union")
|
||||
throw new InternalCompilerError("llvm emitter bug: race adapter to a non-union");
|
||||
const toDef = host.unionsById.get(to.unionId);
|
||||
|
||||
@@ -535,6 +535,27 @@ export function emitDynamicExpr(
|
||||
e.key.type.kind === "dyn" ? host.dyn.dynComputedKeyGetHelper() : host.dyn.dynKeyGetHelper();
|
||||
const t = B.tmp();
|
||||
const generic = `call ptr @${helper}(ptr ${d.name}, ptr ${k.name}, i1 ${e.optional ? "true" : "false"})`;
|
||||
const inherited = e.key.kind === "strLit" && INHERITED_OBJECT_MEMBERS.has(e.key.value);
|
||||
// Object.prototype itself answers its own members before the ordinary
|
||||
// read (which fences reflection on the prototype).
|
||||
let prototypeJoin: { member: string; from: string; join: string } | null = null;
|
||||
if (inherited) {
|
||||
host.declare(`declare ptr @scr_dyn_object_prototype_member(ptr, ptr)`);
|
||||
const member = B.tmp();
|
||||
B.line(
|
||||
`${member} = call ptr @scr_dyn_object_prototype_member(ptr ${d.name}, ptr ${k.name})`,
|
||||
);
|
||||
const isMember = B.tmp();
|
||||
B.line(`${isMember} = icmp ne ptr ${member}, null`);
|
||||
const from = B.newLabel("dkg.op");
|
||||
const read = B.newLabel("dkg.rd");
|
||||
const join = B.newLabel("dkg.opj");
|
||||
B.condBr(isMember, from, read);
|
||||
B.startBlock(from);
|
||||
B.br(join);
|
||||
B.startBlock(read);
|
||||
prototypeJoin = { member, from, join };
|
||||
}
|
||||
if (e.key.kind === "strLit") {
|
||||
// A constant key on an ordinary object reads through the site's
|
||||
// cache; every other receiver keeps the shared helper.
|
||||
@@ -560,7 +581,27 @@ export function emitDynamicExpr(
|
||||
} else {
|
||||
B.line(`${t} = ${generic}`);
|
||||
}
|
||||
const out = host.own({ name: t, type: e.type });
|
||||
let result = t;
|
||||
if (prototypeJoin) {
|
||||
// An undefined read of an Object.prototype member name answers the
|
||||
// member the receiver inherits (scr_dyn_invoke.c; the frontend
|
||||
// links that unit for these reads).
|
||||
host.declare(`declare ptr @scr_dyn_inherited_read(ptr, ptr, ptr)`);
|
||||
const read = B.tmp();
|
||||
B.line(
|
||||
`${read} = call ptr @scr_dyn_inherited_read(ptr ${d.name}, ptr ${k.name}, ptr ${t})`,
|
||||
);
|
||||
const readEnd = B.newLabel("dkg.rde");
|
||||
B.br(readEnd);
|
||||
B.startBlock(readEnd);
|
||||
B.br(prototypeJoin.join);
|
||||
B.startBlock(prototypeJoin.join);
|
||||
result = B.tmp();
|
||||
B.line(
|
||||
`${result} = phi ptr [ ${prototypeJoin.member}, %${prototypeJoin.from} ], [ ${read}, %${readEnd} ]`,
|
||||
);
|
||||
}
|
||||
const out = host.own({ name: result, type: e.type });
|
||||
host.emitPendingCheck();
|
||||
return out;
|
||||
}
|
||||
@@ -1204,3 +1245,13 @@ function emitUnionEqWrappedScalar(
|
||||
function wrappedStringLiteral(e: IrExpr): string | null {
|
||||
return e.kind === "unionWrap" && e.value.kind === "strLit" ? e.value.value : null;
|
||||
}
|
||||
|
||||
/** Object.prototype member names whose constant-key dynamic reads fall back
|
||||
* to the inherited member (runtime-features.ts links the unit). */
|
||||
export const INHERITED_OBJECT_MEMBERS: ReadonlySet<string> = new Set([
|
||||
"constructor",
|
||||
"valueOf",
|
||||
"toString",
|
||||
"hasOwnProperty",
|
||||
"propertyIsEnumerable",
|
||||
]);
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
isDirentShape,
|
||||
isDynTypedRefType,
|
||||
isRefCounted,
|
||||
shapeHasAccessorSlots,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
|
||||
@@ -941,6 +942,18 @@ 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})`);
|
||||
}
|
||||
// An Error subclass instance's own `name` sits where its class's
|
||||
// construction creates it: after a field, or after every other
|
||||
// property when no constructor sets it.
|
||||
const nameAfter = meta.def.errorNameAfter;
|
||||
if (meta.root.def.name === "%Error" && !meta.def.runtime && nameAfter !== null) {
|
||||
host.declare(`declare void @scr_error_dyn_place_name(ptr, ptr, ${host.sizeType})`);
|
||||
B.line(
|
||||
nameAfter === undefined
|
||||
? `call void @scr_error_dyn_place_name(ptr ${out}, ptr null, ${host.sizeType} 0)`
|
||||
: `call void @scr_error_dyn_place_name(ptr ${out}, ptr ${host.cstr(nameAfter)}, ${host.sizeType} ${Buffer.byteLength(nameAfter, "utf8")})`,
|
||||
);
|
||||
}
|
||||
// A frozen, sealed or non-extensible instance's view says so.
|
||||
if (withAttributes) {
|
||||
host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`);
|
||||
@@ -965,6 +978,7 @@ export function streamTypedRefMaterializeAdapter(
|
||||
* Declared-field records and tuples cover the live stream values. */
|
||||
const ordinary =
|
||||
shape.indexValue ||
|
||||
shapeHasAccessorSlots(shape) ||
|
||||
shape.fields.some((field) => field.name === "handleEvent" && field.type.kind === "func");
|
||||
if (ordinary) {
|
||||
const out = B.tmp();
|
||||
|
||||
@@ -236,6 +236,10 @@ export function emitDatagramLibCall(host: LlvmEmitterContext, e: LibCallExpr): L
|
||||
return finishRuntimeCall(host, e, "scr_dns_lookup_dyn", a);
|
||||
case "dns.lookupService":
|
||||
return finishRuntimeCall(host, e, "scr_dns_lookup_service", a);
|
||||
case "dns.promisesLookup":
|
||||
return finishRuntimeCall(host, e, "scr_dns_promises_lookup", a);
|
||||
case "dns.promisesLookupService":
|
||||
return finishRuntimeCall(host, e, "scr_dns_promises_lookup_service", a);
|
||||
case "dns.getServers":
|
||||
return finishRuntimeCall(host, e, "scr_dns_get_servers", a);
|
||||
case "dns.getDefaultResultOrder":
|
||||
|
||||
@@ -310,12 +310,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"date.now": "scr_date_now",
|
||||
"date.newNow": "scr_date_now",
|
||||
"date.newMs": "scr_date_new_ms",
|
||||
"date.newString": "scr_date_parse_get_time",
|
||||
"date.parse": "scr_date_parse_get_time",
|
||||
"date.newString": "scr_date_parse",
|
||||
"date.parse": "scr_date_parse",
|
||||
"date.getTime": "scr_date_get_time",
|
||||
"date.valueOf": "scr_date_get_time",
|
||||
"date.parseGetTime": "scr_date_parse_get_time",
|
||||
"date.parseGetTime": "scr_date_parse",
|
||||
"date.utc": "scr_date_utc",
|
||||
"date.newLocal": "scr_date_new_local",
|
||||
"date.getFullYear": "scr_date_get_full_year_local",
|
||||
"date.getUTCFullYear": "scr_date_get_full_year_utc",
|
||||
"date.getMonth": "scr_date_get_month_local",
|
||||
@@ -926,8 +927,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.promiseAll": "scr_dyn_promise_all",
|
||||
"dyn.promiseAllSettled": "scr_dyn_promise_all_settled",
|
||||
"dyn.promiseAny": "scr_dyn_promise_any",
|
||||
"dyn.promiseResolve": "scr_dyn_promise_resolve",
|
||||
"dyn.typeof": "scr_dyn_typeof",
|
||||
"dyn.objectTag": "scr_dyn_object_tag",
|
||||
"dyn.objectTag": "scr_dyn_object_tag_of",
|
||||
"dyn.freeze": "scr_dyn_freeze",
|
||||
"dyn.isFrozen": "scr_dyn_is_frozen",
|
||||
"class.defineField": "scr_class_define_field",
|
||||
@@ -941,7 +943,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"dyn.nativeMapIs": "scr_dyn_native_map_is",
|
||||
"dyn.nativeUrlIs": "scr_dyn_native_url_is",
|
||||
"dyn.nativeDateIs": "scr_dyn_native_date_is",
|
||||
"dyn.globalConstructor": "scr_dyn_global_constructor",
|
||||
"dyn.builtinFunction": "scr_dyn_builtin_function",
|
||||
"date.nativeNew": "scr_dyn_native_date_new",
|
||||
"date.nativeNewFields": "scr_dyn_native_date_new_fields",
|
||||
"date.nativeNewValue": "scr_dyn_native_date_new_value",
|
||||
"date.checkedValue": "scr_dyn_native_date_value",
|
||||
"dyn.nativeRegexIs": "scr_dyn_native_regex_is",
|
||||
"dyn.toString": "scr_dyn_to_string_argument",
|
||||
@@ -1675,4 +1681,5 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"timers.queueMicrotaskDyn",
|
||||
"timers.setImmediateFnValue",
|
||||
"timers.immediatePromise",
|
||||
"dyn.builtinFunction",
|
||||
]);
|
||||
|
||||
@@ -52,6 +52,8 @@ export interface NativeLinkFeatures {
|
||||
http: boolean;
|
||||
http2: boolean;
|
||||
dgram: boolean;
|
||||
/** node:dns/promises: scr_dns_promises.c. */
|
||||
dnsPromises: boolean;
|
||||
watch: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
|
||||
@@ -177,6 +177,11 @@ export interface CcOptions {
|
||||
* on the IR): compiles scr_dgram.c into the binary — the net gating
|
||||
* precedent, so dgram-free binaries keep their exact link line. */
|
||||
dgram?: boolean;
|
||||
/** The program uses node:dns/promises (the dns.promisesLookup libCalls):
|
||||
* compiles scr_dns_promises.c over the dgram unit, so callback-only
|
||||
* binaries keep their exact link line. */
|
||||
dnsPromises?: boolean;
|
||||
|
||||
/** The program uses fs.watch (moduleUsesFsWatch on the IR): compiles
|
||||
* scr_watch.c into the binary — the net gating precedent, so watch-free
|
||||
* binaries keep their exact link line. */
|
||||
|
||||
@@ -252,6 +252,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
|
||||
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.dgram && u.opts.dnsPromises, "scr_dns_promises.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"),
|
||||
|
||||
@@ -113,6 +113,7 @@ const features = {
|
||||
http: false,
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -50,6 +50,7 @@ const BASE: NativeLinkFeatures = {
|
||||
http: false,
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -56,6 +56,7 @@ export async function warmNativeCaches(
|
||||
http: false,
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
|
||||
@@ -59,8 +59,9 @@
|
||||
* order that differs from insertion order where the program
|
||||
* observes it (SC6002), localeCompare over text the built-in
|
||||
* collation does not cover (SC6003), a call feeding an instance
|
||||
* to a cast to an unrelated class (SC6004), and Date strings the native parser does not
|
||||
* accept (SC6005)
|
||||
* to a cast to an unrelated class (SC6004); SC6005 (Date strings
|
||||
* outside the native parser's grammar) is retired — the runtime
|
||||
* parses date strings with V8's grammar
|
||||
* SC9xxx internal compiler errors (still source-anchored)
|
||||
*/
|
||||
import type { SrcLoc } from "../ir/ir.js";
|
||||
@@ -200,18 +201,6 @@ export function unrelatedClassCastDivergenceDiag(
|
||||
};
|
||||
}
|
||||
|
||||
/** SC6005 — a Date string literal outside the grammar the native parser
|
||||
* accepts (ISO 8601 with an explicit offset, date-only forms, and the
|
||||
* certificate-time shape): Node parses many more forms. */
|
||||
export function dateLiteralDivergenceDiag(text: string, loc: SrcLoc): ScrDiagnostic {
|
||||
return {
|
||||
code: "SC6005",
|
||||
message: `the date string '${text}' parses to an invalid date (NaN) natively, while Node accepts it (the native parser takes ISO 8601 date-times with an explicit offset, such as '2024-01-02T03:04:05Z', and date-only forms)`,
|
||||
loc,
|
||||
hint: "write the date in ISO 8601 with an explicit offset ('Z' or '+HH:MM'), or build it from numbers with Date.UTC",
|
||||
};
|
||||
}
|
||||
|
||||
/* ── SC5xxx: native FFI ───────────────────────────────────────────────── */
|
||||
|
||||
/** SC5001 — the outbound native-FFI manifest is malformed or unreadable. */
|
||||
@@ -282,11 +271,11 @@ export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
|
||||
"imports (#alias via the imports field, self-name references via " +
|
||||
"exports), statically compiled npm packages, " +
|
||||
"and the built-in fs, fs/promises, path, os, url, crypto, zlib, " +
|
||||
"child_process, net, http, tls, https, http2, dgram, dns, util, " +
|
||||
"child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, " +
|
||||
"util/types, string_decoder, querystring, readline, events, stream, " +
|
||||
"stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, " +
|
||||
"stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, " +
|
||||
"cluster, tty, async_hooks, timers, timers/promises, " +
|
||||
"diagnostics_channel, perf_hooks, module, and console modules (bare or " +
|
||||
"diagnostics_channel, perf_hooks, module, v8, and console modules (bare or " +
|
||||
"node:-prefixed) and node:test (node:-prefixed only, like in Node) " +
|
||||
"are supported",
|
||||
},
|
||||
|
||||
@@ -51,6 +51,7 @@ const native: EarlyExecutableNativeFeatures = {
|
||||
http: false,
|
||||
http2: false,
|
||||
dgram: false,
|
||||
dnsPromises: false,
|
||||
watch: false,
|
||||
foreignFfi: false,
|
||||
nodeTest: false,
|
||||
@@ -199,7 +200,7 @@ test("early executable cache misses on source and resolution changes", async ()
|
||||
expect(await readEarlyExecutableCache(f.root, f.options)).toBeNull();
|
||||
});
|
||||
|
||||
test("early executable keys isolate compile modes, paths, implementation, and FFI manifests", async () => {
|
||||
test("early executable keys isolate compile modes, paths, implementation, checker, and FFI manifests", async () => {
|
||||
const f = await fixture();
|
||||
const tracker = new FrontendInputTracker();
|
||||
tracker.run(() => trackedReadFile(f.source));
|
||||
@@ -216,6 +217,7 @@ test("early executable keys isolate compile modes, paths, implementation, and FF
|
||||
{ ...f.options, optimization: "dev" },
|
||||
{ ...f.options, optimization: "speed" },
|
||||
{ ...f.options, detachedExit: true },
|
||||
{ ...f.options, checker: "tsc-ts-executable-identity" },
|
||||
{ ...f.options, npmStatic: "auto" },
|
||||
{ ...f.options, npmStatic: ["commander"] },
|
||||
{ ...f.options, outPath: join(f.options.outDir, "other") },
|
||||
|
||||
@@ -77,6 +77,7 @@ export interface EarlyExecutableNativeFeatures {
|
||||
http: boolean;
|
||||
http2: boolean;
|
||||
dgram: boolean;
|
||||
dnsPromises: boolean;
|
||||
watch: boolean;
|
||||
foreignFfi: boolean;
|
||||
nodeTest: boolean;
|
||||
@@ -116,6 +117,9 @@ export interface EarlyExecutableCacheOptions {
|
||||
windowsSubsystem?: "gui";
|
||||
/** Omitted unless --detached-exit is set. */
|
||||
detachedExit?: true;
|
||||
/** Omitted for the default tsgo checker; with SCRIPTC_CHECKER=tsc-ts, the
|
||||
* tsc-ts executable's file identity. */
|
||||
checker?: string;
|
||||
/** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */
|
||||
youngNursery?: true;
|
||||
npmStatic: readonly string[] | "auto" | null;
|
||||
@@ -191,6 +195,7 @@ const BOOLEAN_NATIVE_KEYS = [
|
||||
"http",
|
||||
"http2",
|
||||
"dgram",
|
||||
"dnsPromises",
|
||||
"watch",
|
||||
"foreignFfi",
|
||||
"nodeTest",
|
||||
@@ -230,6 +235,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
|
||||
...(options.strip ? ["strip"] : []),
|
||||
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
|
||||
...(options.detachedExit ? ["detached-exit"] : []),
|
||||
...(options.checker !== undefined ? [`checker:${options.checker}`] : []),
|
||||
...(options.youngNursery ? ["young-nursery"] : []),
|
||||
options.npmStatic === null
|
||||
? "<npm-static-off>"
|
||||
@@ -270,6 +276,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
|
||||
if (options.strip) hash.update("strip\0");
|
||||
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
|
||||
if (options.detachedExit) hash.update("detached-exit\0");
|
||||
if (options.checker !== undefined) hash.update(`checker:${options.checker}\0`);
|
||||
if (options.youngNursery) hash.update("young-nursery\0");
|
||||
hash
|
||||
.update(
|
||||
|
||||
@@ -42,6 +42,9 @@ export const SUPPORTED_BUILTIN_MODULES = [
|
||||
"module",
|
||||
"console",
|
||||
"process",
|
||||
"v8",
|
||||
"stream/web",
|
||||
"dns/promises",
|
||||
"punycode",
|
||||
"constants",
|
||||
] as const;
|
||||
@@ -76,14 +79,33 @@ export function builtinDefaultImportModule(spec: string): string | null {
|
||||
export function canonicalBuiltinModule(spec: string): string | null {
|
||||
const bare = spec.startsWith("node:") ? spec.slice(5) : spec;
|
||||
if (bare === spec && PREFIX_ONLY_BUILTIN_MODULES.has(bare)) return null;
|
||||
return (SUPPORTED_BUILTIN_MODULES as readonly string[]).includes(bare) ? bare : null;
|
||||
if ((SUPPORTED_BUILTIN_MODULES as readonly string[]).includes(bare)) return bare;
|
||||
return bare === spec ? npmBuiltinAliasOf(spec) : null;
|
||||
}
|
||||
|
||||
/** npm packages served as a builtin module instead of compiling their
|
||||
* JavaScript (maintainer decision): graceful-fs monkeypatches the fs
|
||||
* module object and queues retries on EMFILE/ENFILE, which statically
|
||||
* lowered fs calls cannot honor, and it behaves as fs everywhere else. Its
|
||||
* import and require resolve to node:fs; gracefulify is a no-op statement.
|
||||
* The one documented difference: no retry queueing under file-descriptor
|
||||
* exhaustion. */
|
||||
const NPM_BUILTIN_ALIASES: Record<string, string | undefined> = { "graceful-fs": "fs" };
|
||||
|
||||
/** The builtin module an npm package specifier is served as, or null. */
|
||||
export function npmBuiltinAliasOf(spec: string): string | null {
|
||||
return Object.hasOwn(NPM_BUILTIN_ALIASES, spec) ? (NPM_BUILTIN_ALIASES[spec] ?? null) : null;
|
||||
}
|
||||
|
||||
/** Both specifier spellings of every supported builtin (prefix-only ones
|
||||
* keep just the node: form) — the ambient-module allowlist preflight uses. */
|
||||
export const SUPPORTED_NODE_MODULES: readonly string[] = SUPPORTED_BUILTIN_MODULES.flatMap((m) =>
|
||||
PREFIX_ONLY_BUILTIN_MODULES.has(m) ? [`node:${m}`] : [m, `node:${m}`],
|
||||
);
|
||||
* keep just the node: form) — the ambient-module allowlist preflight uses
|
||||
* — plus the npm packages served as builtins. */
|
||||
export const SUPPORTED_NODE_MODULES: readonly string[] = [
|
||||
...SUPPORTED_BUILTIN_MODULES.flatMap((m) =>
|
||||
PREFIX_ONLY_BUILTIN_MODULES.has(m) ? [`node:${m}`] : [m, `node:${m}`],
|
||||
),
|
||||
...Object.keys(NPM_BUILTIN_ALIASES),
|
||||
];
|
||||
|
||||
/* ── out-of-scope builtins ───────────────────────────────────────────────
|
||||
* Builtin modules a compiled binary is not going to serve — not "yet",
|
||||
@@ -92,8 +114,8 @@ export const SUPPORTED_NODE_MODULES: readonly string[] = SUPPORTED_BUILTIN_MODUL
|
||||
* so 'node:sqlite' keys as 'sqlite'. Exact-match only: an npm package
|
||||
* whose name collides with none of these keys keeps the generic wording.
|
||||
* Everything absent from this table stays the plain "not supported yet"
|
||||
* story — genuinely pending surface (console's Console class, stream/web)
|
||||
* must not read as permanently refused. */
|
||||
* story — genuinely pending surface (console's Console class) must not
|
||||
* read as permanently refused. */
|
||||
|
||||
/** Node's own HTTP/TLS/stream implementation internals, requireable by
|
||||
* legacy convention (underscore-prefixed lib/ modules). They expose
|
||||
@@ -102,11 +124,15 @@ export const SUPPORTED_NODE_MODULES: readonly string[] = SUPPORTED_BUILTIN_MODUL
|
||||
const NODE_INTERNAL_REASON =
|
||||
"a Node-internal module: it exposes Node's own implementation objects, which the scriptc runtime does not replicate";
|
||||
|
||||
const V8_REASON =
|
||||
"it observes V8 engine internals — heap statistics and snapshots, GC and CPU profiles, serialize/deserialize — and a compiled binary embeds no V8 engine to observe";
|
||||
/** node:v8 is importable (packages require it at top level and touch it
|
||||
* only on optional paths), and getHeapStatistics answers allocator-derived
|
||||
* figures; every other member observes the engine itself. Member fences
|
||||
* carry this reason instead of a module-level refusal. */
|
||||
export const V8_MEMBER_REASON =
|
||||
"by design: this member observes V8 engine internals — heap snapshots and spaces, GC and CPU profiles, flags, serialize/deserialize — and a compiled binary embeds no V8 engine to observe";
|
||||
|
||||
const OUT_OF_SCOPE_BUILTIN_REASONS: Record<string, string | undefined> = {
|
||||
v8: V8_REASON,
|
||||
vm: "it compiles and runs JavaScript source at runtime, and a compiled binary embeds no JavaScript engine to run it",
|
||||
inspector:
|
||||
"it drives the V8 inspector protocol — debugger, profiler, heap access — and a compiled binary embeds no V8 engine to inspect",
|
||||
"inspector/promises":
|
||||
@@ -142,3 +168,10 @@ export function unsupportedModuleFeatureOf(spec: string): string {
|
||||
: undefined;
|
||||
return reason === undefined ? `the '${spec}' module` : `the '${spec}' module (${reason})`;
|
||||
}
|
||||
|
||||
/** The ESNext temporal global's name. Node 24 does not define it: programs
|
||||
* that name it type-check against TS 7's lib slice (program.ts adds it only
|
||||
* for them) and lower to Node's answers. Spelled in two pieces so the
|
||||
* compiler's own sources never trip program.ts's scan when scriptc
|
||||
* compiles itself. */
|
||||
export const TEMPORAL_GLOBAL = "Tempo" + "ral";
|
||||
|
||||
@@ -15,6 +15,8 @@ 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";
|
||||
import { resultIsDiscarded } from "./call-position.js";
|
||||
import { locOf, requireSpecOf } from "../program.js";
|
||||
|
||||
/** `events.addAbortListener(signal, listener)`: the runtime registers the
|
||||
* once/resist-stop-propagation abort listener (or queues the call for an
|
||||
@@ -54,6 +56,38 @@ export function lowerAddAbortListener(
|
||||
return { kind: "dynObjLit", fields: [{ key, value: dispose }], type: DYN, loc };
|
||||
}
|
||||
|
||||
/** graceful-fs's `gracefulify(fsModule)` (graceful-fs is served as node:fs —
|
||||
* builtin-modules.ts): it patches a module object with the retry queue,
|
||||
* which statically lowered fs calls never consult, so the call statement
|
||||
* is a no-op. The argument still evaluates unless it is a plain reference
|
||||
* (the patched module, typically `require("fs")` or a namespace binding).
|
||||
* The returned module object has no representation; other positions
|
||||
* fence. */
|
||||
function lowerGracefulify(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
|
||||
const args = expr.arguments.nodes;
|
||||
if (!resultIsDiscarded(expr) || args.length !== 1 || ts.isSpreadElement(args[0]!)) {
|
||||
lowerer.noLowering(
|
||||
"gracefulify",
|
||||
expr,
|
||||
"graceful-fs compiles as node:fs; gracefulify(fsModule) lowers as a statement whose result is unused",
|
||||
);
|
||||
}
|
||||
let arg: ts.Expression = args[0]!;
|
||||
while (ts.isParenthesizedExpression(arg)) arg = arg.expression;
|
||||
const plain =
|
||||
ts.isIdentifier(arg) ||
|
||||
(ts.isCallExpression(arg) && requireSpecOf(arg) !== null) ||
|
||||
(ts.isPropertyAccessExpression(arg) && ts.isIdentifier(arg.expression));
|
||||
if (plain) return { kind: "seqExpr", stmts: [], result: dynUndefinedExpr(loc), type: DYN, loc };
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }],
|
||||
result: dynUndefinedExpr(loc),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Named imports, namespace members and callable aliases share validation
|
||||
* and dispatch. Resolve diagnostic provenance only if no handler accepts. */
|
||||
export function lowerBuiltinCall(
|
||||
@@ -126,6 +160,8 @@ export function lowerBuiltinCall(
|
||||
if (bi.module === "worker_threads" && bi.member === "markAsUncloneable") {
|
||||
return lowerUncloneable(lowerer, expr);
|
||||
}
|
||||
if (bi.module === "fs" && bi.member === "gracefulify")
|
||||
return lowerGracefulify(lowerer, expr, loc);
|
||||
const builtinFn = builtinModuleFnOf(lowerer, bi.module, bi.member);
|
||||
if (!builtinFn) {
|
||||
lowerer.noLowering(
|
||||
|
||||
@@ -58,9 +58,8 @@ export function lowerDateCall(
|
||||
}
|
||||
return { kind: "libCall", fn: "date.now", args: [], type: F64, loc };
|
||||
}
|
||||
// Date.parse uses the same bounded date-string parser as
|
||||
// new Date(dateString).getTime(); keep their NaN and TimeClip behavior
|
||||
// identical for the ISO timestamps used by CLI applications.
|
||||
// Date.parse uses the same date-string parser as new Date(dateString)
|
||||
// (V8's grammar in the runtime), so their NaN and TimeClip behavior agree.
|
||||
if (lowerer.stdlibGlobalMember(access, "Date") === "parse") {
|
||||
if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!)) {
|
||||
lowerer.noLowering(`Date.parse with ${call.arguments.nodes.length} arguments`, call);
|
||||
|
||||
@@ -23,6 +23,17 @@ import { stripTypeCasts } from "./arguments.js";
|
||||
export function builtinImportOf(
|
||||
lowerer: Lowerer,
|
||||
ident: ts.Identifier,
|
||||
): { module: string; member: string } | null {
|
||||
const binding = builtinModuleBindingOf(lowerer, ident);
|
||||
// node:stream/web's exports ARE the web stream globals: its bindings
|
||||
// lower through the globals' own paths (stdlibGlobalNameOf resolves the
|
||||
// alias to the same name), not through module-member tables.
|
||||
return binding?.module === "stream/web" ? null : binding;
|
||||
}
|
||||
|
||||
function builtinModuleBindingOf(
|
||||
lowerer: Lowerer,
|
||||
ident: ts.Identifier,
|
||||
): { module: string; member: string } | null {
|
||||
const symbol =
|
||||
ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from "./operating-system.js";
|
||||
import { lowerQuerystringParseCall, lowerQuerystringStringifyCall } from "./querystring.js";
|
||||
import { lowerFsGlobCallbackCall, lowerFsGlobSyncCall, lowerFspGlobCall } from "./fs-glob.js";
|
||||
import { lowerV8HeapStatisticsCall } from "./v8.js";
|
||||
|
||||
export function lowerBuiltinModuleCall(
|
||||
lowerer: Lowerer,
|
||||
@@ -134,6 +135,9 @@ export function lowerBuiltinModuleCall(
|
||||
if (bi.module === "child_process" && (bi.member === "execFileSync" || bi.member === "execSync")) {
|
||||
return lowerer.lowerExecSyncCall(expr, bi.member === "execSync", loc);
|
||||
}
|
||||
if (bi.module === "v8" && bi.member === "getHeapStatistics") {
|
||||
return lowerV8HeapStatisticsCall(lowerer, expr, loc);
|
||||
}
|
||||
if (bi.module === "os" && bi.member === "networkInterfaces") {
|
||||
return lowerOsNetworkInterfacesCall(lowerer, expr, loc);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
funcOf,
|
||||
} from "../../../ir/ir.js";
|
||||
import { lowerBuiltinLoaderValue } from "../lower-builtin-values.js";
|
||||
import { builtinFunctionValue } from "../lower-exprs.js";
|
||||
import { canonicalBuiltinModule } from "../../builtin-modules.js";
|
||||
import { lowerProcessIpcSend, ipcMessageListener, ipcDisconnectListener } from "./ipc.js";
|
||||
import { lowerDiagnosticsSubscriber, lowerTracingArguments } from "./async-context.js";
|
||||
@@ -578,6 +579,11 @@ export function lowerProcessProperty(
|
||||
if (member === "hrtime") {
|
||||
return { kind: "libCall", fn: "process.hrtimeValue", args: [], type: DYN, loc };
|
||||
}
|
||||
// A stored process.nextTick in JavaScript (`cond ? setImmediate :
|
||||
// process.nextTick`): the interned builtin function value.
|
||||
if (member === "nextTick" && isJsSourceFile(expr.getSourceFile())) {
|
||||
return builtinFunctionValue("process.nextTick", loc);
|
||||
}
|
||||
if (member === "getBuiltinModule") {
|
||||
// Keep the native loader's argument validation when the function is
|
||||
// stored. The declaration's string signature must not insert a checked
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { checkedPromiseAll } from "../checked-promise-all.js";
|
||||
import { checkedPromiseAll, thenReader } from "../checked-promise-all.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { newFnCtx, type Lowerer } from "../lowerer.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import {
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
arrayOf,
|
||||
isUnitType,
|
||||
@@ -17,6 +19,168 @@ import {
|
||||
typeKey,
|
||||
} from "../../../ir/ir.js";
|
||||
|
||||
/** `Promise.all([p, q, r])` over DIFFERENT promise types — the checker's
|
||||
* tuple overload, `Promise<[A, B, C]>`. One interned async helper per
|
||||
* entry/result signature takes the entries as arguments, awaits the
|
||||
* runtime combinator over the same promises (each widened to
|
||||
* Promise<void>, a type-only view), so settlement is Node's exactly: the
|
||||
* first rejection in settlement order wins and every later one counts as
|
||||
* handled; then it reads each (now fulfilled) entry and returns the
|
||||
* tuple. Entries keep their identity — no value converts through the
|
||||
* checked-dynamic tree. Non-promise entries (numbers, strings, booleans,
|
||||
* unit values — never thenable) pass straight into their slot; a
|
||||
* checked-dynamic entry (a JavaScript function's result) joins through
|
||||
* PromiseResolve: the promise it holds, a promise that adopts a thenable,
|
||||
* or a promise fulfilled with its plain value. The tuple
|
||||
* fulfills one microtask turn after the combinator does. Null when the
|
||||
* result type or an entry has no typed home here. */
|
||||
function lowerTuplePromiseAll(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
elems: IrExpr[],
|
||||
): IrExpr | null {
|
||||
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (resultT?.kind !== "promise" || resultT.inner.kind !== "record") return null;
|
||||
const shape = lowerer.shapes.get(resultT.inner.shapeId);
|
||||
if (!shape?.tuple || shape.fields.length !== elems.length) return null;
|
||||
const plainValue = (t: IrType): boolean =>
|
||||
t.kind === "f64" || t.kind === "string" || t.kind === "bool" || isUnitType(t);
|
||||
// A checked-dynamic entry (a JavaScript function's result) may hold a
|
||||
// promise or a plain value at run time; it joins through its promise view.
|
||||
const settles = (t: IrType): boolean => t.kind === "promise" || t.kind === "dyn";
|
||||
if (!elems.some((e) => settles(e.type))) return null;
|
||||
if (!elems.every((e) => settles(e.type) || plainValue(e.type))) return null;
|
||||
const loc = locOf(call);
|
||||
const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`;
|
||||
let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`);
|
||||
if (name === undefined) {
|
||||
name = `%promise.allTuple.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name);
|
||||
const ctx = newFnCtx(false, null, null, resultT.inner);
|
||||
ctx.isAsync = true;
|
||||
lowerer.fnStack.push(ctx);
|
||||
try {
|
||||
const params = elems.map((e, i) => ({
|
||||
localId: `entry.${i}`,
|
||||
name: `entry${i}`,
|
||||
type: e.type,
|
||||
}));
|
||||
const ref = (i: number): IrExpr => varRef(`entry.${i}`, elems[i]!.type, loc);
|
||||
const dynPromise: IrType = { kind: "promise", inner: DYN };
|
||||
const body: IrStmt[] = [];
|
||||
// Each dyn entry's promise view, in entry order: the promise it
|
||||
// holds, a promise that adopts a thenable's settlement, or a promise
|
||||
// fulfilled with the plain value (PromiseResolve's rule).
|
||||
const views = new Map<number, IrExpr>();
|
||||
elems.forEach((e, i) => {
|
||||
if (e.type.kind !== "dyn") return;
|
||||
const view = lowerer.declareHiddenLocal("%allEntry", dynPromise);
|
||||
body.push({
|
||||
kind: "varDecl",
|
||||
localId: view.id,
|
||||
init: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.promiseResolve",
|
||||
args: [ref(i), thenReader(lowerer, loc)],
|
||||
type: dynPromise,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
views.set(i, varRef(view.id, dynPromise, loc));
|
||||
});
|
||||
const widened: IrExpr[] = [];
|
||||
elems.forEach((e, i) => {
|
||||
const view = views.get(i);
|
||||
if (view !== undefined) {
|
||||
widened.push({
|
||||
kind: "promiseVoidWiden",
|
||||
value: view,
|
||||
type: { kind: "promise", inner: VOID },
|
||||
loc,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.type.kind !== "promise") return;
|
||||
widened.push(
|
||||
e.type.inner.kind === "void"
|
||||
? ref(i)
|
||||
: {
|
||||
kind: "promiseVoidWiden",
|
||||
value: ref(i),
|
||||
type: { kind: "promise", inner: VOID },
|
||||
loc,
|
||||
},
|
||||
);
|
||||
});
|
||||
const settled: IrExpr = {
|
||||
kind: "awaitExpr",
|
||||
value: {
|
||||
kind: "intrinsic",
|
||||
name: "promise.all",
|
||||
args: [
|
||||
{
|
||||
kind: "arrayLit",
|
||||
elems: widened,
|
||||
type: arrayOf({ kind: "promise", inner: VOID }),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: { kind: "promise", inner: VOID },
|
||||
loc,
|
||||
},
|
||||
type: VOID,
|
||||
loc,
|
||||
};
|
||||
// Shape fields are sorted by name ("10" before "2"); a tuple field's
|
||||
// name is its position.
|
||||
const fields = shape.fields.map((field) => {
|
||||
const i = Number(field.name);
|
||||
const entry = elems[i]!;
|
||||
let value: IrExpr;
|
||||
const view = views.get(i);
|
||||
if (view !== undefined) {
|
||||
value = { kind: "awaitExpr", value: view, type: DYN, loc };
|
||||
} else if (entry.type.kind === "promise") {
|
||||
value =
|
||||
entry.type.inner.kind === "void"
|
||||
? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }
|
||||
: { kind: "awaitExpr", value: ref(i), type: entry.type.inner, loc };
|
||||
} else {
|
||||
value = ref(i);
|
||||
}
|
||||
return { name: field.name, value: lowerer.coerceToExpected(value, field.type) };
|
||||
});
|
||||
body.push(
|
||||
{ kind: "exprStmt", expr: settled, loc },
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "recordLit", fields, type: resultT.inner, loc },
|
||||
loc,
|
||||
},
|
||||
);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params,
|
||||
returnType: resultT.inner,
|
||||
locals: [
|
||||
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
||||
...lowerer.ctx.locals,
|
||||
],
|
||||
body,
|
||||
async: true,
|
||||
loc,
|
||||
});
|
||||
} catch (e) {
|
||||
lowerer.arrHofHelpers.delete(`promise.allTuple:${key}`);
|
||||
throw e;
|
||||
} finally {
|
||||
lowerer.fnStack.pop();
|
||||
}
|
||||
}
|
||||
return { kind: "call", callee: name, args: elems, type: resultT, loc };
|
||||
}
|
||||
|
||||
/** `Promise.race([...])` on THE Promise global: the entries lower
|
||||
* individually (the array never materializes — promise-element arrays
|
||||
* have no representation) into a promise.race intrinsic; the result
|
||||
@@ -74,6 +238,17 @@ export function lowerPromiseStaticCall(
|
||||
}
|
||||
return resultArms.some((a) => typeEquals(a, inner));
|
||||
};
|
||||
// A Promise<never> entry (the deadline idiom: `new Promise<never>((_,
|
||||
// reject) => setTimeout(reject, ms))`) can only reject, so it races
|
||||
// under any result type; its inner type maps to void.
|
||||
const neverEntry = (el: ts.Expression): boolean => {
|
||||
const t = lowerer.typeOf(el);
|
||||
const arg =
|
||||
t.getSymbol()?.name === "Promise"
|
||||
? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
|
||||
: undefined;
|
||||
return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
|
||||
};
|
||||
const entries = argNode.elements.nodes.map((el) => {
|
||||
const entry = lowerer.lowerExpr(el);
|
||||
if (entry.type.kind !== "promise") {
|
||||
@@ -83,7 +258,7 @@ export function lowerPromiseStaticCall(
|
||||
"JS would resolve plain values — wrap them: new Promise((r) => r(v))",
|
||||
);
|
||||
}
|
||||
if (!compatible(entry.type.inner)) {
|
||||
if (!compatible(entry.type.inner) && !(entry.type.inner.kind === "void" && neverEntry(el))) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
el,
|
||||
@@ -143,6 +318,10 @@ export function lowerPromiseStaticCall(
|
||||
return { kind: "intrinsic", name: "promise.all", args: [entriesArr], type: resultT, loc };
|
||||
}
|
||||
|
||||
if (!isJsSourceFile(call.getSourceFile())) {
|
||||
const tuple = lowerTuplePromiseAll(lowerer, call, elems);
|
||||
if (tuple) return tuple;
|
||||
}
|
||||
if (
|
||||
isJsSourceFile(call.getSourceFile()) &&
|
||||
elems.every((entry) => lowerer.dynConvertible(entry.type))
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/* node:v8 — importable, with one lowered member. Packages require it at
|
||||
* top level and touch it on optional paths (dd-trace's runtime metrics),
|
||||
* so the module itself is not refused. getHeapStatistics answers
|
||||
* allocator-derived figures in Node's shape and field order, in the
|
||||
* spirit of process.memoryUsage(): approximate values, not V8 heap
|
||||
* figures (a compiled binary embeds no V8 engine). Every other member
|
||||
* fences at its use with the by-design reason (builtinFenceHintOf). */
|
||||
import { numLit } from "../../../ir/build.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import { type Lowerer } from "../lowerer.js";
|
||||
import { F64, type IrExpr, type SrcLoc, VOID } from "../../../ir/ir.js";
|
||||
|
||||
/** v8.getHeapStatistics()'s fields, in Node's order. */
|
||||
const HEAP_STATISTICS_FIELDS = [
|
||||
"total_heap_size",
|
||||
"total_heap_size_executable",
|
||||
"total_physical_size",
|
||||
"total_available_size",
|
||||
"used_heap_size",
|
||||
"heap_size_limit",
|
||||
"malloced_memory",
|
||||
"peak_malloced_memory",
|
||||
"does_zap_garbage",
|
||||
"number_of_native_contexts",
|
||||
"number_of_detached_contexts",
|
||||
"total_global_handles_size",
|
||||
"used_global_handles_size",
|
||||
"external_memory",
|
||||
];
|
||||
|
||||
/** `v8.getHeapStatistics()`: one memoryUsage sample (the runtime heap's
|
||||
* total and used bytes), with the machine's memory as the heap limit (the
|
||||
* runtime heap has no fixed ceiling) and the available size the limit
|
||||
* minus used bytes. V8-only figures (executable code space, malloced
|
||||
* memory, global handles, external memory, zapping) read 0, and one
|
||||
* native context exists. The record is the call site's interned HeapInfo
|
||||
* shape, built by a shared helper per shape. */
|
||||
export function lowerV8HeapStatisticsCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
if (call.arguments.nodes.length !== 0) {
|
||||
lowerer.noLowering(`v8.getHeapStatistics with ${call.arguments.nodes.length} arguments`, call);
|
||||
}
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
if (t?.kind !== "record") lowerer.badType(call, lowerer.typeOf(call));
|
||||
const shape = lowerer.shapes.get(t.shapeId);
|
||||
if (
|
||||
!shape ||
|
||||
!shape.fields.every((f) => HEAP_STATISTICS_FIELDS.includes(f.name) && f.type.kind === "f64")
|
||||
) {
|
||||
lowerer.badType(call, lowerer.typeOf(call));
|
||||
}
|
||||
const key = `v8.getHeapStatistics:${t.shapeId}`;
|
||||
let helper = lowerer.valueHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%v8.getHeapStatistics.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, helper);
|
||||
const sample = (index: number): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "process.memoryUsageField",
|
||||
args: [numLit(index, loc)],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const limit = (): IrExpr => ({ kind: "libCall", fn: "os.totalmem", args: [], type: F64, loc });
|
||||
const value = (name: string): IrExpr => {
|
||||
switch (name) {
|
||||
case "total_heap_size":
|
||||
case "total_physical_size":
|
||||
return sample(1);
|
||||
case "used_heap_size":
|
||||
return sample(2);
|
||||
case "heap_size_limit":
|
||||
return limit();
|
||||
case "total_available_size":
|
||||
return { kind: "bin", op: "-", left: limit(), right: sample(2), type: F64, loc };
|
||||
case "number_of_native_contexts":
|
||||
return numLit(1, loc);
|
||||
default:
|
||||
return numLit(0, loc);
|
||||
}
|
||||
};
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [],
|
||||
returnType: t,
|
||||
locals: [],
|
||||
body: [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "libCall", fn: "process.memoryUsageTake", args: [], type: VOID, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "recordLit",
|
||||
fields: shape.fields.map((f) => ({ name: f.name, value: value(f.name) })),
|
||||
type: t,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [], type: t, loc };
|
||||
}
|
||||
@@ -11,7 +11,7 @@ const COMBINATOR_LIB_CALLS = {
|
||||
|
||||
/** `value.then` through the program's own property dispatch, so a class
|
||||
* instance's `then` method is visible to the runtime's thenable check. */
|
||||
function thenReader(lowerer: Lowerer, loc: SrcLoc): IrExpr {
|
||||
export function thenReader(lowerer: Lowerer, loc: SrcLoc): IrExpr {
|
||||
const name = "%promise.thenOf";
|
||||
const type = funcOf([DYN], DYN);
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { Lowerer } from "./lowerer.js";
|
||||
import type { ParamShape } from "./call-signatures.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
import { classInstanceOf } from "./class-dynamic-dispatch.js";
|
||||
import { inheritedBuiltinCtorFence } from "./lower-classes.js";
|
||||
|
||||
const PREFIX = "%class.construct:";
|
||||
const PACKED = `${PREFIX}packed`;
|
||||
@@ -380,6 +381,29 @@ export class ClassConstructionDispatch {
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
const type: IrType = { kind: "classval", className: name };
|
||||
const callee = varRef("p.0", DYN, loc);
|
||||
// A constructor inherited from a runtime stream or EventEmitter
|
||||
// takes no arguments here: arguments report at run time, as they do
|
||||
// at compile time for typed construction.
|
||||
const fence = inheritedBuiltinCtorFence(lowerer, info, loc);
|
||||
if (fence !== null && fn.name !== PACKED && fn.params.length > 1) {
|
||||
fn.body.unshift({
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.classIs",
|
||||
args: [callee, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [fence],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
changed = true;
|
||||
continue;
|
||||
}
|
||||
const args = constructorArguments(
|
||||
lowerer,
|
||||
info.ctorParams,
|
||||
@@ -388,8 +412,6 @@ export class ClassConstructionDispatch {
|
||||
fn.name === PACKED ? varRef("p.1", DYN, loc) : undefined,
|
||||
);
|
||||
if (!args) continue;
|
||||
const type: IrType = { kind: "classval", className: name };
|
||||
const callee = varRef("p.0", DYN, loc);
|
||||
const value = constructedValue(lowerer, info, {
|
||||
kind: "newValue",
|
||||
callee: { kind: "dynCheck", value: callee, type, loc },
|
||||
@@ -400,6 +422,28 @@ export class ClassConstructionDispatch {
|
||||
const body: IrStmt[] = [
|
||||
{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc },
|
||||
];
|
||||
// A spread call's argument count is known only at run time.
|
||||
if (fence !== null && fn.name === PACKED)
|
||||
body.unshift({
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "bin",
|
||||
op: ">",
|
||||
left: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.arrLen",
|
||||
args: [varRef("p.1", DYN, loc)],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
right: { kind: "numLit", value: 0, type: F64, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [fence],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
fn.body.unshift({
|
||||
kind: "if",
|
||||
cond: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { primitiveConstructorOr } from "./lower-exprs.js";
|
||||
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { constructedValue } from "./constructor-returns.js";
|
||||
import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js";
|
||||
@@ -2070,6 +2071,49 @@ export class ClassDynamicDispatch {
|
||||
// A read compares the bag with the receiver and then reads it:
|
||||
// evaluate it once, so each read retains the receiver once.
|
||||
const bagRead = varRef("bag.0", DYN, loc);
|
||||
const bagGet: IrExpr | null = read
|
||||
? {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bagGet",
|
||||
args: [bagRead, read.key, varRef("p.0", DYN, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: null;
|
||||
// A class instance whose class chain declares no member of an
|
||||
// Object.prototype method's name (classes that do answer in
|
||||
// their own branches) inherits it unless an own property
|
||||
// shadows it; a class constructor's static bag reads as before.
|
||||
const instanceRead: IrExpr | null =
|
||||
read && INHERITED_OBJECT_METHODS.has(name)
|
||||
? {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "dynTest",
|
||||
test: "function",
|
||||
value: varRef("p.0", DYN, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: bagGet!,
|
||||
else_: {
|
||||
kind: "ternary",
|
||||
cond: this.readBagHas(lowerer, "dyn.hasKey", bagRead, read.key, loc),
|
||||
then: bagGet!,
|
||||
else_: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.builtinMethod",
|
||||
args: [{ kind: "strLit", value: "Object", type: STRING, loc }, read.key],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
: bagGet;
|
||||
const fallback: IrExpr = read
|
||||
? {
|
||||
kind: "ternary",
|
||||
@@ -2081,13 +2125,7 @@ export class ClassDynamicDispatch {
|
||||
loc,
|
||||
},
|
||||
then: { ...read, value: bagRead },
|
||||
else_: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bagGet",
|
||||
args: [bagRead, read.key, varRef("p.0", DYN, loc)],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
else_: instanceRead!,
|
||||
type: DYN,
|
||||
loc,
|
||||
}
|
||||
@@ -2112,7 +2150,20 @@ export class ClassDynamicDispatch {
|
||||
]
|
||||
: [
|
||||
{ kind: "varDecl", localId: "bag.0", init: bag, loc },
|
||||
{ kind: "return", value: fallback, loc },
|
||||
{
|
||||
kind: "return",
|
||||
// Primitives answer their wrapper constructors.
|
||||
value:
|
||||
name === "constructor"
|
||||
? primitiveConstructorOr(
|
||||
lowerer,
|
||||
varRef("p.0", DYN, loc),
|
||||
fallback,
|
||||
loc,
|
||||
)
|
||||
: fallback,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
@@ -4820,19 +4871,40 @@ export class ClassDynamicDispatch {
|
||||
if (!field && !accessor) {
|
||||
const instance = lowerer.coerceToExpected(receiver, DYN);
|
||||
const bag = this.readPropertyBagOrEmpty(lowerer, instance, loc);
|
||||
return [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bagGet",
|
||||
args: [bag, { kind: "strLit", value: name, type: STRING, loc }, instance],
|
||||
type: DYN,
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
const own: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bagGet",
|
||||
args: [bag, key, instance],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// An Object.prototype method no class in the chain declares: an
|
||||
// own property shadows it, otherwise the read answers the same
|
||||
// interned value `Object.prototype.<name>` reads.
|
||||
if (INHERITED_OBJECT_METHODS.has(name) && !findMethodOn(lowerer, info, name)) {
|
||||
return [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc),
|
||||
then: own,
|
||||
else_: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.builtinMethod",
|
||||
args: [{ kind: "strLit", value: "Object", type: STRING, loc }, key],
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
];
|
||||
}
|
||||
return [{ kind: "return", value: own, loc }];
|
||||
}
|
||||
const type = field ?? accessor!.sig.ret;
|
||||
if (!canConvertToDyn(type, getRecord, getUnion)) return fence();
|
||||
@@ -5355,3 +5427,12 @@ export class ClassDynamicDispatch {
|
||||
return body;
|
||||
}
|
||||
}
|
||||
|
||||
/** Object.prototype methods every class instance inherits unless its class
|
||||
* chain declares them. */
|
||||
const INHERITED_OBJECT_METHODS: ReadonlySet<string> = new Set([
|
||||
"valueOf",
|
||||
"toString",
|
||||
"hasOwnProperty",
|
||||
"propertyIsEnumerable",
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { IrExpr, IrType } from "../../ir/ir.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { accessorCall } from "./lower-classes.js";
|
||||
|
||||
/** The generator a class instance's `[Symbol.iterator]` returns, when that
|
||||
* method is a generator method taking no arguments: `obj[Symbol.iterator]()`,
|
||||
* called once, as for-of, spread and Array.from call it. Null for other
|
||||
* values, and for classes whose iterator the program can replace at run
|
||||
* time (an instance property, a prototype write). */
|
||||
export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr | null {
|
||||
if (value.type.kind !== "object" || !iteratesThroughGenerator(lowerer, value.type)) return null;
|
||||
const className = value.type.className;
|
||||
const ret = lowerer.findMethodOn(lowerer.classes.get(className)!, "sym:iterator")!.sig.ret;
|
||||
return accessorCall(lowerer, className, "sym:iterator", value, [], ret, value.loc);
|
||||
}
|
||||
|
||||
/** Whether values of `type` iterate through a generator method (the check
|
||||
* classGeneratorIterable makes, from the type alone). */
|
||||
export function iteratesThroughGenerator(lowerer: Lowerer, type: IrType): boolean {
|
||||
if (type.kind !== "object") return false;
|
||||
const info = lowerer.classes.get(type.className);
|
||||
if (!info || info.def.runtime || info.fields.has("sym:iterator")) return false;
|
||||
if (
|
||||
lowerer.prototypeMethodAccesses.has("sym:iterator") ||
|
||||
lowerer.replacedSymbolMethods.has("sym:iterator")
|
||||
)
|
||||
return false;
|
||||
const found = lowerer.findMethodOn(info, "sym:iterator");
|
||||
return (
|
||||
!!found &&
|
||||
found.sig.params.length === 0 &&
|
||||
found.sig.ret.kind === "generator" &&
|
||||
!found.sig.ret.async
|
||||
);
|
||||
}
|
||||
@@ -43,9 +43,19 @@ export function collectClassMethodMutations(
|
||||
const jsAssignments: (ts.PropertyAccessExpression | ts.ElementAccessExpression)[] = [];
|
||||
const computedMethods: ts.Expression[] = [];
|
||||
const typedClasses: (ts.ClassDeclaration | ts.ClassExpression)[] = [];
|
||||
const symbolMethods = new Set<string>();
|
||||
let unknownKey: ts.Node | undefined;
|
||||
let numericKey: ts.Node | undefined;
|
||||
let unknownOwnField = false;
|
||||
// `sym:iterator` for `Symbol.iterator`: the name a class method declared
|
||||
// with a well-known symbol key takes.
|
||||
const wellKnownMethodName = (key: ts.Expression): string | null => {
|
||||
while (ts.isParenthesizedExpression(key)) key = key.expression;
|
||||
if (!ts.isPropertyAccessExpression(key)) return null;
|
||||
const member = lowerer.stdlibGlobalMember(key, "Symbol");
|
||||
if (member === null || !(lowerer.typeOf(key).flags & ts.TypeFlags.ESSymbolLike)) return null;
|
||||
return `sym:${member}`;
|
||||
};
|
||||
// JavaScript method inference and dynamic class factories keep their
|
||||
// existing dispatch contract. The native-call proof applies to typed
|
||||
// declarations (including JavaScript files TypeScript checks), while
|
||||
@@ -555,6 +565,19 @@ export function collectClassMethodMutations(
|
||||
lowerer.prototypeMethodAccesses.set(name, site);
|
||||
}
|
||||
for (const { key, receiver, site } of computedWrites) {
|
||||
// A class method declared with a well-known symbol key is not a write;
|
||||
// any other write with one can replace that method.
|
||||
const symbolName = key ? wellKnownMethodName(unwrap(key)) : null;
|
||||
if (symbolName) {
|
||||
const declaration =
|
||||
(ts.isMethodDeclaration(site) ||
|
||||
ts.isGetAccessorDeclaration(site) ||
|
||||
ts.isSetAccessorDeclaration(site)) &&
|
||||
(ts.isClassDeclaration(site.parent) || ts.isClassExpression(site.parent));
|
||||
if (declaration) symbolMethods.add(symbolName);
|
||||
else lowerer.replacedSymbolMethods.add(symbolName);
|
||||
continue;
|
||||
}
|
||||
const type = key ? lowerer.typeOf(key) : undefined;
|
||||
const names = type && (literalValues(type) ?? (key ? literalLoopKeys(unwrap(key)) : null));
|
||||
if (names) {
|
||||
@@ -585,8 +608,10 @@ export function collectClassMethodMutations(
|
||||
if (String(Number(name)) === name) markName(name, numericKey);
|
||||
}
|
||||
}
|
||||
if (unknownKey)
|
||||
if (unknownKey) {
|
||||
for (const method of methods) lowerer.prototypeMethodAccesses.set(method, unknownKey);
|
||||
for (const method of symbolMethods) lowerer.replacedSymbolMethods.add(method);
|
||||
}
|
||||
if (!unknownKey && !unknownOwnField)
|
||||
for (const field of arrowFields) {
|
||||
if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field);
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
/* Code generation from strings: `eval(...)`, `new Function(...)` and
|
||||
* `Function(...)`. A compiled binary embeds no JavaScript engine, so scriptc
|
||||
* answers exactly as `node --disallow-code-generation-from-strings` does:
|
||||
* every call throws the catchable EvalError "Code generation from strings
|
||||
* disallowed for this context". Packages that probe for code generation
|
||||
* (zod 4's `allowsEval`, template engines, CSP-aware JITs) then take their
|
||||
* interpreted paths. The arguments still evaluate first, in order, and the
|
||||
* Function constructor converts each to a string before it refuses, as
|
||||
* V8's CreateDynamicFunction does; eval refuses without converting. */
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import { DYN, F64, type IrExpr, type IrStmt, type IrType, STRING } from "../../ir/ir.js";
|
||||
import { nodeThrowExpr } from "../../ir/build.js";
|
||||
import { ambientUndefReadType, contextualUndefReadType } from "./lower-namespaces.js";
|
||||
import { stdlibGlobalNameOf } from "./surfaces.js";
|
||||
|
||||
export const CODE_GENERATION_DISALLOWED =
|
||||
"Code generation from strings disallowed for this context";
|
||||
|
||||
/** The code-generation entry point a callee names: the stdlib `eval` or
|
||||
* `Function` global (directly, through `globalThis.`, or through a stable
|
||||
* alias), or the indirect-eval spelling `(0, eval)`. Null otherwise. */
|
||||
export function codeGenerationCalleeOf(
|
||||
lowerer: Lowerer,
|
||||
callee: ts.Expression,
|
||||
): "eval" | "Function" | null {
|
||||
let e = callee;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
||||
return codeGenerationCalleeOf(lowerer, e.right) === "eval" ? "eval" : null;
|
||||
}
|
||||
// Syntactic pre-check first: every call and construction asks, and only
|
||||
// these spellings can name the globals (aliases are stdlib-resolved
|
||||
// symbols, checked below only when the spelling could match).
|
||||
const text = ts.isIdentifier(e) ? e.text : ts.isPropertyAccessExpression(e) ? e.name.text : null;
|
||||
if (text === null) return null;
|
||||
if (text !== "eval" && text !== "Function" && !hasCodeGenerationAlias(lowerer)) return null;
|
||||
const name = stdlibGlobalNameOf(lowerer, e);
|
||||
return name === "eval" || name === "Function" ? name : null;
|
||||
}
|
||||
|
||||
/** Some stable binding aliases `eval` or `Function` (`const F = Function`,
|
||||
* zod 4's spelling). */
|
||||
function hasCodeGenerationAlias(lowerer: Lowerer): boolean {
|
||||
for (const name of lowerer.stdlibGlobalAliases.values()) {
|
||||
if (name === "eval" || name === "Function") return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The comma operands an indirect eval evaluates before the call: `a` in
|
||||
* `(a, eval)(...)`, outermost first. */
|
||||
function indirectEvalPrefix(callee: ts.Expression): ts.Expression[] {
|
||||
let e = callee;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.CommaToken) return [];
|
||||
return [...indirectEvalPrefix(e.left), e.left, ...indirectEvalPrefix(e.right)].filter(
|
||||
(operand) => {
|
||||
let o = operand;
|
||||
while (ts.isParenthesizedExpression(o)) o = o.expression;
|
||||
return !(ts.isBinaryExpression(o) && o.operatorToken.kind === ts.SyntaxKind.CommaToken);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Lowers a code-generation call or construction to Node's refusal under
|
||||
* --disallow-code-generation-from-strings: the callee's own operands, then
|
||||
* the arguments in order (converted to strings for the Function
|
||||
* constructor), then the EvalError. The result carries the use site's type
|
||||
* (the throw always fires, so the value is never observed). */
|
||||
export function lowerCodeGenerationCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression | ts.NewExpression,
|
||||
kind: "eval" | "Function",
|
||||
): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const stmts: IrStmt[] = [];
|
||||
for (const operand of indirectEvalPrefix(expr.expression)) {
|
||||
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(operand), loc: locOf(operand) });
|
||||
}
|
||||
for (const arg of expr.arguments?.nodes ?? []) {
|
||||
if (ts.isSpreadElement(arg)) {
|
||||
lowerer.noLowering(
|
||||
`${kind === "eval" ? "eval" : "the Function constructor"} with spread arguments`,
|
||||
arg,
|
||||
"code generation from strings throws EvalError in compiled programs, as under node --disallow-code-generation-from-strings; pass the arguments directly",
|
||||
);
|
||||
}
|
||||
const value = lowerer.lowerExpr(arg);
|
||||
const argLoc = locOf(arg);
|
||||
if (kind === "Function" && !isPrimitiveIrType(value.type)) {
|
||||
stmts.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.toStringCoerce",
|
||||
args: [lowerer.coerceToExpected(value, DYN)],
|
||||
type: STRING,
|
||||
loc: argLoc,
|
||||
},
|
||||
loc: argLoc,
|
||||
});
|
||||
} else {
|
||||
stmts.push({ kind: "exprStmt", expr: value, loc: argLoc });
|
||||
}
|
||||
}
|
||||
const type = useSiteType(lowerer, expr);
|
||||
const refusal = nodeThrowExpr(6, "", CODE_GENERATION_DISALLOWED, type, loc);
|
||||
return stmts.length === 0 ? refusal : { kind: "seqExpr", stmts, result: refusal, type, loc };
|
||||
}
|
||||
|
||||
function isPrimitiveIrType(t: IrType): boolean {
|
||||
return (
|
||||
t.kind === "string" ||
|
||||
t.kind === "f64" ||
|
||||
t.kind === "bool" ||
|
||||
t.kind === "void" ||
|
||||
t.kind === "bigint"
|
||||
);
|
||||
}
|
||||
|
||||
function useSiteType(lowerer: Lowerer, expr: ts.Expression): IrType {
|
||||
// The Function interface has no mapping (mapping it would fence): the
|
||||
// constructed value is checked-dynamic in JavaScript, and a typed dummy
|
||||
// elsewhere — the throw always fires first.
|
||||
const type = lowerer.typeOf(expr);
|
||||
const symbol = type.getSymbol();
|
||||
if (symbol?.name === "Function" && lowerer.isStdlibSymbol(symbol)) {
|
||||
return isJsSourceFile(expr.getSourceFile()) ? DYN : F64;
|
||||
}
|
||||
const mapped = lowerer.mapTypeOf(type);
|
||||
if (
|
||||
mapped &&
|
||||
mapped.kind !== "void" &&
|
||||
mapped.kind !== "func" &&
|
||||
!lowerer.typeNamesUnregisteredClass(mapped)
|
||||
) {
|
||||
return mapped;
|
||||
}
|
||||
return ambientUndefReadType(lowerer, expr) ?? contextualUndefReadType(lowerer, expr) ?? F64;
|
||||
}
|
||||
@@ -23,7 +23,19 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
|
||||
}
|
||||
if (dst.kind === "union") {
|
||||
if (src.kind === "union") {
|
||||
return lowerer.unionRetagMappable(src.unionId, dst.unionId) ? { how: "retag" } : null;
|
||||
if (lowerer.unionRetagMappable(src.unionId, dst.unionId)) return { how: "retag" };
|
||||
// A field the checker types `undefined` alone (an object literal's
|
||||
// omitted member, normalized to `?: undefined` across a union of
|
||||
// literal types) carries the unit-only union. Into a slot with an
|
||||
// undefined arm it lifts as that arm; its null arm cannot occur.
|
||||
const srcArms = lowerer.unions.get(src.unionId)?.arms ?? [];
|
||||
if (
|
||||
srcArms.length > 0 &&
|
||||
srcArms.every(isUnitType) &&
|
||||
lowerer.armTag(dst.unionId, UNDEFINED_T) >= 0
|
||||
)
|
||||
return { how: "unitRetag" };
|
||||
return null;
|
||||
}
|
||||
// A unit-typed source can't wrap here (unionWrap requires the
|
||||
// LITERAL unit — and no lowered shape carries a bare unit field).
|
||||
|
||||
@@ -125,6 +125,19 @@ export function applyWidthLift(
|
||||
throw new InternalCompilerError("lowerer bug: planned retag lift failed to intern");
|
||||
return { kind: "call", callee: retag, args: [value], type: dst, loc };
|
||||
}
|
||||
case "unitRetag": {
|
||||
if (dst.kind !== "union" || value.type.kind !== "union")
|
||||
throw new InternalCompilerError("lowerer bug: unit retag lift shape");
|
||||
const fromId = value.type.unionId;
|
||||
const trappable = new Set<number>();
|
||||
(lowerer.unions.get(fromId)?.arms ?? []).forEach((arm, tag) => {
|
||||
if (lowerer.armTag(dst.unionId, arm) < 0) trappable.add(tag);
|
||||
});
|
||||
const retag = lowerer.unionRetagHelper(fromId, dst.unionId, loc, trappable);
|
||||
if (!retag)
|
||||
throw new InternalCompilerError("lowerer bug: planned unit retag lift failed to intern");
|
||||
return { kind: "call", callee: retag, args: [value], type: dst, loc };
|
||||
}
|
||||
case "liftWrap": {
|
||||
if (dst.kind !== "union")
|
||||
throw new InternalCompilerError("lowerer bug: liftWrap lift against a non-union");
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
lowerArrayCallback,
|
||||
requireProducedArrayElement,
|
||||
callbackArrayElement,
|
||||
truthyFilterCallback,
|
||||
} from "./callback-arguments.js";
|
||||
import {
|
||||
reverseArrayLoop,
|
||||
@@ -85,7 +86,9 @@ export function lowerArrayHofCall(
|
||||
const valueT = callbackValueType(lowerer, elem, present);
|
||||
if (booleanFilter) {
|
||||
if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode);
|
||||
if (valueT.kind === "dyn" || valueT.kind === "void" || isUnitType(valueT)) {
|
||||
// Checked-dynamic elements (`anyText.split("\n").map(...).filter(Boolean)`)
|
||||
// keep their runtime kind, so their truthiness is the runtime's.
|
||||
if (valueT.kind === "void" || isUnitType(valueT)) {
|
||||
lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
}
|
||||
}
|
||||
@@ -231,33 +234,6 @@ function filterResultElem(
|
||||
return outElem;
|
||||
}
|
||||
|
||||
/** A native closure for the ambient Boolean callback. Reifying the
|
||||
* JavaScript constructor itself is unnecessary; Array#filter only observes
|
||||
* its one-argument ToBoolean result. */
|
||||
function truthyFilterCallback(
|
||||
lowerer: Lowerer,
|
||||
valueT: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr & { type: IrType & { kind: "func" } } {
|
||||
const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: BOOL };
|
||||
const key = `filterBoolean:${typeKey(valueT)}`;
|
||||
let name = lowerer.arrHofHelpers.get(key);
|
||||
if (!name) {
|
||||
name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const value = varRef("v.0", valueT, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "v.0", name: "v", type: valueT }],
|
||||
returnType: BOOL,
|
||||
locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }],
|
||||
body: [{ kind: "return", value: { kind: "toBool", operand: value, type: BOOL, loc }, loc }],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "closure", fnName: name, captures: [], type: fnT, loc };
|
||||
}
|
||||
|
||||
function filterPredicateOk(lowerer: Lowerer, ret: IrType): boolean {
|
||||
if (ret.kind === "bool") return true;
|
||||
// VOID is a TYPE erasure, not a runtime value: TS lets a value-returning
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
lowerArrayCallback,
|
||||
callbackArrayElement,
|
||||
} from "./callback-arguments.js";
|
||||
import { classGeneratorIterable } from "../class-iterables.js";
|
||||
|
||||
/** Build the argument array for a mutating/copying operation while keeping
|
||||
* the receiver's scalar payload ABI. A missing read is represented as the
|
||||
@@ -265,6 +266,22 @@ function appendWidenedArrayValues(
|
||||
}
|
||||
|
||||
/** The Array constructor's element and count forms, shared by calls and new. */
|
||||
/** The value of `.fill(v)` when `expr` is that call's receiver. */
|
||||
function fillArgumentOfReceiver(expr: ts.Expression): ts.Expression | null {
|
||||
let receiver: ts.Node = expr;
|
||||
let parent = expr.parent;
|
||||
while (ts.isParenthesizedExpression(parent)) {
|
||||
receiver = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (!ts.isPropertyAccessExpression(parent) || parent.expression !== receiver) return null;
|
||||
if (parent.name.text !== "fill" || parent.questionDotToken) return null;
|
||||
const call = parent.parent;
|
||||
if (!ts.isCallExpression(call) || call.expression !== parent) return null;
|
||||
const value = call.arguments.nodes[0];
|
||||
return value !== undefined && !ts.isSpreadElement(value) ? value : null;
|
||||
}
|
||||
|
||||
export function lowerArrayConstructor(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression | ts.NewExpression,
|
||||
@@ -295,6 +312,15 @@ export function lowerArrayConstructor(
|
||||
const contextual = lowerer.checker.getContextualType(expr);
|
||||
if (contextual) result = lowerer.mapTypeOf(contextual);
|
||||
}
|
||||
// `Array(n).fill(v)`: the checker types the length-constructed array as
|
||||
// any[], but fill gives every element v's type.
|
||||
if (result?.kind !== "array") {
|
||||
const fillValue = fillArgumentOfReceiver(expr);
|
||||
const elem = fillValue
|
||||
? lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(fillValue)))
|
||||
: null;
|
||||
if (elem !== null && isSupportedArrayElem(elem)) result = arrayOf(elem);
|
||||
}
|
||||
if (isJsSourceFile(expr.getSourceFile()) && (result === null || result.kind === "dyn"))
|
||||
result = arrayOf(DYN);
|
||||
if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) {
|
||||
@@ -913,7 +939,7 @@ export function lowerArrayFromCall(
|
||||
// and materializes holes while keeping element reference identity.
|
||||
if ((args.length === 1 || args.length === 2) && !ts.isObjectLiteralExpression(args[0]!)) {
|
||||
const source = lowerer.lowerExpr(args[0]!);
|
||||
const src =
|
||||
let src =
|
||||
source.type.kind === "string" && args.length === 2
|
||||
? strCharsCall(lowerer, source, loc)
|
||||
: source;
|
||||
@@ -1008,6 +1034,9 @@ export function lowerArrayFromCall(
|
||||
if (src.type.kind === "searchParams" && args.length === 1) {
|
||||
return lowerCheckedArrayFrom(lowerer, lowerer.coerceInto(args[0]!, src, DYN), loc);
|
||||
}
|
||||
// A class whose [Symbol.iterator] is a generator method iterates the
|
||||
// generator that method returns.
|
||||
src = classGeneratorIterable(lowerer, src) ?? src;
|
||||
// A generator (or an iterator adapted into one) drains in order.
|
||||
if (src.type.kind === "generator" && args.length === 1) {
|
||||
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { isJsSourceFile, locOf } from "../../program.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
type IrExpr,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
UNDEFINED_T,
|
||||
funcOf,
|
||||
isSupportedArrayElem,
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
} from "../../../ir/ir.js";
|
||||
|
||||
@@ -44,6 +50,24 @@ export function lowerArrayCallback(
|
||||
bindUntyped = false,
|
||||
expectedReturn?: IrType,
|
||||
): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number } {
|
||||
// The ambient `Boolean` as the callback (`flags.every(Boolean)`,
|
||||
// `xs.some(Boolean)`, `xs.map(Boolean)`): Boolean(value) answers the
|
||||
// ToBoolean of its FIRST argument and ignores the rest, so the callback
|
||||
// is the interned one-parameter truthiness closure over the lead slot.
|
||||
// The constructor's own generic signature has no compiled value form.
|
||||
// JavaScript files keep their primitive-constructor closure.
|
||||
if (
|
||||
ts.isIdentifier(argNode) &&
|
||||
argNode.text === "Boolean" &&
|
||||
!isJsSourceFile(argNode.getSourceFile()) &&
|
||||
lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined)
|
||||
) {
|
||||
const valueT = lead[0];
|
||||
if (valueT === undefined || valueT.kind === "void" || isUnitType(valueT))
|
||||
lowerer.badType(argNode, lowerer.typeOf(argNode));
|
||||
if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode);
|
||||
return { fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), arity: 1 };
|
||||
}
|
||||
const full = [...lead, F64, arrT];
|
||||
// A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`):
|
||||
// the contextual signature types the unannotated param `any` (the
|
||||
@@ -248,3 +272,39 @@ function runtimeOptionalPairVariant(lowerer: Lowerer, expected: IrType, mapped:
|
||||
: t;
|
||||
return lowerer.isRuntimeOptionalRecordVariant(strip(expected), strip(mapped));
|
||||
}
|
||||
|
||||
/** A native closure for the ambient Boolean callback. Reifying the
|
||||
* JavaScript constructor itself is unnecessary; Array#filter only observes
|
||||
* its one-argument ToBoolean result. */
|
||||
export function truthyFilterCallback(
|
||||
lowerer: Lowerer,
|
||||
valueT: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr & { type: IrType & { kind: "func" } } {
|
||||
const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: BOOL };
|
||||
const key = `filterBoolean:${typeKey(valueT)}`;
|
||||
let name = lowerer.arrHofHelpers.get(key);
|
||||
if (!name) {
|
||||
name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, name);
|
||||
const value = varRef("v.0", valueT, loc);
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [{ localId: "v.0", name: "v", type: valueT }],
|
||||
returnType: BOOL,
|
||||
locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value:
|
||||
valueT.kind === "dyn"
|
||||
? { kind: "dynTest", test: "truthy", value, type: BOOL, loc }
|
||||
: { kind: "toBool", operand: value, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return { kind: "closure", fnName: name, captures: [], type: fnT, loc };
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
import { strCharsCall } from "./array-construction.js";
|
||||
import { arrayElementRead } from "./array-iteration.js";
|
||||
import { isSearchParamsIterable } from "../builtins/url.js";
|
||||
import { classGeneratorIterable } from "../class-iterables.js";
|
||||
import { generatorDrain } from "../iterator-adapters.js";
|
||||
|
||||
/** Ambient Map method calls. `get`/`set`/`has`/`delete`/`clear` lower to
|
||||
* mapIntrinsic; `forEach` desugars to a direct call of a synthetic loop
|
||||
@@ -1048,6 +1050,14 @@ function setFromSeedValue(
|
||||
}
|
||||
: null;
|
||||
}
|
||||
// A generator, or a class whose [Symbol.iterator] is a generator method:
|
||||
// the Set takes every value it yields, in order.
|
||||
const generator =
|
||||
classGeneratorIterable(lowerer, source) ?? (source.type.kind === "generator" ? source : null);
|
||||
if (generator) {
|
||||
const drained = generatorDrain(lowerer, generator, setT.elem, loc);
|
||||
return drained ? { kind: "setNew", seed: drained, type: setT, loc } : null;
|
||||
}
|
||||
let seed: IrExpr;
|
||||
if (source.type.kind === "set" && typeEquals(source.type, setT)) {
|
||||
return { kind: "setIntrinsic", method: "clone", receiver: source, args: [], type: setT, loc };
|
||||
@@ -1246,6 +1256,24 @@ export function mapFromSeedValue(
|
||||
}
|
||||
: null;
|
||||
}
|
||||
// A generator of entries, or a class whose [Symbol.iterator] is one: the
|
||||
// Map takes every [key, value] pair it yields, in order.
|
||||
const generator =
|
||||
classGeneratorIterable(lowerer, seed) ?? (seed.type.kind === "generator" ? seed : null);
|
||||
if (generator) {
|
||||
const entry: IrType = {
|
||||
kind: "record",
|
||||
shapeId: lowerer.shapes.intern(
|
||||
[
|
||||
{ name: "0", type: mapT.key },
|
||||
{ name: "1", type: mapT.value },
|
||||
],
|
||||
true,
|
||||
),
|
||||
};
|
||||
const drained = generatorDrain(lowerer, generator, entry, loc);
|
||||
return drained ? mapFromSeedValue(lowerer, drained, mapT) : null;
|
||||
}
|
||||
if (seed.type.kind === "map") {
|
||||
if (!typeEquals(seed.type, mapT)) return null;
|
||||
return { kind: "mapIntrinsic", method: "clone", receiver: seed, args: [], type: mapT, loc };
|
||||
|
||||
@@ -28,6 +28,8 @@ import { tryLowerExpression } from "../expressions/try-lower-expression.js";
|
||||
import { locOf } from "../../program.js";
|
||||
import { typeKey } from "../../type-mapper.js";
|
||||
import { type WidthLift } from "../lowerer.js";
|
||||
import { classGeneratorIterable, iteratesThroughGenerator } from "../class-iterables.js";
|
||||
import { generatorDrain } from "../iterator-adapters.js";
|
||||
|
||||
/** Object() and new Object() preserve known object inputs. Primitive
|
||||
* boxing and unproven checked-dynamic values retain their refusal. */
|
||||
@@ -148,12 +150,29 @@ export function objectIterOverIndexShape(
|
||||
}
|
||||
// The overflow value must surface as the element type (identity, an
|
||||
// arm of a union element, or a dyn element over a dyn signature).
|
||||
// A cast that narrows the signature's union (`Object.entries(headers as
|
||||
// Record<string, string | string[] | undefined>)` over a signature that
|
||||
// also holds numbers): each overflow value checks into the element type,
|
||||
// so a value of a dropped arm throws instead of mistyping.
|
||||
let ivNarrows = false;
|
||||
if (valueT) {
|
||||
const ivSurfaces =
|
||||
typeEquals(iv, valueT) ||
|
||||
(valueT.kind === "union" && lowerer.armTag(valueT.unionId, iv) >= 0) ||
|
||||
(valueT.kind === "dyn" && iv.kind === "dyn");
|
||||
if (!ivSurfaces) {
|
||||
if (
|
||||
!ivSurfaces &&
|
||||
iv.kind === "union" &&
|
||||
lowerer.dynConvertible(iv) &&
|
||||
lowerer.dynConvertible(valueT)
|
||||
) {
|
||||
const ivUnion = iv;
|
||||
const narrowArms =
|
||||
valueT.kind === "union" ? (lowerer.unions.get(valueT.unionId)?.arms ?? []) : [valueT];
|
||||
ivNarrows =
|
||||
narrowArms.length > 0 && narrowArms.every((a) => lowerer.armTag(ivUnion.unionId, a) >= 0);
|
||||
}
|
||||
if (!ivSurfaces && !ivNarrows) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
@@ -267,6 +286,21 @@ export function objectIterOverIndexShape(
|
||||
}
|
||||
}
|
||||
}
|
||||
// A cast that narrows the field's type (the element type keeps
|
||||
// some of its arms): the value checks into the element type, so a
|
||||
// dropped arm throws instead of mistyping.
|
||||
if (lowerer.dynConvertible(f.type) && lowerer.dynConvertible(valueT)) {
|
||||
const armsOf = (t: IrType): IrType[] =>
|
||||
t.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? []) : [t];
|
||||
const target = armsOf(valueT);
|
||||
if (armsOf(f.type).some((a) => target.some((b) => typeEquals(a, b))))
|
||||
return {
|
||||
kind: "dynCheck",
|
||||
value: { kind: "dynFrom", value: raw, type: DYN, loc },
|
||||
type: valueT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const pushedOf = (element: IrExpr): IrExpr =>
|
||||
@@ -337,17 +371,37 @@ export function objectIterOverIndexShape(
|
||||
const ksT = arrayOf(STRING);
|
||||
const ksRef = varRef("ks.0", ksT, loc);
|
||||
const kRef = varRef("k.0", STRING, loc);
|
||||
const readValue: IrExpr | null = valueT
|
||||
? {
|
||||
kind: "recordKeyGet",
|
||||
obj: rRef,
|
||||
shapeId: argIr.shapeId,
|
||||
key: kRef,
|
||||
overflowOnly: true,
|
||||
type: valueT,
|
||||
loc,
|
||||
}
|
||||
: null;
|
||||
const readValue: IrExpr | null = !valueT
|
||||
? null
|
||||
: ivNarrows
|
||||
? {
|
||||
kind: "dynCheck",
|
||||
value: {
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "recordKeyGet",
|
||||
obj: rRef,
|
||||
shapeId: argIr.shapeId,
|
||||
key: kRef,
|
||||
overflowOnly: true,
|
||||
type: iv,
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
type: valueT,
|
||||
loc,
|
||||
}
|
||||
: {
|
||||
kind: "recordKeyGet",
|
||||
obj: rRef,
|
||||
shapeId: argIr.shapeId,
|
||||
key: kRef,
|
||||
overflowOnly: true,
|
||||
type: valueT,
|
||||
loc,
|
||||
};
|
||||
const loopPushed: IrExpr =
|
||||
member === "keys"
|
||||
? kRef
|
||||
@@ -472,9 +526,37 @@ function lowerTypedObjectFromEntriesCall(
|
||||
if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!))
|
||||
return null;
|
||||
const argNode = call.arguments.nodes[0]!;
|
||||
const argIr = lowerer.mapTypeOf(lowerer.typeOf(argNode));
|
||||
let argIr = lowerer.mapTypeOf(lowerer.typeOf(argNode));
|
||||
// A generator of entries, or a class whose [Symbol.iterator] is one,
|
||||
// drains into the entry array the object is built from.
|
||||
let drained: IrExpr | null = null;
|
||||
if (argIr?.kind === "generator" || (argIr && iteratesThroughGenerator(lowerer, argIr))) {
|
||||
const value = lowerer.lowerExpr(argNode);
|
||||
const generator =
|
||||
classGeneratorIterable(lowerer, value) ?? (value.type.kind === "generator" ? value : null);
|
||||
if (generator?.type.kind !== "generator") return null;
|
||||
// The entries take the result's value type: `[string, V]` for an
|
||||
// object `{ [k: string]: V }`.
|
||||
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
|
||||
const valueT =
|
||||
result?.kind === "record" ? lowerer.shapes.get(result.shapeId)?.indexValue : undefined;
|
||||
if (!valueT) return null;
|
||||
const entry: IrType = {
|
||||
kind: "record",
|
||||
shapeId: lowerer.shapes.intern(
|
||||
[
|
||||
{ name: "0", type: STRING },
|
||||
{ name: "1", type: valueT },
|
||||
],
|
||||
true,
|
||||
),
|
||||
};
|
||||
drained = generatorDrain(lowerer, generator, entry, locOf(call));
|
||||
if (!drained) return null;
|
||||
argIr = drained.type;
|
||||
}
|
||||
if (argIr?.kind !== "array") return null;
|
||||
const represented = tryLowerExpression(lowerer, argNode);
|
||||
const represented = drained ?? tryLowerExpression(lowerer, argNode);
|
||||
if (represented && !typeEquals(represented.type, argIr)) return null;
|
||||
// `Object.fromEntries(rows)` over a `string[][]` VALUE — the env-line
|
||||
// idiom (`envArray.map((env) => env.split('='))`). The checker has no
|
||||
@@ -522,7 +604,7 @@ function lowerTypedObjectFromEntriesCall(
|
||||
// Enumeration/filtering may keep the array in checked-dynamic storage.
|
||||
// Validate its tuple layout before calling a helper with a native-array
|
||||
// ABI; opaque tuple values themselves retain their original references.
|
||||
const receiver = lowerer.lowerExprExpecting(argNode, argIr);
|
||||
const receiver = drained ?? lowerer.lowerExprExpecting(argNode, argIr);
|
||||
const key = `obj.fromEntries:${argIr.elem.shapeId}:${resultT.shapeId}`;
|
||||
let helper = lowerer.arrHofHelpers.get(key);
|
||||
if (!helper) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
genValue,
|
||||
stepDrivable,
|
||||
} from "../lower-generators.js";
|
||||
import { classGeneratorIterable, iteratesThroughGenerator } from "../class-iterables.js";
|
||||
import { exitBeforeReturns } from "./for-of.js";
|
||||
import {
|
||||
iteratorSlotAdapter,
|
||||
@@ -317,9 +318,11 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean {
|
||||
* it rather than a wrapper. A generator (including a helper object or an
|
||||
* array iterator) is itself; an array, string or typed array gives its
|
||||
* values iterator; a Set its values and a Map its entries, as a generator
|
||||
* over the live collection. The result converts into the call's
|
||||
* IteratorObject type. Null for other arguments (objects with their own
|
||||
* next method, untyped values), which keep the Iterator fence. */
|
||||
* over the live collection; a class instance whose `[Symbol.iterator]` is
|
||||
* a generator method gives the generator it returns. The result converts
|
||||
* into the call's IteratorObject type. Null for other arguments (objects
|
||||
* with their own next method, untyped values), which keep the Iterator
|
||||
* fence. */
|
||||
export function lowerIteratorFromCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -332,7 +335,9 @@ export function lowerIteratorFromCall(
|
||||
const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode));
|
||||
if (resultT?.kind !== "generator" || resultT.async || argT === null) return null;
|
||||
const iterableKinds = ["generator", "array", "string", "bytes", "set", "map"];
|
||||
if (!iterableKinds.includes(argT.kind) || (argT.kind === "generator" && argT.async)) return null;
|
||||
const classIterable = iteratesThroughGenerator(lowerer, argT);
|
||||
if (!classIterable && !iterableKinds.includes(argT.kind)) return null;
|
||||
if (argT.kind === "generator" && argT.async) return null;
|
||||
const loc = locOf(call);
|
||||
const value = lowerer.lowerExpr(argNode);
|
||||
const own = (yieldT: IrType): GenType => ({
|
||||
@@ -344,6 +349,7 @@ export function lowerIteratorFromCall(
|
||||
let iterator: IrExpr | null = null;
|
||||
const t = value.type;
|
||||
if (t.kind === "generator") iterator = value;
|
||||
else if (classIterable) iterator = classGeneratorIterable(lowerer, value);
|
||||
else if (t.kind === "array")
|
||||
iterator = lowerArrayIteratorValue(lowerer, value, "values", own(t.elem), loc);
|
||||
else if (t.kind === "string")
|
||||
|
||||
@@ -302,6 +302,53 @@ function lowerMethodReceiver(
|
||||
return lowerer.coerceInto(node, lowered, expected);
|
||||
}
|
||||
|
||||
/** A `string | undefined | null` receiver of a string method: evaluated
|
||||
* once, each unit arm throws the TypeError Node's member read throws
|
||||
* ("Cannot read properties of undefined (reading 'replace')") before the
|
||||
* arguments evaluate, and the string arm is the receiver. */
|
||||
function nullableStringReceiver(lowerer: Lowerer, node: ts.Expression, member: string): IrExpr {
|
||||
const lowered = lowerer.lowerExpr(node);
|
||||
if (lowered.type.kind !== "union") return lowerer.coerceInto(node, lowered, STRING);
|
||||
const loc = locOf(node);
|
||||
const unionId = lowered.type.unionId;
|
||||
const local = lowerer.declareHiddenLocal("%nullableReceiver", lowered.type);
|
||||
const ref: IrExpr = { kind: "varRef", localId: local.id, type: lowered.type, loc };
|
||||
const arms = lowerer.unions.get(unionId)?.arms ?? [];
|
||||
let result: IrExpr = {
|
||||
kind: "unionNarrow",
|
||||
unionId,
|
||||
tag: arms.findIndex((arm) => arm.kind === "string"),
|
||||
value: ref,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
for (const [tag, arm] of arms.entries()) {
|
||||
if (!isUnitType(arm)) continue;
|
||||
const unit = arm.kind === "nullT" ? "null" : "undefined";
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "unionIsTag", unionId, tag, value: ref, negated: false, type: BOOL, loc },
|
||||
then: nodeThrowExpr(
|
||||
1,
|
||||
"",
|
||||
`Cannot read properties of ${unit} (reading '${member}')`,
|
||||
STRING,
|
||||
loc,
|
||||
),
|
||||
else_: result,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: lowered, loc }],
|
||||
result,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** Relative string indexing and code-point decoding over the UTF-16 intrinsics. */
|
||||
export function lowerStringIndexCall(
|
||||
lowerer: Lowerer,
|
||||
@@ -733,6 +780,15 @@ export function lowerStringPaddingCall(
|
||||
return { kind: "call", callee: helper, args: values, type: STRING, loc };
|
||||
}
|
||||
|
||||
/** String methods whose regex-argument forms lower in lowerRegexMethodCall. */
|
||||
const REGEX_ARGUMENT_STRING_METHODS = new Set([
|
||||
"replace",
|
||||
"replaceAll",
|
||||
"split",
|
||||
"search",
|
||||
"matchAll",
|
||||
]);
|
||||
|
||||
export function lowerRegexMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
@@ -747,11 +803,30 @@ export function lowerRegexMethodCall(
|
||||
// by construction — the symbol gate doesn't apply to `any` receivers.
|
||||
if (dynReceiver === undefined && !lowerer.isStdlibMember(access)) return null;
|
||||
const name = access.name.text;
|
||||
const receiverKind = dynReceiver
|
||||
? "string"
|
||||
: lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind;
|
||||
const mappedReceiver = dynReceiver ? null : lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
// A nullable string receiver of a REGEX-argument call
|
||||
// (`part.replace(re, "")` where an unchecked index read typed `part`
|
||||
// `string | undefined`) takes the string path, as
|
||||
// lowerStringMethodCall's gate does for the string-argument forms:
|
||||
// lowerMethodReceiver throws Node's "Cannot read properties of
|
||||
// undefined" on the unit arms. (`match` keeps its own gate below.)
|
||||
const regexArgument = call.arguments.nodes[0];
|
||||
const nullableString =
|
||||
REGEX_ARGUMENT_STRING_METHODS.has(name) &&
|
||||
regexArgument !== undefined &&
|
||||
!ts.isSpreadElement(regexArgument) &&
|
||||
lowerer.mapTypeOf(lowerer.typeOf(regexArgument))?.kind === "regex" &&
|
||||
mappedReceiver?.kind === "union" &&
|
||||
(lowerer.unions.get(mappedReceiver.unionId)?.arms.some((arm) => arm.kind === "string") ??
|
||||
false) &&
|
||||
(lowerer.unions
|
||||
.get(mappedReceiver.unionId)
|
||||
?.arms.every((arm) => arm.kind === "string" || isUnitType(arm)) ??
|
||||
false);
|
||||
const receiverKind = dynReceiver || nullableString ? "string" : mappedReceiver?.kind;
|
||||
const lowerReceiver = (): IrExpr => {
|
||||
if (dynReceiver) return dynReceiver();
|
||||
if (nullableString) return nullableStringReceiver(lowerer, access.expression, name);
|
||||
if (receiverKind === "string")
|
||||
return lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
|
||||
if (receiverKind === "regex")
|
||||
|
||||
@@ -14,7 +14,6 @@ import * as ts from "../ts7/adapter.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { isNodeModulesPath } from "../resolve.js";
|
||||
import {
|
||||
dateLiteralDivergenceDiag,
|
||||
keyOrderDivergenceDiag,
|
||||
localeCompareDivergenceDiag,
|
||||
unrelatedClassCastDivergenceDiag,
|
||||
@@ -57,7 +56,6 @@ export function scanDivergences(
|
||||
const literals: ts.ObjectLiteralExpression[] = [];
|
||||
const observers: ts.Expression[] = [];
|
||||
const casts: (ts.AsExpression | ts.TypeAssertion)[] = [];
|
||||
const dates: { call: ts.CallExpression | ts.NewExpression; arg: ts.Expression }[] = [];
|
||||
const localeCompares: ts.CallExpression[] = [];
|
||||
/** binding name → member names assigned through it (`q.a = 2`). */
|
||||
const assignedMembers = new Map<string, Set<string>>();
|
||||
@@ -99,20 +97,9 @@ export function scanDivergences(
|
||||
member === "dir")
|
||||
) {
|
||||
for (const a of node.arguments.nodes) if (!isLiteralLike(a)) observers.push(a);
|
||||
} else if (recv.text === "Date" && member === "parse") {
|
||||
if (node.arguments.nodes[0] !== undefined)
|
||||
dates.push({ call: node, arg: node.arguments.nodes[0] });
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (ts.isNewExpression(node)) {
|
||||
if (
|
||||
ts.isIdentifier(node.expression) &&
|
||||
node.expression.text === "Date" &&
|
||||
node.arguments?.nodes.length === 1
|
||||
) {
|
||||
dates.push({ call: node, arg: node.arguments.nodes[0]! });
|
||||
}
|
||||
} else if (ts.isForInStatement(node)) {
|
||||
observers.push(node.expression);
|
||||
} else if (ts.isVariableDeclaration(node)) {
|
||||
@@ -399,55 +386,9 @@ export function scanDivergences(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Date strings the native parser does not accept ────────────────
|
||||
for (const { call, arg } of dates) {
|
||||
const callee = ts.isNewExpression(call)
|
||||
? call.expression
|
||||
: ts.isPropertyAccessExpression(call.expression)
|
||||
? call.expression.expression
|
||||
: undefined;
|
||||
if (callee === undefined || !lowerer.isStdlibGlobal(callee, "Date")) continue;
|
||||
for (const literal of dateStringLiterals(checker, arg)) {
|
||||
const text = stringLiteralText(literal)!;
|
||||
if (nativeDateParses(text) || !/\d/.test(text)) continue;
|
||||
out.push(dateLiteralDivergenceDiag(text, locOf(literal)));
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The string literals a Date argument can be: the literal itself, or
|
||||
* each element of a const array of literals a for-of binding iterates. */
|
||||
function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Expression[] {
|
||||
if (stringLiteralText(arg) !== null) return [arg];
|
||||
if (!ts.isIdentifier(arg)) return [];
|
||||
const symbol = safely(() => checker.getSymbolAtLocation(arg));
|
||||
const decl = symbol === undefined ? undefined : symbol.valueDeclaration;
|
||||
const list = decl?.parent;
|
||||
const loop = list?.parent;
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
list === undefined ||
|
||||
loop === undefined ||
|
||||
!ts.isForOfStatement(loop) ||
|
||||
loop.initializer !== list
|
||||
)
|
||||
return [];
|
||||
let iterated: ts.Expression = loop.expression;
|
||||
if (ts.isIdentifier(iterated)) {
|
||||
const source = safely(() => checker.getSymbolAtLocation(iterated));
|
||||
const init = source === undefined ? undefined : source.valueDeclaration;
|
||||
if (init === undefined || !ts.isVariableDeclaration(init) || init.initializer === undefined)
|
||||
return [];
|
||||
if (init.parent === undefined || (init.parent.flags & ts.NodeFlags.Const) === 0) return [];
|
||||
iterated = init.initializer;
|
||||
}
|
||||
if (!ts.isArrayLiteralExpression(iterated)) return [];
|
||||
return iterated.elements.nodes.filter((e) => stringLiteralText(e) !== null);
|
||||
}
|
||||
|
||||
/** A checker query that fails here only loses a warning. */
|
||||
function safely<T>(f: () => T): T | undefined {
|
||||
try {
|
||||
@@ -870,20 +811,3 @@ function classExtends(
|
||||
function className(decl: ts.ClassLikeDeclaration): string {
|
||||
return decl.name?.text ?? "(anonymous class)";
|
||||
}
|
||||
|
||||
const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
||||
|
||||
/** The date-string grammar the native runtime accepts (scr_lib.c
|
||||
* scr_date_parse_get_time): ECMAScript's date-time format with an explicit
|
||||
* offset (date-only forms are UTC) and the certificate-time shape
|
||||
* "Mon DD HH:MM:SS YYYY GMT". Everything else parses to NaN natively. */
|
||||
export function nativeDateParses(text: string): boolean {
|
||||
const iso =
|
||||
/^(?:[+-]\d{6}|\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.test(text) ||
|
||||
/^(?:[+-]\d{6}|\d{4})-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
|
||||
text,
|
||||
);
|
||||
if (iso) return true;
|
||||
const cert = /^([A-Za-z]{3}) {1,2}\d{1,2} \d{2}:\d{2}:\d{2} \d{4} GMT$/.exec(text);
|
||||
return cert !== null && MONTHS.includes(cert[1]!.toLowerCase());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { isClassOwnEnumerableFieldName } from "../../ir/ir.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
|
||||
type ErrorClassLink = Pick<ClassInfo, "decl" | "ctor" | "paramProps"> & {
|
||||
base: ErrorClassLink | null;
|
||||
};
|
||||
|
||||
/** The property key a member or element-access name spells statically:
|
||||
* an identifier, a quoted string, a number in its canonical spelling, or a
|
||||
* computed string literal. Null for other keys. */
|
||||
function staticKeyText(name: ts.Node): string | null {
|
||||
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
||||
if (ts.isNumericLiteral(name)) return String(Number(name.text));
|
||||
if (ts.isComputedPropertyName(name)) return staticKeyText(name.expression);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether `node` is an assignment to `this.<name>` or `this["<name>"]`,
|
||||
* and the name. */
|
||||
function thisAssignmentName(node: ts.Node): string | null {
|
||||
if (!ts.isBinaryExpression(node) || node.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
||||
return null;
|
||||
const target = node.left;
|
||||
if (ts.isPropertyAccessExpression(target) && target.expression.kind === ts.SyntaxKind.ThisKeyword)
|
||||
return ts.isIdentifier(target.name) ? target.name.text : null;
|
||||
if (ts.isElementAccessExpression(target) && target.expression.kind === ts.SyntaxKind.ThisKeyword)
|
||||
return staticKeyText(target.argumentExpression);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Where an Error subclass instance's own `name` sits among its fields when
|
||||
* a class of its chain gives it one during construction. A property's
|
||||
* position is fixed when it is created: a `name` field takes its place
|
||||
* among the fields (parameter properties first, then the declared ones),
|
||||
* and `this.name = ...` in a constructor follows the fields created so far,
|
||||
* including the ones that constructor created before it. The answer is the
|
||||
* field `name` follows, null when it precedes every field, and undefined
|
||||
* when no class of the chain sets it during construction. `fields` are the
|
||||
* instance's own enumerable fields. */
|
||||
export function errorNameAfterField(
|
||||
info: ErrorClassLink,
|
||||
fields: ReadonlySet<string>,
|
||||
): string | null | undefined {
|
||||
const chain: ErrorClassLink[] = [];
|
||||
for (let current: ErrorClassLink | null | undefined = info; current; current = current.base)
|
||||
if (current.decl) chain.unshift(current);
|
||||
const created = new Set<string>();
|
||||
let last: string | null = null;
|
||||
const create = (name: string): void => {
|
||||
if (!fields.has(name) || created.has(name)) return;
|
||||
created.add(name);
|
||||
last = name;
|
||||
};
|
||||
for (const link of chain) {
|
||||
for (const property of link.paramProps ?? []) create(property.name);
|
||||
for (const member of link.decl!.members.nodes) {
|
||||
if (!ts.isPropertyDeclaration(member)) continue;
|
||||
const key = staticKeyText(member.name);
|
||||
if (key === null) continue;
|
||||
if (
|
||||
member.modifiers?.nodes.some(
|
||||
(m) => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.DeclareKeyword,
|
||||
)
|
||||
)
|
||||
continue;
|
||||
if (key === "name") return last;
|
||||
create(key);
|
||||
}
|
||||
const body = link.ctor?.body;
|
||||
if (!body) continue;
|
||||
let found = false;
|
||||
ts.walkPreorder(body, (node) => {
|
||||
if (ts.isTypeNode(node)) return "skip";
|
||||
if (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return "skip";
|
||||
const name = thisAssignmentName(node);
|
||||
if (name === "name") {
|
||||
found = true;
|
||||
return "stop";
|
||||
}
|
||||
if (name !== null) create(name);
|
||||
return undefined;
|
||||
});
|
||||
if (found) return last;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** The fields an Error subclass's own-property view lists. */
|
||||
export function errorViewFields(fields: readonly { name: string }[]): Set<string> {
|
||||
return new Set(
|
||||
fields
|
||||
.map((field) => field.name)
|
||||
.filter(
|
||||
(name) => isClassOwnEnumerableFieldName(name) && name !== "name" && name !== "message",
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import { generatorDrain } from "../iterator-adapters.js";
|
||||
import { isSearchParamsIterable } from "../builtins/url.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { arrayValueStore } from "../array-values.js";
|
||||
import { classGeneratorIterable } from "../class-iterables.js";
|
||||
|
||||
/** JavaScript: whether `expr` is or holds an existing checked-dynamic
|
||||
* object — a binding stored as a dyn value whose checker type is an
|
||||
@@ -431,6 +432,9 @@ export function lowerArrayLiteral(
|
||||
loc: locOf(el),
|
||||
};
|
||||
}
|
||||
// `[...obj]` over a class whose [Symbol.iterator] is a generator
|
||||
// method spreads the generator that method returns.
|
||||
src = classGeneratorIterable(lowerer, src) ?? src;
|
||||
// `[...it]` over a generator (or an iterator adapted into one):
|
||||
// drain it once, in order, into a fresh element array.
|
||||
if (src.type.kind === "generator") {
|
||||
|
||||
@@ -21,21 +21,30 @@ import {
|
||||
import { lowerNodeModuleIdentifier } from "../lower-node-module.js";
|
||||
import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js";
|
||||
import { captureContextArguments } from "../function-context.js";
|
||||
import { isCjsExportTableLiteral, isJsSourceFile, isNodeEsmFile, locOf } from "../../program.js";
|
||||
import {
|
||||
isCjsExportTableLiteral,
|
||||
isCjsJsFile,
|
||||
isJsSourceFile,
|
||||
isNodeEsmFile,
|
||||
locOf,
|
||||
} from "../../program.js";
|
||||
import { builtinClassAliasOwnsNoSlot, runtimeClassValueOf } from "../builtins/class-values.js";
|
||||
import {
|
||||
cjsAccessorCall,
|
||||
cjsAccessorSnapshotOf,
|
||||
primitiveCtorClosure,
|
||||
runtimeOptionalElementKey,
|
||||
runtimeOptionalUseOf,
|
||||
} from "../lower-exprs.js";
|
||||
import { runtimeOptionalReceiverRead } from "./narrowing.js";
|
||||
import { builtinGlobalValueAt } from "./property-access.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import {
|
||||
ambientUndefReadType,
|
||||
ambientUndefinedFnSymbolOf,
|
||||
contextualUndefReadType,
|
||||
fenceEarlyAliasUse,
|
||||
isTemporalGlobalRoot,
|
||||
lowerNsIdentifierValue,
|
||||
nsUndefRead,
|
||||
} from "../lower-namespaces.js";
|
||||
@@ -535,7 +544,12 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
return builtinFunctionFenceValue(lowerer, bi, expr, loc);
|
||||
return {
|
||||
kind: "strLit",
|
||||
value: `[builtin ${bi.module}.${bi.member}]`,
|
||||
// node:stream/web exports the web stream globals themselves:
|
||||
// the same identity token as the global spelling.
|
||||
value:
|
||||
bi.module === "stream/web"
|
||||
? `[builtin ${bi.member}]`
|
||||
: `[builtin ${bi.module}.${bi.member}]`,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
@@ -715,9 +729,26 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
|
||||
}
|
||||
{
|
||||
const sym = lowerer.checker.getSymbolAtLocation(expr);
|
||||
// The temporal global is typed (TS 7's lib slice) but Node 24 does
|
||||
// not define it: the read throws Node's ReferenceError.
|
||||
if (isTemporalGlobalRoot(lowerer, expr, sym)) {
|
||||
const t =
|
||||
ambientUndefReadType(lowerer, expr) ?? contextualUndefReadType(lowerer, expr) ?? F64;
|
||||
return nsUndefRead(lowerer, expr.text, expr, t);
|
||||
}
|
||||
if (lowerer.isStdlibSymbol(sym) || expr.text === "globalThis") {
|
||||
const canonical = stdlibGlobalNameOf(lowerer, expr) ?? expr.text;
|
||||
// Builtin constructors have real function values in both source
|
||||
// languages (TypeScript reads them as dynamic values).
|
||||
const builtinValue = builtinGlobalValueAt(
|
||||
lowerer,
|
||||
expr,
|
||||
canonical,
|
||||
loc,
|
||||
isJsSourceFile(expr.getSourceFile()),
|
||||
);
|
||||
if (builtinValue !== null) return builtinValue;
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
const canonical = stdlibGlobalNameOf(lowerer, expr) ?? expr.text;
|
||||
return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc };
|
||||
}
|
||||
// The families with a WHY: each hint states what makes the
|
||||
@@ -930,5 +961,18 @@ function cjsExportAccessorRead(lowerer: Lowerer, ident: ts.Identifier): IrExpr |
|
||||
if (!ts.isObjectLiteralExpression(getter.parent) || !isCjsExportTableLiteral(getter.parent)) {
|
||||
return null;
|
||||
}
|
||||
// An ES-module import binding reads the value Node copied when the
|
||||
// importer linked the CommonJS module (cjsAccessorSnapshotStores).
|
||||
const raw = lowerer.checker.getSymbolAtLocation(ident);
|
||||
const binding = raw ? raw.declarations[0] : undefined;
|
||||
if (
|
||||
binding !== undefined &&
|
||||
(ts.isImportSpecifier(binding) || ts.isExportSpecifier(binding)) &&
|
||||
!isCjsJsFile(binding.getSourceFile(), lowerer.program)
|
||||
) {
|
||||
const snapshot = cjsAccessorSnapshotOf(lowerer, getter);
|
||||
if (snapshot !== null)
|
||||
return { kind: "varRef", localId: snapshot.id, type: snapshot.type, loc: locOf(ident) };
|
||||
}
|
||||
return cjsAccessorCall(lowerer, getter, locOf(ident));
|
||||
}
|
||||
|
||||
@@ -640,6 +640,74 @@ function literalUnionArmOf(
|
||||
/** Object spread treats nullish sources as empty. Preserve evaluation once,
|
||||
* then turn `Record<K, V> | undefined` into either that record or a fresh
|
||||
* empty record of the same represented shape. */
|
||||
/** `{ ...unknownValue }` into a pure index-signature record (headers
|
||||
* forwarded from an untyped parameter): Node copies the source's own
|
||||
* enumerable keys in order, and spreads nothing for null, undefined,
|
||||
* numbers and booleans. An object source converts through the checked
|
||||
* extraction into the target's own shape, which keeps its keys and order
|
||||
* and checks each value against the slot type (a value outside it throws
|
||||
* the catchable TypeError of every unknown-to-typed conversion). A string
|
||||
* source takes the same extraction, which rejects it, rather than
|
||||
* silently dropping the characters Node would spread. Null for other
|
||||
* sources and targets. */
|
||||
function dynIndexSpreadSource(
|
||||
lowerer: Lowerer,
|
||||
source: IrExpr,
|
||||
target: IrType,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (source.type.kind !== "dyn" || target.kind !== "record") return null;
|
||||
const shape = lowerer.shapes.get(target.shapeId);
|
||||
if (!shape?.indexValue || shape.fields.length > 0 || shape.tuple) return null;
|
||||
if (
|
||||
!canDynCheckTo(
|
||||
target,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const local = lowerer.declareHiddenLocal("%dynSpread", DYN);
|
||||
const ref = varRef(local.id, DYN, loc);
|
||||
const test = (kind: "object" | "string" | "nullish"): IrExpr => ({
|
||||
kind: "dynTest",
|
||||
test: kind,
|
||||
value: ref,
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const extract: IrExpr = { kind: "dynCheck", value: ref, type: target, loc };
|
||||
const empty: IrExpr = { kind: "recordLit", fields: [], type: target, loc };
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: source, loc }],
|
||||
result: {
|
||||
kind: "ternary",
|
||||
cond: test("nullish"),
|
||||
then: empty,
|
||||
else_: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: test("object"),
|
||||
right: test("string"),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: extract,
|
||||
else_: empty,
|
||||
type: target,
|
||||
loc,
|
||||
},
|
||||
type: target,
|
||||
loc,
|
||||
},
|
||||
type: target,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function lowerOptionalIndexSpreadSource(
|
||||
lowerer: Lowerer,
|
||||
source: IrExpr,
|
||||
@@ -1372,7 +1440,7 @@ export function lowerObjectLiteral(
|
||||
vNode,
|
||||
);
|
||||
contributors.push({
|
||||
kind: "field",
|
||||
kind: "condField",
|
||||
name: csProp.name.text,
|
||||
value: {
|
||||
kind: "ternary",
|
||||
@@ -1387,6 +1455,7 @@ export function lowerObjectLiteral(
|
||||
}
|
||||
let src = lowerer.lowerExpr(prop.expression);
|
||||
src = lowerOptionalIndexSpreadSource(lowerer, src, locOf(prop)) ?? src;
|
||||
src = dynIndexSpreadSource(lowerer, src, type, locOf(prop)) ?? src;
|
||||
if (src.type.kind !== "record") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1631,7 +1700,11 @@ export function lowerObjectLiteral(
|
||||
}
|
||||
shapeMismatch(prop);
|
||||
}
|
||||
const absent = lowerer.absentFieldValue(fieldType, locOf(prop));
|
||||
// An 'unknown' slot holds the checked-dynamic undefined for the
|
||||
// empty arm, the absent-property read the width copy uses too.
|
||||
const absent =
|
||||
lowerer.absentFieldValue(fieldType, locOf(prop)) ??
|
||||
(fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null);
|
||||
if (!absent) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -2559,6 +2632,14 @@ export function andSpreadOf(
|
||||
while (ts.isParenthesizedExpression(right)) right = right.expression;
|
||||
if (!ts.isObjectLiteralExpression(right)) return null;
|
||||
const spreadType = lowerer.mapTypeOf(lowerer.typeOf(e));
|
||||
// Every falsy value is a primitive, which spreads nothing, so `c && {
|
||||
// ... }` contributes the literal's members exactly when c is truthy,
|
||||
// whatever c's type. The checker's union of the two decides the
|
||||
// representation where it maps; where it does not (a member typed by a
|
||||
// checked-dynamic value such as an AbortSignal), the syntax decides.
|
||||
if (spreadType === null || spreadType.kind === "dyn") {
|
||||
return syntacticAndSpread(right, e.left);
|
||||
}
|
||||
if (spreadType?.kind !== "union") return null;
|
||||
const arms = lowerer.unions.get(spreadType.unionId)?.arms ?? [];
|
||||
if (
|
||||
@@ -2590,6 +2671,26 @@ export function andSpreadOf(
|
||||
return { cond: e.left, props, whenTrue: true };
|
||||
}
|
||||
|
||||
function syntacticAndSpread(
|
||||
right: ts.ObjectLiteralExpression,
|
||||
cond: ts.Expression,
|
||||
): { cond: ts.Expression; props: CondSpreadProp[]; whenTrue: boolean } | "unsupported" {
|
||||
if (right.properties.nodes.length === 0) return "unsupported";
|
||||
const props: CondSpreadProp[] = [];
|
||||
for (const p of right.properties.nodes) {
|
||||
if (
|
||||
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
||||
p.name &&
|
||||
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
|
||||
) {
|
||||
props.push(p as CondSpreadProp);
|
||||
} else {
|
||||
return "unsupported";
|
||||
}
|
||||
}
|
||||
return { cond, props, whenTrue: true };
|
||||
}
|
||||
|
||||
/** A conditional spread's carrier property: `name: value` or shorthand,
|
||||
* with an identifier/string-literal name. */
|
||||
export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & {
|
||||
|
||||
@@ -96,6 +96,7 @@ import {
|
||||
lowerRequireMainProperty,
|
||||
} from "../lower-node-module.js";
|
||||
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import {
|
||||
cjsClassExprWholeExportOf,
|
||||
@@ -741,6 +742,12 @@ export function lowerPropertyAccessExpr(
|
||||
lowerStreamObjectProperty(lowerer, expr) ??
|
||||
lowerEventObjectProperty(lowerer, expr) ??
|
||||
lowerer.lowerUnionProperty(expr) ??
|
||||
// A builtin constructor reached through another binding (`const D =
|
||||
// Date; D.prototype`) holds its interned function value, which
|
||||
// models only name, length and constructor: a static member or the
|
||||
// prototype that no lowering above claimed keeps the SC2020 fence
|
||||
// rather than reading undefined through the field and dynamic reads.
|
||||
fenceBuiltinConstructorStaticRead(lowerer, expr) ??
|
||||
lowerer.lowerFieldRead(expr);
|
||||
// A union-typed field read narrows like an identifier when the
|
||||
// checker has narrowed this use to one arm.
|
||||
@@ -775,6 +782,8 @@ export function lowerPropertyAccessExpr(
|
||||
const canonical = stdlibGlobalNameOf(lowerer, expr);
|
||||
if (canonical !== null) {
|
||||
if (canonical === "globalThis") return lowerGlobalValue(lowerer, expr);
|
||||
// Typed for the programs that name it, absent in Node 24.
|
||||
if (canonical === TEMPORAL_GLOBAL) return dynUndefinedExpr(loc);
|
||||
if (canonical === "ArrayBuffer")
|
||||
return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc };
|
||||
if (canonical === "fetch")
|
||||
@@ -787,7 +796,14 @@ export function lowerPropertyAccessExpr(
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc };
|
||||
return (
|
||||
builtinGlobalValueAt(lowerer, expr, canonical, loc, true) ?? {
|
||||
kind: "strLit",
|
||||
value: `[builtin ${canonical}]`,
|
||||
type: STRING,
|
||||
loc,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// A JS receiver whose CHECKER type has no mapping (`mustCallChecks
|
||||
@@ -866,6 +882,43 @@ export function lowerPropertyAccessExpr(
|
||||
};
|
||||
}
|
||||
}
|
||||
// A plain record's inherited Object.prototype members READ as values
|
||||
// (`o.constructor`, `o.valueOf`, `({}).toString`, the isPlainObject and
|
||||
// deep-equal idioms): the record boxes into its dynamic view, whose
|
||||
// read answers the interned Object constructor and Object.prototype
|
||||
// methods — the same values `Object` and `Object.prototype.valueOf`
|
||||
// read, so identity comparisons are exact. Calls keep their own
|
||||
// lowerings; a record declaring the name reads its field.
|
||||
if (
|
||||
OBJECT_PROTOTYPE_VALUE_READS.has(expr.name.text) &&
|
||||
!isCalleeOf(expr) &&
|
||||
// `X.prototype.toString` keeps the builtin prototype-method lowering.
|
||||
!(ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "prototype")
|
||||
) {
|
||||
const receiver = tryLowerExpression(lowerer, expr.expression);
|
||||
const name = expr.name.text;
|
||||
const shape =
|
||||
receiver?.type.kind === "record" ? lowerer.shapes.get(receiver.type.shapeId) : undefined;
|
||||
const info =
|
||||
receiver?.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined;
|
||||
const inherits =
|
||||
receiver?.type.kind === "dyn" ||
|
||||
(shape !== undefined && !shape.tuple && !shape.fields.some((field) => field.name === name)) ||
|
||||
(info !== undefined &&
|
||||
name !== "constructor" &&
|
||||
!info.fields.has(name) &&
|
||||
!lowerer.findMethodOn(info, name) &&
|
||||
!lowerer.findMethodOn(info, `get:${name}`));
|
||||
if (receiver !== null && receiver !== undefined && inherits) {
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(receiver, DYN),
|
||||
key: { kind: "strLit", value: name, type: STRING, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
// `f.name` / `f.length` / own properties on a FUNCTION-typed JS
|
||||
// value (the mustCall wrapper's function-instance members): read
|
||||
// through the dyn box — the closure's own-property table answers
|
||||
@@ -1255,6 +1308,8 @@ export function lowerPropertyAccessExpr(
|
||||
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
return mapped && mapped.kind !== "void" ? lowerer.coerceInto(expr, read, mapped) : read;
|
||||
}
|
||||
const asserted = assertedMemberRead(lowerer, expr, recvLowered, loc);
|
||||
if (asserted) return asserted;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
@@ -1262,6 +1317,101 @@ export function lowerPropertyAccessExpr(
|
||||
);
|
||||
}
|
||||
|
||||
/** `(source as { repoName: string }).repoName` where the assertion names a
|
||||
* member some arms of the stored union of records do not declare: Node
|
||||
* reads the property off whichever object it is, so arms that declare it
|
||||
* answer their slot, and the others answer undefined (native records are
|
||||
* closed, so they cannot hold it). The result keeps that undefined arm
|
||||
* whatever the assertion claims; a consumer that needs the asserted type
|
||||
* checks it like any other narrowing. The arms that declare the member
|
||||
* must agree on its type. Null when the receiver is not such an
|
||||
* assertion over records, or no arm declares the member. */
|
||||
function assertedMemberRead(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
recv: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
let receiverNode: ts.Expression = expr.expression;
|
||||
while (ts.isParenthesizedExpression(receiverNode)) receiverNode = receiverNode.expression;
|
||||
if (!ts.isAsExpression(receiverNode) && !ts.isTypeAssertion(receiverNode)) return null;
|
||||
if (recv.type.kind !== "union") return null;
|
||||
const unionId = recv.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms ?? [];
|
||||
const name = expr.name.text;
|
||||
let resultType: IrType | null = null;
|
||||
let records = 0;
|
||||
const declaring: { tag: number; shapeId: string; type: IrType }[] = [];
|
||||
const units: number[] = [];
|
||||
for (let tag = 0; tag < arms.length; tag++) {
|
||||
const arm = arms[tag]!;
|
||||
if (isUnitType(arm)) {
|
||||
units.push(tag);
|
||||
continue;
|
||||
}
|
||||
if (arm.kind !== "record") return null;
|
||||
records++;
|
||||
const shape = lowerer.shapes.get(arm.shapeId);
|
||||
if (!shape || shape.tuple || shape.indexValue) return null;
|
||||
const field = shape.fields.find((f) => f.name === name);
|
||||
if (!field) continue;
|
||||
// The declaring arms agree up to an undefined arm (`string` beside
|
||||
// `string | undefined`).
|
||||
const armResult = lowerer.withUndefinedArmOf(field.type);
|
||||
if (!armResult || (resultType && !typeEquals(resultType, armResult))) return null;
|
||||
resultType = armResult;
|
||||
declaring.push({ tag, shapeId: arm.shapeId, type: field.type });
|
||||
}
|
||||
if (!resultType || declaring.length === records) return null;
|
||||
const stable = lowerer.declareHiddenLocal("%assertedRecv", recv.type);
|
||||
const ref = (): IrExpr => ({ kind: "varRef", localId: stable.id, type: recv.type, loc });
|
||||
let result: IrExpr | null = lowerer.wrappedUndefined(resultType, loc);
|
||||
if (!result) return null;
|
||||
// A unit arm the checker narrowed away reads like Node would read it.
|
||||
for (const tag of units) {
|
||||
const unit = arms[tag]!.kind === "nullT" ? "null" : "undefined";
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: ref(), type: BOOL, loc },
|
||||
then: nodeThrowExpr(
|
||||
1,
|
||||
"",
|
||||
`Cannot read properties of ${unit} (reading '${name}')`,
|
||||
resultType,
|
||||
loc,
|
||||
),
|
||||
else_: result,
|
||||
type: resultType,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
for (const { tag, shapeId, type: fieldType } of declaring) {
|
||||
const read: IrExpr = {
|
||||
kind: "recordGet",
|
||||
obj: { kind: "unionNarrow", unionId, tag, value: ref(), type: arms[tag]!, loc },
|
||||
shapeId,
|
||||
field: name,
|
||||
type: fieldType,
|
||||
loc,
|
||||
};
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "unionIsTag", unionId, tag, negated: false, value: ref(), type: BOOL, loc },
|
||||
then: lowerer.coerceToExpected(read, resultType),
|
||||
else_: result,
|
||||
type: resultType,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: stable.id, init: recv, loc }],
|
||||
result,
|
||||
type: resultType,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function importMetaProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
|
||||
return isImportMeta(expr.expression) ? importMetaField(lowerer, expr, expr.name.text) : null;
|
||||
}
|
||||
@@ -1305,6 +1455,13 @@ export function lowerIntrinsicProperty(
|
||||
return { kind: "dynFrom", value, fnName: "ownKeys", type: DYN, loc: locOf(expr) };
|
||||
}
|
||||
if (lowerer.chainBlocked(expr)) return null;
|
||||
// Object.prototype.constructor: the Object constructor value.
|
||||
if (
|
||||
expr.name.text === "constructor" &&
|
||||
ts.isPropertyAccessExpression(expr.expression) &&
|
||||
lowerer.stdlibGlobalMember(expr.expression, "Object") === "prototype"
|
||||
)
|
||||
return builtinGlobalValue("Object", locOf(expr));
|
||||
const builtinPrototype = builtinPrototypeMethod(lowerer, expr);
|
||||
if (builtinPrototype)
|
||||
return {
|
||||
@@ -2584,6 +2741,27 @@ export function lowerFieldRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressi
|
||||
loc: locOf(expr),
|
||||
};
|
||||
}
|
||||
// A member the checker declares on the receiver, stored in a pure
|
||||
// index-signature record (an object literal that spread one, see
|
||||
// dictSpreadLiteralShape): the key is always present, so the keyed
|
||||
// read answers the member through the slot type.
|
||||
if (
|
||||
actual?.type.kind === "record" &&
|
||||
shape.fields.length === 0 &&
|
||||
declaredMemberOf(lowerer, expr.expression, expr.name.text)
|
||||
) {
|
||||
return lowerer.maybeNarrow(
|
||||
{
|
||||
kind: "recordKeyGet",
|
||||
obj: actual,
|
||||
shapeId: actual.type.shapeId,
|
||||
key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
|
||||
type: shape.indexValue,
|
||||
loc: locOf(expr),
|
||||
},
|
||||
expr,
|
||||
);
|
||||
}
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr,
|
||||
@@ -3557,6 +3735,223 @@ function lowerStreamObjectProperty(
|
||||
return lowerStreamProperty(lowerer, expr, info);
|
||||
}
|
||||
|
||||
/** The builtin constructors JavaScript sources read as real function
|
||||
* values rather than identity tokens: the runtime interns one function per
|
||||
* constructor, and dynamic values answer `.constructor` with the same one,
|
||||
* so `o.constructor === Object` is an exact identity test. */
|
||||
const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet<string> = new Set([
|
||||
"Object",
|
||||
"Function",
|
||||
"Date",
|
||||
"RegExp",
|
||||
"Map",
|
||||
"Set",
|
||||
"Promise",
|
||||
"WeakMap",
|
||||
"WeakSet",
|
||||
]);
|
||||
|
||||
/** The builtin global functions JavaScript sources read as interned
|
||||
* function values (the `cond ? setImmediate : process.nextTick` feature
|
||||
* probe of bcryptjs and async libraries): typeof answers "function",
|
||||
* identity holds across spellings, and calls run the timer APIs. */
|
||||
const GLOBAL_FUNCTION_VALUES: ReadonlySet<string> = new Set([
|
||||
"setTimeout",
|
||||
"setInterval",
|
||||
"setImmediate",
|
||||
"clearTimeout",
|
||||
"clearInterval",
|
||||
"clearImmediate",
|
||||
"queueMicrotask",
|
||||
]);
|
||||
|
||||
/** A builtin function value: one interned runtime function per name. */
|
||||
export function builtinFunctionValue(name: string, loc: SrcLoc): IrExpr {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.builtinFunction",
|
||||
args: [{ kind: "strLit", value: name, type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The value a source reads for the stdlib global `canonical` (builtin
|
||||
* constructors in both languages, builtin functions in JavaScript), or
|
||||
* null when it stays an identity token (JavaScript) or fenced. */
|
||||
export function builtinGlobalValue(canonical: string, loc: SrcLoc, js = false): IrExpr | null {
|
||||
if (js && GLOBAL_FUNCTION_VALUES.has(canonical)) return builtinFunctionValue(canonical, loc);
|
||||
if (GLOBAL_CONSTRUCTOR_VALUES.has(canonical)) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "dyn.globalConstructor",
|
||||
args: [{ kind: "strLit", value: canonical, type: STRING, loc }],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The members the interned builtin function values model (the runtime
|
||||
* answers each one as Node does); any other member read through them would
|
||||
* answer undefined, so it keeps its static lowering or its fence. */
|
||||
const BUILTIN_VALUE_MEMBERS: ReadonlySet<string> = new Set(["name", "length", "constructor"]);
|
||||
|
||||
/** The Object statics whose answers for an interned builtin function value
|
||||
* match Node's (identity, and the empty enumerable own-key lists). */
|
||||
const BUILTIN_VALUE_REFLECTION: ReadonlySet<string> = new Set(["is", "keys", "values", "entries"]);
|
||||
|
||||
/** The value `builtinGlobalValue` gives the stdlib global `expr` read at its
|
||||
* use site: null when the use reads the global's members (they keep their
|
||||
* lowerings and fences), and SC2020 when it is the target of reflection or
|
||||
* redefinition (`Object.hasOwn(Object, "hasOwn")`,
|
||||
* `Object.defineProperty(Promise, ...)`, `hasOwnProperty.call(Map, ...)`),
|
||||
* which would observe or change members the interned value does not
|
||||
* model. */
|
||||
export function builtinGlobalValueAt(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
canonical: string,
|
||||
loc: SrcLoc,
|
||||
js: boolean,
|
||||
): IrExpr | null {
|
||||
if (readsBuiltinGlobalMembers(expr)) return null;
|
||||
const value = builtinGlobalValue(canonical, loc, js);
|
||||
if (value === null) return null;
|
||||
let node: ts.Node = expr;
|
||||
while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const call = node.parent;
|
||||
if (
|
||||
call !== undefined &&
|
||||
ts.isCallExpression(call) &&
|
||||
call.expression !== node &&
|
||||
ts.isPropertyAccessExpression(call.expression)
|
||||
) {
|
||||
const callee = call.expression;
|
||||
const owner = stdlibGlobalNameOf(lowerer, callee.expression);
|
||||
const reflects =
|
||||
owner === "Reflect" ||
|
||||
(owner === "Object" && !BUILTIN_VALUE_REFLECTION.has(callee.name.text)) ||
|
||||
((callee.name.text === "call" || callee.name.text === "apply") &&
|
||||
call.arguments.nodes[0] === node &&
|
||||
lowerer.isStdlibSymbol(
|
||||
lowerer.checker.getSymbolAtLocation(
|
||||
ts.isPropertyAccessExpression(callee.expression)
|
||||
? callee.expression.name
|
||||
: callee.expression,
|
||||
),
|
||||
));
|
||||
if (reflects) {
|
||||
const name =
|
||||
owner !== null ? `${owner}.${callee.name.text}` : callee.getText().replace(/\s+/g, "");
|
||||
lowerer.noLowering(
|
||||
`${name}(${canonical}, ...)`,
|
||||
expr,
|
||||
"builtin constructor and function values model only name, length and constructor, so reflecting on or redefining their members has no lowering",
|
||||
);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Whether the stdlib global `expr` is read for its members (`Map.prototype`,
|
||||
* `Date["UTC"]`, `"prototype" in Map`) rather than as a value. Those reads
|
||||
* keep the global's member lowerings and fences: its interned function value
|
||||
* models only BUILTIN_VALUE_MEMBERS. */
|
||||
function readsBuiltinGlobalMembers(expr: ts.Expression): boolean {
|
||||
let node: ts.Node = expr;
|
||||
while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (parent === undefined) return false;
|
||||
if (ts.isPropertyAccessExpression(parent) && parent.expression === node)
|
||||
return !BUILTIN_VALUE_MEMBERS.has(parent.name.text);
|
||||
if (ts.isElementAccessExpression(parent) && parent.expression === node) return true;
|
||||
return (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
parent.operatorToken.kind === ts.SyntaxKind.InKeyword &&
|
||||
parent.right === node
|
||||
);
|
||||
}
|
||||
|
||||
/** Reports SC2020 when `access` reads a stdlib static member or the
|
||||
* prototype of a builtin constructor with an interned function value through
|
||||
* a receiver the checker types as the constructor's static side
|
||||
* (`MapConstructor`); null otherwise. Calls that lower statically never
|
||||
* reach the member read. */
|
||||
function fenceBuiltinConstructorStaticRead(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): null {
|
||||
if (BUILTIN_VALUE_MEMBERS.has(access.name.text)) return null;
|
||||
const symbol = lowerer.typeOf(access.expression).getSymbol();
|
||||
if (
|
||||
symbol !== undefined &&
|
||||
symbol.name.endsWith("Constructor") &&
|
||||
GLOBAL_CONSTRUCTOR_VALUES.has(symbol.name.slice(0, -"Constructor".length)) &&
|
||||
lowerer.isStdlibSymbol(symbol)
|
||||
)
|
||||
lowerer.stdlibMemberFence(access);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Object.prototype members a plain record inherits, read as values. */
|
||||
const OBJECT_PROTOTYPE_VALUE_READS: ReadonlySet<string> = new Set([
|
||||
"constructor",
|
||||
"valueOf",
|
||||
"toString",
|
||||
"hasOwnProperty",
|
||||
"propertyIsEnumerable",
|
||||
]);
|
||||
|
||||
/** Whether `expr` is the callee of a call (`o.toString()`), which keeps its
|
||||
* call lowering rather than reading the member as a value. */
|
||||
function isCalleeOf(expr: ts.PropertyAccessExpression): boolean {
|
||||
let node: ts.Node = expr;
|
||||
while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
return (
|
||||
parent !== undefined &&
|
||||
(ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
|
||||
parent.expression === node
|
||||
);
|
||||
}
|
||||
|
||||
/** The constructor a primitive dynamic value answers for `.constructor`:
|
||||
* the same interned closure the `Number`/`String`/`Boolean`/`BigInt`
|
||||
* identifiers read (dequal's `(ctor = foo.constructor) === bar.constructor`
|
||||
* over numbers), or `otherwise` for non-primitives. `value` must be safe to
|
||||
* read repeatedly. */
|
||||
export function primitiveConstructorOr(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
otherwise: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
let result = otherwise;
|
||||
for (const [test, name] of [
|
||||
["bigint", "BigInt"],
|
||||
["boolean", "Boolean"],
|
||||
["string", "String"],
|
||||
["number", "Number"],
|
||||
] as const) {
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: { kind: "dynTest", test, value, type: BOOL, loc },
|
||||
then: {
|
||||
kind: "dynFrom",
|
||||
value: primitiveCtorClosure(lowerer, name, loc, true),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
else_: result,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** True when `access` reads a member that JavaScript sources attach to a
|
||||
* function by assignment (`fn.member = value`): the receiver's type is
|
||||
* callable and every declaration of the member is such an assignment in a
|
||||
@@ -3581,3 +3976,10 @@ function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpr
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether the checker declares `name` as a property of `receiver`'s type
|
||||
* (as opposed to reading it through an index signature). */
|
||||
export function declaredMemberOf(lowerer: Lowerer, receiver: ts.Expression, name: string): boolean {
|
||||
const symbol = lowerer.checker.getPropertyOfType(lowerer.typeOf(receiver), name);
|
||||
return !!symbol && symbol.declarations.length > 0;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ import { isSafeToDiscard, isSafeToRepeat } from "./evaluation-safety.js";
|
||||
import { varRef } from "../../../ir/build.js";
|
||||
import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors.js";
|
||||
import { InternalCompilerError } from "../../../errors.js";
|
||||
import { TEMPORAL_GLOBAL } from "../../builtin-modules.js";
|
||||
import { isTemporalGlobalRoot } from "../lower-namespaces.js";
|
||||
|
||||
export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc: SrcLoc): IrExpr {
|
||||
const webAssembly = lowerWebAssemblyTypeof(lowerer, expr.expression);
|
||||
@@ -158,6 +160,15 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
|
||||
// declarations type some (AbortSignal) without a construct signature,
|
||||
// so neither the operand nor the type can answer; the read has no
|
||||
// effects.
|
||||
// `typeof globalThis.<temporal>`: the global object has no such
|
||||
// property in Node 24.
|
||||
if (
|
||||
ts.isPropertyAccessExpression(tested) &&
|
||||
tested.name.text === TEMPORAL_GLOBAL &&
|
||||
stdlibGlobalNameOf(lowerer, tested.expression) === "globalThis"
|
||||
) {
|
||||
return { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
}
|
||||
if (constructorGlobalTypeof(lowerer, tested) !== null)
|
||||
return { kind: "strLit", value: "function", type: STRING, loc };
|
||||
// `typeof globalThis.AbortSignal.any` (ky's feature probe): a static
|
||||
@@ -182,6 +193,11 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
|
||||
if (!sym) {
|
||||
return { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
}
|
||||
// Node 24 defines no temporal global: typeof answers "undefined"
|
||||
// without reading it, like any undeclared name.
|
||||
if (isTemporalGlobalRoot(lowerer, tested, sym)) {
|
||||
return { kind: "strLit", value: "undefined", type: STRING, loc };
|
||||
}
|
||||
// `const { inspect } = require("util")` / `import { inspect } from
|
||||
// "node:util"`: a binding of a builtin module's exported function,
|
||||
// class or object. JavaScript sources represent such a value without
|
||||
|
||||
@@ -248,6 +248,8 @@ import {
|
||||
genericCallInstance,
|
||||
} from "./generic-functions.js";
|
||||
|
||||
import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js";
|
||||
|
||||
const ZLIB_STREAM_METHODS: ReadonlySet<string> = new Set(["flush", "params", "close", "reset"]);
|
||||
|
||||
export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
@@ -268,6 +270,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
const json = lowerJsonRequireCall(lowerer, expr);
|
||||
if (json) return json;
|
||||
|
||||
// eval(...) and Function(...): Node's EvalError under
|
||||
// --disallow-code-generation-from-strings (code-generation.ts).
|
||||
{
|
||||
const codeGeneration = codeGenerationCalleeOf(lowerer, expr.expression);
|
||||
if (codeGeneration !== null) return lowerCodeGenerationCall(lowerer, expr, codeGeneration);
|
||||
}
|
||||
// Inline require() of a program module (cjs-inline-require.ts): a
|
||||
// member call over its export table resolves like a binding's member
|
||||
// call after the module's %init at this position; the require itself
|
||||
|
||||
@@ -16,6 +16,8 @@ import { InternalCompilerError } from "../../errors.js";
|
||||
* accessor access, upcasts, `new` expressions, and the builtin Error
|
||||
* hierarchy registration. */
|
||||
import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js";
|
||||
import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js";
|
||||
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js";
|
||||
import { everyStmtList } from "../../ir/traverse.js";
|
||||
@@ -70,6 +72,7 @@ import {
|
||||
} from "./instantiation-path.js";
|
||||
import { appendImplicitUndefinedReturn, hasExplicitJsDocReturn } from "./function-returns.js";
|
||||
import { bodyReadsArguments } from "../arguments-usage.js";
|
||||
import { errorNameAfterField, errorViewFields } from "./error-name-order.js";
|
||||
import {
|
||||
checkedJsDocumentedParameter,
|
||||
documentedParameter,
|
||||
@@ -144,6 +147,7 @@ import {
|
||||
ambientUndefReadType,
|
||||
ambientUndefVarRootOf,
|
||||
ambientUndefinedFnSymbolOf,
|
||||
contextualUndefReadType,
|
||||
fenceEarlyAliasUse,
|
||||
fenceEarlyNsMemberRef,
|
||||
nsMemberIdentOf,
|
||||
@@ -2213,11 +2217,7 @@ export function collectClassShapeInner(
|
||||
}
|
||||
if (lowerer.collectingExprClasses.has(decl)) {
|
||||
for (let parent = base; parent; parent = parent.base) {
|
||||
if (
|
||||
parent.builtinEmitter ||
|
||||
parent.builtinStream !== undefined ||
|
||||
parent.builtinEventTarget !== undefined
|
||||
) {
|
||||
if (parent.builtinEventTarget !== undefined) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
decl,
|
||||
@@ -4260,6 +4260,12 @@ export function collectClassShapeInner(
|
||||
...(deferredInitFields.size > 0 ? { deferredInitFields } : {}),
|
||||
...(escape.escapes ? { constructionEscapes: true as const } : {}),
|
||||
};
|
||||
let root: ClassInfo | null = info;
|
||||
while (root?.base) root = root.base;
|
||||
if (root !== info && root?.def.name === "%Error") {
|
||||
const after = errorNameAfterField(info, errorViewFields(info.def.fields));
|
||||
if (after !== undefined) info.def.errorNameAfter = after;
|
||||
}
|
||||
// GENERIC members get their declaring-class backlink now that the
|
||||
// info exists (instance lowering reads it for `this` typing and the
|
||||
// generic-class binding merge).
|
||||
@@ -5183,16 +5189,11 @@ export function classValueRef(lowerer: Lowerer, info: ClassInfo, blame: ts.Node)
|
||||
loc: locOf(blame),
|
||||
};
|
||||
}
|
||||
if (
|
||||
lowerer.inheritsBuiltinEmitterCtor(info) ||
|
||||
inheritsBuiltinStreamCtor(lowerer, info) ||
|
||||
eventTargetRootOf(info) !== null ||
|
||||
(() => {
|
||||
for (let c = info.base; c; c = c.base)
|
||||
if (c.builtinEmitter || c.builtinStream !== undefined) return true;
|
||||
return false;
|
||||
})()
|
||||
) {
|
||||
// Classes extending EventEmitter or a stream class construct through
|
||||
// their own (or synthesized) `%C.constructor`, whose super call runs the
|
||||
// runtime initializer, so the class object's construct thunk serves them
|
||||
// like any program class. EventTarget subclasses keep the refusal.
|
||||
if (eventTargetRootOf(info) !== null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
blame,
|
||||
@@ -8737,6 +8738,60 @@ function inheritsBuiltinStreamCtor(lowerer: Lowerer, info: ClassInfo): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The run-time form of refuseInheritedBuiltinCtorArgs, for construction
|
||||
* from checked-dynamic code (`new F(options)` with F an untyped value
|
||||
* holding C), whose argument count is known only to the caller: a
|
||||
* runtimeFence located at C, or null when C's constructor takes its
|
||||
* arguments. */
|
||||
export function inheritedBuiltinCtorFence(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
loc: SrcLoc,
|
||||
): IrStmt | null {
|
||||
let feature: string | null = null;
|
||||
if (inheritsBuiltinStreamCtor(lowerer, info))
|
||||
feature = `new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor (declare a constructor that passes an inline options object to super(...))`;
|
||||
else if (lowerer.inheritsBuiltinEmitterCtor(info))
|
||||
feature = "EventEmitter constructor options ('captureRejections')";
|
||||
if (feature === null) return null;
|
||||
// Located at the class, whose constructor takes no arguments.
|
||||
const at = info.decl !== null ? locOf(info.decl) : loc;
|
||||
const diag = unsupportedDiag("SC1090", at, feature);
|
||||
lowerer.runtimeFences.push(diag);
|
||||
const line =
|
||||
info.decl !== null
|
||||
? ts.getLineAndCharacterOfPosition(info.decl.getSourceFile(), at.start).line + 1
|
||||
: null;
|
||||
const where = line !== null ? ` [${diag.code} at ${at.file}:${line}]` : "";
|
||||
return { kind: "runtimeFence", code: diag.code, message: `${diag.message}${where}`, loc };
|
||||
}
|
||||
|
||||
/** `new C(args)` where C's constructor is the inherited runtime stream or
|
||||
* EventEmitter constructor (a ctor-less chain into the base): that
|
||||
* constructor takes zero arguments here (the options bag fences, like the
|
||||
* super() form), so arguments report at the first one. Applies to direct
|
||||
* construction and to construction through a class value of C. Stream
|
||||
* subclasses come first: their chain roots at the emitter too, but the
|
||||
* message should name the stream story. */
|
||||
function refuseInheritedBuiltinCtorArgs(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
expr: ts.NewExpression,
|
||||
): void {
|
||||
const first = expr.arguments?.nodes[0];
|
||||
if (first === undefined) return;
|
||||
if (inheritsBuiltinStreamCtor(lowerer, info)) {
|
||||
lowerer.noLowering(
|
||||
`new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor`,
|
||||
first,
|
||||
"declare a constructor that passes an inline options object to super(...)",
|
||||
);
|
||||
}
|
||||
if (lowerer.inheritsBuiltinEmitterCtor(info)) {
|
||||
lowerer.unsupported("SC1090", first, "EventEmitter constructor options ('captureRejections')");
|
||||
}
|
||||
}
|
||||
|
||||
/** Preserve argument evaluation order and option presence through builtin
|
||||
* and inherited Error constructors. Checked-dynamic conversion retains
|
||||
* Error identity and distinguishes a missing cause from undefined. */
|
||||
@@ -8861,24 +8916,7 @@ function lowerProgramClassNew(
|
||||
"construction through a function-local class's internal name",
|
||||
);
|
||||
lowerer.noteEdge(`%${info.def.name}.constructor`);
|
||||
// A ctor-less chain into an EventEmitter base inherits `new C()` —
|
||||
// zero arguments (the options bag fences, like the super() form).
|
||||
// Stream subclasses come first: their chain roots at the emitter
|
||||
// too, but the message should name the stream story.
|
||||
if (inheritsBuiltinStreamCtor(lowerer, info) && (expr.arguments?.nodes ?? []).length > 0) {
|
||||
lowerer.noLowering(
|
||||
`new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor`,
|
||||
expr.arguments!.nodes[0]!,
|
||||
"declare a constructor that passes an inline options object to super(...)",
|
||||
);
|
||||
}
|
||||
if (lowerer.inheritsBuiltinEmitterCtor(info) && (expr.arguments?.nodes ?? []).length > 0) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.arguments!.nodes[0]!,
|
||||
"EventEmitter constructor options ('captureRejections')",
|
||||
);
|
||||
}
|
||||
refuseInheritedBuiltinCtorArgs(lowerer, info, expr);
|
||||
// A ctor-less chain into a builtin error base inherits both arguments;
|
||||
// each synthesized constructor forwards the same pair to its base.
|
||||
const args = lowerer.inheritsBuiltinErrorCtor(info)
|
||||
@@ -8925,6 +8963,20 @@ function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
|
||||
|
||||
export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
// new Function(...): Node's EvalError under
|
||||
// --disallow-code-generation-from-strings (code-generation.ts).
|
||||
if (codeGenerationCalleeOf(lowerer, expr.expression) === "Function")
|
||||
return lowerCodeGenerationCall(lowerer, expr, "Function");
|
||||
// `new <temporal>.X(...)`: Node 24 has no such global, so the callee's
|
||||
// root read throws the ReferenceError before any argument evaluates.
|
||||
{
|
||||
const root = ambientUndefVarRootOf(lowerer, expr.expression);
|
||||
if (root !== null && root.text === TEMPORAL_GLOBAL) {
|
||||
const t =
|
||||
ambientUndefReadType(lowerer, expr) ?? contextualUndefReadType(lowerer, expr) ?? F64;
|
||||
return nsUndefRead(lowerer, root.text, expr, t);
|
||||
}
|
||||
}
|
||||
// `new (require("./x").C)()`: the module's %init at the require's
|
||||
// position, then construction through the member as for a binding.
|
||||
if (ts.isPropertyAccessExpression(expr.expression) && !expr.expression.questionDotToken) {
|
||||
@@ -9469,18 +9521,57 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
// fenced, so copying the scalar cannot create an observable lie.
|
||||
if (symbol && symbol.name === "Date" && lowerer.isStdlibSymbol(symbol)) {
|
||||
const args = expr.arguments?.nodes ?? [];
|
||||
if (args.some(ts.isSpreadElement) || args.length > 1) {
|
||||
if (args.some(ts.isSpreadElement)) {
|
||||
lowerer.noLowering(
|
||||
`new Date with ${args.length} arguments`,
|
||||
`new Date with spread arguments`,
|
||||
expr,
|
||||
"new Date(), new Date(milliseconds), and new Date(dateString) are supported; the local-time year/month field constructor has no lowering",
|
||||
"new Date(), new Date(milliseconds), new Date(dateString), and new Date(year, monthIndex, ...) with listed arguments are supported",
|
||||
symbol,
|
||||
);
|
||||
}
|
||||
// new Date(year, monthIndex[, date[, hours[, minutes[, seconds[, ms]]]]]):
|
||||
// local-time fields, converted to UTC in the host zone at run time.
|
||||
// JavaScript callers pass their arguments to the runtime's ToNumber
|
||||
// (in order, exactly the constructor's coercions); typed callers
|
||||
// pass numbers, completed with the spec's defaults (date 1, time
|
||||
// parts 0) for the seven-f64 ABI.
|
||||
if (args.length > 1) {
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "date.nativeNewFields",
|
||||
args: [
|
||||
{
|
||||
kind: "dynArrLit",
|
||||
elems: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (args.length > 7) {
|
||||
lowerer.noLowering(`new Date with ${args.length} arguments`, expr, undefined, symbol);
|
||||
}
|
||||
const defaults = [0, 0, 1, 0, 0, 0, 0];
|
||||
const fields: IrExpr[] = [];
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const arg = args[i];
|
||||
fields.push(
|
||||
arg !== undefined
|
||||
? lowerer.lowerExprExpecting(arg, F64)
|
||||
: { kind: "numLit", value: defaults[i]!, type: F64, loc },
|
||||
);
|
||||
}
|
||||
return { kind: "libCall", fn: "date.newLocal", args: fields, type: DATE_T, loc };
|
||||
}
|
||||
if (isJsSourceFile(expr.getSourceFile())) {
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "date.nativeNew",
|
||||
// One argument: a string parses with the full date parser.
|
||||
fn: args.length === 1 ? "date.nativeNewValue" : "date.nativeNew",
|
||||
args: [
|
||||
{
|
||||
kind: "dynArrLit",
|
||||
@@ -10126,6 +10217,9 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
`EventEmitter and stream class values constructed with options (construct with 'new ${info.def.name.slice(1)}({ ... })' directly)`,
|
||||
);
|
||||
}
|
||||
// A program class whose constructor is the inherited runtime one
|
||||
// takes no arguments through its class value either.
|
||||
refuseInheritedBuiltinCtorArgs(lowerer, info, expr);
|
||||
defineClassValueConstruct(lowerer, info);
|
||||
const args = lowerer.completeArgs(
|
||||
expr.arguments?.nodes ?? [],
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
BOOL,
|
||||
canBoxFuncIntoDyn,
|
||||
canDynCheckTo,
|
||||
typeKey,
|
||||
DGRAMSOCK_T,
|
||||
DYN,
|
||||
F64,
|
||||
@@ -131,6 +132,7 @@ export function lowerDgramDnsModuleCall(
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (bi.module === "dns") return lowerDnsModuleCall(lowerer, expr, bi, loc);
|
||||
if (bi.module === "dns/promises") return lowerDnsPromisesCall(lowerer, expr, bi, loc);
|
||||
if (bi.module !== "dgram") return null;
|
||||
const args = expr.arguments.nodes;
|
||||
if (bi.member === "createSocket") {
|
||||
@@ -278,6 +280,124 @@ function lowerDnsCallback(lowerer: Lowerer, node: ts.Expression, what: string):
|
||||
);
|
||||
}
|
||||
|
||||
/** node:dns/promises calls (and dns.promises members): lookup(hostname[,
|
||||
* options]) and lookupService(address, port) as promises over the callback
|
||||
* forms' runtime (argument errors throw synchronously, as Node validates
|
||||
* before it creates the promise), plus the shared getServers and result
|
||||
* order functions. A typed result (Promise<LookupAddress>) checks the
|
||||
* settled value through the checked-dynamic boundary. */
|
||||
function lowerDnsPromisesCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
bi: { module: string; member: string },
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const args = expr.arguments.nodes;
|
||||
const dyn = (node: ts.Expression): IrExpr =>
|
||||
lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
|
||||
if (bi.member === "lookup" || bi.member === "lookupService") {
|
||||
const service = bi.member === "lookupService";
|
||||
if (service ? args.length !== 2 : args.length !== 1 && args.length !== 2) {
|
||||
lowerer.noLowering(
|
||||
`dnsPromises.${bi.member} with ${args.length} arguments`,
|
||||
expr,
|
||||
service
|
||||
? "the form is lookupService(address, port)"
|
||||
: "the supported forms are lookup(hostname) and lookup(hostname, options)",
|
||||
);
|
||||
}
|
||||
const promise: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: service ? "dns.promisesLookupService" : "dns.promisesLookup",
|
||||
args: [dyn(args[0]!), args.length === 2 ? dyn(args[1]!) : dynUndefinedExpr(loc)],
|
||||
type: { kind: "promise", inner: DYN },
|
||||
loc,
|
||||
};
|
||||
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (declared === null || declared.kind !== "promise" || declared.inner.kind === "dyn") {
|
||||
return promise;
|
||||
}
|
||||
return checkedDynPromise(lowerer, promise, declared, expr, loc);
|
||||
}
|
||||
if (["getServers", "getDefaultResultOrder", "setDefaultResultOrder"].includes(bi.member)) {
|
||||
return lowerDnsModuleCall(lowerer, expr, { module: "dns", member: bi.member }, loc);
|
||||
}
|
||||
lowerer.noLowering(
|
||||
`dnsPromises.${bi.member}`,
|
||||
expr,
|
||||
"lookup, lookupService, getServers, getDefaultResultOrder, and setDefaultResultOrder are the lowered dns/promises functions",
|
||||
ts.isIdentifier(expr.expression) ? lowerer.resolveValueSymbol(expr.expression) : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
/** A promise<dyn> as the call site's typed promise: an interned async
|
||||
* helper awaits it and checks the settled value into the declared inner
|
||||
* type (a mismatch rejects with the checked-dynamic TypeError). */
|
||||
function checkedDynPromise(
|
||||
lowerer: Lowerer,
|
||||
promise: IrExpr,
|
||||
declared: IrType & { kind: "promise" },
|
||||
node: ts.Node,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const inner = declared.inner;
|
||||
if (
|
||||
!canDynCheckTo(
|
||||
inner,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
(id) => lowerer.unions.get(id),
|
||||
)
|
||||
) {
|
||||
lowerer.noLowering(
|
||||
`a dns/promises result typed '${lowerer.fmt(declared)}'`,
|
||||
node,
|
||||
"the settled value must have a checked-dynamic conversion (type it as the documented LookupAddress shape or unknown)",
|
||||
);
|
||||
}
|
||||
const key = `dns.promise:${typeKey(declared)}`;
|
||||
let helper = lowerer.valueHelpers.get(key);
|
||||
if (!helper) {
|
||||
helper = `%dns.promise.${lowerer.valueHelpers.size}`;
|
||||
lowerer.valueHelpers.set(key, helper);
|
||||
const promiseT: IrType = { kind: "promise", inner: DYN };
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "promise", name: "promise", type: promiseT }],
|
||||
returnType: inner,
|
||||
locals: [
|
||||
{ id: "promise", name: "promise", type: promiseT, mutable: false },
|
||||
{ id: "settled", name: "settled", type: DYN, mutable: false },
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "settled",
|
||||
init: {
|
||||
kind: "awaitExpr",
|
||||
value: { kind: "varRef", localId: "promise", type: promiseT, loc },
|
||||
type: DYN,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "dynCheck",
|
||||
value: { kind: "varRef", localId: "settled", type: DYN, loc },
|
||||
type: inner,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
async: true,
|
||||
});
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [promise], type: declared, loc };
|
||||
}
|
||||
|
||||
/** dns module calls: lookup(hostname[, options], callback) with Node's
|
||||
* option forms, lookupService(address, port, callback), getServers() and
|
||||
* get/setDefaultResultOrder(). The runtime validates the arguments as
|
||||
@@ -290,8 +410,17 @@ function lowerDnsModuleCall(
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const args = expr.arguments.nodes;
|
||||
const dyn = (node: ts.Expression): IrExpr =>
|
||||
lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
|
||||
const dyn = (node: ts.Expression): IrExpr => {
|
||||
// `opts as dns.LookupOneOptions` over a checked-dynamic value (the
|
||||
// overload-satisfying cast of a pass-through lookup): the cast changes
|
||||
// no value, so the options reach the runtime as they are.
|
||||
let inner = node;
|
||||
while (ts.isAsExpression(inner) || ts.isParenthesizedExpression(inner))
|
||||
inner = inner.expression;
|
||||
if (inner !== node && lowerer.mapTypeOf(lowerer.typeOf(inner))?.kind === "dyn")
|
||||
return lowerer.lowerExpr(inner);
|
||||
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
|
||||
};
|
||||
// A callback whose second parameter is typed (string, LookupAddress[])
|
||||
// cannot take the undefined Node passes after an error.
|
||||
const typedResult = (node: ts.Expression): IrExpr => {
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { F64, type IrExpr, STRING } from "../../ir/ir.js";
|
||||
import { BOOL, F64, type IrExpr, STRING, UNDEFINED_T } from "../../ir/ir.js";
|
||||
import { nodeThrowExpr } from "../../ir/build.js";
|
||||
|
||||
/** The member's source-name text (identifiers and string-literal names —
|
||||
* the only name forms tsc allows on enum members). */
|
||||
@@ -182,6 +183,15 @@ function lowerEnumReverseRead(
|
||||
// existence tracking; nothing needs it — fence through the generic path.
|
||||
if (decls.some((d) => isAmbientEnumDecl(d))) return null;
|
||||
const idx = foldConstIndex(lowerer, key);
|
||||
const keyType = lowerer.typeOf(key);
|
||||
if (
|
||||
idx === null &&
|
||||
((keyType.flags & ts.TypeFlags.StringLike) !== 0 ||
|
||||
(keyType.isUnionType() &&
|
||||
ts.constituentTypes(keyType).every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0)))
|
||||
) {
|
||||
return lowerEnumKeyRead(lowerer, expr, decls, key);
|
||||
}
|
||||
if (idx === null) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -206,6 +216,109 @@ function lowerEnumReverseRead(
|
||||
return { kind: "strLit", value: name, type: STRING, loc };
|
||||
}
|
||||
|
||||
/** Object.prototype members every enum object inherits: a runtime key
|
||||
* naming one reads a function, which the member value types cannot hold. */
|
||||
const OBJECT_PROTOTYPE_KEYS = new Set([
|
||||
"__defineGetter__",
|
||||
"__defineSetter__",
|
||||
"__lookupGetter__",
|
||||
"__lookupSetter__",
|
||||
"__proto__",
|
||||
"constructor",
|
||||
"hasOwnProperty",
|
||||
"isPrototypeOf",
|
||||
"propertyIsEnumerable",
|
||||
"toLocaleString",
|
||||
"toString",
|
||||
"valueOf",
|
||||
]);
|
||||
|
||||
/** `E[key]` with a RUNTIME string key (`Color[name as keyof typeof
|
||||
* Color]` — the name-to-value lookup): the key evaluates once and is
|
||||
* compared against every member NAME, each hit answering that member's
|
||||
* constant, exactly the forward entries tsc's emit stores. A key naming no
|
||||
* member is Node's undefined, so the read carries an undefined arm. Keys
|
||||
* Node would answer from elsewhere on the object throw a named runtime
|
||||
* refusal instead of a wrong value: a numeric member's reverse entry (its
|
||||
* value spelled as a string — Node answers the member NAME) and the
|
||||
* inherited Object.prototype members. */
|
||||
function lowerEnumKeyRead(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.ElementAccessExpression,
|
||||
decls: readonly ts.EnumDeclaration[],
|
||||
key: ts.Expression,
|
||||
): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
const enumName = decls[0]!.name.text;
|
||||
const valueType = lowerer.mapTypeOf(lowerer.typeOf(expr));
|
||||
if (valueType === null) lowerer.badType(expr, lowerer.typeOf(expr));
|
||||
const resultType = lowerer.withUndefinedArm(valueType);
|
||||
const keyValue = lowerer.coerceToExpected(lowerer.lowerExpr(key), STRING);
|
||||
const keyLocal = lowerer.declareHiddenLocal("%enumKey", STRING);
|
||||
const keyRef: IrExpr = { kind: "varRef", localId: keyLocal.id, type: STRING, loc };
|
||||
const matches = (text: string): IrExpr => ({
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left: keyRef,
|
||||
right: { kind: "strLit", value: text, type: STRING, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const refusal = (what: string): IrExpr =>
|
||||
nodeThrowExpr(
|
||||
0,
|
||||
"SC1090",
|
||||
`${what} through a runtime key ('${enumName}[key]') is not supported yet`,
|
||||
resultType,
|
||||
loc,
|
||||
);
|
||||
const arms: { cond: IrExpr; value: IrExpr }[] = [];
|
||||
const names = new Set<string>();
|
||||
const reverse = new Set<string>();
|
||||
for (const d of decls) {
|
||||
for (const member of d.members.nodes) {
|
||||
const name = memberNameText(member);
|
||||
const constant = lowerEnumMemberRead(lowerer, expr, member);
|
||||
if (constant === null)
|
||||
lowerer.unsupported("SC1090", expr, `'${enumName}[key]' over this enum`);
|
||||
if (!names.has(name)) {
|
||||
names.add(name);
|
||||
arms.push({ cond: matches(name), value: lowerer.coerceToExpected(constant, resultType) });
|
||||
}
|
||||
const value = lowerer.checker.getConstantValue(member);
|
||||
if (typeof value === "number") reverse.add(String(value));
|
||||
}
|
||||
}
|
||||
for (const text of reverse) {
|
||||
if (names.has(text)) continue;
|
||||
arms.push({ cond: matches(text), value: refusal("enum reverse lookups") });
|
||||
}
|
||||
for (const text of OBJECT_PROTOTYPE_KEYS) {
|
||||
if (names.has(text) || reverse.has(text)) continue;
|
||||
arms.push({ cond: matches(text), value: refusal("inherited enum object members") });
|
||||
}
|
||||
let result: IrExpr = { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc };
|
||||
result = lowerer.coerceToExpected(result, resultType);
|
||||
for (let i = arms.length - 1; i >= 0; i--) {
|
||||
const arm = arms[i]!;
|
||||
result = {
|
||||
kind: "ternary",
|
||||
cond: arm.cond,
|
||||
then: arm.value,
|
||||
else_: result,
|
||||
type: resultType,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: keyLocal.id, init: keyValue, loc }],
|
||||
result,
|
||||
type: resultType,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A compile-time-constant NUMERIC index: a numeric literal, its negation,
|
||||
* or an enum member read whose constant is a number. */
|
||||
function foldConstIndex(lowerer: Lowerer, e: ts.Expression): number | null {
|
||||
|
||||
@@ -38,7 +38,14 @@ import {
|
||||
typeEquals,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import {
|
||||
cjsExportAssignmentOf,
|
||||
cjsExportDiscardReason,
|
||||
cjsExportTargetLiteral,
|
||||
isJsSourceFile,
|
||||
locOf,
|
||||
} from "../program.js";
|
||||
import { npmStaticLexerVisibleNames } from "../npm-static.js";
|
||||
import {
|
||||
type CompoundOp,
|
||||
NARROW_FIRST,
|
||||
@@ -84,6 +91,8 @@ import { lowerElementCompoundParts } from "./expressions/element-access.js";
|
||||
export {
|
||||
DEFERRED_READ_MESSAGE,
|
||||
abstractPropertyDeclOf,
|
||||
builtinFunctionValue,
|
||||
builtinGlobalValue,
|
||||
fieldGetExpr,
|
||||
fieldTarget,
|
||||
isMatchSliceType,
|
||||
@@ -92,6 +101,7 @@ export {
|
||||
lowerIntrinsicProperty,
|
||||
lowerUnionProperty,
|
||||
matchResultNamedGroupsOf,
|
||||
primitiveConstructorOr,
|
||||
} from "./expressions/property-access.js";
|
||||
export {
|
||||
lowerAsExpression,
|
||||
@@ -2057,6 +2067,102 @@ function aliasTypeofArmOf(
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The names Node's ES-module loader copies off a CommonJS module that a
|
||||
* bundler rewrite spells as accessors (npmStaticLexerVisibleNames), with
|
||||
* their getters in that order; empty for every other module. */
|
||||
function lexerVisibleAccessors(dep: ts.SourceFile): ts.GetAccessorDeclaration[] {
|
||||
const names = npmStaticLexerVisibleNames(dep.fileName);
|
||||
if (names.length === 0) return [];
|
||||
let table: ts.ObjectLiteralExpression | null = null;
|
||||
for (const stmt of dep.statements.nodes) {
|
||||
const cjs = cjsExportAssignmentOf(stmt);
|
||||
if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) {
|
||||
table = cjsExportTargetLiteral(cjs.expr.right, dep);
|
||||
}
|
||||
}
|
||||
if (table === null) return [];
|
||||
const getters = new Map<string, ts.GetAccessorDeclaration>();
|
||||
for (const p of table.properties.nodes) {
|
||||
if (ts.isGetAccessorDeclaration(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)))
|
||||
getters.set(p.name.text, p);
|
||||
}
|
||||
return names.flatMap((n) => {
|
||||
const g = getters.get(n);
|
||||
return g === undefined ? [] : [g];
|
||||
});
|
||||
}
|
||||
|
||||
/** The module global holding the value an accessor export had when an
|
||||
* ES-module importer first linked its CommonJS module. */
|
||||
export function cjsAccessorSnapshotOf(
|
||||
lowerer: Lowerer,
|
||||
getter: ts.GetAccessorDeclaration,
|
||||
): { id: string; type: IrType } | null {
|
||||
const hit = lowerer.cjsAccessorSnapshots.get(getter);
|
||||
if (hit !== undefined) return hit;
|
||||
if (!lexerVisibleAccessors(getter.getSourceFile()).includes(getter)) return null;
|
||||
const call = cjsAccessorCall(lowerer, getter, locOf(getter));
|
||||
if (call === null) return null;
|
||||
const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type };
|
||||
lowerer.globalsList.push({
|
||||
id: snapshot.id,
|
||||
name: "%cjsSnapshot",
|
||||
type: call.type,
|
||||
mutable: true,
|
||||
});
|
||||
lowerer.cjsAccessorSnapshots.set(getter, snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** Node's ES-module loader reads every lexer-visible export of a CommonJS
|
||||
* module once, when an importer first links it, and the import bindings
|
||||
* keep those values. For the accessors a bundler rewrite introduced, the
|
||||
* importer's link point runs each getter once, in the lexer's order, and
|
||||
* stores the values its bindings read. Empty when the module has none. */
|
||||
export function cjsAccessorSnapshotStores(
|
||||
lowerer: Lowerer,
|
||||
dep: ts.SourceFile,
|
||||
loc: SrcLoc,
|
||||
): IrStmt[] {
|
||||
const getters = lexerVisibleAccessors(dep);
|
||||
if (getters.length === 0) return [];
|
||||
let readyId = lowerer.cjsSnapshotReady.get(dep);
|
||||
if (readyId === undefined) {
|
||||
readyId = `%g.cjsSnapshotReady.${lowerer.cjsSnapshotReady.size}`;
|
||||
lowerer.globalsList.push({ id: readyId, name: "%cjsSnapshotReady", type: BOOL, mutable: true });
|
||||
lowerer.cjsSnapshotReady.set(dep, readyId);
|
||||
}
|
||||
const stores: IrStmt[] = [
|
||||
{
|
||||
kind: "assign",
|
||||
localId: readyId,
|
||||
value: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
];
|
||||
for (const getter of getters) {
|
||||
const snapshot = cjsAccessorSnapshotOf(lowerer, getter);
|
||||
const call = cjsAccessorCall(lowerer, getter, loc);
|
||||
if (snapshot === null || call === null) return [];
|
||||
stores.push({ kind: "assign", localId: snapshot.id, value: call, loc });
|
||||
}
|
||||
return [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unary",
|
||||
op: "!",
|
||||
operand: { kind: "varRef", localId: readyId, type: BOOL, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: stores,
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** The interned lifted-getter call both accessor-read paths share. */
|
||||
export function cjsAccessorCall(
|
||||
lowerer: Lowerer,
|
||||
@@ -2588,6 +2694,24 @@ export function symbolFieldTarget(
|
||||
};
|
||||
}
|
||||
|
||||
/** `return Promise.reject(reason)` in an async function: the checker's
|
||||
* context is the awaited return type (or its union with PromiseLike), not
|
||||
* a promise, but the async function adopts the returned promise, so the
|
||||
* promise of its own result type is the honest type. Null elsewhere. */
|
||||
function asyncReturnPromise(lowerer: Lowerer, call: ts.CallExpression): IrType | null {
|
||||
if (lowerer.ctx.isAsync !== true || lowerer.ctx.generator) return null;
|
||||
let node: ts.Node = call;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
const returned =
|
||||
(ts.isReturnStatement(parent) && parent.expression === node) ||
|
||||
(ts.isArrowFunction(parent) && parent.body === node);
|
||||
if (!returned) return null;
|
||||
const ret = lowerer.ctx.returnType;
|
||||
if (ret.kind === "void" || ret.kind === "promise") return null;
|
||||
return { kind: "promise", inner: ret };
|
||||
}
|
||||
|
||||
/** `Promise.reject(reason)` on THE Promise global: a pre-rejected
|
||||
* promise. The reason is pinned to the Error hierarchy (the executor
|
||||
* reject parameter's contract — rejection payloads share the
|
||||
@@ -2619,7 +2743,9 @@ export function lowerPromiseRejectCall(
|
||||
// A maybe-promise context (`return Promise.reject(e)` from a function
|
||||
// returning `T | Promise<T>`) names its one promise arm.
|
||||
const maybePromise = contextual ? maybePromiseUnionOf(lowerer, contextual) : null;
|
||||
const type = maybePromise ? maybePromise.promise : contextual;
|
||||
const type = maybePromise
|
||||
? maybePromise.promise
|
||||
: (asyncReturnPromise(lowerer, call) ?? contextual);
|
||||
const reasonNode = call.arguments.nodes[0]!;
|
||||
// A CHECKED-DYNAMIC reason (`Promise.reject(value)` where value rode
|
||||
// an untyped binding — the tracingChannel suite's shape): the checked-dynamic tree
|
||||
|
||||
@@ -3,15 +3,19 @@ import { DYN, STRING, arrayOf, type IrExpr } from "../../ir/ir.js";
|
||||
import { strLit } from "../../ir/build.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
|
||||
/** A stored global object has identity and survives module/function boundaries.
|
||||
* Known globals without a value implementation refuse at runtime; absent host
|
||||
* capabilities answer undefined. The checker supplies names, never values. */
|
||||
export function lowerGlobalValue(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
const loc = locOf(expr);
|
||||
// The temporal global is typed for the programs that name it (TS 7's
|
||||
// lib slice) but absent in Node 24: reads answer undefined.
|
||||
const names = lowerer.checker
|
||||
.getPropertiesOfType(lowerer.typeOf(expr))
|
||||
.map((property) => property.name);
|
||||
.map((property) => property.name)
|
||||
.filter((name) => name !== TEMPORAL_GLOBAL);
|
||||
return {
|
||||
kind: "libCall",
|
||||
fn: "global.native",
|
||||
|
||||
@@ -145,7 +145,7 @@ import { forkTargetPaths } from "../fork-target.js";
|
||||
import { workerTargetPaths } from "../worker-target.js";
|
||||
import { isNativeProxyInitializer } from "./expressions/native-proxy.js";
|
||||
import { handleEventReaderPrelude } from "./lower-event-target.js";
|
||||
import { jsHoldsDynObject } from "./lower-exprs.js";
|
||||
import { cjsAccessorSnapshotStores, jsHoldsDynObject } from "./lower-exprs.js";
|
||||
import { keptIdentityAssertion } from "./assertion-identity.js";
|
||||
|
||||
/** One file's declarations, split for collection and init-body lowering. */
|
||||
@@ -2851,6 +2851,14 @@ export function lowerFileInit(
|
||||
});
|
||||
} else {
|
||||
header.push({ kind: "exprStmt", expr: call, loc });
|
||||
// An ES-module importer links a CommonJS dependency: Node
|
||||
// copies its lexer-visible exports here.
|
||||
if (
|
||||
(ts.isImportDeclaration(stmt) || ts.isExportDeclaration(stmt)) &&
|
||||
!isCjsJsFile(sf, lowerer.program) &&
|
||||
isCjsJsFile(dep, lowerer.program)
|
||||
)
|
||||
header.push(...cjsAccessorSnapshotStores(lowerer, dep, loc));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import { boundIdentifiersOf } from "./lowerer.js";
|
||||
import type { FileParts } from "./lower-modules.js";
|
||||
import { locOf, resolveImport } from "../program.js";
|
||||
import { F64, type IrExpr, type IrStmt, type IrType, STRING } from "../../ir/ir.js";
|
||||
import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
|
||||
|
||||
/** True when this module declaration produces NO runtime construct at all:
|
||||
* ambient (`declare namespace/module`, `declare global`, string-named
|
||||
@@ -425,7 +426,7 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id
|
||||
const sym = lowerer.resolveValueSymbol(root);
|
||||
if (!sym) return null;
|
||||
if (lowerer.trapBindings.has(sym)) return root;
|
||||
if (lowerer.isStdlibSymbol(sym)) return null;
|
||||
if (lowerer.isStdlibSymbol(sym)) return isTemporalGlobalRoot(lowerer, root, sym) ? root : null;
|
||||
if (ambientUndefinedFnSymbolOf(lowerer, root) !== null) return root;
|
||||
const decls = sym.declarations;
|
||||
if (decls.length === 0) return null;
|
||||
@@ -440,6 +441,24 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id
|
||||
}
|
||||
}
|
||||
|
||||
/** True when `ident` reads the ESNext temporal global, which the program
|
||||
* type-checks against TS 7's lib slice (program.ts adds it only to programs
|
||||
* that name it) but Node 24 does not define: Node's read throws the
|
||||
* catchable ReferenceError "<name> is not defined", so every chain rooted
|
||||
* here lowers to that throw (typeof answers "undefined" in lower-exprs). */
|
||||
export function isTemporalGlobalRoot(
|
||||
lowerer: Lowerer,
|
||||
ident: ts.Identifier,
|
||||
sym: ts.Symbol | undefined | null,
|
||||
): boolean {
|
||||
return (
|
||||
ident.text === TEMPORAL_GLOBAL &&
|
||||
sym !== undefined &&
|
||||
sym !== null &&
|
||||
lowerer.isStdlibSymbol(sym)
|
||||
);
|
||||
}
|
||||
|
||||
/** The declaration-classification form of ambientUndefVarRootOf: the root
|
||||
* that makes `decl` a TRAP BINDING — its initializer provably throws the
|
||||
* root's ReferenceError, so the binding never holds a value and needs no
|
||||
|
||||
@@ -42,6 +42,8 @@ import {
|
||||
STATIC_RESPONSE_CALLS,
|
||||
STATIC_RESPONSE_READS,
|
||||
} from "../../compat/fetch-profile.js";
|
||||
import { classGeneratorIterable } from "./class-iterables.js";
|
||||
import { generatorDrain } from "./iterator-adapters.js";
|
||||
|
||||
export function refuseUnsupportedApi(
|
||||
lowerer: Lowerer,
|
||||
@@ -1464,7 +1466,8 @@ export function lowerStaticFetchCompanionCall(
|
||||
}
|
||||
const root = call.expression.expression;
|
||||
const symbol = lowerer.resolveValueSymbol(root);
|
||||
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
|
||||
if ((!symbol || !lowerer.isStdlibSymbol(symbol)) && !isStreamWebReadableStream(lowerer, root))
|
||||
return null;
|
||||
const loc = locOf(call);
|
||||
if (root.text === "AbortSignal") {
|
||||
noteEventListeners(lowerer, loc);
|
||||
@@ -1877,6 +1880,12 @@ export function lowerAbortControllerNew(lowerer: Lowerer, expr: ts.NewExpression
|
||||
};
|
||||
}
|
||||
|
||||
/** An import binding of node:stream/web's ReadableStream, which IS the
|
||||
* global class (`import { ReadableStream } from "node:stream/web"`). */
|
||||
function isStreamWebReadableStream(lowerer: Lowerer, ident: ts.Identifier): boolean {
|
||||
return ident.text === "ReadableStream" && lowerer.isStdlibGlobal(ident, "ReadableStream");
|
||||
}
|
||||
|
||||
export function lowerStaticReadableStreamNew(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.NewExpression,
|
||||
@@ -1885,7 +1894,11 @@ export function lowerStaticReadableStreamNew(
|
||||
return null;
|
||||
}
|
||||
const symbol = lowerer.resolveValueSymbol(expr.expression);
|
||||
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
|
||||
if (
|
||||
(!symbol || !lowerer.isStdlibSymbol(symbol)) &&
|
||||
!isStreamWebReadableStream(lowerer, expr.expression)
|
||||
)
|
||||
return null;
|
||||
const args = expr.arguments?.nodes ?? [];
|
||||
if (args.length > 1) return null;
|
||||
const loc = locOf(expr);
|
||||
@@ -2436,6 +2449,10 @@ export function lowerStdlibMethodCall(
|
||||
src = lowerer.coerceInto(spread.expression, src, inferred);
|
||||
}
|
||||
}
|
||||
// A generator, or a class whose [Symbol.iterator] is a generator
|
||||
// method, drains into the number[] the fold reads.
|
||||
src = classGeneratorIterable(lowerer, src) ?? src;
|
||||
if (src.type.kind === "generator") src = generatorDrain(lowerer, src, F64, loc) ?? src;
|
||||
if (src.type.kind !== "array" || src.type.elem.kind !== "f64") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { declaredMemberOf } from "./expressions/property-access.js";
|
||||
import { hostsAll } from "./expressions/hosted-narrowing.js";
|
||||
import {
|
||||
matchAllRowsReadByElements,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
import {
|
||||
cjsExportMemberWriteGlobalOf,
|
||||
isCjsExportsIdentifier,
|
||||
isCjsModuleExports,
|
||||
isFreshExportsReset,
|
||||
} from "./cjs-exports-object.js";
|
||||
import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js";
|
||||
@@ -195,6 +197,7 @@ import {
|
||||
import { globalSymbolKey } from "./expressions/global-symbols.js";
|
||||
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
|
||||
import {
|
||||
builtinGlobalValue,
|
||||
jsHoldsDynObject,
|
||||
lowerEnvironmentKey,
|
||||
lowerNativeFunctionAssignment,
|
||||
@@ -223,6 +226,7 @@ import { constituentTypes } from "../ts7/checker.js";
|
||||
import { npmStaticPackageOfPath } from "../npm-static.js";
|
||||
import { fenceNodeModuleMutation } from "./lower-node-module.js";
|
||||
import { fencePerfEntryKeyWalk } from "./builtins/performance.js";
|
||||
import { classGeneratorIterable } from "./class-iterables.js";
|
||||
|
||||
interface UsingRegistration {
|
||||
acquire: IrStmt[];
|
||||
@@ -3773,6 +3777,9 @@ function stdlibGlobalTokenDestructure(
|
||||
const canonical = member.name === "global" ? "globalThis" : member.name;
|
||||
token = `[builtin ${canonical}]`;
|
||||
alias = canonical;
|
||||
// Builtin constructors read as their interned function values, the
|
||||
// same values the bare and `globalThis.X` spellings read.
|
||||
value = builtinGlobalValue(canonical, locOf(el), true);
|
||||
} else if (globalName === "console" && !CONSOLE_CALL_MEMBERS.has(propName)) {
|
||||
token = `[builtin console.${propName}]`;
|
||||
} else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) {
|
||||
@@ -3816,6 +3823,10 @@ function stdlibGlobalTokenDestructure(
|
||||
const out: IrStmt[] = [];
|
||||
for (const b of binds) {
|
||||
const loc = locOf(b.name);
|
||||
if (b.value !== undefined && b.alias !== null) {
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(b.name);
|
||||
if (symbol) lowerer.stdlibGlobalAliases.set(symbol, b.alias);
|
||||
}
|
||||
if (b.value !== undefined) {
|
||||
if (b.g !== undefined) {
|
||||
out.push({
|
||||
@@ -6910,6 +6921,25 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
"require() outside the module's top level (move it to the top of the file)",
|
||||
);
|
||||
}
|
||||
// Replacing the export object (or rebinding `exports`) anywhere but a
|
||||
// top-level statement makes the module's exports depend on which path
|
||||
// ran (`cond ? module.exports = a : module.exports = b`, an
|
||||
// environment-dependent branch); the static export table has one shape.
|
||||
if (
|
||||
!topLevelJs &&
|
||||
isJsSourceFile(expr.getSourceFile()) &&
|
||||
ts.isBinaryExpression(expr) &&
|
||||
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
(isCjsModuleExports(lowerer, expr.left) || isCjsExportsIdentifier(lowerer, expr.left))
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.left,
|
||||
isCjsModuleExports(lowerer, expr.left)
|
||||
? "replacing module.exports outside a top-level statement (a conditional or environment-dependent export object)"
|
||||
: "rebinding 'exports' outside a top-level statement (a conditional or environment-dependent export object)",
|
||||
);
|
||||
}
|
||||
if (topLevelJs && ts.isExpressionStatement(stmtNode)) {
|
||||
if (isFreshExportsReset(lowerer, stmtNode))
|
||||
return { kind: "block", body: [], loc: locOf(expr) };
|
||||
@@ -7475,6 +7505,31 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
const recvIr = lowerer.mapTypeOf(lowerer.typeOf(expr.left.expression));
|
||||
if (recvIr?.kind === "record") {
|
||||
const shape = lowerer.shapes.get(recvIr.shapeId);
|
||||
if (
|
||||
shape?.indexValue &&
|
||||
shape.fields.length === 0 &&
|
||||
declaredMemberOf(lowerer, expr.left.expression, expr.left.name.text)
|
||||
) {
|
||||
// A declared member of a literal stored as a pure index-signature
|
||||
// record (dictSpreadLiteralShape): a keyed write of that key.
|
||||
const loc = locOf(expr);
|
||||
const obj = lowerer.lowerExpr(expr.left.expression);
|
||||
if (obj.type.kind === "record") {
|
||||
const value = lowerer.intoIndexValueSlot(
|
||||
lowerer.lowerExprExpecting(expr.right, shape.indexValue),
|
||||
shape.indexValue,
|
||||
expr.right,
|
||||
);
|
||||
return {
|
||||
kind: "recordKeySet",
|
||||
obj,
|
||||
shapeId: obj.type.shapeId,
|
||||
key: { kind: "strLit", value: expr.left.name.text, type: STRING, loc },
|
||||
value,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (
|
||||
shape?.indexValue &&
|
||||
!shape.fields.some(
|
||||
@@ -9149,6 +9204,16 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
|
||||
);
|
||||
}
|
||||
if (iterable.type.kind === "object") {
|
||||
// A [Symbol.iterator] generator method: the loop drives the
|
||||
// generator it returns.
|
||||
const generator = classGeneratorIterable(lowerer, iterable);
|
||||
if (generator)
|
||||
return lowerForOfGenerator(
|
||||
lowerer,
|
||||
stmt,
|
||||
generator as IrExpr & { type: IrType & { kind: "generator" } },
|
||||
labels,
|
||||
);
|
||||
// Class iterables ([Symbol.iterator]() + next() — classIteratorOf)
|
||||
// drive the same protocol with ordinary method calls.
|
||||
const cit = lowerer.classIteratorOf(iterable.type);
|
||||
|
||||
@@ -3674,8 +3674,10 @@ export function lowerStreamMethodCall(
|
||||
: lowerer.lowerExprExpecting(args[0]!, STRING);
|
||||
const fn = kind === "bytes" ? "writable.write" : "writable.writeStr";
|
||||
if (cbArg !== undefined) {
|
||||
const cb = lowerer.lowerExprExpecting(cbArg, funcOf([], VOID));
|
||||
if (cb.type.kind !== "func" || cb.type.params.length > 0) {
|
||||
const cb = callbackDeclaresParameters(cbArg)
|
||||
? null
|
||||
: lowerer.lowerExprExpecting(cbArg, funcOf([], VOID));
|
||||
if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) {
|
||||
lowerer.noLowering(
|
||||
"write completion callbacks taking the error argument",
|
||||
cbArg,
|
||||
@@ -3732,8 +3734,10 @@ export function lowerStreamMethodCall(
|
||||
);
|
||||
}
|
||||
if (cbArg !== undefined) {
|
||||
const cb = lowerer.lowerExprExpecting(cbArg, funcOf([], VOID));
|
||||
if (cb.type.kind !== "func" || cb.type.params.length > 0) {
|
||||
const cb = callbackDeclaresParameters(cbArg)
|
||||
? null
|
||||
: lowerer.lowerExprExpecting(cbArg, funcOf([], VOID));
|
||||
if (cb === null || cb.type.kind !== "func" || cb.type.params.length > 0) {
|
||||
lowerer.noLowering(
|
||||
"end callbacks with parameters",
|
||||
cbArg,
|
||||
@@ -3998,3 +4002,17 @@ export function lowerStreamProperty(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Whether an inline completion callback declares a parameter. Node calls
|
||||
* write and end callbacks with an error argument (null on success), but the
|
||||
* native callback receives none, so the parameter would read undefined
|
||||
* where Node passes null or the error. A function literal with parameters
|
||||
* would otherwise adapt to the zero-parameter slot silently. */
|
||||
function callbackDeclaresParameters(node: ts.Expression): boolean {
|
||||
let callback = node;
|
||||
while (ts.isParenthesizedExpression(callback)) callback = callback.expression;
|
||||
return (
|
||||
(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) &&
|
||||
callback.parameters.nodes.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2578,6 +2578,10 @@ export class Lowerer {
|
||||
* lifted lazily as module-level functions and interned per accessor
|
||||
* declaration: member reads call the getter (lower-exprs). */
|
||||
readonly cjsAccessorFns = new Map<ts.Node, { fnName: string; type: IrType & { kind: "func" } }>();
|
||||
/** Accessor exports' link-time values for ES-module importers, and the
|
||||
* per-module flag that the copy already ran (cjsAccessorSnapshotStores). */
|
||||
readonly cjsAccessorSnapshots = new Map<ts.Node, { id: string; type: IrType }>();
|
||||
readonly cjsSnapshotReady = new Map<ts.SourceFile, string>();
|
||||
/** Interned `%iter.drain.<n>` helpers (classIteratorDrainCall): one per
|
||||
* receiver class — the eager drain of a class iterable's protocol into
|
||||
* a fresh element array, behind array/call spreads. */
|
||||
@@ -2657,6 +2661,10 @@ export class Lowerer {
|
||||
/** Named method slots observed before class collection need a callable
|
||||
* ABI and shared prototype descriptors instead of call-site specialization. */
|
||||
readonly prototypeMethodAccesses = new Map<string, ts.Node>();
|
||||
/** Well-known symbol methods (`sym:iterator`) the program may replace
|
||||
* through a symbol-keyed write: an instance or prototype property, or a
|
||||
* reflection call. */
|
||||
readonly replacedSymbolMethods = new Set<string>();
|
||||
/** Typed calls through class-backed protocols, by signature; class
|
||||
* dynamic dispatch adds their native arms (class-protocol-dispatch.ts). */
|
||||
readonly protocolDispatches = new Map<string, ProtocolDispatch>();
|
||||
@@ -8653,6 +8661,17 @@ export class Lowerer {
|
||||
* else (including a DIFFERENT union) is left for requireExactShape, which
|
||||
* rejects union mismatches with SC2003. */
|
||||
coerceToExpected(expr: IrExpr, expected: IrType): IrExpr {
|
||||
// A comma expression ending in a unit literal (`return log(), null`):
|
||||
// its effects run first, unchanged, and the unit value takes the
|
||||
// destination's representation (a bare unit literal has none).
|
||||
if (
|
||||
expr.kind === "seqExpr" &&
|
||||
expr.result.kind === "unitLit" &&
|
||||
!typeEquals(expr.type, expected)
|
||||
) {
|
||||
const result = this.coerceToExpected(expr.result, expected);
|
||||
return { ...expr, result, type: result.type };
|
||||
}
|
||||
// A destination that holds undefined takes an unassigned deferred-init
|
||||
// field's slot as is (Node passes the undefined along).
|
||||
if (expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) {
|
||||
@@ -9027,6 +9046,10 @@ export class Lowerer {
|
||||
return this.applyWidthLift(lift, narrowed, expected, expr.loc);
|
||||
}
|
||||
}
|
||||
if (expr.type.kind === "union" && expected.kind === "record") {
|
||||
const lifted = unionArmsWidthLift(this, expr, expected);
|
||||
if (lifted) return lifted;
|
||||
}
|
||||
if (!typeEquals(expr.type, expected)) {
|
||||
const w = this.widthCoerce(expr, expected);
|
||||
if (w) return w;
|
||||
@@ -9364,6 +9387,7 @@ export class Lowerer {
|
||||
* lowering goes through here (via lowerExprExpecting) or calls this
|
||||
* directly when the expression was already lowered. */
|
||||
coerceInto(node: ts.Node, expr: IrExpr, expected: IrType): IrExpr {
|
||||
refuseInferredCallbackDrops(this, node, expr.type, expected);
|
||||
// A union destination can accept the binding's tagged storage directly.
|
||||
// The checker's single-arm flow type can be stale after a callback writes
|
||||
// the binding. Extracting that arm and wrapping it again would discard the
|
||||
@@ -10101,6 +10125,15 @@ export class Lowerer {
|
||||
e = this.returnedPromiseAwait(e);
|
||||
}
|
||||
if (e.kind === "unitLit") return { kind: "return", value: null, loc };
|
||||
// `return c ? f() : g()` over void calls: a void conditional has no
|
||||
// value representation, so its branch selection becomes control flow.
|
||||
if (e.kind === "ternary" && e.type.kind === "void") {
|
||||
const branch = (x: IrExpr): IrStmt =>
|
||||
x.kind === "ternary" && x.type.kind === "void"
|
||||
? { kind: "if", cond: x.cond, then: [branch(x.then)], else_: [branch(x.else_)], loc }
|
||||
: { kind: "exprStmt", expr: x, loc };
|
||||
return { kind: "block", body: [branch(e), { kind: "return", value: null, loc }], loc };
|
||||
}
|
||||
if (e.type.kind === "void") return { kind: "return", value: e, loc };
|
||||
return {
|
||||
kind: "block",
|
||||
@@ -12550,6 +12583,12 @@ export class Lowerer {
|
||||
const callable = this.lowerBuiltinCallableValue(bi, loc);
|
||||
if (callable) return callable;
|
||||
}
|
||||
// node:stream/web's exports are the web stream globals themselves: in
|
||||
// JavaScript sources the same identity token as the global spelling
|
||||
// (`require("stream/web").ReadableStream === globalThis.ReadableStream`).
|
||||
if (bi.module === "stream/web" && isJsSourceFile(expr.getSourceFile())) {
|
||||
return { kind: "strLit", value: `[builtin ${bi.member}]`, type: STRING, loc };
|
||||
}
|
||||
// A bare `fs.constants` read (not one of the baked bits above).
|
||||
if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc);
|
||||
if (builtinModuleFnOf(this, bi.module, bi.member)) {
|
||||
@@ -13100,3 +13139,180 @@ export class Lowerer {
|
||||
return lowerStdlibMethodCall(this, call, access);
|
||||
}
|
||||
}
|
||||
|
||||
/** A union of record layouts flowing into one record slot: a function
|
||||
* returning differently-shaped object literals (`{ ok: false }` and
|
||||
* `{ ok: true, size }`, whose union the checker normalizes to arms with
|
||||
* `size?: undefined`) into its declared result type. Each record arm
|
||||
* width-lifts into the slot on its own tag; unit arms take the stranded
|
||||
* TypeError (the checker excluded them). Null when some record arm does
|
||||
* not lift, or when there is a non-record, non-unit arm. */
|
||||
function unionArmsWidthLift(
|
||||
lowerer: Lowerer,
|
||||
expr: IrExpr,
|
||||
expected: IrType & { kind: "record" },
|
||||
): IrExpr | null {
|
||||
if (expr.type.kind !== "union") return null;
|
||||
const unionId = expr.type.unionId;
|
||||
const arms = lowerer.unions.get(unionId)?.arms ?? [];
|
||||
const lifts: (WidthLift | null)[] = [];
|
||||
let records = 0;
|
||||
for (const arm of arms) {
|
||||
if (isUnitType(arm)) {
|
||||
lifts.push(null);
|
||||
continue;
|
||||
}
|
||||
if (arm.kind !== "record") return null;
|
||||
const lift = lowerer.widthLiftPlan(arm, expected);
|
||||
if (!lift) return null;
|
||||
lifts.push(lift);
|
||||
records++;
|
||||
}
|
||||
if (records < 2) return null;
|
||||
const loc = expr.loc;
|
||||
const local = lowerer.declareHiddenLocal("%unionLift", expr.type);
|
||||
const ref = (): IrExpr => ({ kind: "varRef", localId: local.id, type: expr.type, loc });
|
||||
let result: IrExpr | null = null;
|
||||
for (let tag = arms.length - 1; tag >= 0; tag--) {
|
||||
const lift = lifts[tag];
|
||||
const arm = arms[tag]!;
|
||||
const branch: IrExpr = lift
|
||||
? lowerer.applyWidthLift(
|
||||
lift,
|
||||
{ kind: "unionNarrow", unionId, tag, value: ref(), type: arm, loc },
|
||||
expected,
|
||||
loc,
|
||||
)
|
||||
: nodeThrowExpr(
|
||||
1,
|
||||
"",
|
||||
`${arm.kind === "nullT" ? "null" : "undefined"} is not representable in a '${lowerer.fmt(expected)}' slot (a value narrowed or asserted past the type still held it)`,
|
||||
expected,
|
||||
loc,
|
||||
);
|
||||
result =
|
||||
result === null
|
||||
? branch
|
||||
: {
|
||||
kind: "ternary",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId,
|
||||
tag,
|
||||
negated: false,
|
||||
value: ref(),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: branch,
|
||||
else_: result,
|
||||
type: expected,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "varDecl", localId: local.id, init: expr, loc }],
|
||||
result: result!,
|
||||
type: expected,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A callback passed to a generic call without a return annotation, whose
|
||||
* returns the checker unified into a narrower result type: `trace(name,
|
||||
* async () => (cond ? items : items.map((i) => ({ ...i, sum }))))` infers
|
||||
* `T` from the narrower `Item[]`, though at run time the extended objects
|
||||
* keep `sum`. Converting such a return into the inferred type copies the
|
||||
* records and drops the members the narrower type lacks, which Node keeps
|
||||
* and any later wider view (`Promise<ItemWithSum[]>`) reads back. Refuse
|
||||
* that conversion by name rather than lose the members silently; an
|
||||
* annotation on the callback's return type or on the call's type argument
|
||||
* names a type that keeps them. */
|
||||
function refuseInferredCallbackDrops(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Node,
|
||||
actual: IrType,
|
||||
expected: IrType,
|
||||
): void {
|
||||
if (typeEquals(actual, expected)) return;
|
||||
const structured = (t: IrType): boolean =>
|
||||
t.kind === "record" || t.kind === "array" || t.kind === "promise";
|
||||
if (!structured(actual) || !structured(expected)) return;
|
||||
let site: ts.Node = node;
|
||||
while (yieldsOperand(site)) site = site.parent!;
|
||||
let fn: ts.Node | undefined;
|
||||
if (ts.isReturnStatement(site.parent) && site.parent.expression === site) {
|
||||
fn = site.parent.parent;
|
||||
while (fn && !ts.isFunctionLike(fn)) fn = fn.parent;
|
||||
} else if (ts.isArrowFunction(site.parent) && site.parent.body === site) {
|
||||
fn = site.parent;
|
||||
}
|
||||
if (!fn || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) || fn.type) return;
|
||||
let arg: ts.Node = fn;
|
||||
while (ts.isParenthesizedExpression(arg.parent)) arg = arg.parent;
|
||||
const call = arg.parent;
|
||||
if (!(ts.isCallExpression(call) || ts.isNewExpression(call))) return;
|
||||
if (!call.arguments?.nodes.includes(arg as ts.Expression)) return;
|
||||
const dropped = droppedMembers(lowerer, actual, expected, new Set());
|
||||
if (dropped.length === 0) return;
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
`returning '${lowerer.fmt(actual)}' from a callback whose result the checker infers as the narrower '${lowerer.fmt(expected)}' (the conversion would drop ${dropped.map((d) => `'${d}'`).join(", ")}, which the object keeps at run time — annotate the callback's return type or the call's type argument with a type that declares them)`,
|
||||
);
|
||||
}
|
||||
|
||||
/** True when the value of `node`'s parent can be `node`'s own value:
|
||||
* parentheses, `await`, `satisfies`, a non-null assertion, a conditional's
|
||||
* branch, either operand of `??`, `||` and `&&`, and the right operand of a
|
||||
* comma. */
|
||||
function yieldsOperand(node: ts.Node): boolean {
|
||||
const p = node.parent;
|
||||
if (p === undefined) return false;
|
||||
if (
|
||||
ts.isParenthesizedExpression(p) ||
|
||||
ts.isAwaitExpression(p) ||
|
||||
ts.isSatisfiesExpression(p) ||
|
||||
ts.isNonNullExpression(p)
|
||||
)
|
||||
return true;
|
||||
if (ts.isConditionalExpression(p)) return p.whenTrue === node || p.whenFalse === node;
|
||||
if (ts.isBinaryExpression(p)) {
|
||||
const op = p.operatorToken.kind;
|
||||
if (
|
||||
op === ts.SyntaxKind.QuestionQuestionToken ||
|
||||
op === ts.SyntaxKind.BarBarToken ||
|
||||
op === ts.SyntaxKind.AmpersandAmpersandToken
|
||||
)
|
||||
return true;
|
||||
return op === ts.SyntaxKind.CommaToken && p.right === node;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Member names a structural conversion from `from` into `to` drops:
|
||||
* declared fields of a source record the target record lacks, through
|
||||
* array elements and promise payloads. */
|
||||
function droppedMembers(lowerer: Lowerer, from: IrType, to: IrType, seen: Set<string>): string[] {
|
||||
if (typeEquals(from, to)) return [];
|
||||
if (from.kind === "array" && to.kind === "array")
|
||||
return droppedMembers(lowerer, from.elem, to.elem, seen);
|
||||
if (from.kind === "promise" && to.kind === "promise")
|
||||
return droppedMembers(lowerer, from.inner, to.inner, seen);
|
||||
if (from.kind !== "record" || to.kind !== "record") return [];
|
||||
const key = `${from.shapeId}>${to.shapeId}`;
|
||||
if (seen.has(key)) return [];
|
||||
seen.add(key);
|
||||
const source = lowerer.shapes.get(from.shapeId);
|
||||
const target = lowerer.shapes.get(to.shapeId);
|
||||
if (!source || !target || source.tuple || target.tuple || target.indexValue) return [];
|
||||
const out: string[] = [];
|
||||
for (const f of source.fields) {
|
||||
if (f.name.startsWith("%")) continue;
|
||||
const t = target.fields.find((x) => x.name === f.name);
|
||||
if (!t) out.push(f.name);
|
||||
else out.push(...droppedMembers(lowerer, f.type, t.type, seen).map((n) => `${f.name}.${n}`));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile, isNodeTypesPath, requireSpecOf } from "../program.js";
|
||||
import { ZLIB_CONSTANTS } from "./zlib-constants.js";
|
||||
import { V8_MEMBER_REASON } from "../builtin-modules.js";
|
||||
|
||||
/** zlib stream instances: runtime Transform streams (scr_zlib_stream.c). */
|
||||
const ZLIB_STREAM: IrType = { kind: "object", className: "%Transform" };
|
||||
@@ -1908,6 +1909,18 @@ export const BUILTIN_MODULE_FNS: Record<
|
||||
// special-cased); the keys exist for the same reason net/http's do.
|
||||
dgram: {},
|
||||
dns: {},
|
||||
// node:dns/promises rides the same spoke (lookup's promise form).
|
||||
"dns/promises": {},
|
||||
// node:v8: getHeapStatistics answers allocator-derived figures in
|
||||
// Node's shape (builtins/v8.ts — the entry carries the canonical
|
||||
// shape); every other member fences with the by-design reason
|
||||
// (builtinFenceHintOf).
|
||||
v8: {
|
||||
getHeapStatistics: { fn: "process.memoryUsageTake", params: [], result: VOID },
|
||||
},
|
||||
// node:stream/web re-exports the web stream globals (the same objects);
|
||||
// its members lower through the globals' own paths.
|
||||
"stream/web": {},
|
||||
// node:assert lowers ENTIRELY through the assert spoke (lower-assert.ts
|
||||
// — every call shape is special-cased: optional messages complete, the
|
||||
// comparisons pick per-type libCalls, deep equality synthesizes
|
||||
@@ -2221,14 +2234,22 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
|
||||
kind: "stdlib",
|
||||
name: "Date.parse",
|
||||
fns: ["date.parse"],
|
||||
note: "one date string, using the bounded parser shared with new Date(dateString).getTime()",
|
||||
note: "one date string, using V8's date parser shared with new Date(dateString)",
|
||||
},
|
||||
{
|
||||
id: "stdlib.date.constructor",
|
||||
kind: "stdlib",
|
||||
name: "Date constructor",
|
||||
fns: ["date.newNow", "date.newMs", "date.newString", "date.nativeNew"],
|
||||
note: "zero arguments, or one milliseconds/date-string argument; values are the read-only TimeClip scalar slice",
|
||||
fns: [
|
||||
"date.newNow",
|
||||
"date.newMs",
|
||||
"date.newString",
|
||||
"date.nativeNew",
|
||||
"date.newLocal",
|
||||
"date.nativeNewFields",
|
||||
"date.nativeNewValue",
|
||||
],
|
||||
note: "zero arguments, one milliseconds/date-string argument, or local-time year/month fields; values are the read-only TimeClip scalar slice",
|
||||
},
|
||||
{
|
||||
id: "stdlib.date.UTC",
|
||||
@@ -2803,7 +2824,11 @@ export const FS_CALLBACK_MEMBERS = new Set([
|
||||
* inherited function into the diagnostic text). */
|
||||
export function builtinFenceHintOf(module: string, member: string): string | undefined {
|
||||
const mod = ownEntry(BUILTIN_MODULE_FENCE_HINTS, module);
|
||||
return mod ? ownEntry(mod, member) : undefined;
|
||||
const hint = mod ? ownEntry(mod, member) : undefined;
|
||||
// node:v8's unlowered members observe the engine itself: the by-design
|
||||
// reason, not a pending-surface hint (kept out of the hint table so the
|
||||
// compatibility rows stay by-design rather than refused).
|
||||
return hint === undefined && module === "v8" ? V8_MEMBER_REASON : hint;
|
||||
}
|
||||
|
||||
/** BUILTIN_MODULE_CONSTS with the win32-target overrides applied — the
|
||||
@@ -3433,6 +3458,10 @@ export function stdlibGlobalAliasNameOf(
|
||||
"Number",
|
||||
"String",
|
||||
"Boolean",
|
||||
// Code generation from strings throws Node's EvalError at every call
|
||||
// (code-generation.ts); zod 4 probes `const F = Function; new F("")`.
|
||||
"Function",
|
||||
"eval",
|
||||
].includes(name)
|
||||
? name
|
||||
: null;
|
||||
|
||||
@@ -2,10 +2,11 @@ import * as ts from "../ts7/adapter.js";
|
||||
import { locOf } from "../program.js";
|
||||
import type { IrExpr } from "../../ir/ir.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { accessorCall } from "./lower-classes.js";
|
||||
import { accessorCall, type ClassInfo } from "./lower-classes.js";
|
||||
import { classSymbolKeyOf } from "./symbol-fields.js";
|
||||
|
||||
/** A stable symbol method uses the same completed arguments and virtual
|
||||
/** A stable symbol method (a module-scope symbol, or a well-known symbol
|
||||
* such as `Symbol.iterator`) uses the same completed arguments and virtual
|
||||
* dispatch as a string-named class method. Runtime-selected keys stay on
|
||||
* the checked-property path. */
|
||||
export function lowerClassSymbolMethodCall(
|
||||
@@ -15,12 +16,13 @@ export function lowerClassSymbolMethodCall(
|
||||
const access = call.expression;
|
||||
if (!ts.isElementAccessExpression(access) || access.questionDotToken || call.questionDotToken)
|
||||
return null;
|
||||
const key = classSymbolKeyOf(lowerer, access.argumentExpression);
|
||||
if (!key) return null;
|
||||
const type = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
|
||||
if (type?.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(type.className);
|
||||
const name = info?.symbolMethods?.get(key.identity);
|
||||
const key = classSymbolKeyOf(lowerer, access.argumentExpression);
|
||||
const name = key
|
||||
? info?.symbolMethods?.get(key.identity)
|
||||
: wellKnownSymbolMethod(lowerer, info, access.argumentExpression);
|
||||
if (!name) return null;
|
||||
const method = lowerer.findMethodOn(info ?? null, name);
|
||||
if (!method) return null;
|
||||
@@ -29,3 +31,21 @@ export function lowerClassSymbolMethodCall(
|
||||
const args = lowerer.completeArgs(call.arguments.nodes, method.sig.params, loc, call);
|
||||
return accessorCall(lowerer, type.className, name, receiver, args, method.sig.ret, loc);
|
||||
}
|
||||
|
||||
/** The `sym:<name>` method a well-known symbol key such as `Symbol.iterator`
|
||||
* names, when the class declares it as a method and the program cannot
|
||||
* replace it at run time (an instance property of that name, a prototype
|
||||
* write). */
|
||||
function wellKnownSymbolMethod(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo | undefined,
|
||||
key: ts.Expression,
|
||||
): string | null {
|
||||
while (ts.isParenthesizedExpression(key)) key = key.expression;
|
||||
if (!info || info.def.runtime || !ts.isPropertyAccessExpression(key)) return null;
|
||||
const member = lowerer.stdlibGlobalMember(key, "Symbol");
|
||||
if (member === null) return null;
|
||||
const name = `sym:${member}`;
|
||||
if (info.fields.has(name) || lowerer.replacedSymbolMethods.has(name)) return null;
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export type WidthLift =
|
||||
| { how: "discriminantWrap" }
|
||||
| { how: "discriminantRoute" }
|
||||
| { how: "retag" }
|
||||
| { how: "unitRetag" }
|
||||
| { how: "liftWrap"; tag: number; arm: IrType }
|
||||
| { how: "width" }
|
||||
| { how: "unionWidth" }
|
||||
|
||||
@@ -298,3 +298,21 @@ export const g = import_g.g;
|
||||
degrade: expect.stringContaining("bundled CommonJS modules") as string,
|
||||
});
|
||||
});
|
||||
|
||||
test("a chunk that only re-exports a builtin module becomes that module", () => {
|
||||
const answer = expectSameBehavior(`${keyedHelper}var require_stream = __commonJS({
|
||||
"lib/stream.js"(exports2, module2) {
|
||||
module2.exports = require("stream");
|
||||
}
|
||||
});
|
||||
var require_user = __commonJS({
|
||||
"lib/user.js"(exports2) {
|
||||
var Stream = require_stream();
|
||||
exports2.same = Stream === require("stream");
|
||||
}
|
||||
});
|
||||
console.log(typeof require_stream().PassThrough, require_user().same);
|
||||
`);
|
||||
expect(answer.chunks.map((c) => basename(c.path))).toEqual(["index.js.require_user.cjs"]);
|
||||
expect(answer.text).toContain('typeof require("stream").PassThrough');
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
* beside it). Anything else answers a degrade reason. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import { helperTokens } from "./helper-tokens.js";
|
||||
import { canonicalBuiltinModule } from "./builtin-modules.js";
|
||||
|
||||
export interface BundledChunkSplit {
|
||||
/** The bundle with chunk declarations blanked and calls respelled. */
|
||||
@@ -31,7 +32,7 @@ export interface BundledChunkSplit {
|
||||
chunks: { path: string; source: string }[];
|
||||
}
|
||||
|
||||
interface Edit {
|
||||
export interface Edit {
|
||||
start: number;
|
||||
end: number;
|
||||
text: string;
|
||||
@@ -42,11 +43,11 @@ const CHUNK_SHAPE_DEGRADE =
|
||||
"recognizer cannot verify";
|
||||
|
||||
/** The helper a counter-suffixed bundler name spells (`__commonJS2`). */
|
||||
function helperBase(name: string): string {
|
||||
export function helperBase(name: string): string {
|
||||
return /^__[A-Za-z]+\d+$/.test(name) ? name.replace(/\d+$/, "") : name;
|
||||
}
|
||||
|
||||
function unparen(e: ts.Expression): ts.Expression {
|
||||
export function unparen(e: ts.Expression): ts.Expression {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return e;
|
||||
}
|
||||
@@ -54,7 +55,7 @@ function unparen(e: ts.Expression): ts.Expression {
|
||||
/** 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 {
|
||||
export function isReference(id: ts.Identifier): boolean {
|
||||
const p = id.parent;
|
||||
if (p === undefined) return false;
|
||||
if (ts.isPropertyAccessExpression(p)) return p.expression === id;
|
||||
@@ -83,7 +84,7 @@ function isReference(id: ts.Identifier): boolean {
|
||||
|
||||
/** Names a subtree declares anywhere inside it: variables (destructuring
|
||||
* included), parameters, function and class names, catch bindings. */
|
||||
function declaredNames(root: ts.Node): Set<string> {
|
||||
export 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) => {
|
||||
@@ -272,7 +273,7 @@ class BundleScopes {
|
||||
}
|
||||
|
||||
/** Every binding reference inside a subtree. */
|
||||
function references(root: ts.Node): ts.Identifier[] {
|
||||
export 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)
|
||||
@@ -285,7 +286,7 @@ function references(root: ts.Node): ts.Identifier[] {
|
||||
|
||||
/** True when the reference is written (assignment target, update operand,
|
||||
* loop target, delete operand). */
|
||||
function isWritten(id: ts.Identifier): boolean {
|
||||
export 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)) {
|
||||
@@ -471,7 +472,7 @@ interface TopLevel {
|
||||
}
|
||||
|
||||
/** True when the file opens with a "use strict" directive prologue. */
|
||||
function strictPrologue(stmts: readonly ts.Statement[]): boolean {
|
||||
export 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;
|
||||
@@ -637,10 +638,11 @@ export function splitBundledChunks(
|
||||
return {
|
||||
degrade: `its bundled CommonJS module factory '${r.text}' is used other than as a direct call`,
|
||||
};
|
||||
const forwarded = forwarderSpec(chunk);
|
||||
callEdits.push({
|
||||
start: call.getStart(sf),
|
||||
end: call.getEnd(),
|
||||
text: `require(${JSON.stringify(chunk.spec)})`,
|
||||
text: `require(${JSON.stringify(forwarded ?? chunk.spec)})`,
|
||||
});
|
||||
}
|
||||
for (const name of chunks.keys()) {
|
||||
@@ -652,6 +654,11 @@ export function splitBundledChunks(
|
||||
const out: BundledChunkSplit["chunks"] = [];
|
||||
const bundleEdits: Edit[] = [...callEdits];
|
||||
for (const chunk of chunks.values()) {
|
||||
if (forwarderSpec(chunk) !== null) {
|
||||
// A chunk that only re-exports a builtin module is that module.
|
||||
bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" });
|
||||
continue;
|
||||
}
|
||||
const result = chunkSource(
|
||||
sf,
|
||||
chunk,
|
||||
@@ -670,7 +677,7 @@ export function splitBundledChunks(
|
||||
return { text: applyEdits(source, 0, source.length, bundleEdits), chunks: out };
|
||||
}
|
||||
|
||||
function countDeclarations(sf: ts.SourceFile, name: string): number {
|
||||
export function countDeclarations(sf: ts.SourceFile, name: string): number {
|
||||
let n = 0;
|
||||
ts.walkPreorder(sf, (node) => {
|
||||
if (
|
||||
@@ -694,7 +701,7 @@ function countDeclarations(sf: ts.SourceFile, name: string): number {
|
||||
/** 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 {
|
||||
export 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);
|
||||
@@ -712,6 +719,86 @@ function applyEdits(source: string, from: number, to: number, edits: Edit[]): st
|
||||
return text + source.slice(at, to);
|
||||
}
|
||||
|
||||
/** The builtin module a chunk forwards, or null: a body that is exactly
|
||||
* `module.exports = require("<builtin>")` (readable-stream's
|
||||
* internal/streams/stream.js). Requiring the chunk is then requiring the
|
||||
* builtin itself (same object, and loading a builtin runs no code). */
|
||||
function forwarderSpec(chunk: Chunk): string | null {
|
||||
const moduleParam = chunk.params[1];
|
||||
if (moduleParam === undefined) return null;
|
||||
const stmts = chunk.body.statements.nodes.filter(
|
||||
(st) => !(ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)),
|
||||
);
|
||||
const only = stmts[0];
|
||||
if (stmts.length !== 1 || only === undefined || !ts.isExpressionStatement(only)) return null;
|
||||
const e = unparen(only.expression);
|
||||
if (
|
||||
!ts.isBinaryExpression(e) ||
|
||||
e.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!ts.isPropertyAccessExpression(e.left) ||
|
||||
!ts.isIdentifier(e.left.expression) ||
|
||||
e.left.expression.text !== moduleParam ||
|
||||
e.left.name.text !== "exports"
|
||||
)
|
||||
return null;
|
||||
const call = unparen(e.right);
|
||||
if (
|
||||
!ts.isCallExpression(call) ||
|
||||
!ts.isIdentifier(call.expression) ||
|
||||
call.expression.text !== "require" ||
|
||||
call.arguments.nodes.length !== 1 ||
|
||||
!ts.isStringLiteral(call.arguments.nodes[0]!)
|
||||
)
|
||||
return null;
|
||||
const spec = (call.arguments.nodes[0] as ts.StringLiteral).text;
|
||||
return canonicalBuiltinModule(spec) !== null ? spec : null;
|
||||
}
|
||||
|
||||
/** How code in a factory body (a chunk or a lazy ES-module initializer)
|
||||
* observes the factory's own frame, or null: its receiver, its arguments
|
||||
* object, `super`, a meta property, or (when `returns`) a top-level
|
||||
* return have no module-scope equivalent. */
|
||||
export function factoryFrameUse(body: ts.Block, returns: boolean): string | null {
|
||||
let frameUse: string | null = null;
|
||||
/** The innermost frame boundary between a node and the factory 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 (returns && frameOf(n) === "factory") frameUse = "returns from its top level";
|
||||
return undefined;
|
||||
}
|
||||
if (use !== null && frameOf(n) !== "function") frameUse = use;
|
||||
return undefined;
|
||||
});
|
||||
return frameUse;
|
||||
}
|
||||
|
||||
/** The virtual module text for one chunk, or a degrade reason. */
|
||||
function chunkSource(
|
||||
sf: ts.SourceFile,
|
||||
@@ -741,45 +828,7 @@ function chunkSource(
|
||||
);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
});
|
||||
const frameUse = factoryFrameUse(body, true);
|
||||
if (frameUse !== null) return why(frameUse);
|
||||
|
||||
const edits: Edit[] = callEdits.filter((e) => e.start >= body.pos && e.end <= body.end);
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { nodeFrontendServices } from "./services-node.js";
|
||||
|
||||
const helpers = `var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __esm = (fn, res) => function __init() {
|
||||
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
||||
};
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __create = Object.create;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
`;
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function lower(source: string, file = "/pkg/dist/index.js") {
|
||||
return nodeFrontendServices().lowerEsmInits(source, file);
|
||||
}
|
||||
|
||||
function run(file: string) {
|
||||
const result = spawnSync(process.execPath, [file], { encoding: "utf8" });
|
||||
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
||||
}
|
||||
|
||||
function expectSameBehavior(source: string): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-esm-inits-"));
|
||||
dirs.push(dir);
|
||||
const original = join(dir, "original.js");
|
||||
const lowered = join(dir, "index.js");
|
||||
writeFileSync(original, source);
|
||||
const result = lower(source, lowered);
|
||||
expect(typeof result).toBe("string");
|
||||
writeFileSync(lowered, result as string);
|
||||
const oracle = run(original);
|
||||
expect(oracle.status).toBe(0);
|
||||
expect(run(lowered)).toEqual(oracle);
|
||||
return result as string;
|
||||
}
|
||||
|
||||
const lazyModule = `
|
||||
function helper(x) { COUNT++; return "[" + x + "]"; }
|
||||
var COUNT;
|
||||
var init_helper = __esm({
|
||||
"src/helper.ts"() {
|
||||
console.log("helper init");
|
||||
COUNT = 0;
|
||||
}
|
||||
});
|
||||
var lazy_exports = {};
|
||||
__export(lazy_exports, {
|
||||
VALUE: () => VALUE,
|
||||
value: () => value
|
||||
});
|
||||
function value() {
|
||||
return VALUE + ":" + COUNT + ":" + import_os.default.EOL.length;
|
||||
}
|
||||
var path, import_os, VALUE;
|
||||
var init_lazy = __esm({
|
||||
"src/lazy.ts"() {
|
||||
path = __toESM(require("path"));
|
||||
import_os = __toESM(require("os"));
|
||||
init_helper();
|
||||
console.log("lazy init");
|
||||
VALUE = helper(path.basename("/a/b.txt"));
|
||||
}
|
||||
});
|
||||
`;
|
||||
|
||||
test("initializers run once, at their first call, and namespaces read live bindings", () => {
|
||||
const lowered = expectSameBehavior(`"use strict";
|
||||
${helpers}${lazyModule}
|
||||
console.log("start");
|
||||
function load() {
|
||||
const lazy = (init_lazy(), __toCommonJS(lazy_exports));
|
||||
return lazy.value() + " " + lazy.VALUE;
|
||||
}
|
||||
console.log(load());
|
||||
COUNT = 5;
|
||||
console.log(load(), (init_lazy(), __toCommonJS(lazy_exports)).value());
|
||||
`);
|
||||
expect(lowered).not.toContain("__esm({");
|
||||
expect(lowered).not.toContain("import_os");
|
||||
expect(lowered).toContain('(require("os")).EOL');
|
||||
});
|
||||
|
||||
test("calls of exported functions that never read their own `this` still lower", () => {
|
||||
const lowered = expectSameBehavior(`${helpers}var t_exports = {};
|
||||
__export(t_exports, { plain: () => plain, inner: () => inner });
|
||||
function plain(x) { return "plain " + x; }
|
||||
function inner() {
|
||||
function own() { return typeof this; }
|
||||
return "inner " + own.call(1);
|
||||
}
|
||||
var init_t = __esm({ "t.ts"() {} });
|
||||
const t = (init_t(), __toCommonJS(t_exports));
|
||||
console.log(t.plain(1), (t.plain)(2), t.inner());
|
||||
`);
|
||||
expect(lowered).not.toContain("__toCommonJS(t_exports)");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["a direct call", `NS.who()`],
|
||||
["a parenthesized callee", `(NS.who)()`],
|
||||
["a tagged template", "NS.who`x`"],
|
||||
["a call through a local", `(() => { const m = NS; return m.who(); })()`],
|
||||
["an arrow inside the function", `NS.arrow()()`],
|
||||
])("%s that passes the namespace as `this` keeps the package refused", (_, use) => {
|
||||
const result = lower(`${helpers}var w_exports = {};
|
||||
__export(w_exports, { who: () => who, arrow: () => arrow });
|
||||
function who() { return this.__esModule; }
|
||||
function arrow() { return () => this; }
|
||||
var init_w = __esm({ "w.ts"() {} });
|
||||
console.log(${use.replaceAll("NS", "(init_w(), __toCommonJS(w_exports))")});
|
||||
`);
|
||||
expect(result).toEqual({
|
||||
degrade: expect.stringContaining(
|
||||
"calls only of exported functions that do not read 'this'",
|
||||
) as string,
|
||||
});
|
||||
});
|
||||
|
||||
test("an export that reads `this` in a parameter's default value keeps the package refused", () => {
|
||||
const result = lower(`${helpers}var p_exports = {};
|
||||
__export(p_exports, { who: () => who });
|
||||
function who(value = this.__esModule) { return value; }
|
||||
var init_p = __esm({ "p.ts"() {} });
|
||||
console.log((init_p(), __toCommonJS(p_exports)).who());
|
||||
`);
|
||||
expect(result).toEqual({
|
||||
degrade: expect.stringContaining(
|
||||
"calls only of exported functions that do not read 'this'",
|
||||
) as string,
|
||||
});
|
||||
});
|
||||
|
||||
test("minified initializers sharing a var statement lower the same way", () => {
|
||||
expectSameBehavior(
|
||||
helpers +
|
||||
`var x_exports={};__export(x_exports,{v:()=>v});var v,import_path,init_x=__esm({"x.ts"(){"use strict";import_path=__toESM(require("path")),v=import_path.default.sep}});` +
|
||||
`console.log((init_x(),__toCommonJS(x_exports)).v);`,
|
||||
);
|
||||
});
|
||||
|
||||
test("memoized lazy loaders sharing a var statement lower together", () => {
|
||||
const lowered = expectSameBehavior(`${helpers}var a_exports = {};
|
||||
__export(a_exports, { v: () => v });
|
||||
var v, init_a = __esm({ "a.ts"() { console.log("a init"); v = 1; } });
|
||||
var b_exports = {};
|
||||
__export(b_exports, { default: () => w });
|
||||
var w, init_b = __esm({ "b.ts"() { console.log("b init"); w = 2; } });
|
||||
var nsA, loadA = () => nsA ??= (init_a(), __toCommonJS(a_exports)), nsB, loadB = () => nsB ??= __toESM((init_b(), __toCommonJS(b_exports)));
|
||||
console.log(loadA().v, loadB().default, loadA().v);
|
||||
`);
|
||||
expect(lowered).not.toContain("loadA");
|
||||
expect(lowered).not.toContain("loadB");
|
||||
});
|
||||
|
||||
test("calls through a memoized lazy loader keep the receiver rule", () => {
|
||||
const source = (use: string): string => `${helpers}var w_exports = {};
|
||||
__export(w_exports, { who: () => who, plain: () => plain });
|
||||
function who() { return this.__esModule; }
|
||||
function plain(x) { return "plain " + x; }
|
||||
var init_w = __esm({ "w.ts"() {} });
|
||||
var ns, load = () => ns ??= (init_w(), __toCommonJS(w_exports));
|
||||
console.log(${use});
|
||||
`;
|
||||
expect(lower(source("load().who()"))).toEqual({
|
||||
degrade: expect.stringContaining(
|
||||
"calls only of exported functions that do not read 'this'",
|
||||
) as string,
|
||||
});
|
||||
const lowered = expectSameBehavior(source("load().plain(1), (load().plain)(2)"));
|
||||
expect(lowered).not.toContain("load()");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["is asynchronous", `var init_a = __esm({ async "a.ts"() { await 0; } });\ninit_a();`],
|
||||
[
|
||||
"is used other than as a statement call",
|
||||
`var init_a = __esm({ "a.ts"() {} });\nvar r = init_a();`,
|
||||
],
|
||||
[
|
||||
"reads its factory's receiver",
|
||||
`var init_a = __esm({ "a.ts"() { console.log(this); } });\ninit_a();`,
|
||||
],
|
||||
[
|
||||
"memoized lazy loader 'load' returns the namespace of a bundled ES module as a value",
|
||||
`var a_exports = {};\n__export(a_exports, { v: () => v });\nvar v, init_a = __esm({ "a.ts"() { v = 1; } });\nvar ns, load = () => ns ??= __toESM((init_a(), __toCommonJS(a_exports)));\nload();`,
|
||||
],
|
||||
[
|
||||
"reads the namespace of a bundled ES module ('a_exports')",
|
||||
`var a_exports = {};\n__export(a_exports, { v: () => v });\nvar v, init_a = __esm({ "a.ts"() { v = 1; } });\nconsole.log(Object.keys((init_a(), __toCommonJS(a_exports))));`,
|
||||
],
|
||||
])("an initializer that %s keeps the package refused", (reason, code) => {
|
||||
expect(lower(`${helpers}${code}\n`)).toEqual({
|
||||
degrade: expect.stringContaining(reason) as string,
|
||||
});
|
||||
});
|
||||
|
||||
test("files without __esm initializers and ES-module bundles are left alone", () => {
|
||||
expect(lower("exports.x = 1;\n")).toBeNull();
|
||||
expect(
|
||||
lower(`${helpers}var init_a = __esm({ "a.ts"() {} });\ninit_a();\nexport {};\n`),
|
||||
).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,825 @@
|
||||
/* Lazily initialized ES modules in a CommonJS bundle. esbuild scope-hoists
|
||||
* every ES module it bundles into the bundle's own scope; one that some
|
||||
* code require()s instead of importing runs from a lazy initializer,
|
||||
*
|
||||
* var COUNT, import_fs;
|
||||
* var init_x = __esm({ "src/x.ts"() { import_fs = __toESM(require("fs")); COUNT = 0; } });
|
||||
*
|
||||
* and the requiring site reads its namespace as
|
||||
* `(init_x(), __toCommonJS(x_exports))`. `init_x()` runs the body once (a
|
||||
* call during the body answers immediately). The pass below keeps that
|
||||
* shape in the bundle's scope and respells only what has no static
|
||||
* representation:
|
||||
*
|
||||
* - the initializer becomes a run-once closure over the same body;
|
||||
* - an interop binding the body assigns once from require() (`import_fs`
|
||||
* above) becomes that require at the assignment's position, and its
|
||||
* member reads become member reads of the required module, exactly
|
||||
* like the top-level interop erasure (npm-static-rewrite-syntax.ts);
|
||||
* - a namespace read only through member reads becomes an object
|
||||
* literal of getters over the module's live bindings (what
|
||||
* __toCommonJS copies, minus the non-enumerable __esModule mark no
|
||||
* member read can reach).
|
||||
*
|
||||
* The respelled interop reads resolve the required module even if they
|
||||
* ran before the initializer, where Node would read a member of
|
||||
* undefined and throw; esbuild only reaches them through the module's
|
||||
* own code, which runs after its initializer. Shapes outside the
|
||||
* recognized forms answer a degrade reason. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import { bareRequireSpecOf } from "./cjs-syntax.js";
|
||||
import { helperTokens } from "./helper-tokens.js";
|
||||
import {
|
||||
applyEdits,
|
||||
countDeclarations,
|
||||
declaredNames,
|
||||
factoryFrameUse,
|
||||
helperBase,
|
||||
isWritten,
|
||||
references,
|
||||
unparen,
|
||||
type Edit,
|
||||
} from "./npm-static-bundled-chunks.js";
|
||||
|
||||
export interface EsmInitHost {
|
||||
requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean;
|
||||
}
|
||||
|
||||
const ESM_SHAPE_DEGRADE =
|
||||
"its shipped JS initializes bundled ES modules through the __esm helper in a shape the " +
|
||||
"recognizer cannot verify";
|
||||
|
||||
/* The recognized __esm forms over its parameter names (FN, RES) and the
|
||||
* own-key reader (KEYS): keyed `(FN, RES) => function [name]() { return
|
||||
* <keyed>; }` with a one-method object argument, and arrow
|
||||
* `(FN, RES) => () => (<arrow>)` with a function argument. */
|
||||
const KEYED_RETURN = "FN && (RES = (0, FN[KEYS(FN)[0]])(FN = 0)), RES";
|
||||
const ARROW_BODY = "(FN && (RES = FN(FN = 0)), RES)";
|
||||
|
||||
function template(text: string, fn: string, res: string, keys: string): string | null {
|
||||
return helperTokens(
|
||||
text
|
||||
.replace(/\bFN\b/g, fn)
|
||||
.replace(/\bRES\b/g, res)
|
||||
.replace(/\bKEYS\b/g, keys),
|
||||
);
|
||||
}
|
||||
|
||||
function esmHelperForm(
|
||||
init: ts.Expression,
|
||||
sf: ts.SourceFile,
|
||||
ownNames: (name: string) => boolean,
|
||||
): "keyed" | "arrow" | null {
|
||||
const fn = unparen(init);
|
||||
if (!ts.isArrowFunction(fn) || fn.parameters.nodes.length !== 2 || ts.isBlock(fn.body))
|
||||
return null;
|
||||
const [p0, p1] = fn.parameters.nodes;
|
||||
if (
|
||||
!p0 ||
|
||||
!p1 ||
|
||||
!ts.isIdentifier(p0.name) ||
|
||||
!ts.isIdentifier(p1.name) ||
|
||||
p0.initializer ||
|
||||
p1.initializer ||
|
||||
p0.name.text === p1.name.text
|
||||
)
|
||||
return null;
|
||||
const [f, r] = [p0.name.text, p1.name.text];
|
||||
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 || !ts.isReturnStatement(ret) || !ret.expression) return null;
|
||||
const text = ret.expression.getText(sf);
|
||||
const keys = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(text)?.[1];
|
||||
if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null;
|
||||
return helperTokens(text) === template(KEYED_RETURN, f, r, 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, f, r, "") ? "arrow" : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Pads the given declarators out of their statements (separating commas
|
||||
* included; a statement whose declarators all go pads whole). */
|
||||
function removeDeclarators(sf: ts.SourceFile, dead: ReadonlySet<ts.VariableDeclaration>): Edit[] {
|
||||
const edits: Edit[] = [];
|
||||
const stmts = new Set<ts.VariableStatement>();
|
||||
for (const d of dead) {
|
||||
const stmt = d.parent?.parent;
|
||||
if (stmt !== undefined && ts.isVariableStatement(stmt)) stmts.add(stmt);
|
||||
}
|
||||
for (const stmt of stmts) {
|
||||
const decls = stmt.declarationList.declarations.nodes;
|
||||
const deadAt = decls.map((d) => dead.has(d));
|
||||
if (deadAt.every((x) => x)) {
|
||||
edits.push({ start: stmt.getStart(sf), end: stmt.getEnd(), text: "" });
|
||||
continue;
|
||||
}
|
||||
const lastLive = deadAt.lastIndexOf(false);
|
||||
for (let i = 0; i < decls.length; i++) {
|
||||
if (!deadAt[i]) continue;
|
||||
edits.push(
|
||||
i < lastLive
|
||||
? { start: decls[i]!.getStart(sf), end: decls[i + 1]!.getStart(sf), text: "" }
|
||||
: { start: decls[lastLive]!.getEnd(), end: decls[i]!.getEnd(), text: "" },
|
||||
);
|
||||
}
|
||||
}
|
||||
return edits;
|
||||
}
|
||||
|
||||
/** The elements of an expression statement's comma chain, in order. */
|
||||
function commaElements(e: ts.Expression): ts.Expression[] {
|
||||
const out: ts.Expression[] = [];
|
||||
const visit = (x: ts.Expression): void => {
|
||||
if (ts.isBinaryExpression(x) && x.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
||||
visit(x.left);
|
||||
visit(x.right);
|
||||
} else out.push(x);
|
||||
};
|
||||
visit(e);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** True when a call's result is discarded: an expression statement or a
|
||||
* non-final comma operand (parentheses included). */
|
||||
function resultUnused(call: ts.Expression): boolean {
|
||||
let child: ts.Node = call;
|
||||
let p = call.parent;
|
||||
while (p !== undefined && ts.isParenthesizedExpression(p)) {
|
||||
child = p;
|
||||
p = p.parent;
|
||||
}
|
||||
if (p === undefined) return false;
|
||||
if (ts.isExpressionStatement(p)) return true;
|
||||
if (ts.isBinaryExpression(p) && p.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
||||
if (p.left === child) return true;
|
||||
return resultUnused(p);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True when a member access is a plain read (not written, deleted or
|
||||
* updated). */
|
||||
function plainRead(access: ts.Expression): boolean {
|
||||
const p = access.parent;
|
||||
if (p === undefined) return true;
|
||||
if (ts.isDeleteExpression(p)) return false;
|
||||
if (
|
||||
ts.isBinaryExpression(p) &&
|
||||
p.left === access &&
|
||||
p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
p.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
return false;
|
||||
if (
|
||||
(ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) &&
|
||||
(p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** True when a member read is the callee of a call or a tag, which receives
|
||||
* the object it was read from as `this`. */
|
||||
function calledWithReceiver(access: ts.Expression): boolean {
|
||||
let callee: ts.Node = access;
|
||||
while (callee.parent !== undefined && ts.isParenthesizedExpression(callee.parent))
|
||||
callee = callee.parent;
|
||||
const p = callee.parent;
|
||||
return (
|
||||
p !== undefined &&
|
||||
((ts.isCallExpression(p) && p.expression === callee) ||
|
||||
(ts.isTaggedTemplateExpression(p) && p.tag === callee))
|
||||
);
|
||||
}
|
||||
|
||||
/** True when a function reads its own `this`: in its body or a parameter's
|
||||
* default value, or in an arrow function inside them. */
|
||||
function readsThis(fn: ts.FunctionDeclaration): boolean {
|
||||
let found = false;
|
||||
if (fn.body === undefined) return false;
|
||||
const visit = (n: ts.Node): "stop" | "skip" | undefined => {
|
||||
if (found) return "stop";
|
||||
if (
|
||||
ts.isFunctionDeclaration(n) ||
|
||||
ts.isFunctionExpression(n) ||
|
||||
ts.isMethodDeclaration(n) ||
|
||||
ts.isGetAccessorDeclaration(n) ||
|
||||
ts.isSetAccessorDeclaration(n) ||
|
||||
ts.isConstructorDeclaration(n) ||
|
||||
ts.isClassDeclaration(n) ||
|
||||
ts.isClassExpression(n)
|
||||
)
|
||||
return "skip";
|
||||
if (n.kind === ts.SyntaxKind.ThisKeyword) found = true;
|
||||
return undefined;
|
||||
};
|
||||
for (const parameter of fn.parameters.nodes) ts.walkPreorder(parameter, visit);
|
||||
ts.walkPreorder(fn.body, visit);
|
||||
return found;
|
||||
}
|
||||
|
||||
/** The `__export(T, { name: () => value, … })` getter table of T, or null. */
|
||||
function exportTableOf(
|
||||
call: ts.CallExpression,
|
||||
): { target: string; entries: [string, ts.Expression][] } | null {
|
||||
if (!ts.isIdentifier(call.expression) || helperBase(call.expression.text) !== "__export")
|
||||
return null;
|
||||
const [target, table] = call.arguments.nodes;
|
||||
if (
|
||||
call.arguments.nodes.length !== 2 ||
|
||||
!target ||
|
||||
!table ||
|
||||
!ts.isIdentifier(target) ||
|
||||
!ts.isObjectLiteralExpression(table)
|
||||
)
|
||||
return null;
|
||||
const entries: [string, ts.Expression][] = [];
|
||||
for (const prop of table.properties.nodes) {
|
||||
if (!ts.isPropertyAssignment(prop)) return null;
|
||||
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) return null;
|
||||
const getter = unparen(prop.initializer);
|
||||
if (!ts.isArrowFunction(getter) || getter.parameters.nodes.length !== 0) return null;
|
||||
if (ts.isBlock(getter.body)) return null;
|
||||
entries.push([prop.name.text, getter.body]);
|
||||
}
|
||||
return { target: target.text, entries };
|
||||
}
|
||||
|
||||
/** Lowers a CommonJS bundle's __esm initializers (see the header). Null:
|
||||
* nothing to lower. */
|
||||
export function lowerBundledEsmInits(
|
||||
sf: ts.SourceFile,
|
||||
filePath: string,
|
||||
host: EsmInitHost,
|
||||
): string | { degrade: string } | null {
|
||||
const source = sf.text;
|
||||
if (!source.includes("__esm")) return null;
|
||||
if (!filePath.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
|
||||
|
||||
interface Top {
|
||||
stmt: ts.Statement;
|
||||
node: ts.VariableDeclaration | ts.FunctionDeclaration;
|
||||
}
|
||||
const top = new Map<string, Top>();
|
||||
const duplicate = new Set<string>();
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
const add = (name: string, node: Top["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 helpers = [...top.keys()].filter((n) => helperBase(n) === "__esm");
|
||||
if (helpers.length === 0) return null;
|
||||
|
||||
const allRefs = references(sf);
|
||||
const writes = new Map<string, number>();
|
||||
for (const r of allRefs) if (isWritten(r)) writes.set(r.text, (writes.get(r.text) ?? 0) + 1);
|
||||
const declaredAnywhere = declaredNames(sf);
|
||||
if (declaredAnywhere.has("Object") || writes.has("Object")) return { degrade: ESM_SHAPE_DEGRADE };
|
||||
const stable = (name: string): boolean => !duplicate.has(name) && !writes.has(name);
|
||||
const ownNames = (name: string): boolean => {
|
||||
const t = top.get(name);
|
||||
const init =
|
||||
t !== undefined && ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined;
|
||||
return (
|
||||
init !== undefined &&
|
||||
stable(name) &&
|
||||
helperTokens(init.getText(sf)) === helperTokens("Object.getOwnPropertyNames")
|
||||
);
|
||||
};
|
||||
const forms = new Map<string, "keyed" | "arrow">();
|
||||
for (const name of helpers) {
|
||||
const t = top.get(name)!;
|
||||
const init = ts.isVariableDeclaration(t.node) ? t.node.initializer : undefined;
|
||||
const form = init !== undefined && stable(name) ? esmHelperForm(init, sf, ownNames) : null;
|
||||
if (form === null) return { degrade: ESM_SHAPE_DEGRADE };
|
||||
forms.set(name, form);
|
||||
}
|
||||
|
||||
// The initializers: `NAME = HELPER(factory)` declarators.
|
||||
interface Init {
|
||||
name: string;
|
||||
call: ts.CallExpression;
|
||||
body: ts.Block;
|
||||
}
|
||||
const inits = new Map<string, Init>();
|
||||
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;
|
||||
if (call.arguments.nodes.length !== 1 || !stable(name)) return { degrade: ESM_SHAPE_DEGRADE };
|
||||
let factory: ts.Node = call.arguments.nodes[0]!;
|
||||
if (form === "keyed") {
|
||||
if (!ts.isObjectLiteralExpression(factory) || factory.properties.nodes.length !== 1)
|
||||
return { degrade: ESM_SHAPE_DEGRADE };
|
||||
factory = factory.properties.nodes[0]!;
|
||||
if (!ts.isMethodDeclaration(factory) || ts.isComputedPropertyName(factory.name))
|
||||
return { degrade: ESM_SHAPE_DEGRADE };
|
||||
} else {
|
||||
factory = unparen(factory as ts.Expression);
|
||||
if (!ts.isArrowFunction(factory) && !ts.isFunctionExpression(factory))
|
||||
return { degrade: ESM_SHAPE_DEGRADE };
|
||||
}
|
||||
const fn = factory as ts.MethodDeclaration | ts.ArrowFunction | ts.FunctionExpression;
|
||||
if (fn.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword))
|
||||
return {
|
||||
degrade: `its bundled ES module initializer '${name}' is asynchronous (top-level await), which the lazy initialization rewrite does not cover`,
|
||||
};
|
||||
if (
|
||||
fn.body === undefined ||
|
||||
!ts.isBlock(fn.body) ||
|
||||
fn.asteriskToken !== undefined ||
|
||||
(fn.modifiers?.nodes.length ?? 0) > 0 ||
|
||||
fn.parameters.nodes.length > 0
|
||||
)
|
||||
return { degrade: ESM_SHAPE_DEGRADE };
|
||||
const frame = factoryFrameUse(fn.body, false);
|
||||
if (frame !== null) return { degrade: `its bundled ES module initializer '${name}' ${frame}` };
|
||||
inits.set(name, { name, call, body: fn.body });
|
||||
}
|
||||
if (inits.size === 0) return null;
|
||||
|
||||
const edits: Edit[] = [];
|
||||
// Every initializer reference is a direct call whose result is unused.
|
||||
for (const r of allRefs) {
|
||||
if (!inits.has(r.text)) continue;
|
||||
const call = r.parent;
|
||||
if (
|
||||
call === undefined ||
|
||||
!ts.isCallExpression(call) ||
|
||||
call.expression !== r ||
|
||||
call.arguments.nodes.length !== 0 ||
|
||||
!resultUnused(call)
|
||||
)
|
||||
return {
|
||||
degrade: `its bundled ES module initializer '${r.text}' is used other than as a statement call`,
|
||||
};
|
||||
}
|
||||
for (const name of inits.keys()) {
|
||||
if (countDeclarations(sf, name) !== 1) return { degrade: ESM_SHAPE_DEGRADE };
|
||||
}
|
||||
|
||||
/* Interop bindings an initializer assigns once from require(). */
|
||||
const refsByName = new Map<string, ts.Identifier[]>();
|
||||
for (const r of allRefs) {
|
||||
const list = refsByName.get(r.text);
|
||||
if (list === undefined) refsByName.set(r.text, [r]);
|
||||
else list.push(r);
|
||||
}
|
||||
const deadDeclarators = new Set<ts.VariableDeclaration>();
|
||||
for (const init of inits.values()) {
|
||||
for (const stmt of init.body.statements.nodes) {
|
||||
if (!ts.isExpressionStatement(stmt)) continue;
|
||||
for (const el of commaElements(stmt.expression)) {
|
||||
if (
|
||||
!ts.isBinaryExpression(el) ||
|
||||
el.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!ts.isIdentifier(el.left)
|
||||
)
|
||||
continue;
|
||||
const name = el.left.text;
|
||||
const decl = top.get(name)?.node;
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
decl.initializer !== undefined ||
|
||||
duplicate.has(name) ||
|
||||
writes.get(name) !== 1 ||
|
||||
countDeclarations(sf, name) !== 1
|
||||
)
|
||||
continue;
|
||||
let rhs = unparen(el.right);
|
||||
let isNodeMode = false;
|
||||
let interop = false;
|
||||
if (
|
||||
ts.isCallExpression(rhs) &&
|
||||
ts.isIdentifier(rhs.expression) &&
|
||||
helperBase(rhs.expression.text) === "__toESM"
|
||||
) {
|
||||
const [arg, mode] = rhs.arguments.nodes;
|
||||
if (!arg || rhs.arguments.nodes.length > 2) continue;
|
||||
if (mode !== undefined) {
|
||||
if (ts.isNumericLiteral(mode)) isNodeMode = Number(mode.text) !== 0;
|
||||
else if (mode.kind === ts.SyntaxKind.TrueKeyword) isNodeMode = true;
|
||||
else if (mode.kind !== ts.SyntaxKind.FalseKeyword) continue;
|
||||
}
|
||||
rhs = unparen(arg);
|
||||
interop = true;
|
||||
}
|
||||
const spec = bareRequireSpecOf(rhs);
|
||||
if (spec === null) continue;
|
||||
const req = rhs.getText(sf);
|
||||
const defaultIsModule =
|
||||
interop && (isNodeMode || !host.requireTargetEsModuleStamped(filePath, spec));
|
||||
// Every other reference must be a plain member read.
|
||||
const uses: Edit[] = [];
|
||||
let ok = true;
|
||||
for (const r of refsByName.get(name) ?? []) {
|
||||
if (r === el.left) continue;
|
||||
const access = r.parent;
|
||||
if (
|
||||
access === undefined ||
|
||||
!ts.isPropertyAccessExpression(access) ||
|
||||
access.expression !== r ||
|
||||
!plainRead(access)
|
||||
) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (defaultIsModule && access.name.text === "default")
|
||||
uses.push({ start: access.getStart(sf), end: access.getEnd(), text: `(${req})` });
|
||||
else uses.push({ start: r.getStart(sf), end: r.getEnd(), text: req });
|
||||
}
|
||||
if (!ok) continue;
|
||||
edits.push({ start: el.getStart(sf), end: el.getEnd(), text: req }, ...uses);
|
||||
deadDeclarators.add(decl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Namespaces of lazily initialized modules read through members. */
|
||||
let mainTarget: string | null = null;
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue;
|
||||
const e = stmt.expression;
|
||||
const rhs = e.right;
|
||||
if (
|
||||
e.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(e.left) &&
|
||||
ts.isIdentifier(e.left.expression) &&
|
||||
e.left.expression.text === "module" &&
|
||||
e.left.name.text === "exports" &&
|
||||
ts.isCallExpression(rhs) &&
|
||||
ts.isIdentifier(rhs.expression) &&
|
||||
helperBase(rhs.expression.text) === "__toCommonJS" &&
|
||||
rhs.arguments.nodes.length === 1 &&
|
||||
ts.isIdentifier(rhs.arguments.nodes[0]!)
|
||||
)
|
||||
mainTarget = (rhs.arguments.nodes[0] as ts.Identifier).text;
|
||||
}
|
||||
const tables = new Map<string, { stmt: ts.Statement; entries: [string, ts.Expression][] }>();
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (!ts.isExpressionStatement(stmt) || !ts.isCallExpression(stmt.expression)) continue;
|
||||
const table = exportTableOf(stmt.expression);
|
||||
if (table !== null && table.target !== mainTarget) {
|
||||
if (tables.has(table.target)) return { degrade: ESM_SHAPE_DEGRADE };
|
||||
tables.set(table.target, { stmt, entries: table.entries });
|
||||
}
|
||||
}
|
||||
const lowered = new Set<string>();
|
||||
/** Some binding between the use site and the top level rebinds a name
|
||||
* the table's getters read. */
|
||||
const shadowed = (site: ts.Node, entries: readonly [string, ts.Expression][]): boolean => {
|
||||
const read = new Set<string>();
|
||||
for (const [, value] of entries) for (const x of references(value)) read.add(x.text);
|
||||
for (let a = site.parent; a !== undefined && !ts.isSourceFile(a); a = a.parent) {
|
||||
if (
|
||||
ts.isFunctionLike(a) ||
|
||||
ts.isBlock(a) ||
|
||||
ts.isClassDeclaration(a) ||
|
||||
ts.isClassExpression(a)
|
||||
) {
|
||||
for (const n of declaredNames(a)) if (read.has(n)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const getterText = (value: ts.Expression): string =>
|
||||
applyEdits(source, value.getStart(sf), value.getEnd(), edits);
|
||||
/** A call through the namespace (`ns.fn()`, `L().fn()`) passes the object
|
||||
* the read came from as `this`; the lowered object differs from Node's
|
||||
* module object (`__esModule`, property attributes), so it must stay
|
||||
* unobservable: the callee is a top-level function that never reads
|
||||
* `this`. */
|
||||
const receiverHidden = (values: ReadonlyMap<string, ts.Expression>, name: string): boolean => {
|
||||
const value = values.get(name);
|
||||
if (value === undefined || !ts.isIdentifier(value) || !stable(value.text)) return false;
|
||||
const decl = top.get(value.text)?.node;
|
||||
return decl !== undefined && ts.isFunctionDeclaration(decl) && !readsThis(decl);
|
||||
};
|
||||
|
||||
/* Memoized lazy loaders of those namespaces, `L = () => M ??= NS` (a
|
||||
* bundle plugin's lazy re-exports): every `L().name` becomes the
|
||||
* initializer call followed by the export's live read, so the first
|
||||
* access still initializes the module, once, and later accesses read
|
||||
* the binding. */
|
||||
const loaderSpans: { start: number; end: number }[] = [];
|
||||
for (const [name, t] of top) {
|
||||
const d = t.node;
|
||||
if (!ts.isVariableDeclaration(d) || d.initializer === undefined) continue;
|
||||
const fn = unparen(d.initializer);
|
||||
if (!ts.isArrowFunction(fn) || fn.parameters.nodes.length !== 0 || ts.isBlock(fn.body))
|
||||
continue;
|
||||
const memoize = unparen(fn.body);
|
||||
if (
|
||||
!ts.isBinaryExpression(memoize) ||
|
||||
(memoize.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionEqualsToken &&
|
||||
memoize.operatorToken.kind !== ts.SyntaxKind.BarBarEqualsToken) ||
|
||||
!ts.isIdentifier(memoize.left)
|
||||
)
|
||||
continue;
|
||||
let w = unparen(memoize.right);
|
||||
let interop = false;
|
||||
let nodeMode = false;
|
||||
if (
|
||||
ts.isCallExpression(w) &&
|
||||
ts.isIdentifier(w.expression) &&
|
||||
helperBase(w.expression.text) === "__toESM"
|
||||
) {
|
||||
const [arg, mode] = w.arguments.nodes;
|
||||
if (!arg || w.arguments.nodes.length > 2) continue;
|
||||
if (mode !== undefined) {
|
||||
if (ts.isNumericLiteral(mode)) nodeMode = Number(mode.text) !== 0;
|
||||
else if (mode.kind === ts.SyntaxKind.TrueKeyword) nodeMode = true;
|
||||
else if (mode.kind !== ts.SyntaxKind.FalseKeyword) continue;
|
||||
}
|
||||
interop = true;
|
||||
w = unparen(arg);
|
||||
}
|
||||
let initCall: ts.CallExpression | null = null;
|
||||
if (ts.isBinaryExpression(w) && w.operatorToken.kind === ts.SyntaxKind.CommaToken) {
|
||||
const left = unparen(w.left);
|
||||
if (
|
||||
!ts.isCallExpression(left) ||
|
||||
!ts.isIdentifier(left.expression) ||
|
||||
!inits.has(left.expression.text) ||
|
||||
left.arguments.nodes.length !== 0
|
||||
)
|
||||
continue;
|
||||
initCall = left;
|
||||
w = unparen(w.right);
|
||||
}
|
||||
if (
|
||||
!ts.isCallExpression(w) ||
|
||||
!ts.isIdentifier(w.expression) ||
|
||||
helperBase(w.expression.text) !== "__toCommonJS" ||
|
||||
w.arguments.nodes.length !== 1 ||
|
||||
!ts.isIdentifier(w.arguments.nodes[0]!)
|
||||
)
|
||||
continue;
|
||||
const target = (w.arguments.nodes[0] as ts.Identifier).text;
|
||||
const table = tables.get(target);
|
||||
if (table === undefined) continue;
|
||||
const loaderWhy = {
|
||||
degrade: `its memoized lazy loader '${name}' returns the namespace of a bundled ES module as a value, a module object the static rewrite cannot represent (only member reads of its result compile, and calls only of exported functions that do not read 'this')`,
|
||||
};
|
||||
const memo = memoize.left.text;
|
||||
const memoDecl = top.get(memo)?.node;
|
||||
if (
|
||||
memoDecl === undefined ||
|
||||
!ts.isVariableDeclaration(memoDecl) ||
|
||||
memoDecl.initializer !== undefined ||
|
||||
writes.get(memo) !== 1 ||
|
||||
countDeclarations(sf, memo) !== 1 ||
|
||||
(refsByName.get(memo) ?? []).some((x) => x !== memoize.left) ||
|
||||
writes.has(name) ||
|
||||
countDeclarations(sf, name) !== 1
|
||||
)
|
||||
return loaderWhy;
|
||||
const entries = new Map(table.entries);
|
||||
for (const ref of refsByName.get(name) ?? []) {
|
||||
const call = ref.parent;
|
||||
if (
|
||||
call === undefined ||
|
||||
!ts.isCallExpression(call) ||
|
||||
call.expression !== ref ||
|
||||
call.arguments.nodes.length !== 0
|
||||
)
|
||||
return loaderWhy;
|
||||
let access: ts.Node | undefined = call.parent;
|
||||
if (
|
||||
access === undefined ||
|
||||
!ts.isPropertyAccessExpression(access) ||
|
||||
access.expression !== call ||
|
||||
!plainRead(access)
|
||||
)
|
||||
return loaderWhy;
|
||||
// Node mode binds `default` to the namespace object itself.
|
||||
if (interop && nodeMode && access.name.text === "default") {
|
||||
const outer: ts.Node | undefined = access.parent;
|
||||
if (
|
||||
outer === undefined ||
|
||||
!ts.isPropertyAccessExpression(outer) ||
|
||||
outer.expression !== access ||
|
||||
!plainRead(outer)
|
||||
)
|
||||
return loaderWhy;
|
||||
access = outer;
|
||||
}
|
||||
const member = (access as ts.PropertyAccessExpression).name.text;
|
||||
const value = entries.get(member);
|
||||
if (value === undefined || shadowed(access, table.entries)) return loaderWhy;
|
||||
// The rewrite reads the export without the namespace object, so a
|
||||
// call through it (`L().fn()`) loses its receiver.
|
||||
if (calledWithReceiver(access as ts.Expression) && !receiverHidden(entries, member))
|
||||
return loaderWhy;
|
||||
const init = initCall === null ? "" : `${initCall.getText(sf)}, `;
|
||||
edits.push({
|
||||
start: access.getStart(sf),
|
||||
end: access.getEnd(),
|
||||
text: `(${init}${getterText(value)})`,
|
||||
});
|
||||
}
|
||||
deadDeclarators.add(d);
|
||||
deadDeclarators.add(memoDecl);
|
||||
loaderSpans.push({ start: d.getStart(sf), end: d.getEnd() });
|
||||
lowered.add(target);
|
||||
}
|
||||
|
||||
for (const r of allRefs) {
|
||||
const table = tables.get(r.text);
|
||||
if (table === undefined) continue;
|
||||
if (loaderSpans.some((span) => r.getStart(sf) >= span.start && r.getEnd() <= span.end))
|
||||
continue;
|
||||
const p = r.parent;
|
||||
// The table's own declaration and __export call are handled below.
|
||||
if (p !== undefined && ts.isVariableDeclaration(p) && p.name === r) continue;
|
||||
if (
|
||||
p !== undefined &&
|
||||
ts.isCallExpression(p) &&
|
||||
p.arguments.nodes[0] === r &&
|
||||
ts.isExpressionStatement(p.parent) &&
|
||||
p.parent === table.stmt
|
||||
)
|
||||
continue;
|
||||
const why = `reads the namespace of a bundled ES module ('${r.text}') in a way the static rewrite cannot follow (only member reads of its exports compile, and calls only of exported functions that do not read 'this')`;
|
||||
if (
|
||||
p === undefined ||
|
||||
!ts.isCallExpression(p) ||
|
||||
!ts.isIdentifier(p.expression) ||
|
||||
helperBase(p.expression.text) !== "__toCommonJS" ||
|
||||
p.arguments.nodes.length !== 1
|
||||
)
|
||||
return { degrade: `its shipped JS ${why}` };
|
||||
// The namespace expression: `(init_x(), __toCommonJS(T))` or the call.
|
||||
let ns: ts.Expression = p;
|
||||
for (;;) {
|
||||
const up = ns.parent;
|
||||
if (up !== undefined && ts.isParenthesizedExpression(up)) ns = up;
|
||||
else if (
|
||||
up !== undefined &&
|
||||
ts.isBinaryExpression(up) &&
|
||||
up.operatorToken.kind === ts.SyntaxKind.CommaToken &&
|
||||
up.right === ns
|
||||
)
|
||||
ns = up;
|
||||
else break;
|
||||
}
|
||||
const values = new Map(table.entries);
|
||||
const memberRead = (access: ts.Node | undefined, of: ts.Node): boolean =>
|
||||
access !== undefined &&
|
||||
ts.isPropertyAccessExpression(access) &&
|
||||
access.expression === of &&
|
||||
values.has(access.name.text) &&
|
||||
plainRead(access) &&
|
||||
(!calledWithReceiver(access) || receiverHidden(values, access.name.text));
|
||||
const holder = ns.parent;
|
||||
// `__reExport(main, ns, module.exports)`: a star re-export of the
|
||||
// module into the bundle's own exports (`export * from` in its entry).
|
||||
// The initializer runs at the statement; the names become live getter
|
||||
// entries of the export table, after its explicit ones (first wins)
|
||||
// and without `default`, exactly the names __reExport copies.
|
||||
if (
|
||||
holder !== undefined &&
|
||||
ts.isCallExpression(holder) &&
|
||||
ts.isIdentifier(holder.expression) &&
|
||||
helperBase(holder.expression.text) === "__reExport" &&
|
||||
holder.arguments.nodes[1] === ns &&
|
||||
holder.arguments.nodes.length <= 3 &&
|
||||
ts.isExpressionStatement(holder.parent) &&
|
||||
mainTarget !== null &&
|
||||
ts.isIdentifier(holder.arguments.nodes[0]!) &&
|
||||
(holder.arguments.nodes[0] as ts.Identifier).text === mainTarget
|
||||
) {
|
||||
const inner = unparen(ns);
|
||||
const init =
|
||||
ts.isBinaryExpression(inner) && inner.operatorToken.kind === ts.SyntaxKind.CommaToken
|
||||
? `${unparen(inner.left).getText(sf)}; `
|
||||
: "";
|
||||
if (shadowed(holder, table.entries)) return { degrade: `its shipped JS ${why}` };
|
||||
const entries = table.entries
|
||||
.filter(([n]) => n !== "default")
|
||||
.map(([n, value]) => `${JSON.stringify(n)}: () => ${getterText(value)}`)
|
||||
.join(", ");
|
||||
edits.push({
|
||||
start: holder.parent.getStart(sf),
|
||||
end: holder.parent.getEnd(),
|
||||
text: `${init}__export(${mainTarget}, { ${entries} });`,
|
||||
});
|
||||
lowered.add(r.text);
|
||||
continue;
|
||||
}
|
||||
let readable = memberRead(holder, ns);
|
||||
if (
|
||||
!readable &&
|
||||
holder !== undefined &&
|
||||
ts.isVariableDeclaration(holder) &&
|
||||
holder.initializer === ns &&
|
||||
ts.isIdentifier(holder.name)
|
||||
) {
|
||||
const local = holder.name.text;
|
||||
const localRefs = (refsByName.get(local) ?? []).filter((x) => x !== holder.name);
|
||||
readable =
|
||||
countDeclarations(sf, local) === 1 &&
|
||||
!writes.has(local) &&
|
||||
localRefs.every((x) => memberRead(x.parent, x));
|
||||
}
|
||||
if (!readable) {
|
||||
// A namespace kept for later (`x ??= __toESM(ns)`, a hand-rolled
|
||||
// lazy loader) is a value with no static representation.
|
||||
let outer: ts.Node | undefined = holder;
|
||||
while (
|
||||
outer !== undefined &&
|
||||
(ts.isParenthesizedExpression(outer) ||
|
||||
(ts.isCallExpression(outer) &&
|
||||
ts.isIdentifier(outer.expression) &&
|
||||
helperBase(outer.expression.text) === "__toESM"))
|
||||
)
|
||||
outer = outer.parent;
|
||||
if (
|
||||
outer !== undefined &&
|
||||
ts.isBinaryExpression(outer) &&
|
||||
outer.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
outer.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
||||
)
|
||||
return {
|
||||
degrade: `its shipped JS stores the namespace of a bundled ES module ('${r.text}') in a variable (a memoized lazy loader), a module object the static rewrite cannot represent`,
|
||||
};
|
||||
return { degrade: `its shipped JS ${why}` };
|
||||
}
|
||||
// The getters read top-level bindings: nothing between the use site
|
||||
// and the top level may rebind one of their names.
|
||||
if (shadowed(p, table.entries)) return { degrade: `its shipped JS ${why}` };
|
||||
const getters = table.entries
|
||||
.map(
|
||||
([n, value]) =>
|
||||
`get ${/^[A-Za-z_$][\w$]*$/.test(n) ? n : JSON.stringify(n)}() { return ${getterText(value)}; }`,
|
||||
)
|
||||
.join(", ");
|
||||
edits.push({ start: p.getStart(sf), end: p.getEnd(), text: `{ ${getters} }` });
|
||||
lowered.add(r.text);
|
||||
}
|
||||
// A table nothing reads (its module is never required) goes too: its
|
||||
// names are no exports of the bundle.
|
||||
for (const [target, table] of tables) {
|
||||
if (lowered.has(target)) continue;
|
||||
const live = (refsByName.get(target) ?? []).some((x) => {
|
||||
const p = x.parent;
|
||||
if (p !== undefined && ts.isVariableDeclaration(p) && p.name === x) return false;
|
||||
return !(x.getStart(sf) >= table.stmt.getStart(sf) && x.getEnd() <= table.stmt.getEnd());
|
||||
});
|
||||
if (!live) lowered.add(target);
|
||||
}
|
||||
for (const target of lowered) {
|
||||
const table = tables.get(target)!;
|
||||
const decl = top.get(target)?.node;
|
||||
if (
|
||||
decl === undefined ||
|
||||
!ts.isVariableDeclaration(decl) ||
|
||||
decl.initializer === undefined ||
|
||||
!ts.isObjectLiteralExpression(unparen(decl.initializer)) ||
|
||||
(unparen(decl.initializer) as ts.ObjectLiteralExpression).properties.nodes.length !== 0 ||
|
||||
writes.has(target)
|
||||
)
|
||||
return { degrade: ESM_SHAPE_DEGRADE };
|
||||
edits.push({ start: table.stmt.getStart(sf), end: table.stmt.getEnd(), text: "" });
|
||||
deadDeclarators.add(decl);
|
||||
}
|
||||
// Every removed declarator in one pass: declarators of one statement
|
||||
// removed separately would leave overlapping edits, and applyEdits keeps
|
||||
// only the first of them.
|
||||
edits.push(...removeDeclarators(sf, deadDeclarators));
|
||||
|
||||
/* The run-once closures. */
|
||||
for (const init of inits.values()) {
|
||||
const { call, body } = init;
|
||||
edits.push(
|
||||
{
|
||||
start: call.getStart(sf),
|
||||
end: body.getStart(sf) + 1,
|
||||
text: "(() => { let __scriptc_done = false; function __scriptc_init() {",
|
||||
},
|
||||
{
|
||||
start: body.getEnd() - 1,
|
||||
end: call.getEnd(),
|
||||
text: "} return function () { if (__scriptc_done) return; __scriptc_done = true; __scriptc_init(); }; })()",
|
||||
},
|
||||
);
|
||||
}
|
||||
return applyEdits(source, 0, source.length, edits);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { nodeFrontendServices } from "./services-node.js";
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function rewrite(source: string): string | null {
|
||||
return nodeFrontendServices().cjsFunctionExports(source, "/pkg/index.js");
|
||||
}
|
||||
|
||||
function run(file: string) {
|
||||
const result = spawnSync(process.execPath, [file], { encoding: "utf8" });
|
||||
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
||||
}
|
||||
|
||||
/** The rewritten module prints what the original prints. */
|
||||
function expectSameBehavior(source: string): string {
|
||||
const result = rewrite(source);
|
||||
expect(typeof result).toBe("string");
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-cjs-function-exports-"));
|
||||
dirs.push(dir);
|
||||
const original = join(dir, "original.cjs");
|
||||
const rewritten = join(dir, "rewritten.cjs");
|
||||
writeFileSync(original, source);
|
||||
writeFileSync(rewritten, result as string);
|
||||
const oracle = run(original);
|
||||
expect(oracle.status).toBe(0);
|
||||
expect(run(rewritten)).toEqual(oracle);
|
||||
return result as string;
|
||||
}
|
||||
|
||||
const report = `
|
||||
console.log(typeof module.exports, module.exports === Version, Object.keys(Version).join(","));
|
||||
`;
|
||||
|
||||
test("members of a function export object read and write the function", () => {
|
||||
const rewritten = expectSameBehavior(`exports = module.exports = Version;
|
||||
exports.parse = parse;
|
||||
module.exports.major = (v) => parse(v)[0];
|
||||
function Version(v) { return { v }; }
|
||||
function parse(v) { return v.split(".").map(Number); }
|
||||
console.log(exports.major("2.1.0"), module.exports.parse("1.2").length);
|
||||
${report}`);
|
||||
expect(rewritten).toContain("Version.parse = parse");
|
||||
expect(rewritten).not.toContain("exports = module.exports");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["a parameter", "function inner(Version) { return exports.parse; }"],
|
||||
["a local", "function inner() { const Version = {}; return exports.parse; }"],
|
||||
["a nested function", "function inner() { function Version() {} return exports.parse; }"],
|
||||
[
|
||||
"a catch binding",
|
||||
"function inner() { try { throw 0; } catch (Version) { return exports.parse; } }",
|
||||
],
|
||||
])("a nested binding of the function's name (%s) keeps the module as it is", (_, inner) => {
|
||||
expect(
|
||||
rewrite(`exports = module.exports = Version;
|
||||
exports.parse = parse;
|
||||
function Version() {}
|
||||
function parse() {}
|
||||
${inner}
|
||||
`),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("an `exports` shorthand property keeps its key", () => {
|
||||
const rewritten = expectSameBehavior(`exports = module.exports = Version;
|
||||
exports.tag = "v";
|
||||
function Version() {}
|
||||
const holder = { exports };
|
||||
console.log(Object.keys(holder).join(","), holder.exports === Version, holder.exports.tag);
|
||||
${report}`);
|
||||
expect(rewritten).toContain("{ exports: Version }");
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
/* CommonJS modules whose export object is one of their own functions:
|
||||
*
|
||||
* exports = module.exports = SemVer; // or module.exports = SemVer;
|
||||
* exports.parse = parse; // or module.exports.parse = …
|
||||
* function SemVer(version) { … }
|
||||
*
|
||||
* After that statement `module.exports` (and, after the rebinding form,
|
||||
* `exports`) IS the function, so every member read or write through them
|
||||
* is a member of the function itself. The pass respells those accesses
|
||||
* through the function's name, the expando form both resolvers model
|
||||
* (members on a function declaration), and drops the `exports =`
|
||||
* rebinding the static export protocol has no storage for.
|
||||
*
|
||||
* Exact only while nothing can observe the module before the statement
|
||||
* runs and nothing replaces the export object again: the statement is a
|
||||
* top-level statement preceded only by inert declarations, it is the
|
||||
* file's only write of `module.exports` and `exports`, the function is a
|
||||
* top-level declaration nothing reassigns and the file's only binding of
|
||||
* its name, and `exports`/`module` are the CommonJS globals (never
|
||||
* declared in the file). Other shapes answer
|
||||
* null and keep their existing treatment. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import {
|
||||
applyEdits,
|
||||
countDeclarations,
|
||||
declaredNames,
|
||||
isWritten,
|
||||
references,
|
||||
unparen,
|
||||
type Edit,
|
||||
} from "./npm-static-bundled-chunks.js";
|
||||
|
||||
function isModuleExports(e: ts.Expression): e is ts.PropertyAccessExpression {
|
||||
const x = unparen(e);
|
||||
return (
|
||||
ts.isPropertyAccessExpression(x) &&
|
||||
!x.questionDotToken &&
|
||||
ts.isIdentifier(x.expression) &&
|
||||
x.expression.text === "module" &&
|
||||
x.name.text === "exports"
|
||||
);
|
||||
}
|
||||
|
||||
/** True for a statement whose evaluation runs no code that could observe
|
||||
* the export object: declarations and member writes (`F.prototype.m =
|
||||
* function …`) without calls, and directives. */
|
||||
function inert(stmt: ts.Statement): boolean {
|
||||
if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) return true;
|
||||
if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) return true;
|
||||
if (ts.isExpressionStatement(stmt)) {
|
||||
const e = unparen(stmt.expression);
|
||||
if (
|
||||
!ts.isBinaryExpression(e) ||
|
||||
e.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!(ts.isPropertyAccessExpression(e.left) || ts.isElementAccessExpression(e.left))
|
||||
)
|
||||
return false;
|
||||
let root: ts.Expression = e.left;
|
||||
while (ts.isPropertyAccessExpression(root) || ts.isElementAccessExpression(root))
|
||||
root = root.expression;
|
||||
if (!ts.isIdentifier(root) || root.text === "module" || root.text === "exports") return false;
|
||||
} else if (!ts.isVariableStatement(stmt)) return false;
|
||||
let calls = false;
|
||||
ts.walkPreorder(stmt, (n) => {
|
||||
if (calls) return "stop";
|
||||
if (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) return "skip";
|
||||
if (ts.isCallExpression(n) || ts.isNewExpression(n) || ts.isTaggedTemplateExpression(n))
|
||||
calls = true;
|
||||
return undefined;
|
||||
});
|
||||
return !calls;
|
||||
}
|
||||
|
||||
export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null {
|
||||
const source = sf.text;
|
||||
if (!source.includes("module.exports")) return null;
|
||||
if (!sf.fileName.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
|
||||
const declared = declaredNames(sf);
|
||||
if (declared.has("exports") || declared.has("module")) return null;
|
||||
|
||||
// The whole-export statement: `[exports =] module.exports = W;`.
|
||||
let whole: { stmt: ts.ExpressionStatement; fn: string; rebinds: boolean } | null = null;
|
||||
const stmts = sf.statements.nodes;
|
||||
for (let i = 0; i < stmts.length; i++) {
|
||||
const stmt = stmts[i]!;
|
||||
if (!ts.isExpressionStatement(stmt)) continue;
|
||||
const top = unparen(stmt.expression);
|
||||
if (!ts.isBinaryExpression(top) || top.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
||||
continue;
|
||||
let e: ts.BinaryExpression = top;
|
||||
let rebinds = false;
|
||||
if (ts.isIdentifier(top.left) && top.left.text === "exports") {
|
||||
const inner = unparen(top.right);
|
||||
if (!ts.isBinaryExpression(inner) || inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
||||
continue;
|
||||
rebinds = true;
|
||||
e = inner;
|
||||
}
|
||||
if (!isModuleExports(e.left)) continue;
|
||||
const value = unparen(e.right);
|
||||
if (!ts.isIdentifier(value)) return null;
|
||||
if (whole !== null || !stmts.slice(0, i).every(inert)) return null;
|
||||
whole = { stmt, fn: value.text, rebinds };
|
||||
}
|
||||
if (whole === null) return null;
|
||||
const fnName = whole.fn;
|
||||
const fnDecls = stmts.filter(
|
||||
(st) => ts.isFunctionDeclaration(st) && st.name?.text === fnName && st.body !== undefined,
|
||||
);
|
||||
// The respelled reads name the function from wherever they sit, so no
|
||||
// other binding of its name (a parameter, a local, a nested function)
|
||||
// may exist anywhere in the file.
|
||||
if (fnDecls.length !== 1 || countDeclarations(sf, fnName) !== 1) return null;
|
||||
|
||||
const edits: Edit[] = [];
|
||||
let unsafe = false;
|
||||
const refs = references(sf);
|
||||
for (const r of refs) {
|
||||
if (r.text === fnName && isWritten(r)) unsafe = true;
|
||||
if (r.text === "exports") {
|
||||
if (isWritten(r)) {
|
||||
// Only the whole statement's own rebinding.
|
||||
const assign = r.parent;
|
||||
if (
|
||||
!whole.rebinds ||
|
||||
assign === undefined ||
|
||||
!ts.isBinaryExpression(assign) ||
|
||||
assign.parent !== whole.stmt ||
|
||||
assign.left !== r
|
||||
)
|
||||
unsafe = true;
|
||||
continue;
|
||||
}
|
||||
// Without the rebinding, `exports` keeps the replaced object.
|
||||
if (!whole.rebinds || r.getStart(sf) < whole.stmt.getEnd()) {
|
||||
unsafe = true;
|
||||
continue;
|
||||
}
|
||||
// `{ exports }` keeps its key: the shorthand spells out the value.
|
||||
const shorthand =
|
||||
r.parent !== undefined && ts.isShorthandPropertyAssignment(r.parent) && r.parent.name === r;
|
||||
edits.push({
|
||||
start: r.getStart(sf),
|
||||
end: r.getEnd(),
|
||||
text: shorthand ? `exports: ${fnName}` : fnName,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (r.text === "module") {
|
||||
const access = r.parent;
|
||||
if (access === undefined || !isModuleExports(access as ts.Expression)) continue;
|
||||
const outer = access.parent;
|
||||
if (outer !== undefined && ts.isBinaryExpression(outer) && outer.left === access) {
|
||||
if (
|
||||
outer.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
outer.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
|
||||
!(
|
||||
(outer.parent === whole.stmt && !whole.rebinds) ||
|
||||
(whole.rebinds && outer.parent?.parent === whole.stmt)
|
||||
)
|
||||
)
|
||||
unsafe = true;
|
||||
continue;
|
||||
}
|
||||
// A member access through module.exports: a member of the function.
|
||||
if (
|
||||
outer !== undefined &&
|
||||
(ts.isPropertyAccessExpression(outer) || ts.isElementAccessExpression(outer)) &&
|
||||
outer.expression === access
|
||||
) {
|
||||
if (access.getStart(sf) < whole.stmt.getEnd() && !insideFunction(access)) unsafe = true;
|
||||
edits.push({ start: access.getStart(sf), end: access.getEnd(), text: fnName });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unsafe) return null;
|
||||
if (whole.rebinds) {
|
||||
// `exports = module.exports = W` → `module.exports = W`.
|
||||
const e = unparen(whole.stmt.expression) as ts.BinaryExpression;
|
||||
edits.push({ start: e.getStart(sf), end: unparen(e.right).getStart(sf), text: "" });
|
||||
}
|
||||
if (edits.length === 0) return null;
|
||||
return applyEdits(source, 0, source.length, edits);
|
||||
}
|
||||
|
||||
function insideFunction(node: ts.Node): boolean {
|
||||
for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
|
||||
if (ts.isFunctionLike(p)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/* CommonJS modules that name their export object through a variable:
|
||||
*
|
||||
* var ip = exports;
|
||||
* ip.isPrivate = function (addr) { … };
|
||||
* ip.isPublic = function (addr) { return !ip.isPrivate(addr); };
|
||||
*
|
||||
* `ip` IS the export object, so every member read or write through it is
|
||||
* an export. The pass respells those accesses as `exports.<name>`, the
|
||||
* form the static export protocol models, and drops the alias declaration
|
||||
* (the export object has no storage of its own to bind).
|
||||
*
|
||||
* Exact only while the variable is nothing but that alias: one top-level
|
||||
* declaration initialized with the CommonJS `exports` global, never
|
||||
* reassigned, never declared again anywhere in the file (so no nested
|
||||
* binding shadows it), and used only as the object of member accesses.
|
||||
* `exports` itself must be the global (never declared in the file) and is
|
||||
* never reassigned. Other shapes answer null and keep their existing
|
||||
* treatment. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
|
||||
function unparen(e: ts.Expression): ts.Expression {
|
||||
let x = e;
|
||||
while (ts.isParenthesizedExpression(x)) x = x.expression;
|
||||
return x;
|
||||
}
|
||||
|
||||
function isDeclarationName(id: ts.Identifier): boolean {
|
||||
const p = id.parent;
|
||||
return (
|
||||
((ts.isVariableDeclaration(p) ||
|
||||
ts.isParameter(p) ||
|
||||
ts.isBindingElement(p) ||
|
||||
ts.isFunctionDeclaration(p) ||
|
||||
ts.isFunctionExpression(p) ||
|
||||
ts.isClassDeclaration(p) ||
|
||||
ts.isClassExpression(p)) &&
|
||||
p.name === id) ||
|
||||
(ts.isPropertyAccessExpression(p) && p.name === id) ||
|
||||
((ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p) || ts.isPropertyDeclaration(p)) &&
|
||||
p.name === id)
|
||||
);
|
||||
}
|
||||
|
||||
function isFunctionScope(n: ts.Node): boolean {
|
||||
return (
|
||||
ts.isFunctionDeclaration(n) ||
|
||||
ts.isFunctionExpression(n) ||
|
||||
ts.isArrowFunction(n) ||
|
||||
ts.isMethodDeclaration(n) ||
|
||||
ts.isGetAccessorDeclaration(n) ||
|
||||
ts.isSetAccessorDeclaration(n) ||
|
||||
ts.isConstructorDeclaration(n)
|
||||
);
|
||||
}
|
||||
|
||||
function bindsName(name: ts.BindingName | undefined, text: string): boolean {
|
||||
if (name === undefined) return false;
|
||||
if (ts.isIdentifier(name)) return name.text === text;
|
||||
for (const element of name.elements.nodes) {
|
||||
if (!ts.isOmittedExpression(element) && bindsName(element.name, text)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True when one of `statements` declares `text` lexically: a let/const,
|
||||
* or a class or function declaration. */
|
||||
function declaresLexically(statements: readonly ts.Statement[], text: string): boolean {
|
||||
for (const stmt of statements) {
|
||||
if (
|
||||
ts.isVariableStatement(stmt) &&
|
||||
(stmt.declarationList.flags & ts.NodeFlags.BlockScoped) !== 0 &&
|
||||
stmt.declarationList.declarations.nodes.some((d) => bindsName(d.name, text))
|
||||
)
|
||||
return true;
|
||||
if ((ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) && stmt.name?.text === text)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True when a function body declares `text` with `var` (or a function
|
||||
* declaration in a nested block) anywhere outside nested functions. */
|
||||
function declaresVar(body: ts.Node, text: string): boolean {
|
||||
let found = false;
|
||||
ts.walkPreorder(body, (n) => {
|
||||
if (found) return "stop";
|
||||
if (n === body) return undefined;
|
||||
if (isFunctionScope(n)) {
|
||||
if (ts.isFunctionDeclaration(n) && n.name?.text === text) found = true;
|
||||
return "skip";
|
||||
}
|
||||
if (
|
||||
ts.isVariableDeclarationList(n) &&
|
||||
(n.flags & ts.NodeFlags.BlockScoped) === 0 &&
|
||||
n.declarations.nodes.some((d) => bindsName(d.name, text))
|
||||
)
|
||||
found = true;
|
||||
return undefined;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
/** True when `scope` declares `text` itself, so references inside it name
|
||||
* that binding instead of the module-level alias: a function's parameters,
|
||||
* own name, `var`s and the let/const, class and function declarations of
|
||||
* its body; a block's or case block's own let/const, class and function
|
||||
* declarations; a catch binding; a for loop's let/const. A let/const in a
|
||||
* nested block shadows only that block, which the walk reaches as its own
|
||||
* scope. */
|
||||
function shadows(scope: ts.Node, text: string): boolean {
|
||||
if (isFunctionScope(scope)) {
|
||||
const fn = scope as ts.SignatureDeclaration;
|
||||
if (fn.parameters.nodes.some((p) => bindsName(p.name, text))) return true;
|
||||
if (ts.isFunctionExpression(scope) && scope.name?.text === text) return true;
|
||||
const body = (scope as { body?: ts.Node }).body;
|
||||
if (body === undefined || !ts.isBlock(body)) return false;
|
||||
return declaresVar(body, text) || declaresLexically(body.statements.nodes, text);
|
||||
}
|
||||
if (ts.isBlock(scope)) return declaresLexically(scope.statements.nodes, text);
|
||||
if (ts.isCaseBlock(scope))
|
||||
return declaresLexically(
|
||||
scope.clauses.nodes.flatMap((clause) => clause.statements.nodes),
|
||||
text,
|
||||
);
|
||||
if (ts.isCatchClause(scope)) return bindsName(scope.variableDeclaration?.name, text);
|
||||
if (ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) {
|
||||
const init = scope.initializer;
|
||||
return (
|
||||
init !== undefined &&
|
||||
ts.isVariableDeclarationList(init) &&
|
||||
(init.flags & ts.NodeFlags.BlockScoped) !== 0 &&
|
||||
init.declarations.nodes.some((d) => bindsName(d.name, text))
|
||||
);
|
||||
}
|
||||
if (ts.isClassExpression(scope)) return scope.name?.text === text;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function normalizeCjsExportsVar(sf: ts.SourceFile): string | null {
|
||||
const source = sf.text;
|
||||
if (!/\b(?:var|let|const)\s+[A-Za-z_$][\w$]*\s*=\s*exports\s*[;,\n]/.test(source)) return null;
|
||||
if (!sf.fileName.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
|
||||
let alias: { name: string; stmt: ts.VariableStatement } | null = null;
|
||||
for (const stmt of sf.statements.nodes) {
|
||||
if (!ts.isVariableStatement(stmt)) continue;
|
||||
const decls = stmt.declarationList.declarations.nodes;
|
||||
if (decls.length !== 1) continue;
|
||||
const decl = decls[0]!;
|
||||
const init = decl.initializer !== undefined ? unparen(decl.initializer) : undefined;
|
||||
if (
|
||||
ts.isIdentifier(decl.name) &&
|
||||
init !== undefined &&
|
||||
ts.isIdentifier(init) &&
|
||||
init.text === "exports"
|
||||
) {
|
||||
if (alias !== null) return null; /* two aliases: keep the plain treatment */
|
||||
alias = { name: decl.name.text, stmt };
|
||||
}
|
||||
}
|
||||
if (alias === null) return null;
|
||||
const name = alias.name;
|
||||
const aliasDecl = alias.stmt.declarationList.declarations.nodes[0]!;
|
||||
const edits: { start: number; end: number; text: string }[] = [];
|
||||
let ok = true;
|
||||
ts.walkPreorder(sf, (n) => {
|
||||
if (!ok) return "stop";
|
||||
if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode)
|
||||
return "skip";
|
||||
if (
|
||||
ts.isBinaryExpression(n) &&
|
||||
n.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isPropertyAccessExpression(unparen(n.left)) &&
|
||||
(unparen(n.left) as ts.PropertyAccessExpression).name.text === "exports" &&
|
||||
ts.isIdentifier((unparen(n.left) as ts.PropertyAccessExpression).expression) &&
|
||||
((unparen(n.left) as ts.PropertyAccessExpression).expression as ts.Identifier).text ===
|
||||
"module"
|
||||
) {
|
||||
ok = false; /* module.exports replaced: the alias no longer names the exports */
|
||||
return "stop";
|
||||
}
|
||||
// A scope that declares the alias's name again: its references name
|
||||
// that binding, not the export object.
|
||||
if (
|
||||
n !== sf &&
|
||||
(isFunctionScope(n) ||
|
||||
ts.isBlock(n) ||
|
||||
ts.isCaseBlock(n) ||
|
||||
ts.isCatchClause(n) ||
|
||||
ts.isForStatement(n) ||
|
||||
ts.isForInStatement(n) ||
|
||||
ts.isForOfStatement(n) ||
|
||||
ts.isClassExpression(n)) &&
|
||||
shadows(n, name)
|
||||
)
|
||||
return "skip";
|
||||
if (!ts.isIdentifier(n)) return undefined;
|
||||
if (n.text === "exports" && n !== aliasDecl.initializer) {
|
||||
// `exports` declared in the file, or the export object replaced
|
||||
if (isDeclarationName(n) && !ts.isPropertyAccessExpression(n.parent)) ok = false;
|
||||
const p = n.parent;
|
||||
if (
|
||||
ts.isBinaryExpression(p) &&
|
||||
p.left === n &&
|
||||
p.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
||||
)
|
||||
ok = false;
|
||||
return undefined;
|
||||
}
|
||||
if (n.text !== name) return undefined;
|
||||
if (n === aliasDecl.name) return undefined;
|
||||
const p = n.parent;
|
||||
if (ts.isPropertyAccessExpression(p) && p.name === n) return undefined;
|
||||
if ((ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p)) && p.name === n) return undefined;
|
||||
if (isDeclarationName(n)) {
|
||||
ok = false; /* declared again (a shadowing binding): not a plain alias */
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(p) || ts.isElementAccessExpression(p)) &&
|
||||
p.expression === n
|
||||
) {
|
||||
edits.push({ start: n.getStart(sf), end: n.getEnd(), text: "exports" });
|
||||
return undefined;
|
||||
}
|
||||
ok = false; /* used as a value, assigned, or passed along */
|
||||
return undefined;
|
||||
});
|
||||
if (!ok || edits.length === 0) return null;
|
||||
edits.push({ start: alias.stmt.getStart(sf), end: alias.stmt.getEnd(), text: "" });
|
||||
edits.sort((a, b) => a.start - b.start);
|
||||
let out = "";
|
||||
let at = 0;
|
||||
for (const e of edits) {
|
||||
out += source.slice(at, e.start) + e.text;
|
||||
at = e.end;
|
||||
}
|
||||
return out + source.slice(at);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { resolveExports } from "./resolve.js";
|
||||
import { packageNameOfSpecifier } from "./workspace-registry.js";
|
||||
import { isBundlerCjsCandidate, rewriteBundlerCjsSyntax } from "./npm-static-rewrite-syntax.js";
|
||||
import { lowerBundledEsmInits } from "./npm-static-bundled-esm.js";
|
||||
|
||||
const NODE_REQUIRE_CONDITIONS = new Set(["require", "node", "default"]);
|
||||
|
||||
@@ -164,15 +165,28 @@ function starTargetNames(parser: Ts7SourceParser, file: string): Set<string> {
|
||||
}
|
||||
}
|
||||
|
||||
export function lowerEsmInitsWithParser(
|
||||
parser: Ts7SourceParser,
|
||||
source: string,
|
||||
filePath: string,
|
||||
): string | { degrade: string } | null {
|
||||
if (!source.includes("__esm")) return null;
|
||||
return lowerBundledEsmInits(parser.parse(filePath, source, "js"), filePath, {
|
||||
requireTargetEsModuleStamped: (from, spec) => requireTargetEsModuleStamped(parser, from, spec),
|
||||
});
|
||||
}
|
||||
|
||||
export function rewriteBundlerCjsWithParser(
|
||||
parser: Ts7SourceParser,
|
||||
source: string,
|
||||
filePath: string,
|
||||
liveExportTable?: (filePath: string) => void,
|
||||
lexerVisibleNames?: (filePath: string, names: readonly string[]) => void,
|
||||
): string | { degrade: string } | null {
|
||||
if (!isBundlerCjsCandidate(source)) return null;
|
||||
return rewriteBundlerCjsSyntax(parser.parse(filePath, source, "js"), filePath, {
|
||||
liveExportTable,
|
||||
lexerVisibleNames,
|
||||
requireOnly: isVirtualSource,
|
||||
requireTargetEsModuleStamped: (from, spec) => requireTargetEsModuleStamped(parser, from, spec),
|
||||
starTargetNames: (from, spec) => {
|
||||
|
||||
@@ -22,6 +22,9 @@ export interface CjsRewriteHost {
|
||||
* 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;
|
||||
/** Told the names Node's lexer detects in the original file that the
|
||||
* rewritten text spells only as accessors (which the lexer skips). */
|
||||
lexerVisibleNames?: ((filePath: string, names: readonly string[]) => void) | undefined;
|
||||
}
|
||||
|
||||
/** True when esbuild's export object (`module.exports = __toCommonJS(T)`
|
||||
@@ -119,8 +122,12 @@ interface EntrySpec {
|
||||
name: string;
|
||||
/** "ident": VALUE is a plain identifier spelled directly in the table.
|
||||
* "hoist": VALUE is an expression text hoisted to a tail const first.
|
||||
* "getter": VALUE is a getter body's returned expression the table
|
||||
* keeps as a live accessor (read on every access, like the original
|
||||
* getter, so a lazily initialized module still initializes at the
|
||||
* first read).
|
||||
* "any": the checked-dynamic fallback (`__scriptc_any`). */
|
||||
kind: "ident" | "hoist" | "any";
|
||||
kind: "ident" | "hoist" | "getter" | "any";
|
||||
value?: string;
|
||||
}
|
||||
|
||||
@@ -158,6 +165,26 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string }
|
||||
return null;
|
||||
}
|
||||
|
||||
/** An object literal of side-effect-free member values (`exports.default
|
||||
* = { context: context_api_1.context, metrics: metrics_api_1.metrics }`,
|
||||
* tsc's default-export object): the tail const can build it once, like a
|
||||
* chased value. Null for any other shape (spreads, methods, accessors,
|
||||
* computed keys, or a value chaseValue rejects). */
|
||||
function chaseObjectLiteral(e: ts.Expression): { kind: "hoist"; text: string } | null {
|
||||
let cur: ts.Expression = e;
|
||||
while (ts.isParenthesizedExpression(cur)) cur = cur.expression;
|
||||
if (!ts.isObjectLiteralExpression(cur)) return null;
|
||||
for (const prop of cur.properties.nodes) {
|
||||
if (
|
||||
!ts.isPropertyAssignment(prop) ||
|
||||
!(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) ||
|
||||
(chaseValue(prop.initializer) === null && chaseObjectLiteral(prop.initializer) === null)
|
||||
)
|
||||
return null;
|
||||
}
|
||||
return { kind: "hoist", text: cur.getText() };
|
||||
}
|
||||
|
||||
/** The getter body's returned expression for the two descriptor getter
|
||||
* spellings (`get() { return X }` / `get: function () { return X }`) and
|
||||
* the arrow forms the esbuild table uses (`() => X`, `() => { return X }`). */
|
||||
@@ -760,7 +787,14 @@ export function rewriteBundlerCjsSyntax(
|
||||
...exportTables.filter((t) => t.target !== esbuildMainTarget),
|
||||
];
|
||||
for (const t of tablesOrdered) {
|
||||
for (const [name, value] of t.entries) claim(name, classify(name, value));
|
||||
for (const [name, value] of t.entries) {
|
||||
const spec = classify(name, value);
|
||||
// The table feeding module.exports keeps a getter it cannot chase
|
||||
// as a live accessor rather than a snapshot.
|
||||
if (spec.kind === "any" && value !== null && t.target === esbuildMainTarget)
|
||||
claim(name, { name, kind: "getter", value: value.getText() });
|
||||
else claim(name, spec);
|
||||
}
|
||||
}
|
||||
for (const d of definePropEntries) claim(d.name, classify(d.name, d.value));
|
||||
|
||||
@@ -792,7 +826,7 @@ export function rewriteBundlerCjsSyntax(
|
||||
// anything else keeps the file untouched (the offender path owns
|
||||
// it: neutralizing would drop an evaluation, keeping it meets the
|
||||
// lowering's discard fence at load).
|
||||
const chased = chaseValue(cur);
|
||||
const chased = chaseValue(cur) ?? chaseObjectLiteral(cur);
|
||||
if (chased === null) {
|
||||
return toEsm.pads.length > 0 ? { degrade: TO_ESM_MIXED_DEGRADE } : null;
|
||||
}
|
||||
@@ -893,6 +927,7 @@ export function rewriteBundlerCjsSyntax(
|
||||
"__toESM",
|
||||
"__toCommonJS",
|
||||
"__commonJS",
|
||||
"__esm",
|
||||
"__exportStar",
|
||||
"__createBinding",
|
||||
"__setModuleDefault",
|
||||
@@ -1038,6 +1073,8 @@ export function rewriteBundlerCjsSyntax(
|
||||
const v = `__scriptc_e${hoistN++}`;
|
||||
hoists.push(`const ${v} = ${dropInteropDefault(spec.value!)};`);
|
||||
plainParts.push(`${keyOf(spec.name)}: ${v},`);
|
||||
} else if (spec.kind === "getter") {
|
||||
plainParts.push(`get ${keyOf(spec.name)}() { return ${dropInteropDefault(spec.value!)}; },`);
|
||||
} else {
|
||||
plainParts.push(`${keyOf(spec.name)}: __scriptc_any,`);
|
||||
}
|
||||
@@ -1063,5 +1100,12 @@ export function rewriteBundlerCjsSyntax(
|
||||
lines.push(
|
||||
`module.exports = {${spreadParts.join("")}${plainParts.join("")}${esModulePart}${memberParts.join("")}};`,
|
||||
);
|
||||
/* Node's lexer skips accessors (and stops reading an object literal at
|
||||
* the first one), so a canonical table with accessor entries hides
|
||||
* names the original showed it (esbuild's dead annotation): the host
|
||||
* keeps the original's lexer answer, so ES-module importers link
|
||||
* exactly as they do against the original. */
|
||||
if ([...entries.values()].some((spec) => spec.kind === "getter"))
|
||||
host.lexerVisibleNames?.(filePath, [...lexed.exports]);
|
||||
return text + lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
trackedReadFile,
|
||||
trackedRealpath,
|
||||
} from "./input-tracker.js";
|
||||
import { npmBuiltinAliasOf } from "./builtin-modules.js";
|
||||
|
||||
let activePackages: ReadonlySet<string> = new Set();
|
||||
let declarationOverloads: ReadonlyMap<string, NpmStaticDeclarationOverloads> = new Map();
|
||||
@@ -43,17 +44,29 @@ export function npmStaticLiveExportTable(path: string): boolean {
|
||||
return liveExportFiles.has(path.split("\\").join("/"));
|
||||
}
|
||||
|
||||
/** Names Node's lexer detects in a rewritten file's original text that
|
||||
* its rewrite spells only as accessors (the lexer skips those). */
|
||||
const lexerNames = new Map<string, readonly string[]>();
|
||||
|
||||
export function npmStaticLexerVisibleNames(path: string): readonly string[] {
|
||||
return lexerNames.get(path.split("\\").join("/")) ?? [];
|
||||
}
|
||||
|
||||
export function setNpmStaticPackages<T extends Iterable<string>>(packages: T): void {
|
||||
activePackages = new Set(packages);
|
||||
// npm packages served as builtins (graceful-fs as fs) never compile
|
||||
// their JavaScript, even when named.
|
||||
activePackages = new Set([...packages].filter((name) => npmBuiltinAliasOf(name) === null));
|
||||
declarationOverloads = new Map();
|
||||
declarationProperties = new Map();
|
||||
offenders.clear();
|
||||
fileDegrades.clear();
|
||||
rewriteCache.clear();
|
||||
liveExportFiles.clear();
|
||||
lexerNames.clear();
|
||||
clearVirtualSources();
|
||||
untypedPkgCache.clear();
|
||||
realpathProbed.clear();
|
||||
aliasNames.clear();
|
||||
}
|
||||
|
||||
export function setNpmStaticDeclarationOverloads(
|
||||
@@ -168,9 +181,32 @@ function shadowTargetOf(path: string): { pkg: string; viaTypes: boolean } | null
|
||||
if (mangledTypesName(pkg) === mangled) return { pkg, viaTypes: true };
|
||||
}
|
||||
}
|
||||
// An npm alias (`"x-updated": "npm:x@6.3.0"`) installs the package under
|
||||
// another directory name: its own manifest names the package.
|
||||
const aliased = aliasedPackageName(`${norm.slice(0, idx)}/node_modules/${dirName}`);
|
||||
if (aliased !== null && aliased !== dirName && activePackages.has(aliased))
|
||||
return { pkg: aliased, viaTypes: false };
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The "name" a node_modules directory's own package.json declares, or
|
||||
* null (cached per load). */
|
||||
const aliasNames = new Map<string, string | null>();
|
||||
function aliasedPackageName(pkgDir: string): string | null {
|
||||
const hit = aliasNames.get(pkgDir);
|
||||
if (hit !== undefined) return hit;
|
||||
let name: string | null = null;
|
||||
try {
|
||||
const text = trackedReadFile(`${pkgDir}/package.json`);
|
||||
const declared = text === null ? undefined : (JSON.parse(text) as { name?: unknown }).name;
|
||||
if (typeof declared === "string") name = declared;
|
||||
} catch {
|
||||
name = null;
|
||||
}
|
||||
aliasNames.set(pkgDir, name);
|
||||
return name;
|
||||
}
|
||||
|
||||
/** The opted-in package.json transform, applied identically in BOTH
|
||||
* resolution worlds (the tsgo host's fs shadow and resolve.ts):
|
||||
* 1. every types claim goes — the top-level "types"/"typings" fields
|
||||
@@ -407,6 +443,24 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
for (const chunk of split.chunks) setVirtualSource(chunk.path, chunk.source);
|
||||
original = split.text;
|
||||
}
|
||||
// Then its lazily initialized ES modules become run-once closures.
|
||||
const esm = original === null ? null : services.lowerEsmInits(original, path);
|
||||
if (esm !== null && typeof esm === "object") {
|
||||
reportFileDegrade(path, target.pkg, esm.degrade);
|
||||
original = null;
|
||||
} else if (esm !== null) {
|
||||
original = esm;
|
||||
}
|
||||
// A module exporting one of its functions reaches its members
|
||||
// through that function.
|
||||
const fnExports = original === null ? null : services.cjsFunctionExports(original, path);
|
||||
if (fnExports !== null) original = fnExports;
|
||||
// A module that names its export object through a variable reaches
|
||||
// its members through `exports`.
|
||||
const exportsVar = original === null ? null : services.cjsExportsVar(original, path);
|
||||
if (exportsVar !== null) original = exportsVar;
|
||||
const respelled =
|
||||
split !== null || esm !== null || fnExports !== null || exportsVar !== null;
|
||||
if (original !== null) {
|
||||
const namepaths = services.jsDocNamepaths(path, original);
|
||||
const source = namepaths ?? original;
|
||||
@@ -430,6 +484,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
source,
|
||||
path,
|
||||
(live) => liveExportFiles.add(live.split("\\").join("/")),
|
||||
(file, names) => lexerNames.set(file.split("\\").join("/"), names),
|
||||
);
|
||||
if (answer !== null && typeof answer === "object") {
|
||||
reportFileDegrade(path, target.pkg, answer.degrade);
|
||||
@@ -441,7 +496,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
findWidened?.text ??
|
||||
classFields?.text ??
|
||||
namepaths ??
|
||||
(split !== null ? original : null);
|
||||
(respelled ? original : null);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -492,6 +547,20 @@ function hasTransformMarkers(source: string): boolean {
|
||||
return TRANSFORM_MARKERS.some((m) => source.includes(m));
|
||||
}
|
||||
|
||||
/** Why the declarations at `typesFile` cannot stand for the runtime
|
||||
* package `pkg`, or null when they are its DefinitelyTyped twin
|
||||
* (`@types/<mangled pkg>`). The declarations only check the importing
|
||||
* program; the JavaScript compiles with its inferred types, and an
|
||||
* inferred surface the program does not typecheck against refuses the
|
||||
* package afterwards, whatever release the twin describes. */
|
||||
export function definitelyTypedMismatch(pkg: string, typesFile: string): string | null {
|
||||
const owner = npmPackageNameOf(typesFile.split("\\").join("/"));
|
||||
const twin = `@types/${mangledTypesName(pkg)}`;
|
||||
return owner === twin
|
||||
? null
|
||||
: `its declared types come from ${owner ?? typesFile}, which is not ${pkg}'s DefinitelyTyped package ${twin}`;
|
||||
}
|
||||
|
||||
/** Auto-mode eligibility for one candidate package: `typesFile` is what
|
||||
* the ordinary (types-first) resolution answered for the import,
|
||||
* `jsEntry` what the js-only resolution answers. Returns null when
|
||||
@@ -500,12 +569,17 @@ export function npmStaticIneligibleReason(
|
||||
pkgName: string,
|
||||
typesFile: string,
|
||||
jsEntry: string | null,
|
||||
/** A dependency another package's JavaScript imports: no declarations
|
||||
* check that importer, so the package need not ship its own. */
|
||||
untypedDependency = false,
|
||||
): string | null {
|
||||
if (npmPackageNameOf(typesFile) !== pkgName) {
|
||||
return "its declared types come from a third-party @types package, not the package itself";
|
||||
}
|
||||
if (!/\.d\.(ts|mts|cts)$/.test(typesFile)) {
|
||||
return "it ships no own .d.ts declaration surface";
|
||||
if (!untypedDependency) {
|
||||
if (npmPackageNameOf(typesFile) !== pkgName) {
|
||||
return "its declared types come from a third-party @types package, not the package itself";
|
||||
}
|
||||
if (!/\.d\.(ts|mts|cts)$/.test(typesFile)) {
|
||||
return "it ships no own .d.ts declaration surface";
|
||||
}
|
||||
}
|
||||
if (jsEntry === null) return "no runtime JS entry resolves";
|
||||
const source = trackedReadFile(jsEntry);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { entryFunctionExports, type EntryExportInfo } from "./lib-exports.js";
|
||||
import { entryContractFacts, type ContractFacts } from "./lib-contract.js";
|
||||
import {
|
||||
canonicalBuiltinModule,
|
||||
npmBuiltinAliasOf,
|
||||
checkPreflight,
|
||||
isNodeTypesPath,
|
||||
locOf,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
type LoadResult,
|
||||
} from "./program.js";
|
||||
import {
|
||||
definitelyTypedMismatch,
|
||||
npmStaticFileDegrades,
|
||||
npmStaticIneligibleReason,
|
||||
npmStaticOffenders,
|
||||
@@ -197,6 +199,9 @@ function detectAutoPackages(
|
||||
// never npm candidates. Auto keeps its original path (the
|
||||
// @types/node answer skips them below), unchanged.
|
||||
if (mode === "lib" && canonicalBuiltinModule(spec) !== null) continue;
|
||||
// npm packages served as builtins (graceful-fs as fs) never compile
|
||||
// their JavaScript.
|
||||
if (npmBuiltinAliasOf(spec) !== null) continue;
|
||||
// Explicit attempts name the runtime package. Types-first resolution
|
||||
// may instead identify its @types twin, which has no runtime to admit.
|
||||
if (judged?.has(packageNameOfSpecifier(spec))) continue;
|
||||
@@ -219,13 +224,26 @@ function detectAutoPackages(
|
||||
chosen.push(runtimePkg);
|
||||
continue;
|
||||
}
|
||||
// Both answers compile the JavaScript with inferred types; the
|
||||
// declarations only check the importer, and an inferred surface
|
||||
// the program does not typecheck against still refuses the package
|
||||
// below. A DefinitelyTyped twin qualifies; a package without
|
||||
// declarations qualifies as a dependency of another package, whose
|
||||
// JavaScript importer has no declarations to check.
|
||||
const refusal =
|
||||
npm === null
|
||||
? owner === null && !sf.fileName.includes("/node_modules/")
|
||||
? "it ships no own .d.ts declaration surface"
|
||||
: null
|
||||
: definitelyTypedMismatch(runtimePkg, npm.typesFile);
|
||||
if (refusal === null) {
|
||||
chosen.push(runtimePkg);
|
||||
continue;
|
||||
}
|
||||
statuses.push({
|
||||
package: runtimePkg,
|
||||
status: "refused",
|
||||
detail:
|
||||
npm === null
|
||||
? "it ships no own .d.ts declaration surface"
|
||||
: `its declared types come from the separate ${npm.packageName} package, not the package itself`,
|
||||
detail: refusal,
|
||||
hint: npmStaticOptInHint(runtimePkg, owner),
|
||||
});
|
||||
continue;
|
||||
@@ -256,6 +274,7 @@ function detectAutoPackages(
|
||||
pkg,
|
||||
typesFile,
|
||||
jsEntry !== null && isJsSourceFileName(jsEntry.typesFile) ? jsEntry.typesFile : null,
|
||||
mode === "lib" && isJsSourceFileName(typesFile) && fromFile.includes("/node_modules/"),
|
||||
);
|
||||
if (reason === null) chosen.push(pkg);
|
||||
else
|
||||
|
||||
@@ -68,6 +68,7 @@ import {
|
||||
isNpmStaticPackage,
|
||||
npmStaticActive,
|
||||
npmStaticFsShadow,
|
||||
npmStaticLexerVisibleNames,
|
||||
npmStaticLiveExportTable,
|
||||
npmStaticPackageOfPath,
|
||||
reportNpmStaticOffender,
|
||||
@@ -100,8 +101,10 @@ import {
|
||||
import {
|
||||
builtinDefaultImportModule,
|
||||
canonicalBuiltinModule,
|
||||
npmBuiltinAliasOf,
|
||||
SUPPORTED_NODE_MODULES,
|
||||
unsupportedModuleFeatureOf,
|
||||
TEMPORAL_GLOBAL,
|
||||
} from "./builtin-modules.js";
|
||||
import {
|
||||
clearWorkspacePackages,
|
||||
@@ -712,6 +715,42 @@ function entryPackageProgramRoots7(
|
||||
return [...roots].sort();
|
||||
}
|
||||
|
||||
/** TS 7's ESNext temporal declarations: a lib slice scriptc adds only for
|
||||
* programs whose sources name the global (programNamesTemporal7). */
|
||||
const TEMPORAL_LIB = "lib.esnext.temporal.d.ts";
|
||||
|
||||
/** True when a program source (user code or compiled npm JavaScript)
|
||||
* contains the temporal global's name as a whole word. A cheap syntactic
|
||||
* scan of the implementation files root discovery already materialized; a
|
||||
* mention in a comment or string also counts, which only adds the lib
|
||||
* slice. */
|
||||
function programNamesTemporal7(program: ts.Program): boolean {
|
||||
for (const sf of program.getImplementationSourceFiles()) {
|
||||
const text = sf.text;
|
||||
for (
|
||||
let at = text.indexOf(TEMPORAL_GLOBAL);
|
||||
at >= 0;
|
||||
at = text.indexOf(TEMPORAL_GLOBAL, at + 8)
|
||||
) {
|
||||
if (!isIdentifierCharAt(text, at - 1) && !isIdentifierCharAt(text, at + 8)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isIdentifierCharAt(text: string, index: number): boolean {
|
||||
if (index < 0 || index >= text.length) return false;
|
||||
const c = text.charCodeAt(index);
|
||||
return (
|
||||
(c >= 48 && c <= 57) ||
|
||||
(c >= 65 && c <= 90) ||
|
||||
(c >= 97 && c <= 122) ||
|
||||
c === 36 ||
|
||||
c === 95 ||
|
||||
c > 127
|
||||
);
|
||||
}
|
||||
|
||||
/** "require" for a CommonJS edge (`require("x")`, `import x = require("x")`),
|
||||
* "import" for every ESM form — the resolution kind preflight uses. */
|
||||
function moduleSpecifierKind7(specifier: ts.StringLiteralLike): "import" | "require" {
|
||||
@@ -791,6 +830,20 @@ function loadProgram7(
|
||||
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
||||
if (pass === 31) throw new Error("static program-root discovery did not converge");
|
||||
}
|
||||
// Node 24 ships no temporal global, but TS 7 types it in an ESNext
|
||||
// lib slice. Programs that name it (feature probes such as smol-toml's
|
||||
// typeof check) get that slice so they type-check, and lower to Node's
|
||||
// answers (typeof "undefined", ReferenceError on use). Every other
|
||||
// program keeps the identical lib set: the slice would add globals to
|
||||
// the checker's world for everyone.
|
||||
if (programNamesTemporal7(program)) {
|
||||
program.dispose();
|
||||
options = {
|
||||
...options,
|
||||
lib: [...((options.lib as string[] | undefined) ?? []), TEMPORAL_LIB],
|
||||
};
|
||||
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
||||
}
|
||||
const entry = program.getSourceFile(entryPath);
|
||||
if (!entry) throw new Error(`could not load ${entryPath}`);
|
||||
const externalTypeSpecifiersByFile = externalTypeFileClosure7(program, externalTypes);
|
||||
@@ -5207,6 +5260,9 @@ function preflight7(load: LoadResult): {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// An npm package served as a builtin (graceful-fs as fs) is never
|
||||
// a program-module edge, even when --npm-static names it.
|
||||
if (npmBuiltinAliasOf(req.spec) !== null) continue;
|
||||
const isRelative = isRelativeSpecifier(req.spec);
|
||||
let dep = resolveImport7(program, sf, req.spec, "require");
|
||||
if (dep === null && !isRelative) {
|
||||
@@ -5270,6 +5326,7 @@ function preflight7(load: LoadResult): {
|
||||
if (load.externalTypes.has(spec)) {
|
||||
continue;
|
||||
}
|
||||
if (npmBuiltinAliasOf(spec) !== null) continue;
|
||||
const projectDep = resolveImport7(program, sf, spec, "require");
|
||||
if (projectDep !== null) {
|
||||
// A bare require of a JSON document only parses it: nothing to
|
||||
@@ -5520,12 +5577,18 @@ function cjsNamedImportLinkCheck(
|
||||
npmStaticDepSf7(program, from, spec, "require");
|
||||
return dep !== null && isCjsJsFile7(dep, program) ? dep : null;
|
||||
};
|
||||
// A bundler rewrite spells some lexer-visible names as accessors.
|
||||
const lexWithAccessorNames = (sf: ts.SourceFile): ReturnType<typeof cjsLexedExportsOfFile> => {
|
||||
const lexed = cjsLexedExportsOfFile(sf);
|
||||
for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name);
|
||||
return lexed;
|
||||
};
|
||||
const visible = (dep: ts.SourceFile, name: string): boolean =>
|
||||
// `default` is the module.exports binding itself — always provided.
|
||||
// The program already owns native source ASTs; reuse them without
|
||||
// starting another syntax session or reparsing their source text.
|
||||
name === "default" ||
|
||||
cjsVisibleNames(dep, cjsLexedExportsOfFile, resolveCjsDep, lexMemo).has(name);
|
||||
cjsVisibleNames(dep, lexWithAccessorNames, resolveCjsDep, lexMemo).has(name);
|
||||
|
||||
/** The statement's resolved LOCAL CommonJS dependency, when it is an
|
||||
* import/re-export from one. */
|
||||
@@ -6039,6 +6102,7 @@ export { ambientDtsPath, fallbackDtsPath, isNodeTypesPath, overridesDtsPath } fr
|
||||
export {
|
||||
builtinDefaultImportModule,
|
||||
canonicalBuiltinModule,
|
||||
npmBuiltinAliasOf,
|
||||
SUPPORTED_BUILTIN_MODULES,
|
||||
} from "./builtin-modules.js";
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import {
|
||||
isNpmStaticPackage,
|
||||
npmStaticActive,
|
||||
npmStaticPackageOfPath,
|
||||
npmStaticTransformPkgJson,
|
||||
} from "./npm-static.js";
|
||||
@@ -868,7 +869,11 @@ export function resolveBareModule(
|
||||
// never consulted — mirroring the shadowed world the tsgo host serves.
|
||||
const runtimeImport = mode === "runtime-js" || mode === "runtime-source";
|
||||
const npmStatic =
|
||||
mode === "js-only" || (!runtimeImport && mode !== "types-only" && isNpmStaticPackage(pkgName));
|
||||
mode === "js-only" ||
|
||||
(!runtimeImport &&
|
||||
mode !== "types-only" &&
|
||||
(isNpmStaticPackage(pkgName) ||
|
||||
(npmStaticActive() && isNpmStaticPackage(aliasedPackageOf(fromFile, pkgName)))));
|
||||
const conditions = runtimeImport
|
||||
? RUNTIME_IMPORT_CONDITIONS
|
||||
: npmStatic
|
||||
@@ -970,6 +975,23 @@ export function resolveBareModule(
|
||||
return npmStatic ? passOnce("js") : (passOnce("types") ?? passOnce("js"));
|
||||
}
|
||||
|
||||
/** The package an npm alias directory installs (`"x-updated":
|
||||
* "npm:x@6.3.0"` puts package x under node_modules/x-updated): the name
|
||||
* the nearest node_modules/<dirName>/package.json declares when it
|
||||
* differs from the directory name, else null. */
|
||||
function aliasedPackageOf(fromFile: string, dirName: string): string | null {
|
||||
for (let dir = dirname(resolve(fromFile)); ;) {
|
||||
const pkgDir = join(dir, "node_modules", dirName);
|
||||
if (isDirectory(pkgDir)) {
|
||||
const name = pkgJsonOf(pkgDir)?.name;
|
||||
return typeof name === "string" && name !== dirName ? name : null;
|
||||
}
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) return null;
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveWorkspaceSourceModule(
|
||||
fromFile: string,
|
||||
specifier: string,
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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 { lowerEsmInitsWithParser, rewriteBundlerCjsWithParser } from "./npm-static-rewrite-host.js";
|
||||
import { isBundlerCjsCandidate } from "./npm-static-rewrite-syntax.js";
|
||||
import { normalizeCjsFunctionExports } from "./npm-static-cjs-exports-alias.js";
|
||||
import {
|
||||
mayBundleChunks,
|
||||
splitBundledChunks,
|
||||
type BundledChunkSplit,
|
||||
} from "./npm-static-bundled-chunks.js";
|
||||
import * as syntax from "./npm-static-declaration-syntax.js";
|
||||
import { normalizeCjsExportsVar } from "./npm-static-cjs-exports-var.js";
|
||||
import type { SourceFile } from "./ts7/ast-types.js";
|
||||
|
||||
/** Evaluate an isolated, self-contained TypeScript callback under a finite
|
||||
@@ -64,10 +66,17 @@ export class FrontendServices {
|
||||
source: string,
|
||||
path: string,
|
||||
liveExportTable?: (path: string) => void,
|
||||
lexerVisibleNames?: (path: string, names: readonly string[]) => void,
|
||||
): string | { degrade: string } | null {
|
||||
this.ensureOpen();
|
||||
if (!isBundlerCjsCandidate(source)) return null;
|
||||
return rewriteBundlerCjsWithParser(this.sourceParser(), source, path, liveExportTable);
|
||||
return rewriteBundlerCjsWithParser(
|
||||
this.sourceParser(),
|
||||
source,
|
||||
path,
|
||||
liveExportTable,
|
||||
lexerVisibleNames,
|
||||
);
|
||||
}
|
||||
/** A CommonJS bundle's __commonJS chunks as their own modules (null: no
|
||||
* chunks; a degrade reason: chunks the recognizer cannot move). */
|
||||
@@ -76,6 +85,29 @@ export class FrontendServices {
|
||||
if (!mayBundleChunks(source)) return null;
|
||||
return splitBundledChunks(this.parse(path, source, "js"), path);
|
||||
}
|
||||
/** A CommonJS bundle's lazily initialized ES modules (__esm) lowered to
|
||||
* run-once closures (null: none; a degrade reason: shapes the rewrite
|
||||
* cannot lower). */
|
||||
lowerEsmInits(source: string, path: string): string | { degrade: string } | null {
|
||||
this.ensureOpen();
|
||||
if (!source.includes("__esm")) return null;
|
||||
return lowerEsmInitsWithParser(this.sourceParser(), source, path);
|
||||
}
|
||||
/** A CommonJS module whose export object is one of its functions, with
|
||||
* its member accesses respelled through that function (null: none). */
|
||||
cjsFunctionExports(source: string, path: string): string | null {
|
||||
this.ensureOpen();
|
||||
if (!source.includes("module.exports")) return null;
|
||||
return normalizeCjsFunctionExports(this.parse(path, source, "js"));
|
||||
}
|
||||
/** A CommonJS module that names its export object through a variable
|
||||
* (`var ip = exports`), its member accesses respelled through `exports`
|
||||
* (null: none). */
|
||||
cjsExportsVar(source: string, path: string): string | null {
|
||||
this.ensureOpen();
|
||||
if (!source.includes("exports")) return null;
|
||||
return normalizeCjsExportsVar(this.parse(path, source, "js"));
|
||||
}
|
||||
nullableClassFields(path: string, source: string): syntax.NpmStaticOverloadRewrite | null {
|
||||
return syntax.applyNpmStaticNullableClassFields(this.parse(path, source, "js"), source);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user