diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8a726aee8..bbc9ddb10 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -4,21 +4,23 @@ on:
push:
branches: [main]
pull_request:
- # Nightly full macOS sweep, including on days without a push to main.
+ # Nightly full macOS sweep. Pushes to main run no macOS jobs, and pull requests run them only when they change
+ # darwin code (scripts/ci-lanes.mjs).
schedule:
- cron: "17 7 * * *"
- # A manual run; macos_full runs the complete suite on macOS as the nightly sweep does (for example on a
- # release candidate commit).
+ # A manual run, for example on a release candidate commit. It runs every lane; macos_suite selects the complete
+ # suite on macOS, as the nightly sweep runs it, or the darwin suite that pull requests run.
workflow_dispatch:
inputs:
- macos_full:
- description: Run the complete test suite on macOS
- type: boolean
- default: false
+ macos_suite:
+ description: The test suite to run on macOS
+ type: choice
+ options: [full, darwin]
+ default: full
concurrency:
# A new push to a pull request cancels its previous run. Every push to main runs to completion in a group of
- # its own commit, so each merged commit gets a full result (releases require a green run on their commit). The
+ # its own commit, so each merged commit gets a result (releases require a green run on their commit). The
# nightly sweep and manual runs neither cancel nor are cancelled by main pushes.
group: ci-${{ github.event_name == 'schedule' && 'schedule' || github.event_name == 'workflow_dispatch' && format('dispatch-{0}', github.run_id) || github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
@@ -59,7 +61,8 @@ jobs:
NODE
# Which expensive lanes a pull request needs (scripts/ci-lanes.mjs, run from the base commit against the pull
- # request's diff, so a pull request cannot change its own selection). Pushes and the nightly sweep run every lane.
+ # request's diff, so a pull request cannot change its own selection). Pushes to main run every lane except macos,
+ # whose jobs the nightly sweep runs on main. The nightly sweep and manual runs run every lane.
lanes:
needs: validation_scope
if: ${{ needs.validation_scope.outputs.full == 'true' }}
@@ -86,7 +89,9 @@ jobs:
HEAD: ${{ github.event.pull_request.head.sha }}
run: |
every() { printf 'linux=true\nmacos=true\neffects=true\nbootstrap=true\n' | tee -a "$GITHUB_OUTPUT"; }
- if [ "$EVENT" != pull_request ]; then
+ if [ "$EVENT" = push ]; then
+ printf 'linux=true\nmacos=false\neffects=true\nbootstrap=true\n' | tee -a "$GITHUB_OUTPUT"
+ elif [ "$EVENT" != pull_request ]; then
every
elif git show "$BASE:scripts/ci-lanes.mjs" > "$RUNNER_TEMP/ci-lanes.mjs" 2> /dev/null; then
node "$RUNNER_TEMP/ci-lanes.mjs" --base "$BASE" --head "$HEAD" --github-output
@@ -219,25 +224,25 @@ jobs:
tests_macos:
needs: [validation_scope, lanes, native_macos]
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.macos == 'true' }}
- name: test (macOS, ${{ matrix.flavor }}, ${{ (github.event_name != 'schedule' && !inputs.macos_full) && 'darwin ' || '' }}${{ matrix.shard }}/${{ (github.event_name != 'schedule' && !inputs.macos_full) && '3' || '10' }})
+ name: test (macOS, ${{ matrix.flavor }}, ${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && 'darwin ' || '' }}${{ matrix.shard }}/${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && '3' || '10' }})
# arm64 macOS: same clang/libc the differential + number-formatting
- # oracles were verified against. Pull requests and pushes to main run the
- # darwin suite (scripts/ci-test-plan.mjs) in three slices; the nightly
- # schedule and manual runs with macos_full run the complete suite in ten.
- # The complete suite on every push needed more macOS runners than the
- # repository has.
+ # oracles were verified against. Pull requests that change darwin code and
+ # manual runs with macos_suite set to darwin run the darwin suite
+ # (scripts/ci-test-plan.mjs) in three slices; the nightly schedule and
+ # other manual runs run the complete suite in ten. The complete suite on
+ # every push needed more macOS runners than the repository has.
runs-on: macos-15
strategy:
fail-fast: false
matrix:
flavor: [plain, san]
- shard: ${{ fromJSON((github.event_name != 'schedule' && !inputs.macos_full) && '[1, 2, 3]' || '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]') }}
+ shard: ${{ fromJSON((github.event_name != 'schedule' && inputs.macos_suite != 'full') && '[1, 2, 3]' || '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]') }}
env:
# See the Linux tests job for the worker cap and case partitioning.
SCRIPTC_TEST_WORKERS: "4"
SCRIPTC_SAN: ${{ matrix.flavor == 'san' && '1' || '' }}
- SCRIPTC_TEST_SHARD: ${{ matrix.shard }}/${{ (github.event_name != 'schedule' && !inputs.macos_full) && '3' || '10' }}
- SCRIPTC_CI_SUITE: ${{ (github.event_name != 'schedule' && !inputs.macos_full) && 'darwin' || 'full' }}
+ SCRIPTC_TEST_SHARD: ${{ matrix.shard }}/${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && '3' || '10' }}
+ SCRIPTC_CI_SUITE: ${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && 'darwin' || 'full' }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
@@ -254,7 +259,7 @@ jobs:
path: ${{ runner.temp }}
- run: tar -xzf "$RUNNER_TEMP/native-darwin-arm64.tar.gz"
- run: pnpm build
- - name: Tests (${{ (github.event_name != 'schedule' && !inputs.macos_full) && 'darwin suite' || 'corpus and integration suites' }})
+ - name: Tests (${{ (github.event_name != 'schedule' && inputs.macos_suite != 'full') && 'darwin suite' || 'corpus and integration suites' }})
run: node scripts/ci-test.mjs
native_linux:
@@ -498,6 +503,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' }}
@@ -844,14 +873,25 @@ jobs:
strategy:
fail-fast: false
matrix:
- include:
- - runtime: runtime-ios-arm64
- runner: macos-15
- - runtime: runtime-ios-simulator-arm64
- runner: macos-15
- - runtime: runtime-android-arm64
- runner: ubuntu-24.04
- runs-on: ${{ matrix.runner }}
+ runtime: [runtime-android-arm64]
+ runs-on: ubuntu-24.04
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ./.github/actions/mobile-runtime-pack
+ with:
+ runtime: ${{ matrix.runtime }}
+
+ # The iOS runtime packs build on macOS runners, so they run in the macos lane.
+ mobile_runtime_packs_ios:
+ needs: [validation_scope, lanes]
+ if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.macos == 'true' }}
+ name: test (${{ matrix.runtime }})
+ strategy:
+ fail-fast: false
+ matrix:
+ runtime: [runtime-ios-arm64, runtime-ios-simulator-arm64]
+ runs-on: macos-15
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
@@ -863,7 +903,29 @@ 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,
+ mobile_runtime_packs_ios,
+ ]
# Run even after failed or cancelled lanes: skipped required jobs allow merging.
if: ${{ always() }}
runs-on: ubuntu-latest
@@ -887,12 +949,14 @@ 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 }}"
lane windows_cli "${{ needs.windows_cli.result }}" "${{ needs.lanes.outputs.linux }}"
lane llvm_artifacts_windows_x64 "${{ needs.llvm_artifacts_windows_x64.result }}" "${{ needs.lanes.outputs.linux }}"
lane mobile_runtime_packs "${{ needs.mobile_runtime_packs.result }}" "${{ needs.lanes.outputs.linux }}"
+ lane mobile_runtime_packs_ios "${{ needs.mobile_runtime_packs_ios.result }}" "${{ needs.lanes.outputs.macos }}"
;;
false)
echo "::notice::Full CI is deferred until this PR becomes the lowest unmerged layer. Merge the stack from the bottom up."
diff --git a/docs/content/docs/cli.mdx b/docs/content/docs/cli.mdx
index c4fdf7876..786cf2c63 100644
--- a/docs/content/docs/cli.mdx
+++ b/docs/content/docs/cli.mdx
@@ -134,7 +134,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
Coverage only. After the report, list the functions that already operate on values typed only at runtime and the bindings, fields, functions, classes and modules that unsupported constructs would move to the dynamic tier, each with the diagnostic behind it. With a name such as x, Point, or Point.x, show only the matching units. See Dynamic tier.
--wasm <file.wasm>
- Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and .wasm files shipped by packages that use WebAssembly; a scriptc.wasm array in package.json adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. scriptc does not compile WebAssembly modules to native code yet. Repeatable.
+ Coverage only. Report a WebAssembly module the program loads from a path its sources do not spell out. The report already lists modules whose paths the compiler resolves, modules inlined as base64 literals, and .wasm files shipped by packages that use WebAssembly; a scriptc.wasm array in package.json adds files the same way as this flag. Each module is listed with its size, whether it validates, and the WebAssembly proposals it uses. Builds translate the modules they find to native code; a build reads the scriptc.wasm array but not this flag. Repeatable.
--sanitize
Build an executable with AddressSanitizer and a reference-count audit at exit, the same checks scriptc's own test suite uses. Requires a C compiler and is available only for host builds. Not supported with --emit=asm or --emit=obj.
@@ -178,6 +178,9 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
SCRIPTC_RUNTIME_PACK
A runtime pack directory to use instead of an installed @scriptc/runtime-<target> package.
+
+ SCRIPTC_CHECKER
+ The TypeScript 7 API server that checks the program: tsc-ts (the default), which scriptc includes, or tsgo, from the typescript package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is tsgo, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.
```console
diff --git a/docs/content/docs/coverage.mdx b/docs/content/docs/coverage.mdx
index b27327b91..1bb645bd9 100644
--- a/docs/content/docs/coverage.mdx
+++ b/docs/content/docs/coverage.mdx
@@ -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
diff --git a/docs/content/docs/dependencies.mdx b/docs/content/docs/dependencies.mdx
index 3c6958841..b16c14e7d 100644
--- a/docs/content/docs/dependencies.mdx
+++ b/docs/content/docs/dependencies.mdx
@@ -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).
@@ -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
diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index c64c5cebd..823b7f028 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -102,6 +102,8 @@ title: "Limitations"
../limitations/builtin-apis/process-memory.mdx
+../limitations/builtin-apis/v8.mdx
+
../limitations/builtin-apis/performance-entries.mdx
../limitations/builtin-apis/builtin-module-lookup.mdx
@@ -212,6 +214,10 @@ title: "Limitations"
../limitations/console-and-process-streams.mdx
+### graceful-fs
+
+../limitations/graceful-fs.mdx
+
### DNS lookup errors
../limitations/dns-lookup-errors.mdx
@@ -224,6 +230,10 @@ title: "Limitations"
../limitations/udp-socket-options.mdx
+### Unix domain sockets and request options
+
+../limitations/unix-domain-sockets.mdx
+
### Process arguments and errors
../limitations/process-arguments-and-errors.mdx
diff --git a/docs/content/limitations/builtin-apis/date.mdx b/docs/content/limitations/builtin-apis/date.mdx
index 4c1c7e034..ac8bf9115 100644
--- a/docs/content/limitations/builtin-apis/date.mdx
+++ b/docs/content/limitations/builtin-apis/date.mdx
@@ -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.
diff --git a/docs/content/limitations/builtin-apis/eventemitter.mdx b/docs/content/limitations/builtin-apis/eventemitter.mdx
index 4f7142094..a9f03b857 100644
--- a/docs/content/limitations/builtin-apis/eventemitter.mdx
+++ b/docs/content/limitations/builtin-apis/eventemitter.mdx
@@ -1,4 +1,5 @@
- **EventEmitter:** Literal event names use fixed argument tuples.
Computed string names require a constant prefix or suffix that excludes `error`, meta events, and stream-internal events; their arguments must match the exact arity and support checked conversion.
Unrestricted names, symbols, names that may reach internal events, and typed `listeners()`/`rawListeners()` results for names with possible computed registrations are unsupported.
+ `events.once` accepts EventEmitter and stream instances with a literal event name and an optional object-literal `{ signal }`; a stream's `data` event, other options values, and EventTarget emitters are unsupported.
Reading `defaultMaxListeners` through a CommonJS destructured or member binding (`const { defaultMaxListeners } = require("events")`) is unsupported.
diff --git a/docs/content/limitations/builtin-apis/globals.mdx b/docs/content/limitations/builtin-apis/globals.mdx
index 5d8f70969..f875bd761 100644
--- a/docs/content/limitations/builtin-apis/globals.mdx
+++ b/docs/content/limitations/builtin-apis/globals.mdx
@@ -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.
diff --git a/docs/content/limitations/builtin-apis/performance-entries.mdx b/docs/content/limitations/builtin-apis/performance-entries.mdx
index 92af98cb7..a42940b68 100644
--- a/docs/content/limitations/builtin-apis/performance-entries.mdx
+++ b/docs/content/limitations/builtin-apis/performance-entries.mdx
@@ -1,2 +1,4 @@
- **Performance entries:** `performance.mark()`, `measure()`, and the `getEntries` methods return entries with Node's fields, `toJSON()`, inspection, and identity.
`Object.keys`, `for...in`, object spread, and `hasOwnProperty` on an entry are unsupported, and an entry stored in an `unknown` slot prints as a plain object.
+- **Event-loop metrics:** `performance.eventLoopUtilization()` counts the main thread's CPU time since the event loop started as active time, so blocking system calls count as idle where Node counts them as active.
+ `monitorEventLoopDelay()` supports `enable()`, `disable()`, `reset()`, `percentile()`, and the `min`, `max`, `mean`, `stddev`, `count`, and `exceeds` readings; the BigInt readings, `percentiles`, and non-literal option objects are unsupported.
diff --git a/docs/content/limitations/builtin-apis/prototypes.mdx b/docs/content/limitations/builtin-apis/prototypes.mdx
index 4ac2ee438..3e0b5789d 100644
--- a/docs/content/limitations/builtin-apis/prototypes.mdx
+++ b/docs/content/limitations/builtin-apis/prototypes.mdx
@@ -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.
diff --git a/docs/content/limitations/builtin-apis/streams-and-readline.mdx b/docs/content/limitations/builtin-apis/streams-and-readline.mdx
index 0ac0ca3a6..a21f7a617 100644
--- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx
+++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx
@@ -1,4 +1,10 @@
- **Streams and readline:** Stream chunks are Buffers or strings.
+ `NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances.
+ `readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
+ When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
+ Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
+ A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams at run time; other values throw a not-supported error.
+ Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
`Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported.
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported.
@@ -11,6 +17,7 @@
`stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
+ A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error.
`node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node.
With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output.
@@ -26,3 +33,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.
diff --git a/docs/content/limitations/builtin-apis/v8.mdx b/docs/content/limitations/builtin-apis/v8.mdx
new file mode 100644
index 000000000..066fb000d
--- /dev/null
+++ b/docs/content/limitations/builtin-apis/v8.mdx
@@ -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.
diff --git a/docs/content/limitations/builtin-function-values.mdx b/docs/content/limitations/builtin-function-values.mdx
index c08540c1a..2c08ecc6d 100644
--- a/docs/content/limitations/builtin-function-values.mdx
+++ b/docs/content/limitations/builtin-function-values.mdx
@@ -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.
@@ -12,7 +16,15 @@ Other immutable aliases of table-backed Node builtins support direct calls, but
Native calls accept standard process streams; custom native streams and mutations to `util.inspect.colors` remain unsupported.
`util.inherits` links the prototypes of constructor functions, and `util.deprecate`, `util.callbackify`, and `util.debuglog` return native function values that forward receivers and arguments.
-Class constructors passed to `util.inherits` or `util.deprecate` are refused.
+In JavaScript, a constructor function can inherit from `EventEmitter`, the legacy `Stream` constructor, or a stream class through `util.inherits` or `Object.setPrototypeOf`.
+`EventEmitter.call(this)` and `Stream.call(this)` initialize the object as Node does, and its listener methods keep their state in the object's own `_events`, `_eventsCount`, and `_maxListeners` properties.
+The `captureRejections` option and `events.errorMonitor` listeners are not supported on such objects.
+`Readable.call(this, options)` and the same call on `Writable`, `Duplex`, `Transform`, or `PassThrough` give the object a native stream.
+The stream calls the object's `_read`, `_write`, `_final`, `_transform`, `_flush`, and `_destroy` with the object as `this`, and the stream prototypes' methods and accessors work on it.
+Its listeners live in the native stream rather than in `_events`, and `_readableState` and `_writableState` are read-only views that answer the commonly read fields.
+Typed code that receives such an object as a stream class uses its native stream, and piping a native stream into it works.
+The options the native streams refuse (`objectMode`, `decodeStrings: false`, `signal`, and others) are refused here too, as are `_writev`, `_construct`, symbol event names, `listeners()`, `rawListeners()`, `wrap()`, strings written in encodings other than UTF-8, and pipes into a destination that overrides `write()` or `end()`.
+Other class constructors passed to `util.inherits` or `util.deprecate` are refused.
Deprecation warnings are emitted on the next tick and follow assignments to `process.noDeprecation` and `process.throwDeprecation`; compiled binaries take no Node command-line flags such as `--no-deprecation` or `--trace-deprecation`, and `process.traceDeprecation` is unsupported.
A stored `util.deprecate` reports a `length` of 4, where Node reports 3.
In TypeScript, a `util.callbackify` result must reach a callback whose error parameter accepts `null`; the @types/node overloads that type it as non-null are refused.
diff --git a/docs/content/limitations/child-process-workers.mdx b/docs/content/limitations/child-process-workers.mdx
index 5b41c5e53..9d6602509 100644
--- a/docs/content/limitations/child-process-workers.mdx
+++ b/docs/content/limitations/child-process-workers.mdx
@@ -12,3 +12,7 @@ Message listeners may return `void` or `Promise`.
Messages use newline-delimited JSON, so their static types must be JSON-stringifiable or `unknown` backed by a checked JSON tree.
`send()` returns the channel's backpressure state and invokes its callback later with `null` or an `Error`.
`disconnect()` changes `connected` synchronously, before the later disconnect events.
+
+A `'data'` listener on a child's `stdout` or `stderr` whose chunk is `unknown` or `any` receives strings when a `setEncoding()` statement on the same stream precedes it in its block or an enclosing block, and Buffers when the function never calls `setEncoding()` on that stream.
+Any other placement of `setEncoding()` is refused at compile time, and a `setEncoding()` call that runs elsewhere afterwards makes the listener throw a TypeError instead of receiving strings.
+An inline `'exit'` or `'close'` listener may declare `(code: number, signal: string)`, which TypeScript accepts through the catch-all `on` overload; it receives the `null` values Node passes, and a typed use of a `null` value throws a TypeError.
diff --git a/docs/content/limitations/console-and-process-streams.mdx b/docs/content/limitations/console-and-process-streams.mdx
index 0d7cd8f30..c62b82f0c 100644
--- a/docs/content/limitations/console-and-process-streams.mdx
+++ b/docs/content/limitations/console-and-process-streams.mdx
@@ -9,4 +9,5 @@ JavaScript can store `process.stdin`, `stdout`, and `stderr`, replace output `wr
Input supports data/end/error listeners, pause/resume, buffered reads, and TTY raw mode.
Output writes are synchronous, with successful callbacks on the next-tick queue; queued backpressure is not modeled.
Stored streams do not support piping, encoding changes, arbitrary properties, or other events.
-TypeScript output parameters support `write(string)`.
+A TypeScript `NodeJS.WritableStream` parameter that only receives `process.stdout` or `process.stderr` supports `write(string)`.
+When other streams flow into the same parameter, the process streams join them as `Writable` instances with the full writable surface.
diff --git a/docs/content/limitations/dates-and-timezones.mdx b/docs/content/limitations/dates-and-timezones.mdx
index e5655eb85..cba1d9004 100644
--- a/docs/content/limitations/dates-and-timezones.mdx
+++ b/docs/content/limitations/dates-and-timezones.mdx
@@ -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.
diff --git a/docs/content/limitations/dns-lookup-errors.mdx b/docs/content/limitations/dns-lookup-errors.mdx
index 9ed2c854c..e407922e1 100644
--- a/docs/content/limitations/dns-lookup-errors.mdx
+++ b/docs/content/limitations/dns-lookup-errors.mdx
@@ -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.
diff --git a/docs/content/limitations/graceful-fs.mdx b/docs/content/limitations/graceful-fs.mdx
new file mode 100644
index 000000000..aabf175b7
--- /dev/null
+++ b/docs/content/limitations/graceful-fs.mdx
@@ -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`.
diff --git a/docs/content/limitations/language-features/generics.mdx b/docs/content/limitations/language-features/generics.mdx
index 6e203506f..a234c8f57 100644
--- a/docs/content/limitations/language-features/generics.mdx
+++ b/docs/content/limitations/language-features/generics.mdx
@@ -3,3 +3,9 @@
Independent concrete uses have no specialization-count limit; recursively expanding demands and type layouts produce diagnostics.
Other generic calls compile when the target resolves statically.
Generic function values require a concrete signature at use and a binding that is never reassigned.
+- **Variadic wrappers:** A generic higher-order wrapper is specialized to each wrapped function's exact signature.
+ `Parameters`, `ReturnType`, and rest parameters typed as fixed tuples become that signature's parameter list.
+ A function expression with no parameters that reads `arguments` compiles only when its value is asserted to, or contextually typed as, one fixed-arity signature; `arguments` is then that signature's tuple.
+ Forwarding it with `fn.apply(this, arguments)` passes `this` through.
+ Reading `fn.name` on a wrapped function requires its name to be known where the wrapper is called, such as a function declaration or a `const`-bound function.
+ `Parameters` over a signature with optional or array-typed rest parameters is unsupported, because that tuple has no fixed length.
diff --git a/docs/content/limitations/runtime-code-evaluation.mdx b/docs/content/limitations/runtime-code-evaluation.mdx
index c85e4dcd4..7b8117e70 100644
--- a/docs/content/limitations/runtime-code-evaluation.mdx
+++ b/docs/content/limitations/runtime-code-evaluation.mdx
@@ -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.
diff --git a/docs/content/limitations/unix-domain-sockets.mdx b/docs/content/limitations/unix-domain-sockets.mdx
new file mode 100644
index 000000000..e98bc72e3
--- /dev/null
+++ b/docs/content/limitations/unix-domain-sockets.mdx
@@ -0,0 +1,2 @@
+`server.listen(path)` and the `socketPath` option of `http.request()` and `https.request()` use Unix domain sockets on Linux and macOS. On Windows, where Node maps these paths to named pipes, they fail with `ENOTSUP`. `server.address()` on a server listening on a Unix socket returns the socket path, as in Node, when the result is typed `string | AddressInfo | null` (the @types/node declaration). Where it is typed as the `{ address, family, port }` record alone, which cannot hold the path, the call throws an error for such a server. `listen(path, backlog, callback)` is refused at compile time; pass `listen(path, callback)`.
+When the options of `http.request()` come from a variable, or from an object literal that spreads one, they are read at run time with Node's validation errors. The `auth`, `createConnection`, `family`, `hints`, `insecureHTTPParser`, `joinDuplicateHeaders`, `localAddress`, `localPort`, `lookup` and `uniqueHeaders` options then throw a runtime error when they hold a value, and so do `setHost: false` and `setDefaultHeaders: false`.
diff --git a/docs/content/limitations/webassembly-modules.mdx b/docs/content/limitations/webassembly-modules.mdx
index e3bf6e500..43fa55e48 100644
--- a/docs/content/limitations/webassembly-modules.mdx
+++ b/docs/content/limitations/webassembly-modules.mdx
@@ -1,7 +1,31 @@
-Compiled programs can't load WebAssembly modules yet.
-Code that only mentions the `WebAssembly` types type-checks, including packages whose declarations name them, and `typeof WebAssembly` answers as it does in Node.js.
-Every use of the API itself is rejected: `WebAssembly.compile`, `instantiate` and `validate`, `new WebAssembly.Module` and the other constructors, `instanceof` checks, and reads of `globalThis.WebAssembly`.
-TypeScript sources fail to build with SC2020 at each use, and JavaScript sources throw that diagnostic when the statement runs.
+scriptc translates WebAssembly modules to native code when it builds the program; there is no interpreter or JIT in the executable.
+A build translates every module whose bytes it can see: files at paths the compiler resolves (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`, `new URL("./x.wasm", import.meta.url)`, `path.join(__dirname, ...)`), including `.so` WebAssembly shared libraries such as quickjs-wasi's extensions, modules inlined as base64 or byte-array literals, `.wasm` files shipped by packages whose code uses WebAssembly, and files listed in a `scriptc.wasm` array in `package.json`.
+At run time `WebAssembly.compile`, `instantiate`, `validate` and `new WebAssembly.Module` find the translated module by the SHA-256 of the bytes they are given.
+Bytes the build did not see, including bytes produced at run time, reject with a `CompileError` that says the module was not translated, and `WebAssembly.validate` answers `false` for them.
+`scriptc coverage` lists the modules a build finds, with their size, whether they validate, and the WebAssembly proposals they use.
-`scriptc coverage` lists the modules a program would load, with their size, whether they validate, and the WebAssembly proposals they use.
-It finds modules at paths the compiler resolves, modules inlined as base64 literals, `.wasm` files shipped by packages that use WebAssembly, and files named with [`--wasm`](/docs/cli) or a `scriptc.wasm` array in `package.json`.
+Translated modules can use the WebAssembly 2.0 feature set: the MVP plus sign-extension operators, non-trapping float-to-int conversions, bulk memory, multi-value, mutable globals, reference types, and extended constant expressions.
+Modules that use SIMD, threads, exception handling, tail calls, typed function references or GC, memory64, or multiple memories are not translated, so loading them rejects with the `CompileError` above; packages that fall back to a scalar build when that happens, such as undici and es-module-lexer, take their fallback.
+
+Supported: `WebAssembly.compile`, `instantiate` (with bytes or a `Module`), `validate`, `new WebAssembly.Module`, `new WebAssembly.Instance`, `new WebAssembly.Memory`, `new WebAssembly.Table`, `new WebAssembly.Global`, `WebAssembly.Module.imports`, `exports` and `customSections`, and `instanceof` for every class; the error classes `CompileError`, `LinkError` and `RuntimeError`, constructed with or without `new`; `globalThis.WebAssembly` and its `compile`, `instantiate` and `validate` functions as values.
+Instances expose exported functions, memories (`buffer`, `grow`), tables (`get`, `set`, `grow`, `length`) and globals (`value`, `valueOf`).
+Imports can be functions, numbers or BigInts for immutable globals, and memories, tables and globals that the program constructs or other instances export; their limits, types and mutability are checked with Node.js's `LinkError` messages.
+Function references from a table or global reach JavaScript as callable functions, and exported functions can be stored in tables and globals, which is what dynamic linkers such as quickjs-wasi's extension loader do.
+Traps throw `WebAssembly.RuntimeError` with Node.js's messages, unbounded recursion throws `RangeError`, and an exception thrown by an imported function propagates through the WebAssembly frames unchanged.
+A `WebAssembly.Module` can be sent to a worker through `workerData` or `postMessage`.
+
+Not supported yet: the `Tag` and `Exception` constructors, shared memories (`new WebAssembly.Memory({ shared: true, ... })` throws a `TypeError`), `compileStreaming` and `instantiateStreaming`, and the classes used as values other than with `new`, `instanceof` and `WebAssembly.Module`'s static functions.
+TypeScript sources fail to build with SC2020 at each such use, and JavaScript sources throw that diagnostic when the statement runs.
+Programs built for `wasm32-wasi` can't use the WebAssembly API.
+
+Differences from Node.js:
+
+- Growing a memory, from JavaScript or inside the module, does not detach the previous `ArrayBuffer`.
+ The memory never moves, so views created before the growth stay valid but keep their old length; code that reads `memory.buffer` again after growing, as WebAssembly glue code does, behaves as in Node.js.
+- `valueOf()` answers a `WebAssembly.Global`'s value only when the receiver's static type is `WebAssembly.Global`; through an untyped value it answers the object, and converting a `Global` to a primitive uses the object rather than its value.
+- Instances are never freed, together with their memories, imports and exports.
+- A load whose result the module never uses may be removed instead of trapping when its address is out of bounds.
+ Loads whose result is dropped explicitly still trap.
+- NaN payloads may differ, for example when a signaling NaN passes through arithmetic or a JavaScript number.
+- `WebAssembly.compile` and `instantiate` with bytes settle after the current microtasks, as in Node.js, but can settle after `setImmediate` callbacks that Node.js would run later.
+- The executable reads `.wasm` files at run time from the paths they had when it was built.
diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json
index be1a931cc..e5fd082f6 100644
--- a/docs/src/generated/node-v24-compatibility.json
+++ b/docs/src/generated/node-v24-compatibility.json
@@ -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
@@ -329,8 +329,8 @@
"apiEntries": 92,
"static": {
"supported": 1,
- "partial": 15,
- "refused": 1,
+ "partial": 16,
+ "refused": 0,
"not-implemented": 75,
"by-design": 0,
"unreviewed": 0,
@@ -539,9 +539,9 @@
"apiEntries": 131,
"static": {
"supported": 0,
- "partial": 19,
+ "partial": 25,
"refused": 0,
- "not-implemented": 112,
+ "not-implemented": 106,
"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"
}
},
{
@@ -36590,9 +36590,9 @@
"inherited": true
},
"static": {
- "status": "refused",
- "detail": "Explicitly rejected by a named compiler diagnostic.",
- "verification": "explicit-refusal"
+ "status": "partial",
+ "detail": "Implemented for the call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -54688,9 +54688,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"
}
},
{
@@ -54803,9 +54803,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"
}
},
{
@@ -55286,9 +55286,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"
}
},
{
@@ -55309,9 +55309,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"
}
},
{
@@ -55677,9 +55677,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 call shapes accepted by the compiler lowering.",
+ "verification": "test-backed"
}
},
{
@@ -55815,9 +55815,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"
}
},
{
diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json
index 2d10f73b4..51459d3b3 100644
--- a/internal/compatibility/generated/node-v24-backlog.json
+++ b/internal/compatibility/generated/node-v24-backlog.json
@@ -3,20 +3,20 @@
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"summary": {
- "taskCount": 3453,
- "tierItemCount": 3453,
+ "taskCount": 3454,
+ "tierItemCount": 3454,
"tiers": {
"static": {
- "audit-partial": 1179,
- "verify-gap": 2146,
- "replace-refusal": 41,
+ "audit-partial": 1197,
+ "verify-gap": 2130,
+ "replace-refusal": 40,
"classify": 2,
"implement": 85
}
},
"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"
+ ]
}
}
},
@@ -34202,12 +34235,25 @@
"priority": "high",
"tiers": {
"static": {
- "status": "refused",
- "action": "replace-refusal",
- "verification": "explicit-refusal",
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
"confidence": "high",
- "source": "surface-manifest:unsupported:stdlib.webassembly",
- "tests": []
+ "source": "surface-manifest:stdlib.webassembly",
+ "tests": [
+ "tests/corpus/webassembly-api.mjs",
+ "tests/corpus/webassembly-typed.ts",
+ "tests/corpus/webassembly-worker.ts",
+ "tests/corpus/webassembly-spec-int/main.mjs",
+ "tests/corpus/webassembly-spec-float/main.mjs",
+ "tests/corpus/webassembly-spec-control/main.mjs",
+ "tests/corpus/webassembly-spec-memory/main.mjs",
+ "tests/corpus/webassembly-spec-table/main.mjs",
+ "tests/corpus/webassembly-spec-imports/main.mjs",
+ "tests/corpus/webassembly-jsapi.mjs",
+ "tests/corpus/webassembly-wpt-jsapi.mjs",
+ "tests/corpus/webassembly-dylink/main.mjs"
+ ]
}
}
},
@@ -54231,12 +54277,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:perf_hooks",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:perf_hooks.histogram.percentile",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
}
}
},
@@ -54356,12 +54405,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:perf_hooks",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:perf_hooks.histogram.reset",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
}
}
},
@@ -54881,12 +54933,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:perf_hooks",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:perf_hooks.histogram.disable",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
}
}
},
@@ -54906,12 +54961,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:perf_hooks",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:perf_hooks.histogram.enable",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
}
}
},
@@ -55306,12 +55364,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:perf_hooks",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "surface-manifest:node-builtin.perf_hooks.monitorEventLoopDelay",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
}
}
},
@@ -55466,12 +55527,15 @@
"priority": "high",
"tiers": {
"static": {
- "status": "not-implemented",
- "action": "verify-gap",
- "verification": "registry-gap",
- "confidence": "low",
- "source": "compiler-unmatched:perf_hooks",
- "tests": []
+ "status": "partial",
+ "action": "audit-partial",
+ "verification": "test-backed",
+ "confidence": "high",
+ "source": "compiler-feature:perf_hooks.performance.eventLoopUtilization",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
}
}
},
@@ -77813,6 +77877,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 +82052,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"
+ ]
}
}
},
diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json
index 637a83345..0e1e73a26 100644
--- a/internal/compatibility/generated/node-v24-internal.json
+++ b/internal/compatibility/generated/node-v24-internal.json
@@ -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
@@ -341,8 +341,8 @@
"apiEntries": 92,
"static": {
"supported": 1,
- "partial": 15,
- "refused": 1,
+ "partial": 16,
+ "refused": 0,
"not-implemented": 75,
"by-design": 0,
"unreviewed": 0,
@@ -566,9 +566,9 @@
"apiEntries": 131,
"static": {
"supported": 0,
- "partial": 19,
+ "partial": 25,
"refused": 0,
- "not-implemented": 112,
+ "not-implemented": 106,
"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"
},
@@ -52249,6 +52298,18 @@
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/corpus/6331-jose-root-entry/main.ts",
"tests/corpus/text-codec-values/main.ts",
+ "tests/corpus/webassembly-api.mjs",
+ "tests/corpus/webassembly-dylink/main.mjs",
+ "tests/corpus/webassembly-jsapi.mjs",
+ "tests/corpus/webassembly-spec-control/main.mjs",
+ "tests/corpus/webassembly-spec-float/main.mjs",
+ "tests/corpus/webassembly-spec-imports/main.mjs",
+ "tests/corpus/webassembly-spec-int/main.mjs",
+ "tests/corpus/webassembly-spec-memory/main.mjs",
+ "tests/corpus/webassembly-spec-table/main.mjs",
+ "tests/corpus/webassembly-typed.ts",
+ "tests/corpus/webassembly-worker.ts",
+ "tests/corpus/webassembly-wpt-jsapi.mjs",
"tests/fixtures/node-types/navigator.ts",
"tests/harness/fetch-conformance.test.ts",
"tests/harness/project-config.test.ts"
@@ -53600,8 +53661,22 @@
"inherited": true
},
"static": {
- "status": "refused",
- "evidence": "surface-manifest:unsupported:stdlib.webassembly"
+ "status": "partial",
+ "evidence": "surface-manifest:stdlib.webassembly",
+ "tests": [
+ "tests/corpus/webassembly-api.mjs",
+ "tests/corpus/webassembly-typed.ts",
+ "tests/corpus/webassembly-worker.ts",
+ "tests/corpus/webassembly-spec-int/main.mjs",
+ "tests/corpus/webassembly-spec-float/main.mjs",
+ "tests/corpus/webassembly-spec-control/main.mjs",
+ "tests/corpus/webassembly-spec-memory/main.mjs",
+ "tests/corpus/webassembly-spec-table/main.mjs",
+ "tests/corpus/webassembly-spec-imports/main.mjs",
+ "tests/corpus/webassembly-jsapi.mjs",
+ "tests/corpus/webassembly-wpt-jsapi.mjs",
+ "tests/corpus/webassembly-dylink/main.mjs"
+ ]
},
"anchorSource": "exact"
},
@@ -75571,7 +75646,9 @@
"tests/corpus/5212-perf-marks-measures.ts",
"tests/corpus/5213-perf-timing-errors.ts",
"tests/corpus/5214-perf-entry-inspect.ts",
- "tests/corpus/5215-perf-timing-validation.js"
+ "tests/corpus/5215-perf-timing-validation.js",
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "chapter"
@@ -77612,8 +77689,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:perf_hooks"
+ "status": "partial",
+ "evidence": "compiler-feature:perf_hooks.histogram.percentile",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -77727,8 +77808,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:perf_hooks"
+ "status": "partial",
+ "evidence": "compiler-feature:perf_hooks.histogram.reset",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -78210,8 +78295,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:perf_hooks"
+ "status": "partial",
+ "evidence": "compiler-feature:perf_hooks.histogram.disable",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -78233,8 +78322,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:perf_hooks"
+ "status": "partial",
+ "evidence": "compiler-feature:perf_hooks.histogram.enable",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -78601,8 +78694,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:perf_hooks"
+ "status": "partial",
+ "evidence": "surface-manifest:node-builtin.perf_hooks.monitorEventLoopDelay",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -78751,8 +78848,12 @@
"inherited": true
},
"static": {
- "status": "not-implemented",
- "evidence": "compiler-unmatched:perf_hooks"
+ "status": "partial",
+ "evidence": "compiler-feature:perf_hooks.performance.eventLoopUtilization",
+ "tests": [
+ "tests/corpus/6397-event-loop-metrics.ts",
+ "tests/fixtures/node-types/event-loop-metrics.ts"
+ ]
},
"anchorSource": "exact"
},
@@ -104477,6 +104578,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 +105702,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 +111319,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 +111589,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"
},
diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json
index 701b5aac1..734e038ef 100644
--- a/internal/compatibility/static-support.json
+++ b/internal/compatibility/static-support.json
@@ -28,6 +28,7 @@
"webstreams": "unreviewed"
},
"evidenceByManifestPrefix": {
+ "stdlib.webassembly": ["tests/corpus/webassembly-api.mjs", "tests/corpus/webassembly-typed.ts", "tests/corpus/webassembly-worker.ts", "tests/corpus/webassembly-spec-int/main.mjs", "tests/corpus/webassembly-spec-float/main.mjs", "tests/corpus/webassembly-spec-control/main.mjs", "tests/corpus/webassembly-spec-memory/main.mjs", "tests/corpus/webassembly-spec-table/main.mjs", "tests/corpus/webassembly-spec-imports/main.mjs", "tests/corpus/webassembly-jsapi.mjs", "tests/corpus/webassembly-wpt-jsapi.mjs", "tests/corpus/webassembly-dylink/main.mjs"],
"node-builtin.fs.readFileSync": ["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", "tests/corpus/fs-read-encodings.ts"],
"syntax.using-declarations": ["tests/corpus/2841-using-sync.ts"],
"syntax.await-using-declarations": ["tests/corpus/2842-await-using.ts"],
@@ -47,6 +48,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"],
@@ -134,6 +139,8 @@
"node-builtin.perf_hooks.performance.getEntries": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts", "tests/corpus/5215-perf-timing-validation.js"],
"node-builtin.perf_hooks.performance.clearMarks": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5213-perf-timing-errors.ts", "tests/corpus/5215-perf-timing-validation.js"],
"node-builtin.perf_hooks.performance.clearMeasures": ["tests/corpus/5212-perf-marks-measures.ts"],
+ "node-builtin.perf_hooks.performance.eventLoopUtilization": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"],
+ "node-builtin.perf_hooks.monitorEventLoopDelay": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"],
"node-builtin.process": ["tests/corpus/990-process-basics.ts", "tests/corpus/2314-process-introspection.ts"],
"node-builtin.process.hrtime": ["tests/corpus/process-hrtime.js"],
"node-builtin.process.memoryUsage": ["tests/corpus/5210-process-memory-usage.ts"],
@@ -242,6 +249,8 @@
{ "chapter": "util", "symbols": ["util.parseEnv"], "status": "partial", "evidence": ["tests/corpus/node24-env-parse.ts", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "process", "symbols": ["process.memoryUsage.rss"], "status": "partial", "evidence": ["tests/corpus/5210-process-memory-usage.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.now"], "status": "partial", "evidence": ["tests/corpus/2427-perf-hooks-now.ts", "tests/corpus/2446-performance-global.ts"] },
+ { "chapter": "perf_hooks", "symbols": ["performance.eventLoopUtilization"], "status": "partial", "evidence": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"] },
+ { "chapter": "perf_hooks", "symbols": ["histogram.enable", "histogram.disable", "histogram.percentile", "histogram.reset"], "status": "partial", "evidence": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.mark", "performance.measure", "performance.getEntries", "performance.getEntriesByName", "performance.getEntriesByType", "performance.clearMarks", "performance.clearMeasures"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5213-perf-timing-errors.ts", "tests/corpus/5214-perf-entry-inspect.ts", "tests/corpus/5215-perf-timing-validation.js"] },
{ "chapter": "perf_hooks", "symbols": ["PerformanceEntry", "PerformanceMark", "PerformanceMeasure"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts"] },
{ "chapter": "perf_hooks", "parent": "Class: `PerformanceEntry`", "symbols": ["duration", "entryType", "name", "startTime"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts"] },
@@ -381,7 +390,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"] },
diff --git a/package.json b/package.json
index 7318e04f5..03a763372 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/packages/cli/scripts/install-native.mjs b/packages/cli/scripts/install-native.mjs
index 0d125970d..84eb846bc 100644
--- a/packages/cli/scripts/install-native.mjs
+++ b/packages/cli/scripts/install-native.mjs
@@ -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"),
]) {
diff --git a/packages/cli/src/bootstrap.ts b/packages/cli/src/bootstrap.ts
index 2d12883fa..1466707d6 100644
--- a/packages/cli/src/bootstrap.ts
+++ b/packages/cli/src/bootstrap.ts
@@ -116,6 +116,13 @@ async function tryFastPath(): Promise {
// 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 {
...(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}:${
diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts
index 436bcace3..6dfcc90ef 100644
--- a/packages/compiler/ambient/scriptc-node-fallback.d.ts
+++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts
@@ -80,21 +80,54 @@ declare namespace NodeJS {
path?: string | undefined;
syscall?: string | undefined;
}
- /* The piped child-output stream (child.stdout / child.stderr under
- * stdio ["ignore", "pipe", "pipe"]): 'data' fires one Buffer chunk per
- * read (≤64KB, consumer-driven — no listener, no read), 'end' fires
- * once at EOF and always BEFORE the child's 'exit'. A flowing stream
- * keeps the loop alive. */
+ /* The readable-stream interface every Node stream implements. The
+ * piped child-output stream (child.stdout / child.stderr under stdio
+ * ["ignore", "pipe", "pipe"]) is declared with it: 'data' fires one
+ * Buffer chunk per read (≤64KB, consumer-driven — no listener, no
+ * read), 'end' fires once at EOF and always BEFORE the child's 'exit'. A
+ * flowing stream keeps the loop alive. A program that also passes
+ * other streams (fs, zlib, node:stream) where this interface is
+ * expected compiles it as the node:stream Readable, with child output
+ * bridged into one. */
interface ReadableStream {
on(event: "data", listener: (chunk: Buffer) => void): void;
on(event: "data", listener: (chunk: string) => void): void;
on(event: "end", listener: () => void): void;
+ on(event: string, listener: (...args: any[]) => void): void;
once(event: "data", listener: (chunk: Buffer) => void): void;
once(event: "data", listener: (chunk: string) => void): void;
once(event: "end", listener: () => void): void;
+ once(event: string, listener: (...args: any[]) => void): void;
/* setEncoding('utf8') makes subsequent data chunks strings, including
* multibyte sequences split across native pipe reads. */
setEncoding(encoding: string): this;
+ readonly readable: boolean;
+ pause(): this;
+ resume(): this;
+ isPaused(): boolean;
+ pipe(destination: T, options?: { end?: boolean }): T;
+ unpipe(destination?: WritableStream): this;
+ [Symbol.asyncIterator](): AsyncIterableIterator;
+ }
+ /* The writable-stream interface every Node stream implements:
+ * process.stdout/process.stderr, fs.WriteStream and the node:stream
+ * Writable family. Written only to the process streams, it compiles as
+ * their descriptor; with other streams flowing into it, as the
+ * node:stream Writable (the process streams convert into one). */
+ interface WritableStream {
+ readonly writable: boolean;
+ write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
+ write(
+ data: string | Uint8Array,
+ encoding: BufferEncoding,
+ callback?: (error?: Error | null) => void,
+ ): boolean;
+ end(callback?: () => void): void;
+ end(data: string | Uint8Array, callback?: () => void): void;
+ end(data: string | Uint8Array, encoding: BufferEncoding, callback?: () => void): void;
+ /* Node's EventEmitter signature: listeners take untyped arguments. */
+ on(event: string, listener: (...args: any[]) => void): void;
+ once(event: string, listener: (...args: any[]) => void): void;
}
}
@@ -1271,8 +1304,37 @@ declare module "node:perf_hooks" {
getEntriesByType(type: EntryType): PerformanceEntry[];
clearMarks(name?: string): void;
clearMeasures(name?: string): void;
+ eventLoopUtilization(
+ utilization1?: EventLoopUtilization,
+ utilization2?: EventLoopUtilization,
+ ): EventLoopUtilization;
}
export const performance: Performance;
+ /* Event-loop metrics: utilization (idle/active ms since the loop started)
+ * and monitorEventLoopDelay's sampling histogram (nanoseconds). */
+ export interface EventLoopUtilization {
+ idle: number;
+ active: number;
+ utilization: number;
+ }
+ export interface EventLoopMonitorOptions {
+ resolution?: number | undefined;
+ }
+ export interface Histogram {
+ readonly count: number;
+ readonly exceeds: number;
+ readonly max: number;
+ readonly mean: number;
+ readonly min: number;
+ readonly stddev: number;
+ percentile(percentile: number): number;
+ reset(): void;
+ }
+ export interface IntervalHistogram extends Histogram {
+ enable(): boolean;
+ disable(): boolean;
+ }
+ export function monitorEventLoopDelay(options?: EventLoopMonitorOptions): IntervalHistogram;
}
declare module "perf_hooks" {
export * from "node:perf_hooks";
@@ -1878,6 +1940,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(fsModule: T): T;
+}
/* fs/promises — the SAME synchronous operations behind already-settled
* promises: success fulfills, failure REJECTS with the would-be thrown
@@ -3059,8 +3128,10 @@ declare module "child_process" {
destroy(): void;
on(event: "drain" | "finish", listener: () => void): void;
on(event: "error", listener: (err: Error) => void): void;
+ on(event: string, listener: (...args: any[]) => void): void;
once(event: "drain" | "finish", listener: () => void): void;
once(event: "error", listener: (err: Error) => void): void;
+ once(event: string, listener: (...args: any[]) => void): void;
}
export interface ChildProcess extends Disposable {
@@ -4687,6 +4758,11 @@ declare module "net" {
on(event: "close", listener: (hadError: boolean) => void): void;
on(event: "error", listener: (err: Error) => void): void;
on(event: "session", listener: (session: Buffer) => void): void;
+ /* The host name's resolution, before the connect. */
+ on(
+ event: "lookup",
+ listener: (err: Error | null, address: string, family: number, host: string) => void,
+ ): void;
/* addListener IS on (Node aliases them) — the suite spells both. */
addListener(event: "data", listener: (chunk: Buffer) => void): void;
/* After setEncoding the chunk is a string. */
@@ -4698,6 +4774,11 @@ declare module "net" {
addListener(event: "close", listener: (hadError: boolean) => void): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(event: "session", listener: (session: Buffer) => void): void;
+ /* The host name's resolution, before the connect. */
+ addListener(
+ event: "lookup",
+ listener: (err: Error | null, address: string, family: number, host: string) => void,
+ ): void;
once(event: "data", listener: (chunk: Buffer) => void): void;
/* After setEncoding the chunk is a string. */
once(event: "data", listener: (chunk: string) => void): void;
@@ -4708,6 +4789,11 @@ declare module "net" {
once(event: "close", listener: (hadError: boolean) => void): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: "session", listener: (session: Buffer) => void): void;
+ /* The host name's resolution, before the connect. */
+ once(
+ event: "lookup",
+ listener: (err: Error | null, address: string, family: number, host: string) => void,
+ ): void;
}
export interface Server {
/* Whether the server keeps the event loop alive (ref'd by default). */
@@ -4726,6 +4812,8 @@ declare module "net" {
/* The positional bind address (the options form's host in argument
* spelling). */
listen(port: number, host: string, callback?: () => void): Server;
+ /* A Unix-socket server; the socket file is removed when it closes. */
+ listen(path: string, callback?: () => void): Server;
/* The explicit-interface bind: host is an IP literal (absent = the
* host-less dual-stack any); ipv6Only sets IPV6_V6ONLY and reusePort
* requests kernel connection distribution where Node supports it. */
@@ -5049,10 +5137,11 @@ declare module "http" {
* callback receives the response as an IncomingMessage (statusCode,
* headers, body via on("data"/"end")). */
export interface RequestOptions {
- hostname?: string;
- host?: string;
- port?: number;
- path?: string;
+ hostname?: string | null;
+ host?: string | null;
+ /* A string port (a URL's .port) is accepted, as by Node. */
+ port?: number | string | null;
+ path?: string | null;
method?: string;
timeout?: number;
headers?: OutgoingHttpHeaders;
@@ -5066,6 +5155,8 @@ declare module "http" {
agent?: Agent | boolean | null;
/* Aborting destroys the request with an AbortError. */
signal?: AbortSignal;
+ /* HTTP over the Unix socket at this path instead of host:port. */
+ socketPath?: string;
/* The options-record stance: Node ignores an options record's
* unknown keys, so every key typechecks here — the option WALK in
* the lowering decides per key (lower, fence by name, or drop
@@ -5341,6 +5432,7 @@ declare module "https" {
agent?: Agent | boolean | null;
/* Aborting destroys the request with an AbortError. */
signal?: AbortSignal;
+ socketPath?: string;
/* The options-record stance (see http.RequestOptions). */
[option: string]: unknown;
}
@@ -6158,10 +6250,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;
+ export function lookup(hostname: string, options: LookupOneOptions): Promise;
+ export function lookup(hostname: string, options: LookupAllOptions): Promise;
+ export function lookup(
+ hostname: string,
+ options: LookupOptions,
+ ): Promise;
+ export function lookup(hostname: string): Promise;
+ 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. */
@@ -6317,7 +6478,7 @@ declare module "node:cluster" {
/* node:tty — the fd probe (the same isatty(3) behind process.*.isTTY)
* and the supported structural view of a process output stream. */
declare module "tty" {
- export interface WriteStream {
+ export interface WriteStream extends NodeJS.WritableStream {
write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
write(
data: string | Uint8Array,
@@ -6408,7 +6569,7 @@ declare module "events" {
export { internal as EventEmitter };
// The event's arguments, as checked-dynamic values.
export function once(
- emitter: EventEmitter,
+ emitter: EventEmitter | NodeJS.ReadableStream | NodeJS.WritableStream,
eventName: string,
options?: { signal?: AbortSignal },
): Promise;
@@ -6567,8 +6728,8 @@ declare module "stream" {
resume(): this;
isPaused(): boolean;
setEncoding(encoding: string): this;
- pipe(destination: T, options?: { end?: boolean }): T;
- unpipe(destination?: Writable): this;
+ pipe(destination: T, options?: { end?: boolean }): T;
+ unpipe(destination?: NodeJS.WritableStream): this;
destroy(error?: Error): this;
readonly readable: boolean;
readonly readableEnded: boolean;
@@ -6951,6 +7112,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 = globalThis.ReadableStream;
+ export type ReadableStreamDefaultReader = globalThis.ReadableStreamDefaultReader;
+ export type ReadableStreamDefaultController =
+ globalThis.ReadableStreamDefaultController;
+ export type UnderlyingSource = globalThis.UnderlyingSource;
+ export type QueuingStrategy = globalThis.QueuingStrategy;
+}
+declare module "node:stream/web" {
+ export * from "stream/web";
+}
interface ErrorConstructor {
stackTraceLimit: number;
}
diff --git a/packages/compiler/package.json b/packages/compiler/package.json
index 6e5442dc6..97c7688f5 100644
--- a/packages/compiler/package.json
+++ b/packages/compiler/package.json
@@ -39,6 +39,7 @@
},
"dependencies": {
"@scriptc/runtime": "workspace:*",
+ "tsc-ts": "0.0.5",
"typescript": "7.0.2",
"typescript5": "npm:typescript@5.9.3"
},
diff --git a/packages/compiler/src/backend/executable-features.ts b/packages/compiler/src/backend/executable-features.ts
index eeb6c6900..0fcf86ada 100644
--- a/packages/compiler/src/backend/executable-features.ts
+++ b/packages/compiler/src/backend/executable-features.ts
@@ -16,6 +16,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
fileHandle: features.fileHandle,
fetch: features.fetch,
...(features.eventTarget ? { eventTarget: true } : {}),
+ ...(features.wasm ? { wasm: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
@@ -25,18 +26,23 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
dc: features.dc,
dynAsync: features.dynAsync,
...(features.dynTier ? { dynTier: true } : {}),
+ ...(features.streamDyn ? { streamDyn: true } : {}),
+ ...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
events: features.processEvents,
emitter: features.emitter,
symbol: features.symbol,
bigint: features.bigint,
searchParams: features.searchParams,
qs: features.qs,
+ dynBuiltinClass: features.dynBuiltinClass,
+ dynBuiltinStream: features.dynBuiltinStream,
parseArgs: features.parseArgs,
stream: features.stream,
net: features.net,
http: features.http,
http2: features.http2,
dgram: features.dgram,
+ dnsPromises: features.dnsPromises,
watch: features.fsWatch,
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
nodeTest: features.nodeTest,
@@ -44,5 +50,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
tlsCa: features.tlsCa,
cipher: features.cipher,
webcrypto: features.webcrypto,
+ perfLoop: features.perfLoop,
};
}
diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts
index 5070a47a8..cb929e4b0 100644
--- a/packages/compiler/src/backend/library-pack.ts
+++ b/packages/compiler/src/backend/library-pack.ts
@@ -42,18 +42,22 @@ export async function compilePackedLibrary(
bigint: options.bigint ?? false,
searchParams: options.searchParams ?? false,
qs: false,
+ dynBuiltinClass: false,
+ dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
+ dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
tls: false,
cipher: false,
webcrypto: false,
+ perfLoop: false,
tlsCa: false,
};
const selection = await loadRuntimePack({
diff --git a/packages/compiler/src/backend/llvm/dyn.ts b/packages/compiler/src/backend/llvm/dyn.ts
index 14069f3d9..88bad51a0 100644
--- a/packages/compiler/src/backend/llvm/dyn.ts
+++ b/packages/compiler/src/backend/llvm/dyn.ts
@@ -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,
@@ -129,6 +130,8 @@ export interface DynHost extends WalkerHost {
/** The shared adapter record: live array capsules read its `arrayOps`, so a
* narrower return type would hand the native compiler a copy without it. */
liveDynRefAdapter(t: IrType): LlStreamTypedRefAdapter;
+ /** See moduleUsesDynBuiltinStream. */
+ usesDynBuiltinStream?(): boolean;
liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string;
dynPromiseAdapter(t: IrType): string;
isErrorClass(className: string): boolean;
@@ -139,6 +142,19 @@ export interface DynHost extends WalkerHost {
const FN_ATTRS = "#0";
+/** scr_dyn_builtin_stream_check's kind for a runtime stream class (the
+ * numbering of scr_dyn_builtin_class.c), or null. */
+function builtinStreamKind(className: string): number | null {
+ const kinds: Record = {
+ "%Readable": 2,
+ "%Writable": 3,
+ "%Duplex": 4,
+ "%Transform": 5,
+ "%PassThrough": 6,
+ };
+ return kinds[className] ?? null;
+}
+
export class LlDyn {
private readonly dynMatchers = new Map();
private readonly dynBuilders = new Map();
@@ -613,6 +629,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)`);
@@ -1478,6 +1506,24 @@ export class LlDyn {
B.line(`${ref} = call ptr @scr_dyn_typed_ref_unbox(ptr %d)`);
B.terminate(`ret ptr ${ref}`);
B.startBlock(lNext);
+ // A constructor-function stream (scr_dyn_builtin_stream.c) carries
+ // its runtime stream.
+ const streamKind = t.kind === "object" ? builtinStreamKind(t.className) : null;
+ if (streamKind !== null && this.host.usesDynBuiltinStream?.() === true) {
+ host.declare(`declare ptr @scr_dyn_builtin_stream_check(ptr, double)`);
+ const own = B.tmp(),
+ hit = B.tmp();
+ B.line(
+ `${own} = call ptr @scr_dyn_builtin_stream_check(ptr %d, double ${f64Lit(streamKind)})`,
+ );
+ B.line(`${hit} = icmp ne ptr ${own}, null`);
+ const lOwn = B.newLabel("dc.ctor"),
+ lNotOwn = B.newLabel("dc.notctor");
+ B.condBr(hit, lOwn, lNotOwn);
+ B.startBlock(lOwn);
+ B.terminate(`ret ptr ${own}`);
+ B.startBlock(lNotOwn);
+ }
if (t.kind !== "union" && preserveRefs) {
const kind = this.kindOf(B, "%d");
const capsule = B.tmp();
@@ -2687,13 +2733,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.
diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts
index a42643bca..3ea0f4c84 100644
--- a/packages/compiler/src/backend/llvm/emit-module.ts
+++ b/packages/compiler/src/backend/llvm/emit-module.ts
@@ -1,4 +1,5 @@
/* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */
+import { emitWasmModules } from "./wasm-modules.js";
import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js";
import {
mangleAsyncSpawn,
@@ -177,6 +178,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
// programs additionally stamp the httpReq/httpRes ops — the C main's
// install lines, gated on the same predicates native/executable.ts links by.
const usesNet = runtimeFeatures.net;
+ const usesNetLookup = runtimeFeatures.netLookup;
const usesDgram = runtimeFeatures.dgram;
const usesHttp2 = runtimeFeatures.http2;
const usesHttp = runtimeFeatures.http;
@@ -226,6 +228,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
if (usesFsWatch) emitter.declare(`declare void @scr_watch_install()`);
if (emitter.ffiHasForeignCallback) emitter.declare(`declare void @scr_ffi_install()`);
if (usesStream) emitter.declare(`declare void @scr_stream_install()`);
+ if (usesNetLookup) emitter.declare(`declare void @scr_net_lookup_install()`);
if (usesNet) {
emitter.declare(`declare void @scr_net_install()`);
emitter.declare(`declare void @scr_net_dyn_install()`);
@@ -310,6 +313,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
emitter.declare(`declare zeroext i1 @scr_loop_run(ptr)`);
if (emitter.mod.commonJsEntry === true)
emitter.declare(`declare void @scr_loop_commonjs_entry()`);
+ if (emitter.readsLoopMetrics()) emitter.declare(`declare void @scr_perf_loop_mark()`);
emitter.declare(`declare zeroext i1 @scr_report_unhandled_rejections()`);
emitter.declare(`declare void @scr_discard_unhandled_rejections()`);
}
@@ -686,11 +690,17 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
// runtime finds them before data + cap * 9), and the ScrArr header over
// it (rc == SIZE_MAX, len == cap == filled, SCR_ELEM_STR = 2, no REF
// entry points). Every dense slot is present; reads retain immortal
- // strings — a no-op.
+ // strings — a no-op. Slots are 8 bytes on every target: on 32-bit
+ // targets each holds the pointer in its low half and zero in its high
+ // half, so the states start where the runtime looks for them.
const n = inst.slots.length;
const present = n === 0 ? "zeroinitializer" : `[ ${inst.slots.map(() => "i8 1").join(", ")} ]`;
+ const wide = emitter.sizeType === "i64";
+ const slot = wide ? "ptr" : "<{ ptr, i32 }>";
+ const slotValue = (s: string): string =>
+ wide ? `ptr ${s}` : `<{ ptr, i32 }> <{ ptr ${s}, i32 0 }>`;
out.push(
- `@${inst.sym}_data = internal constant <{ [${n} x ptr], [${n} x i8] }> <{ [${n} x ptr] [ ${inst.slots.map((s) => `ptr ${s}`).join(", ")} ], [${n} x i8] ${present} }>, align 8`,
+ `@${inst.sym}_data = internal constant <{ [${n} x ${slot}], [${n} x i8] }> <{ [${n} x ${slot}] [ ${inst.slots.map(slotValue).join(", ")} ], [${n} x i8] ${present} }>, align 8`,
`@${inst.sym} = internal global %ScrArr { ${emitter.sizeType} -1, i32 ${n}, i32 ${n}, i8 2, i8 0, i16 0, i32 ${n}, ptr null, ptr @${inst.sym}_data, ptr null }`,
);
}
@@ -729,6 +739,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
out.push(fnDefs[0] ?? "");
for (let i = 1; i < fnDefs.length; i++) out.push("", fnDefs[i]!);
out.push("");
+ // WebAssembly modules translated into this program, and the registry.
+ for (const line of emitWasmModules(emitter.mod, flushedDecls, emitter.wasmBoundsChecks))
+ out.push(line);
// main(): scr_init, the program-dependent error-vt interval stamps,
// scr_lib_init(argc, argv), then the entry function. An uncaught
@@ -838,6 +851,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []),
...(usesFetch ? [` call void @scr_fetch_install()`] : []),
...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []),
+ ...(usesNetLookup ? [` call void @scr_net_lookup_install()`] : []),
...(usesHttp ? [` call void @scr_http_dyn_install()`] : []),
...(usesDgram ? [` call void @scr_dgram_install()`] : []),
...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []),
@@ -871,6 +885,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
...(runsLoop
? [
...(emitter.mod.commonJsEntry === true ? [` call void @scr_loop_commonjs_entry()`] : []),
+ // performance.eventLoopUtilization measures from here on.
+ ...(emitter.readsLoopMetrics() ? [` call void @scr_perf_loop_mark()`] : []),
` %loop_rejection = call zeroext i1 @scr_loop_run(ptr ${asyncEntry ? "%top" : "null"})`,
` %lexc = call zeroext i1 @scr_exc_pending()`,
` br i1 %lexc, label %luncaught, label %lok`,
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index 2099067dd..f376dff5e 100644
--- a/packages/compiler/src/backend/llvm/emitter.ts
+++ b/packages/compiler/src/backend/llvm/emitter.ts
@@ -1,3 +1,4 @@
+import { wasmBoundsChecks } from "./wasm-modules.js";
import { jsTruthy } from "./jsval.js";
import { type ArraySearchKind } from "./array-search.js";
import { emitStringInputs } from "./string-lifetimes.js";
@@ -52,7 +53,11 @@ import {
VOID,
classJsName,
} from "../../ir/ir.js";
-import { moduleRuntimeFeatures, moduleUsesMockTimers } from "../../ir/runtime-features.js";
+import {
+ moduleRuntimeFeatures,
+ moduleUsesDynBuiltinStream,
+ moduleUsesMockTimers,
+} from "../../ir/runtime-features.js";
import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
import { type DenseArrayLoop } from "./dense-array-loops.js";
import { findInitializedByteLoopBindings } from "./byte-windows.js";
@@ -181,6 +186,7 @@ import {
genResultThunkFor,
childExitThunkFor,
childExitSignalThunkFor,
+ childDataDynThunkFor,
childDataThunkFor,
execFileThunkFor,
ipcMessageThunkFor,
@@ -413,6 +419,9 @@ export interface LlvmTargetOptions {
* teardown after `process.exit` to a helper process (`--detached-exit`).
* Executables only; the runtime's weak default keeps it off. */
detachedExit?: boolean;
+ /** Translated WebAssembly memories check bounds explicitly instead of
+ * relying on guard pages (wasm-modules.ts wasmBoundsChecks). */
+ wasmBoundsChecks?: boolean;
}
export function emitLlvmModule(input: IrModule, options: LlvmTargetOptions = {}): string {
@@ -675,6 +684,7 @@ export class LlEmitter {
readonly emitLibraryIdentity: boolean;
readonly runtimeAbiMarker: boolean;
readonly detachedExit: boolean;
+ readonly wasmBoundsChecks: boolean;
/** Interned string literals: UTF-8 text → { symbol, byte length } —
* first-use order, the runtime ABI’s determinism discipline. */
readonly literals = new Map();
@@ -889,6 +899,11 @@ export class LlEmitter {
/** Identity-preserving static→dyn capsules for program classes and Web
* APIs whose values remain directly observable. */
readonly liveDynRefAdapters = new Map();
+ private dynBuiltinStreamUse: boolean | undefined;
+ /** See moduleUsesDynBuiltinStream. */
+ get usesDynBuiltinStream(): boolean {
+ return (this.dynBuiltinStreamUse ??= moduleUsesDynBuiltinStream(this.mod));
+ }
/** Runtime-arm dispatchers for live union values. */
readonly liveDynUnionRefAdapters = new Map();
readonly dynPromiseAdapters = new Map();
@@ -1168,6 +1183,13 @@ export class LlEmitter {
this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
this.detachedExit = options.detachedExit === true;
+ this.wasmBoundsChecks =
+ options.wasmBoundsChecks === true ||
+ wasmBoundsChecks({
+ sanitize: false,
+ pointerBits: options.pointerBits ?? 64,
+ targetTriple: options.targetTriple ?? "",
+ });
this.rcHelpers = options.inlineRc !== undefined ? new Set() : null;
this.rcMode = options.inlineRc;
this.rcHelpersNoinline = options.rcHelpersNoinline === true;
@@ -1385,6 +1407,7 @@ export class LlEmitter {
fieldAbsentTestIn: (B, value, unionId) => this.fieldAbsentTestIn(B, value, unionId),
absentInstanceRef: (unionId) => this.absentInstanceRef(unionId),
liveDynRefAdapter: (type) => this.liveDynRefAdapter(type),
+ usesDynBuiltinStream: () => this.usesDynBuiltinStream,
liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type),
dynPromiseAdapter: (type) => this.dynPromiseAdapter(type),
isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error",
@@ -4194,6 +4217,20 @@ export class LlEmitter {
/** Moves need plain slots that nothing else reads behind the binding's
* back: no debugger view, no coroutine frame, no borrowed (+0) return. */
+ /** Whether the program reads event-loop utilization (perf.loopStart /
+ * perf.idleTime): main then marks the loop's start before running it. */
+ private loopMetricsRead: boolean | undefined;
+ readsLoopMetrics(): boolean {
+ if (this.loopMetricsRead !== undefined) return this.loopMetricsRead;
+ const visitor = {
+ expr: (e: IrExpr): boolean =>
+ !(e.kind === "libCall" && (e.fn === "perf.loopStart" || e.fn === "perf.idleTime")),
+ stmt: (): boolean => true,
+ };
+ this.loopMetricsRead = !this.mod.functions.every((fn) => everyStmtList(fn.body, visitor));
+ return this.loopMetricsRead;
+ }
+
private lastUseMovesAllowed(): boolean {
return (
this.debug === null &&
@@ -5557,12 +5594,19 @@ export class LlEmitter {
return childDataThunkFor(this, param);
}
+ childDataDynThunkFor(chunk: "bytes" | "str"): string {
+ return childDataDynThunkFor(this, chunk);
+ }
+
emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string } {
return emitterFixedAdapter(this, cbT);
}
- emitterOnceAdapter(boxerT: IrType & { kind: "func" }): { fn: string; shim: string } {
- return emitterOnceAdapter(this, boxerT);
+ emitterOnceAdapter(
+ boxerT: IrType & { kind: "func" },
+ fire?: string,
+ ): { fn: string; shim: string } {
+ return emitterOnceAdapter(this, boxerT, fire);
}
wrapEmitterListener(target: string, adapterFn: string): string {
diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts
index db1dca1af..b91cdc543 100644
--- a/packages/compiler/src/backend/llvm/expr-callbacks.ts
+++ b/packages/compiler/src/backend/llvm/expr-callbacks.ts
@@ -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 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 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);
@@ -754,6 +771,31 @@ export function childDataThunkFor(host: LlvmEmitterContext, param: IrType): stri
return sym;
}
+/** The child-stream 'data' thunk for an unknown/any chunk parameter: the
+ * runtime's Buffer (or, after setEncoding, string) chunk boxes into a
+ * checked-dynamic value the listener owns. */
+export function childDataDynThunkFor(host: LlvmEmitterContext, chunk: "bytes" | "str"): string {
+ const key = `cdd:${chunk}`;
+ let sym = host.resolveThunks.get(key);
+ if (sym) return sym;
+ sym = `sc_cdd_${host.resolveThunks.size}`;
+ host.resolveThunks.set(key, sym);
+ const box = chunk === "bytes" ? "scr_dyn_new_buffer" : "scr_dyn_new_str";
+ host.declare(`declare ptr @${box}(ptr)`);
+ host.resolveThunkDefs.push(
+ `define internal void @${sym}(ptr %cb, ptr %chunk) ${FN_ATTRS} { ; child 'data' → dyn (${chunk})`,
+ `entry:`,
+ ` %d = call ptr @${box}(ptr %chunk)`,
+ ` %fnp = getelementptr inbounds %ScrClosure, ptr %cb, i64 0, i32 1`,
+ ` %fn = load ptr, ptr %fnp`,
+ ` call void %fn(ptr %cb, ptr %d)`,
+ ` ret void`,
+ `}`,
+ ``,
+ );
+ return sym;
+}
+
export function emitterFixedAdapter(
host: LlvmEmitterContext,
cbT: IrType & { kind: "func" },
@@ -822,23 +864,27 @@ export function emitterFixedAdapter(
* (the event's tuple to any[]): the runtime-built listener closure holds
* the boxer in caps[0], so the adapter calls it with the event's typed
* arguments, as emitterFixedAdapter calls a listener, and hands the array
- * to scr_events_once_fire. */
+ * to scr_events_once_fire (or `fire`, the signal form's entry). */
export function emitterOnceAdapter(
host: LlvmEmitterContext,
boxerT: IrType & { kind: "func" },
+ fire = "scr_events_once_fire",
): { fn: string; shim: string } {
const cursor = boxerT.params.length > 4;
const shim = cursor ? "scr_ee_inv_args" : `scr_ee_inv_fixed${boxerT.params.length}`;
if (cursor) host.declare("declare ptr @scr_ee_arg_next(ptr)");
host.declare(`declare void @${shim}(ptr, ptr)`);
- const key = `eeOnce:${typeKey(boxerT)}`;
+ const key =
+ fire === "scr_events_once_fire"
+ ? `eeOnce:${typeKey(boxerT)}`
+ : `eeOnce:${fire}:${typeKey(boxerT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return { fn: sym, shim };
sym = `sc_ee_once_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
host.declare(`declare void @scr_closure_release(ptr)`);
- host.declare(`declare void @scr_events_once_fire(ptr, ptr)`);
+ host.declare(`declare void @${fire}(ptr, ptr)`);
const params = cursor ? "ptr %args" : boxerT.params.map((_, i) => `ptr %a${i}`).join(", ");
const d: string[] = [
`define internal void @${sym}(ptr %cb${params ? ", " + params : ""}) ${FN_ATTRS} { ; events.once adapter ${typeKey(boxerT)}`,
@@ -869,7 +915,7 @@ export function emitterOnceAdapter(
` %fn = load ptr, ptr %fnp`,
` %arr = call ptr %fn(${passed.join(", ")}) ; the boxed arguments, +1`,
` call void @scr_closure_release(ptr %boxer)`,
- ` call void @scr_events_once_fire(ptr %cb, ptr %arr) ; moves %arr`,
+ ` call void @${fire}(ptr %cb, ptr %arr) ; moves %arr`,
` ret void`,
`}`,
``,
diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts
index 8a24fcf23..bc4f1128a 100644
--- a/packages/compiler/src/backend/llvm/expr-dynamic.ts
+++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts
@@ -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 = new Set([
+ "constructor",
+ "valueOf",
+ "toString",
+ "hasOwnProperty",
+ "propertyIsEnumerable",
+]);
diff --git a/packages/compiler/src/backend/llvm/expr-primitives.test.ts b/packages/compiler/src/backend/llvm/expr-primitives.test.ts
index 1790854ef..d6235306d 100644
--- a/packages/compiler/src/backend/llvm/expr-primitives.test.ts
+++ b/packages/compiler/src/backend/llvm/expr-primitives.test.ts
@@ -306,3 +306,28 @@ test("sums that only feed a bitwise conversion of int32 operands add in 32 bits"
}
expect(body("number_add")).not.toMatch(/ = add i32 |fcmp ord/);
});
+
+test.each([32, 64] as const)(
+ "tagged-template string arrays keep 8-byte slots before their states on %i-bit targets",
+ (bits) => {
+ const strings: IrExpr = {
+ kind: "templateStrings",
+ key: "site",
+ cooked: ["a", "b", "c"],
+ type: { kind: "array", elem: STRING },
+ loc,
+ };
+ const mod = fixture();
+ mod.functions[0]!.body = [{ kind: "exprStmt", expr: strings, loc }];
+ const llvm = emitLlvmModule(mod, { pointerBits: bits });
+ const data = /^@sc_tsa_0_data = internal constant .*$/m.exec(llvm)![0];
+ // The runtime reads each slot as 8 bytes and finds the states after
+ // cap * 8 bytes of slots (scr_arr_states_end).
+ if (bits === 64) expect(data).toContain("<{ [3 x ptr], [3 x i8] }>");
+ else {
+ expect(data).toContain("<{ [3 x <{ ptr, i32 }>], [3 x i8] }>");
+ expect(data.match(/<\{ ptr @\S+, i32 0 \}>/g)).toHaveLength(3);
+ }
+ expect(data).toContain("[3 x i8] [ i8 1, i8 1, i8 1 ]");
+ },
+);
diff --git a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
index eec21af33..02fd53fc1 100644
--- a/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
+++ b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
@@ -15,6 +15,7 @@ import {
isDirentShape,
isDynTypedRefType,
isRefCounted,
+ shapeHasAccessorSlots,
typeKey,
} from "../../ir/ir.js";
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
@@ -765,8 +766,13 @@ export function streamTypedRefMaterializeAdapter(
!meta.def.instancePrototypeHelper &&
!meta.def.prototypeDataHelper
) {
- host.declare(`declare ptr @scr_stream_dyn_view(ptr)`);
- B.line(`${out} = call ptr @scr_stream_dyn_view(ptr %p)`);
+ // Programs with constructor-function streams view them through
+ // scr_dyn_builtin_stream.c, which answers their own objects.
+ const view = host.usesDynBuiltinStream
+ ? "scr_dyn_builtin_stream_view"
+ : "scr_stream_dyn_view";
+ host.declare(`declare ptr @${view}(ptr)`);
+ B.line(`${out} = call ptr @${view}(ptr %p)`);
} else B.line(`${out} = call ptr @scr_dyn_new_obj()`);
const withAttributes = meta.def.fields.some((field) => field.name === CLASS_ATTRIBUTES);
if (meta.def.tracksOwnFields) {
@@ -941,6 +947,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 +983,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();
diff --git a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts
index 3f7961163..9be757a20 100644
--- a/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts
+++ b/packages/compiler/src/backend/llvm/expr-stream-callbacks.ts
@@ -17,7 +17,7 @@ export function streamDataAdapter(
const p = cbT.params[0];
if (
cbT.params.length > 1 ||
- (p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn")
+ (p && p.kind !== "bytes" && p.kind !== "string" && p.kind !== "dyn" && p.kind !== "union")
) {
throw new InternalCompilerError(
"llvm emitter bug: stream data listener param shape (frontend must fence)",
@@ -73,6 +73,46 @@ export function streamDataAdapter(
);
finish("%r0");
d.push(`}`, ``);
+ } else if (p.kind === "union") {
+ // A `Buffer | string` chunk: wrap whichever slot the stream filled at
+ // its arm of the (boxed) union.
+ const def = host.unionsById.get(p.unionId);
+ const bytesTag = def ? def.arms.findIndex((a) => a.kind === "bytes" && a.elem === "u8") : -1;
+ const strTag = def ? def.arms.findIndex((a) => a.kind === "string") : -1;
+ if (
+ !def ||
+ host.nullableUnions.has(p.unionId) ||
+ bytesTag < 0 ||
+ strTag < 0 ||
+ def.arms.length !== 2
+ )
+ throw new InternalCompilerError(
+ "llvm emitter bug: stream data listener union is not Buffer | string (frontend must fence)",
+ );
+ host.declare(`declare ptr @scr_union_new_ref(i32, ptr, ptr, ptr, ptr)`);
+ host.declare(`declare ptr @scr_bytes_retain_v(ptr)`);
+ host.declare(`declare void @scr_bytes_release_v(ptr)`);
+ host.declare(`declare ptr @scr_str_retain_v(ptr)`);
+ host.declare(`declare void @scr_str_release_v(ptr)`);
+ d.push(
+ ` %uslot = alloca ptr`,
+ ` %isb = icmp ne ptr %a0, null`,
+ ` br i1 %isb, label %buf, label %str`,
+ `buf:`,
+ ` %rb = call ptr @scr_bytes_retain_v(ptr %a0)`,
+ ` %ub = call ptr @scr_union_new_ref(i32 ${bytesTag}, ptr %rb, ptr @scr_bytes_retain_v, ptr @scr_bytes_release_v, ptr null)`,
+ ` store ptr %ub, ptr %uslot`,
+ ` br label %go`,
+ `str:`,
+ ` %rs = call ptr @scr_str_retain_v(ptr %a1)`,
+ ` %us = call ptr @scr_union_new_ref(i32 ${strTag}, ptr %rs, ptr @scr_str_retain_v, ptr @scr_str_release_v, ptr null)`,
+ ` store ptr %us, ptr %uslot`,
+ ` br label %go`,
+ `go:`,
+ ` %uv = load ptr, ptr %uslot`,
+ );
+ finish("%uv");
+ d.push(`}`, ``);
} else {
// dyn: box by runtime tag — the JS lane's adapter parameter.
host.declare(`declare ptr @scr_dyn_new_buffer(ptr)`);
diff --git a/packages/compiler/src/backend/llvm/lib-datagram.ts b/packages/compiler/src/backend/llvm/lib-datagram.ts
index b32446bcb..184fdb99e 100644
--- a/packages/compiler/src/backend/llvm/lib-datagram.ts
+++ b/packages/compiler/src/backend/llvm/lib-datagram.ts
@@ -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":
diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts
index 16759a592..924027ee2 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -548,7 +548,12 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
mockTimerSym(host, e) ??
(e.fn === "timers.queueMicrotaskDyn" && host.schedulesInContext
? "scr_queue_microtask_dyn_als"
- : LIB_FN_SYMS[e.fn]);
+ : // Programs with constructor-function streams unbox untyped pipe
+ // destinations and pipeline stages through scr_dyn_builtin_stream.c,
+ // which also takes those streams.
+ e.fn === "stream.unboxDyn" && host.usesDynBuiltinStream
+ ? "scr_dyn_builtin_stream_unbox"
+ : LIB_FN_SYMS[e.fn]);
if (sym === undefined) throw new LlvmUnsupportedError(`libCall:${e.fn}`, e.loc);
const args = READ_ONLY_STRING_LIB_FNS.has(e.fn)
? emitBorrowedInputs(host, e.args)
@@ -755,7 +760,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
}
const prefix = e.fn.slice(0, e.fn.indexOf(".")) as LibCallPrefix;
// events.once builds typed listener adapters beside the emitter calls.
- if (e.fn === "events.once") return host.emitErrorsEventsLibCall(e);
+ if (e.fn === "events.once" || e.fn === "events.onceSignal")
+ return host.emitErrorsEventsLibCall(e);
switch (prefix) {
case "fetch":
case "abort":
@@ -857,6 +863,7 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
case "event":
case "events":
case "blob":
+ case "wasm":
case "record":
case "disposable":
case "punycode":
diff --git a/packages/compiler/src/backend/llvm/lib-network.ts b/packages/compiler/src/backend/llvm/lib-network.ts
index ea37fc2cf..d49bbde14 100644
--- a/packages/compiler/src/backend/llvm/lib-network.ts
+++ b/packages/compiler/src/backend/llvm/lib-network.ts
@@ -60,7 +60,54 @@ export function emitNetworkHttpLibCall(host: LlvmEmitterContext, e: LibCallExpr)
...handler,
]);
}
+ if (
+ e.fn === "http.requestDyn" ||
+ e.fn === "http.requestDynCb" ||
+ e.fn === "https.requestDyn" ||
+ e.fn === "https.requestDynCb"
+ ) {
+ // [options, autoEnd(, cb)]: the runtime walks the options record and
+ // attaches its signal through events.addAbortListener (the event-target
+ // unit, linked for these calls by runtime-features).
+ const args = e.args.map((arg) => host.emitExpr(arg));
+ host.usesTimers = true;
+ const cb = args[2];
+ const handler =
+ cb === undefined
+ ? [ptr(), ptr()]
+ : callbackAdapter(
+ host,
+ cb,
+ callbackType(cb).params.length === 0
+ ? "scr_http_resp_thunk0"
+ : "scr_http_resp_thunk_res",
+ ["ptr", "ptr"],
+ );
+ host.declare(`declare ptr @scr_events_add_abort_listener(ptr, ptr)`);
+ return finishRuntimeCall(
+ host,
+ e,
+ e.fn.startsWith("https.") ? "scr_https_request_dyn" : "scr_http_request_dyn",
+ [
+ abiValue(host, args[0]!),
+ abiValue(host, args[1]!),
+ ...handler,
+ ptr("@scr_events_add_abort_listener"),
+ ],
+ );
+ }
const B = host.B;
+ if (e.fn === "net.listenPath" || e.fn === "net.listenPathCb") {
+ const args = e.args.map((a) => host.emitExpr(a));
+ let cb = "null";
+ if (e.fn === "net.listenPathCb") {
+ host.toHeap(args[2]!);
+ cb = args[2]!.name;
+ }
+ host.declare(`declare void @scr_net_listen_path(ptr, ptr, ptr)`);
+ B.line(`call void @scr_net_listen_path(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ptr ${cb})`);
+ return { name: "", type: e.type };
+ }
if (e.fn === "net.connectOptsChk") {
return emitAlwaysThrowLibCall(host, e, "scr_net_connect_opts_chk");
}
diff --git a/packages/compiler/src/backend/llvm/lib-process.ts b/packages/compiler/src/backend/llvm/lib-process.ts
index eaf7f31cd..097fcb4de 100644
--- a/packages/compiler/src/backend/llvm/lib-process.ts
+++ b/packages/compiler/src/backend/llvm/lib-process.ts
@@ -305,6 +305,28 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
B.line(`${raw} = call ptr @${get}(ptr ${recv.name}) ; +1 or NULL`);
return host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag);
}
+ if (e.fn === "child.stdioStream") {
+ // The general stream representation's stdio read: the child's bridged
+ // runtime stream (+1) or NULL for an unpiped slot.
+ if (e.type.kind !== "union")
+ throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`);
+ const def = host.unionsById.get(e.type.unionId);
+ const streamTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1;
+ const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
+ const fd = e.args[1];
+ if (streamTag < 0 || nullTag < 0 || fd?.kind !== "numLit")
+ throw new InternalCompilerError(`llvm emitter bug: ${e.fn} lacks its arms or slot`);
+ const recv = host.emitExpr(e.args[0]!);
+ const key = fd.value === 0 ? "stdin" : fd.value === 1 ? "stdout" : "stderr";
+ host.declare(`declare ptr @scr_stream_child_stdio(ptr, ptr, ${host.sizeType})`);
+ const raw = B.tmp();
+ B.line(
+ `${raw} = call ptr @scr_stream_child_stdio(ptr ${recv.name}, ptr ${host.cstr(key)}, ${host.sizeType} ${key.length}) ; +1 or NULL`,
+ );
+ const out = host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag);
+ host.emitPendingCheck();
+ return out;
+ }
if (e.fn === "writer.writeString" || e.fn === "writer.writeBytes") {
const writer = host.emitExpr(e.args[0]!);
const data = host.emitExpr(e.args[1]!);
@@ -709,27 +731,34 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
host.emitAlwaysPendingUnwind();
return out;
}
- if (e.fn === "events.once") {
- // (recv, name, boxer, errorBoxer, listenForError): scr_events_once
- // registers one-shot typed listeners behind adapters that box the
- // event's tuple (and the 'error' tuple) into the fulfilled array. The
- // runtime retains the boxers; the frame's temporaries release as usual.
- // May-throw ('newListener' meta listeners run inside).
+ if (e.fn === "events.once" || e.fn === "events.onceSignal") {
+ // (recv, name, boxer, errorBoxer, listenForError[, signal]):
+ // scr_events_once registers one-shot typed listeners behind adapters
+ // that box the event's tuple (and the 'error' tuple) into the fulfilled
+ // array. The runtime retains the boxers; the frame's temporaries
+ // release as usual. May-throw ('newListener' meta listeners run
+ // inside). The signal form's adapters end in its own fire entry.
const boxerT = e.args[2]!.type;
const errorT = e.args[3]!.type;
if (boxerT.kind !== "func" || errorT.kind !== "func")
- throw new InternalCompilerError("llvm emitter bug: events.once boxer not a func");
+ throw new InternalCompilerError(`llvm emitter bug: ${e.fn} boxer not a func`);
+ const signal = e.fn === "events.onceSignal";
const args = e.args.map((a) => host.emitExpr(a));
- const event = host.emitterOnceAdapter(boxerT);
- const error = host.emitterOnceAdapter(errorT);
- host.declare(`declare ptr @scr_events_once(ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr)`);
+ const fire = signal ? "scr_events_once_signal_fire" : undefined;
+ const event = host.emitterOnceAdapter(boxerT, fire);
+ const error = host.emitterOnceAdapter(errorT, fire);
+ const entry = signal ? "scr_events_once_signal" : "scr_events_once";
+ host.declare(
+ `declare ptr @${entry}(ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr${signal ? ", ptr" : ""})`,
+ );
const errorBoxer = B.tmp();
B.line(`${errorBoxer} = select i1 ${args[4]!.name}, ptr ${args[3]!.name}, ptr null`);
const t = B.tmp();
B.line(
- `${t} = call ptr @scr_events_once(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` +
+ `${t} = call ptr @${entry}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` +
`ptr ${args[2]!.name}, ptr @${event.fn}, ptr @${event.shim}, ` +
- `ptr ${errorBoxer}, ptr @${error.fn}, ptr @${error.shim})`,
+ `ptr ${errorBoxer}, ptr @${error.fn}, ptr @${error.shim}` +
+ `${signal ? `, ptr ${args[5]!.name}` : ""})`,
);
const out = host.own({ name: t, type: e.type });
host.emitPendingCheck();
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 294128107..8cd40240f 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -127,6 +127,11 @@ export const LIB_FN_SYMS: Record = {
"str.encodeUriComponent": "scr_str_encode_uri_component",
"error.cause": "scr_error_cause",
"error.setCause": "scr_error_set_cause",
+ "error.setCode": "scr_error_set_code_value",
+ "error.setCodeDyn": "scr_error_set_code_dyn",
+ "error.assign": "scr_error_assign_dyn",
+ "error.hasKey": "scr_error_has_key",
+ "error.hasCode": "scr_error_has_code",
"error.defineCause": "scr_error_define_cause",
"error.deleteCause": "scr_error_delete_cause",
"error.hasCause": "scr_error_has_cause",
@@ -278,6 +283,14 @@ export const LIB_FN_SYMS: Record = {
"perf.entryDetail": "scr_perf_entry_detail",
"perf.entryKeep": "scr_perf_entry_keep",
"perf.entryRef": "scr_perf_entry_ref",
+ "perf.loopStart": "scr_perf_loop_start",
+ "perf.idleTime": "scr_perf_idle_time",
+ "perf.eldNew": "scr_eld_new",
+ "perf.eldEnable": "scr_eld_enable",
+ "perf.eldDisable": "scr_eld_disable",
+ "perf.eldReset": "scr_eld_reset",
+ "perf.eldPercentile": "scr_eld_percentile",
+ "perf.eldStat": "scr_eld_stat",
"process.memoryUsageTake": "scr_process_memory_usage_take",
"process.memoryUsageField": "scr_process_memory_usage_field",
"process.rss": "scr_process_rss",
@@ -310,12 +323,13 @@ export const LIB_FN_SYMS: Record = {
"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",
@@ -726,6 +740,10 @@ export const LIB_FN_SYMS: Record = {
"child.unref": "scr_child_unref",
"child.ref": "scr_child_ref",
"procStream.write": "scr_proc_stream_write",
+ "stream.stdioWritable": "scr_stream_stdio_writable",
+ "stream.unboxDyn": "scr_stream_unbox_dyn",
+ "stream.pipeDyn": "scr_stream_pipe_dyn",
+ "stream.unpipeDyn": "scr_stream_unpipe_dyn",
"process.kill": "scr_process_kill_named",
"process.killNum": "scr_process_kill",
// fs.promises (scr_bytes_io.c / scr_lib.c): the promise forms settle
@@ -858,6 +876,24 @@ export const LIB_FN_SYMS: Record = {
"event.get": "scr_event_obj_get",
"event.set": "scr_event_obj_set",
"blob.new": "scr_blob_new",
+ "wasm.compile": "scr_wasm_compile",
+ "wasm.validate": "scr_wasm_validate",
+ "wasm.instantiate": "scr_wasm_instantiate",
+ "wasm.moduleNew": "scr_wasm_module_new",
+ "wasm.instanceNew": "scr_wasm_instance_new",
+ "wasm.is": "scr_wasm_is",
+ "wasm.errorNew": "scr_wasm_error_new",
+ "wasm.memoryNew": "scr_wasm_memory_construct",
+ "wasm.tableNew": "scr_wasm_table_construct",
+ "wasm.globalNew": "scr_wasm_global_construct",
+ "wasm.withoutNew": "scr_wasm_construct_without_new",
+ "wasm.moduleImports": "scr_wasm_module_imports",
+ "wasm.moduleExports": "scr_wasm_module_exports",
+ "wasm.moduleCustomSections": "scr_wasm_module_custom_sections",
+ "wasm.valueOf": "scr_wasm_value_of",
+ "wasm.function": "scr_wasm_function",
+ "wasm.namespace": "scr_wasm_namespace",
+ "wasm.globalView": "scr_wasm_global_view",
"blob.fileNew": "scr_file_new",
"blob.slice": "scr_blob_slice",
"blob.text": "scr_blob_text",
@@ -926,8 +962,9 @@ export const LIB_FN_SYMS: Record = {
"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 +978,11 @@ export const LIB_FN_SYMS: Record = {
"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",
@@ -1104,6 +1145,7 @@ export const LIB_FN_SYMS: Record = {
"readable.opFind": "scr_stream_op_find",
"readable.opReduce": "scr_stream_op_reduce",
"readable.pipe": "scr_stream_pipe",
+ "readable.pipeGeneral": "scr_stream_pipe_general",
"writable.cork": "scr_stream_cork",
"writable.uncork": "scr_stream_uncork",
"stream.destroyErr": "scr_stream_destroy",
@@ -1151,6 +1193,10 @@ export const LIB_FN_SYMS: Record = {
"net.sockReadyState": "scr_net_sock_ready_state",
"net.sockSetKeepAlive": "scr_net_sock_set_keepalive_ret",
"net.sockOnCloseDyn": "scr_net_sock_on_close_dyn",
+ "net.sockOnLookup": "scr_net_sock_on_lookup",
+ "net.serverCloseDyn": "scr_net_server_close_dyn",
+ "net.serverUnixPath": "scr_net_server_unix_path",
+ "net.serverAddressCheck": "scr_net_server_address_check",
"net.sockOnWriteFlushDyn": "scr_net_sock_on_write_flush_dyn",
"net.sockOnFinishDyn": "scr_net_sock_on_finish_dyn",
"net.isIP": "scr_net_is_ip",
@@ -1414,6 +1460,10 @@ export const LIB_FN_SYMS: Record = {
"dyn.classPrototype": "scr_dyn_class_prototype",
"dyn.classBasePrototype": "scr_dyn_class_base_prototype",
"dyn.classInherit": "scr_dyn_class_inherit",
+ "dyn.builtinClass": "scr_dyn_builtin_class",
+ "dyn.builtinStreamClass": "scr_dyn_builtin_stream_class",
+ "dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof",
+ "dyn.functionInstanceOf": "scr_dyn_function_instanceof",
"dyn.classSuper": "scr_dyn_class_super",
"dyn.assignPrototype": "scr_dyn_assign_prototype",
"dyn.definePrototypeProps": "scr_dyn_define_prototype_props",
@@ -1490,6 +1540,8 @@ export function mockTimerSym(
export const USES_TIMERS_LIB_FNS = new Set([
"process.emitWarning",
+ // Constructor-function streams defer their events on the stream ticks.
+ "dyn.builtinStreamClass",
// Decipheriv.setAuthTag reports DEP0182 on the next tick.
"crypto.cipherNew",
// WebCrypto results settle through the deferred crypto queue.
@@ -1586,6 +1638,7 @@ export const USES_TIMERS_LIB_FNS = new Set([
"readable.opReduce",
"readable.resume",
"readable.pipe",
+ "readable.pipeGeneral",
"readable.unpipe",
"writable.write",
"writable.writeStr",
@@ -1626,6 +1679,8 @@ export const USES_TIMERS_LIB_FNS = new Set([
"sc.arrayBuffer",
"net.listen",
"net.listenCb",
+ "net.listenPath",
+ "net.listenPathCb",
"net.listenOpts",
"net.listenOptsCb",
"net.listenOptsReusePort",
@@ -1644,6 +1699,10 @@ export const USES_TIMERS_LIB_FNS = new Set([
"http.requestCb",
"http.requestUrl",
"http.requestUrlCb",
+ "http.requestDyn",
+ "http.requestDynCb",
+ "https.requestDyn",
+ "https.requestDynCb",
"https.request",
"https.requestCb",
"https.requestUrl",
@@ -1665,4 +1724,5 @@ export const USES_TIMERS_LIB_FNS = new Set([
"timers.queueMicrotaskDyn",
"timers.setImmediateFnValue",
"timers.immediatePromise",
+ "dyn.builtinFunction",
]);
diff --git a/packages/compiler/src/backend/llvm/lib-stream.ts b/packages/compiler/src/backend/llvm/lib-stream.ts
index b62b237df..707fb6dbc 100644
--- a/packages/compiler/src/backend/llvm/lib-stream.ts
+++ b/packages/compiler/src/backend/llvm/lib-stream.ts
@@ -36,6 +36,24 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
host.emitPendingCheck();
return out;
}
+ if (e.fn === "stream.dynNative") {
+ // Checked-dynamic stream slots: the boxed native stream (+1) or NULL.
+ if (e.type.kind !== "union")
+ throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`);
+ const def = host.unionsById.get(e.type.unionId);
+ const streamTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1;
+ const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
+ if (streamTag < 0 || nullTag < 0)
+ throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its arms`);
+ const value = host.emitExpr(e.args[0]!);
+ const side = host.emitExpr(e.args[1]!);
+ host.declare(`declare ptr @scr_stream_dyn_native(ptr, double)`);
+ const raw = B.tmp();
+ B.line(
+ `${raw} = call ptr @scr_stream_dyn_native(ptr ${value.name}, double ${side.name}) ; +1 or NULL`,
+ );
+ return host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag);
+ }
if (e.fn === "stream.onData") {
// The callback MOVES into the stream's listener registry; the
// adapter is per callback shape — runtime-provided for the
@@ -56,6 +74,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
host.declare(`declare void @scr_child_stream_thunk0(ptr, ptr)`);
} else if (param.kind === "union") {
adapter = host.childDataThunkFor(param);
+ } else if (param.kind === "dyn") {
+ adapter = host.childDataDynThunkFor("bytes");
} else {
adapter = "scr_child_stream_thunk_bytes";
host.declare(`declare void @scr_child_stream_thunk_bytes(ptr, ptr)`);
@@ -78,7 +98,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
if (e.fn === "stream.onDataStr") {
host.usesTimers = true;
const cbT = e.args[1]!.type;
- if (cbT.kind !== "func" || cbT.params[0]?.kind !== "string") {
+ const strParam = cbT.kind === "func" ? cbT.params[0]?.kind : undefined;
+ if (strParam !== "string" && strParam !== "dyn") {
throw new InternalCompilerError(
"llvm emitter bug: stream.onDataStr callback not a string func",
);
@@ -87,10 +108,12 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
const cb = host.emitExpr(e.args[1]!);
const once = host.emitExpr(e.args[2]!);
host.toHeap(cb);
- host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
+ let adapter = "scr_child_stream_thunk_str";
+ if (strParam === "dyn") adapter = host.childDataDynThunkFor("str");
+ else host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
host.declare(`declare void @scr_child_stream_on_data_str(ptr, ptr, ptr, i1 zeroext)`);
B.line(
- `call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @scr_child_stream_thunk_str, i1 ${once.name})`,
+ `call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`,
);
return { name: "", type: e.type };
}
diff --git a/packages/compiler/src/backend/llvm/wasm-modules.ts b/packages/compiler/src/backend/llvm/wasm-modules.ts
new file mode 100644
index 000000000..8a7045687
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/wasm-modules.ts
@@ -0,0 +1,77 @@
+/* The WebAssembly modules translated into a program (IrModule.wasmModules):
+ * each module's translation (wasm/translate.ts) and the registry the
+ * runtime looks modules up in (scr_wasm_registry, scr_wasm_abi.h). The
+ * translations join the program module, so the native helper optimizes,
+ * partitions and caches them with it. Programs that do not use the
+ * WebAssembly API carry no wasmModules and emit nothing here. */
+import type { IrModule } from "../../ir/ir.js";
+import { decodeBase64 } from "../../wasm/base64.js";
+import { translateWasmModule, WasmTranslateError } from "../../wasm/translate.js";
+
+function hexBytes(hex: string): Uint8Array {
+ const out = new Uint8Array(hex.length / 2);
+ for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
+ return out;
+}
+
+/** Whether translated memories check bounds explicitly instead of relying
+ * on guard pages and the runtime's fault handler: sanitizer builds (whose
+ * runtime owns SIGSEGV), 32-bit targets, and Windows, which has no
+ * fault handler yet. */
+export function wasmBoundsChecks(options: {
+ sanitize: boolean;
+ pointerBits: 32 | 64;
+ targetTriple: string;
+}): boolean {
+ if (options.sanitize || options.pointerBits === 32) return true;
+ const target = options.targetTriple || process.platform;
+ return /windows|mingw|cygwin|win32|msvc/.test(target);
+}
+
+/** The top-level lines of every translated module plus the registry.
+ * `declared` holds the declarations the module already carries; runtime
+ * entry points it lacks are declared here. A module the translator
+ * cannot translate is left out of the registry, so loading it at run
+ * time rejects with the CompileError for untranslated bytes. */
+export function emitWasmModules(
+ mod: IrModule,
+ declared: ReadonlySet,
+ boundsChecks: boolean,
+): string[] {
+ const modules = mod.wasmModules;
+ if (modules === undefined) return [];
+ const out: string[] = [];
+ const decls = new Set(declared);
+ const descriptors: string[] = [];
+ for (const module of modules) {
+ const bytes = decodeBase64(module.bytes);
+ if (bytes === null) continue;
+ let translation;
+ try {
+ translation = translateWasmModule(bytes, hexBytes(module.sha256), {
+ boundsChecks,
+ customSections: mod.wasmCustomSections === true,
+ });
+ } catch (error) {
+ if (error instanceof WasmTranslateError) continue;
+ throw error;
+ }
+ for (const decl of translation.decls) {
+ if (decls.has(decl)) continue;
+ decls.add(decl);
+ out.push(decl);
+ }
+ for (const part of translation.text) out.push(part);
+ descriptors.push(translation.descriptor);
+ }
+ if (descriptors.length === 0) {
+ out.push(`@scr_wasm_registry = constant { i64, ptr } { i64 0, ptr null }`);
+ } else {
+ out.push(
+ `@sc_wasm_registry_modules = private constant [${descriptors.length} x ptr] [${descriptors.map((d) => `ptr ${d}`).join(", ")}]`,
+ `@scr_wasm_registry = constant { i64, ptr } { i64 ${descriptors.length}, ptr @sc_wasm_registry_modules }`,
+ );
+ }
+ out.push("");
+ return out;
+}
diff --git a/packages/compiler/src/backend/native-link-info-core.ts b/packages/compiler/src/backend/native-link-info-core.ts
index 86146259d..84f448e93 100644
--- a/packages/compiler/src/backend/native-link-info-core.ts
+++ b/packages/compiler/src/backend/native-link-info-core.ts
@@ -25,6 +25,9 @@ export interface NativeLinkFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
+ /** The WebAssembly API (scr_wasm.c). Absent when unused, so programs
+ * without it keep their link identity. */
+ wasm?: boolean;
/** The program assigns process.emitWarning: links the
* SCR_WARNING_REPLACE variant of the dyn-async unit. */
warningReplace?: boolean;
@@ -40,18 +43,28 @@ export interface NativeLinkFeatures {
/** Dynamic-tier jsval operations: the scr_js_* units. Absent (not false)
* for programs without them, so their link features stay unchanged. */
dynTier?: boolean;
+ /** Checked-dynamic stream slots: the scr_stream_dyn.c unit. Absent (not
+ * false) for programs without them. */
+ streamDyn?: boolean;
+ /** events.once with options: scr_events_once_signal.c. Absent (not
+ * false) for programs without it. */
+ eventsOnceSignal?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
bigint: boolean;
searchParams: boolean;
qs: boolean;
+ dynBuiltinClass: boolean;
+ dynBuiltinStream: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
http: boolean;
http2: boolean;
dgram: boolean;
+ /** node:dns/promises: scr_dns_promises.c. */
+ dnsPromises: boolean;
watch: boolean;
foreignFfi: boolean;
nodeTest: boolean;
@@ -59,6 +72,8 @@ export interface NativeLinkFeatures {
/** crypto.createCipheriv/createDecipheriv: scr_cipher.c plus the mbedTLS
* archive. */
cipher: boolean;
+ /** perf_hooks event-loop metrics: scr_perf_loop.c. */
+ perfLoop: boolean;
tlsCa: boolean;
/** crypto.subtle importKey/sign/verify/digest and CryptoKey values:
* scr_webcrypto.c. */
diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts
index 33b13960e..a9c93d3df 100644
--- a/packages/compiler/src/backend/native/contracts.ts
+++ b/packages/compiler/src/backend/native/contracts.ts
@@ -113,11 +113,19 @@ export interface CcOptions {
* the IR): compiles the scr_js_* units (two-tier design I3). Programs
* without them link no scr_js_ symbol. */
dynTier?: boolean;
+ /** Checked-dynamic stream slots (stream.dynNative / stream.pipeDyn):
+ * compiles scr_stream_dyn.c. */
+ streamDyn?: boolean;
+ /** events.once with options (events.onceSignal): compiles
+ * scr_events_once_signal.c. */
+ eventsOnceSignal?: boolean;
/** The program constructs EventTarget/Event/CustomEvent values or
* calls events.addAbortListener (the event.* libCalls), or uses the
* native fetch AbortSignal built on the same engine: compiles
* scr_event_target.c. */
eventTarget?: boolean;
+ /** The WebAssembly API: compiles scr_wasm.c. */
+ wasm?: boolean;
/** SCR_WARNING_REPLACE: process.emitWarning replacement support. */
warningReplace?: boolean;
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
@@ -150,6 +158,15 @@ export interface CcOptions {
* line (escape-only programs ride the always-linked component encoder
* and never flip this). */
qs?: boolean;
+ /** The program uses EventEmitter or a stream class as an untyped
+ * constructor value (a constructor function's base): compiles
+ * scr_dyn_builtin_class.c, which also references the inspection,
+ * dyn-invoke and symbol units. */
+ dynBuiltinClass?: boolean;
+ /** The program uses a stream class as an untyped constructor value:
+ * compiles scr_dyn_builtin_stream.c, the constructors' stream half, which
+ * also references the stream unit. */
+ dynBuiltinStream?: boolean;
/** The program uses native util parsing, comparison, or styling. Optional
* utility units also require their symbol, inspection, and warning features. */
parseArgs?: boolean;
@@ -177,6 +194,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. */
@@ -209,6 +231,8 @@ export interface CcOptions {
cipher?: boolean;
/** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */
webcrypto?: boolean;
+ /** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */
+ perfLoop?: boolean;
/** Internal compiler hook: called only after a strict native artifact hit
* or a successful stable build has installed `outPath`. The executable
* frontend cache uses it to publish its stamp after native dependencies
diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts
index 5ebb4796e..949f03839 100644
--- a/packages/compiler/src/backend/native/executable.ts
+++ b/packages/compiler/src/backend/native/executable.ts
@@ -182,6 +182,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.opts.dc, "scr_dc.c"),
unit((u) => u.opts.dynTier, "scr_js_value.c"),
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
+ unit((u) => u.opts.wasm, "scr_wasm.c"),
unit(
(u) =>
u.opts.dynAsync ||
@@ -195,6 +196,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
// The performance timeline (every perf_hooks user-timing call selects
// dynAsync, whose unit carries the timeline's buffer warning).
unit((u) => u.opts.dynAsync, "scr_perf.c"),
+ // perf_hooks event-loop metrics, only for programs that read them.
+ unit((u) => u.opts.perfLoop, "scr_perf_loop.c"),
// Blob and File (every blob.* call selects dynAsync, whose unit boxes
// their reads as dyn promises).
unit((u) => u.opts.dynAsync, "scr_blob.c"),
@@ -217,6 +220,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
// measures with.
unit((u) => u.opts.events && u.regex, "scr_readline_tty.c"),
unit((u) => u.opts.emitter || u.opts.workers, "scr_events_emitter.c"),
+ // events.once with options registers through the emitter and
+ // event-target units.
+ unit((u) => u.opts.eventsOnceSignal === true, "scr_events_once_signal.c"),
// The checked-dynamic HANDLE support unit (listener gate + runtime
// adapter closures): every referencing unit is one of the emitter or
// net families (http implies net), so handle-free binaries keep
@@ -235,11 +241,33 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
(u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex,
"scr_util_callable.c",
),
+ unit(
+ (u) =>
+ u.opts.dynBuiltinClass &&
+ u.opts.inspect &&
+ u.opts.dynInvoke &&
+ u.opts.symbol &&
+ (u.opts.emitter || u.opts.workers),
+ "scr_dyn_builtin_class.c",
+ ),
+ unit(
+ (u) =>
+ u.opts.dynBuiltinStream &&
+ u.opts.dynBuiltinClass &&
+ u.opts.stream &&
+ u.opts.inspect &&
+ u.opts.dynInvoke &&
+ u.opts.symbol,
+ "scr_dyn_builtin_stream.c",
+ ),
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
// The zlib stream classes reference both the zlib engine and the
// stream unit.
unit((u) => u.opts.zlib && u.opts.stream, "scr_zlib_stream.c"),
unit((u) => u.opts.stream && u.opts.dynAsync, "scr_stream_ops.c"),
+ // Checked-dynamic stream slots test values and call JavaScript stream
+ // members through the dyn-invoke unit.
+ unit((u) => u.opts.streamDyn === true, "scr_stream_dyn.c"),
// FileHandle streams reference both the stream unit and FileHandle.
unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"),
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
@@ -252,6 +280,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"),
diff --git a/packages/compiler/src/backend/runtime-pack-core.ts b/packages/compiler/src/backend/runtime-pack-core.ts
index 6a660e00c..5c9d417c5 100644
--- a/packages/compiler/src/backend/runtime-pack-core.ts
+++ b/packages/compiler/src/backend/runtime-pack-core.ts
@@ -82,7 +82,10 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
workers: boolean;
youngNursery: boolean;
eventTarget: boolean;
+ wasm: boolean;
dynTier: boolean;
+ streamDyn: boolean;
+ eventsOnceSignal: boolean;
warningReplace: boolean;
stdioExtended: boolean;
nativeFetch: boolean;
@@ -272,7 +275,10 @@ export function effectiveRuntimeFeatures(
workers: features.workers === true,
youngNursery: features.youngNursery === true,
eventTarget: features.eventTarget === true,
+ wasm: features.wasm === true,
dynTier: features.dynTier === true,
+ streamDyn: features.streamDyn === true,
+ eventsOnceSignal: features.eventsOnceSignal === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
nativeFetch,
diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts
index e5ac7fa4b..e80d48e28 100644
--- a/packages/compiler/src/backend/runtime-pack-targets.test.ts
+++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts
@@ -107,12 +107,15 @@ const features = {
bigint: false,
searchParams: false,
qs: false,
+ dynBuiltinClass: false,
+ dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
+ dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -120,6 +123,7 @@ const features = {
tlsCa: false,
cipher: false,
webcrypto: false,
+ perfLoop: false,
};
test.each([
diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts
index 6d2978f0c..7a24facd6 100644
--- a/packages/compiler/src/backend/runtime-pack.test.ts
+++ b/packages/compiler/src/backend/runtime-pack.test.ts
@@ -44,12 +44,15 @@ const BASE: NativeLinkFeatures = {
bigint: false,
searchParams: false,
qs: false,
+ dynBuiltinClass: false,
+ dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
+ dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -57,6 +60,7 @@ const BASE: NativeLinkFeatures = {
tlsCa: false,
cipher: false,
webcrypto: false,
+ perfLoop: false,
};
async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
diff --git a/packages/compiler/src/backend/stack-observations.test.ts b/packages/compiler/src/backend/stack-observations.test.ts
index 0e1178ce9..d381c5307 100644
--- a/packages/compiler/src/backend/stack-observations.test.ts
+++ b/packages/compiler/src/backend/stack-observations.test.ts
@@ -84,6 +84,97 @@ test("a computed key reads the stack when the program spells it", () => {
expect(observesStacks(mod([fn("%main", [computed])]))).toBe(false);
});
+test('a computed key that cannot be "stack" reads no stack', () => {
+ const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
+ const spelled: IrStmt = {
+ loc,
+ kind: "exprStmt",
+ expr: { loc, kind: "strLit", value: "stack", type: STRING },
+ };
+ const read = (key: IrExpr): IrStmt => ({
+ loc,
+ kind: "exprStmt",
+ expr: { loc, kind: "dynKeyGet", value, key, type: DYN },
+ });
+ const index: IrExpr = { loc, kind: "varRef", localId: "i", type: F64 };
+ const indexText: IrExpr = { loc, kind: "toString", operand: index, type: STRING };
+ // A number key, and a key converted from one.
+ expect(observesStacks(mod([fn("%main", [read(index), spelled])]))).toBe(false);
+ expect(observesStacks(mod([fn("%main", [read(indexText), spelled])]))).toBe(false);
+ // An immutable local answers with its initializer (the shape of the
+ // dispatch helpers' formatted index keys).
+ const key: IrExpr = { loc, kind: "varRef", localId: "key", type: STRING };
+ const local = (init: IrExpr, mutable = false): IrFunction => ({
+ ...fn("%main", [{ loc, kind: "varDecl", localId: "key", init }, read(key), spelled]),
+ locals: [{ id: "key", name: "key", type: STRING, mutable }],
+ });
+ expect(observesStacks(mod([local(indexText)]))).toBe(false);
+ expect(observesStacks(mod([local(indexText, true)]))).toBe(true);
+ // A parameter of a function called by name answers with its arguments.
+ const helper: IrFunction = {
+ ...fn("readKey", [read(key)]),
+ params: [{ localId: "key", name: "key", type: STRING }],
+ locals: [{ id: "key", name: "key", type: STRING, mutable: false }],
+ };
+ const callWith = (arg: IrExpr): IrStmt => ({
+ loc,
+ kind: "exprStmt",
+ expr: { loc, kind: "call", callee: "readKey", args: [arg], type: DYN },
+ });
+ const numbers = mod([fn("%main", [callWith(indexText), spelled]), helper]);
+ expect(observesStacks(numbers)).toBe(false);
+ const stack = mod([
+ fn("%main", [
+ callWith(indexText),
+ callWith({ loc, kind: "strLit", value: "stack", type: STRING }),
+ ]),
+ helper,
+ ]);
+ expect(observesStacks(stack)).toBe(true);
+});
+
+test("a key forwarded through functions with several call sites is searched once", () => {
+ // f0 calls f1 twice, f1 calls f2 twice, and so on, each passing its
+ // parameter on: 2^40 call paths reach the read in f40.
+ const depth = 40;
+ const value: IrExpr = { loc, kind: "varRef", localId: "d", type: DYN };
+ const key: IrExpr = { loc, kind: "varRef", localId: "key", type: STRING };
+ const forward = (callee: string): IrStmt => ({
+ loc,
+ kind: "exprStmt",
+ expr: { loc, kind: "call", callee, args: [key], type: VOID },
+ });
+ const chain = (root: IrExpr): IrModule => {
+ const functions: IrFunction[] = [];
+ for (let i = 0; i <= depth; i++) {
+ const body: IrStmt[] =
+ i < depth
+ ? [forward(`f${i + 1}`), forward(`f${i + 1}`)]
+ : [{ loc, kind: "exprStmt", expr: { loc, kind: "dynKeyGet", value, key, type: DYN } }];
+ functions.push({
+ ...fn(`f${i}`, body),
+ params: [{ localId: "key", name: "key", type: STRING }],
+ locals: [{ id: "key", name: "key", type: STRING, mutable: false }],
+ });
+ }
+ const spelled: IrStmt = {
+ loc,
+ kind: "exprStmt",
+ expr: { loc, kind: "strLit", value: "stack", type: STRING },
+ };
+ const start: IrStmt = {
+ loc,
+ kind: "exprStmt",
+ expr: { loc, kind: "call", callee: "f0", args: [root], type: VOID },
+ };
+ return mod([fn("%main", [start, spelled]), ...functions]);
+ };
+ expect(observesStacks(chain({ loc, kind: "strLit", value: "message", type: STRING }))).toBe(
+ false,
+ );
+ expect(observesStacks(chain({ loc, kind: "strLit", value: "stack", type: STRING }))).toBe(true);
+});
+
test("programs without stack reads are not", () => {
expect(observesStacks(mod([fn("%main", [call("helper")]), fn("helper", [ret])]))).toBe(false);
});
diff --git a/packages/compiler/src/backend/stack-observations.ts b/packages/compiler/src/backend/stack-observations.ts
index cc6d109e4..fa67c071f 100644
--- a/packages/compiler/src/backend/stack-observations.ts
+++ b/packages/compiler/src/backend/stack-observations.ts
@@ -3,7 +3,7 @@
* it answers true. */
import { exprFunctionReference, moduleFunctionReferences } from "../ir/function-references.js";
import { MAY_THROW_LIB_FNS } from "../ir/builtin-effects.js";
-import type { IrExpr, IrFunction, IrModule, IrStmt } from "../ir/ir.js";
+import type { IrExpr, IrFunction, IrModule, IrStmt, IrType } from "../ir/ir.js";
import { MAY_THROW_ARR_METHODS } from "../ir/ir.js";
import { everyExprChild, everyStmtChild, everyStmtList } from "../ir/traverse.js";
import { unreachableStatements } from "./may-throw.js";
@@ -13,16 +13,135 @@ import { writesPublished } from "./publication.js";
/** Lib calls that read a captured stack trace. */
const STACK_READS: ReadonlySet = new Set(["error.stack"]);
+/** Key types whose property names are never `stack`: a number, bigint or
+ * boolean key names its decimal or `true`/`false` property. */
+const NON_STACK_KEY_TYPES: ReadonlySet = new Set(["f64", "bigint", "bool"]);
+
+/** Whether a computed key can be the string "stack", the question a read
+ * on a checked-dynamic value asks before it counts as a stack read.
+ * - A string literal answers by its value; a key of a number, bigint or
+ * boolean type, or one converted from such a value, cannot be "stack".
+ * - An immutable local answers with its one initializer, and a parameter of
+ * a function the program only calls by name answers with the arguments
+ * its call sites pass.
+ * - Any other key can be "stack" when the program spells the literal
+ * "stack" somewhere, since the literal can reach the key through fields,
+ * containers or calls; a key the program assembles from other strings or
+ * from data it reads keeps the static answer. */
+class StackKeys {
+ private readonly spelled: boolean;
+ private readonly calls = new Map();
+ private readonly named: Set;
+ private readonly inits = new Map>();
+ /** Settled answers by function and local: true where a search reached a
+ * key that can be "stack", false where a search that found none passed. */
+ private readonly answers = new Map();
+
+ constructor(mod: IrModule) {
+ this.spelled = spellsStack(mod);
+ this.named = new Set(moduleFunctionReferences(mod));
+ // Class methods, accessors and constructors are called through dispatch.
+ const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
+ for (const fn of mod.functions) {
+ if (isClassMember(fn.name, classNames)) this.named.add(fn.name);
+ everyStmtList(fn.body, {
+ stmt: () => true,
+ expr: (expr) => {
+ if (expr.kind === "call") {
+ const sites = this.calls.get(expr.callee) ?? [];
+ sites.push({ caller: fn, args: expr.args });
+ this.calls.set(expr.callee, sites);
+ }
+ const referenced = exprFunctionReference(expr);
+ if (referenced !== undefined) this.named.add(referenced);
+ return true;
+ },
+ });
+ }
+ }
+
+ mayBeStack(fn: IrFunction, key: IrExpr): boolean {
+ const visited = new Set();
+ const answer = this.keyMayBeStack(fn, key, visited);
+ // A search that found no way to "stack" explored everything it visited
+ // to the end, so none of it can be "stack" either.
+ if (!answer) for (const visit of visited) this.answers.set(visit, false);
+ return answer;
+ }
+
+ private keyMayBeStack(fn: IrFunction, key: IrExpr, visited: Set): boolean {
+ if (key.kind === "strLit") return key.value === "stack";
+ if (!this.spelled) return false;
+ if (NON_STACK_KEY_TYPES.has(key.type.kind)) return false;
+ if (key.kind === "toString" && NON_STACK_KEY_TYPES.has(key.operand.type.kind)) return false;
+ if (key.kind === "dynFrom" && NON_STACK_KEY_TYPES.has(key.value.type.kind)) return false;
+ if (key.kind !== "varRef") return true;
+ const visit = `${fn.name}\0${key.localId}`;
+ const known = this.answers.get(visit);
+ if (known !== undefined) return known;
+ // Each local is searched once per question: a cycle, or a second path
+ // to a local already searched, adds no value of its own.
+ if (visited.has(visit)) return false;
+ visited.add(visit);
+ const answer = this.localMayBeStack(fn, key.localId, visited);
+ if (answer) this.answers.set(visit, true);
+ return answer;
+ }
+
+ private localMayBeStack(fn: IrFunction, localId: string, visited: Set): boolean {
+ const local = fn.locals.find((candidate) => candidate.id === localId);
+ if (local === undefined || local.mutable) return true;
+ const index = fn.params.findIndex((param) => param.localId === localId);
+ if (index !== -1) {
+ const sites = this.calls.get(fn.name);
+ if (this.named.has(fn.name) || sites === undefined) return true;
+ return sites.some(
+ (site) =>
+ site.args.length !== fn.params.length ||
+ this.keyMayBeStack(site.caller, site.args[index]!, visited),
+ );
+ }
+ const init = this.initializerOf(fn, localId);
+ return init === null || this.keyMayBeStack(fn, init, visited);
+ }
+
+ /** The one initializer of an immutable local, or null when it has none
+ * or more than one declaration. */
+ private initializerOf(fn: IrFunction, localId: string): IrExpr | null {
+ let inits = this.inits.get(fn);
+ if (inits === undefined) {
+ inits = new Map();
+ everyStmtList(fn.body, {
+ stmt: (stmt) => {
+ if (stmt.kind === "varDecl")
+ inits!.set(stmt.localId, inits!.has(stmt.localId) ? null : (stmt.init ?? null));
+ return true;
+ },
+ expr: () => true,
+ });
+ this.inits.set(fn, inits);
+ }
+ return inits.get(localId) ?? null;
+ }
+}
+
+/** Whether `name` follows the `%.` convention of class
+ * methods, accessors and constructors for one of `classNames`. */
+function isClassMember(name: string, classNames: ReadonlySet): boolean {
+ if (!name.startsWith("%")) return false;
+ for (let dot = name.indexOf("."); dot !== -1; dot = name.indexOf(".", dot + 1)) {
+ if (classNames.has(name.slice(1, dot))) return true;
+ }
+ return false;
+}
+
/** A typed `error.stack` read, or a read on a checked-dynamic value that
- * can reach an error's own `stack` accessor: the constant key `stack`, or
- * a computed key when `computedKeys` (the program spells "stack", so a
- * computed key can name it). A key the program assembles from other
- * strings or from data it reads keeps the static answer. */
-function readsStack(expr: IrExpr, computedKeys: boolean): boolean {
+ * can reach an error's own `stack` accessor: one whose key can be "stack"
+ * (StackKeys). */
+function readsStack(fn: IrFunction, expr: IrExpr, keys: StackKeys): boolean {
if (expr.kind === "libCall") return STACK_READS.has(expr.fn);
if (expr.kind !== "dynKeyGet") return false;
- if (expr.key.kind === "strLit") return expr.key.value === "stack";
- return computedKeys;
+ return keys.mayBeStack(fn, expr.key);
}
/** Whether the module contains the string literal "stack". */
@@ -36,10 +155,10 @@ function spellsStack(mod: IrModule): boolean {
);
}
-function containsStackRead(fn: IrFunction, computedKeys: boolean): boolean {
+function containsStackRead(fn: IrFunction, keys: StackKeys): boolean {
return !everyStmtList(fn.body, {
stmt: () => true,
- expr: (expr) => !readsStack(expr, computedKeys),
+ expr: (expr) => !readsStack(fn, expr, keys),
});
}
@@ -65,22 +184,14 @@ function visitLiveExprs(body: IrStmt[], visit: (expr: IrExpr) => void): void {
* and those reads must not cost every call of the program a frame.
* Programs without any stack read answer false without the walk. */
export function observesStacks(mod: IrModule): boolean {
- const computedKeys = spellsStack(mod);
- if (!mod.functions.some((fn) => containsStackRead(fn, computedKeys))) return false;
+ const keys = new StackKeys(mod);
+ if (!mod.functions.some((fn) => containsStackRead(fn, keys))) return false;
const byName = new Map(mod.functions.map((fn) => [fn.name, fn]));
const classNames = new Set((mod.classes ?? []).map((cls) => cls.name));
const pending: string[] = [...moduleFunctionReferences(mod)];
// Class methods, accessors and constructors are reachable through
// dispatch the IR does not name per site: all of them are roots.
- for (const fn of mod.functions) {
- if (!fn.name.startsWith("%")) continue;
- for (let dot = fn.name.indexOf("."); dot !== -1; dot = fn.name.indexOf(".", dot + 1)) {
- if (classNames.has(fn.name.slice(1, dot))) {
- pending.push(fn.name);
- break;
- }
- }
- }
+ for (const fn of mod.functions) if (isClassMember(fn.name, classNames)) pending.push(fn.name);
const reached = new Set();
let observed = false;
while (pending.length > 0 && !observed) {
@@ -90,7 +201,7 @@ export function observesStacks(mod: IrModule): boolean {
const fn = byName.get(name);
if (fn === undefined) continue;
visitLiveExprs(fn.body, (expr) => {
- if (readsStack(expr, computedKeys)) observed = true;
+ if (readsStack(fn, expr, keys)) observed = true;
if (expr.kind === "call" && !reached.has(expr.callee)) pending.push(expr.callee);
const referenced = exprFunctionReference(expr);
if (referenced !== undefined && !reached.has(referenced)) pending.push(referenced);
diff --git a/packages/compiler/src/backend/target-diagnostics.ts b/packages/compiler/src/backend/target-diagnostics.ts
index 365d5e913..d130ce95e 100644
--- a/packages/compiler/src/backend/target-diagnostics.ts
+++ b/packages/compiler/src/backend/target-diagnostics.ts
@@ -192,6 +192,14 @@ export function moduleWasiUnavailableSurface(
found = { surface: "OS signals (WASI Preview 1 has no signal API)", loc };
return false;
}
+ if (node.fn.startsWith("wasm.")) {
+ found = {
+ surface:
+ "the WebAssembly API (a wasm32-wasi program cannot load translated modules into its own linear memory yet)",
+ loc,
+ };
+ return false;
+ }
if (node.fn === "crypto.cipherNew") {
found = {
surface:
diff --git a/packages/compiler/src/backend/warm-cache.ts b/packages/compiler/src/backend/warm-cache.ts
index 2cd111060..389fa19e9 100644
--- a/packages/compiler/src/backend/warm-cache.ts
+++ b/packages/compiler/src/backend/warm-cache.ts
@@ -50,18 +50,22 @@ export async function warmNativeCaches(
bigint: false,
searchParams: false,
qs: false,
+ dynBuiltinClass: false,
+ dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
+ dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
tls: false,
cipher: false,
webcrypto: false,
+ perfLoop: false,
tlsCa: false,
},
});
diff --git a/packages/compiler/src/coverage/report.ts b/packages/compiler/src/coverage/report.ts
index 6570f23d1..89733e255 100644
--- a/packages/compiler/src/coverage/report.ts
+++ b/packages/compiler/src/coverage/report.ts
@@ -221,13 +221,13 @@ export function renderCoverage(input: CoverageInput, opts: CoverageRenderOptions
out.push("");
}
- // The WebAssembly modules the build can see: what translating them would
- // take (wasm/discover.ts). No module compiles to native code yet; the
- // sites that load one are blockers or deferred fences above.
+ // The WebAssembly modules the build can see (wasm/discover.ts). A build
+ // translates the valid ones to native code (wasm/translate.ts) when the
+ // program uses the WebAssembly API; the others fail to load at run time.
const wasm = input.wasmModules ?? [];
if (wasm.length > 0) {
out.push(
- ` ${c(DIM, "WebAssembly modules:")} ${c(DIM, "(found at build time; none compile to native code yet)")}`,
+ ` ${c(DIM, "WebAssembly modules:")} ${c(DIM, "(found at build time; valid ones compile to native code)")}`,
);
for (const m of wasm) {
const name = m.path !== undefined ? displayPath(m.path, opts.root) : "inline module";
diff --git a/packages/compiler/src/coverage/surface-manifest.ts b/packages/compiler/src/coverage/surface-manifest.ts
index d82367611..9867a5617 100644
--- a/packages/compiler/src/coverage/surface-manifest.ts
+++ b/packages/compiler/src/coverage/surface-manifest.ts
@@ -444,16 +444,17 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
}
}
- // ── the WebAssembly global: every reached use refuses (webAssemblyDiag) ─
+ // ── the WebAssembly global (builtins/webassembly.ts, wasm/translate.ts):
+ // the core API lowers over modules translated at build time; the rest
+ // refuses at each use (webAssemblyDiag) ─
{
const refusal = webAssemblyDiag("WebAssembly", { file: "", start: 0, end: 0 });
add({
id: "stdlib.webassembly",
kind: "stdlib",
name: "globalThis.WebAssembly",
- status: "unsupported",
- code: refusal.code,
- ...(refusal.hint !== undefined ? { note: refusal.hint } : {}),
+ status: "static",
+ note: `compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, instanceof, the error classes, exports, imports and Module transfer to workers, over the modules translated at build time; the rest refuses with ${refusal.code} (${refusal.hint ?? ""})`,
});
}
diff --git a/packages/compiler/src/diagnostics/diagnostic.ts b/packages/compiler/src/diagnostics/diagnostic.ts
index 4acd6bc50..e4d4006ca 100644
--- a/packages/compiler/src/diagnostics/diagnostic.ts
+++ b/packages/compiler/src/diagnostics/diagnostic.ts
@@ -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 = {
"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",
},
@@ -751,17 +740,20 @@ export function noLoweringDiag(
};
}
-/** A REACHED use of the WebAssembly API (SC2020): scriptc.d.ts declares
- * its types so packages that mention them typecheck, but no module
- * compiles to native code yet. `surface` names what was used
- * ('WebAssembly.compile', 'new WebAssembly.Module', 'globalThis.WebAssembly'). */
+/** A REACHED use of a part of the WebAssembly API scriptc does not
+ * implement yet (SC2020): the streaming functions, Tag and Exception, the
+ * classes as values. compile, instantiate, validate, the Module, Instance,
+ * Memory, Table and Global constructors, Module.imports/exports/
+ * customSections and the error classes lower (builtins/webassembly.ts).
+ * `surface` names what was used ('WebAssembly.compileStreaming',
+ * 'WebAssembly.Memory'). */
export function webAssemblyDiag(surface: string, loc: SrcLoc): ScrDiagnostic {
return {
code: "SC2020",
- message: `'${surface}' is not supported yet: scriptc does not compile WebAssembly modules to native code yet`,
+ message: `'${surface}' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, and the error classes over modules translated at build time, but not this part of the API`,
loc,
milestone: "later",
- hint: "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)",
+ hint: "the streaming functions, Tag and Exception, and the classes used as values other than with new and instanceof are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)",
};
}
diff --git a/packages/compiler/src/executable/executable-cache.test.ts b/packages/compiler/src/executable/executable-cache.test.ts
index 1b42c3eca..33da0610f 100644
--- a/packages/compiler/src/executable/executable-cache.test.ts
+++ b/packages/compiler/src/executable/executable-cache.test.ts
@@ -45,12 +45,15 @@ const native: EarlyExecutableNativeFeatures = {
bigint: false,
searchParams: false,
qs: false,
+ dynBuiltinClass: false,
+ dynBuiltinStream: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
+ dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -58,6 +61,7 @@ const native: EarlyExecutableNativeFeatures = {
tlsCa: false,
cipher: false,
webcrypto: false,
+ perfLoop: false,
};
async function fixture(): Promise<{
@@ -199,7 +203,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 +220,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") },
diff --git a/packages/compiler/src/executable/executable-cache.ts b/packages/compiler/src/executable/executable-cache.ts
index 8c67c1006..064b1f867 100644
--- a/packages/compiler/src/executable/executable-cache.ts
+++ b/packages/compiler/src/executable/executable-cache.ts
@@ -56,6 +56,7 @@ export interface EarlyExecutableNativeFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
+ wasm?: boolean;
warningReplace?: boolean;
stdioExtended?: boolean;
zlib: boolean;
@@ -65,23 +66,29 @@ export interface EarlyExecutableNativeFeatures {
dc: boolean;
dynAsync: boolean;
dynTier?: boolean;
+ streamDyn?: boolean;
+ eventsOnceSignal?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
bigint: boolean;
searchParams: boolean;
qs: boolean;
+ dynBuiltinClass: boolean;
+ dynBuiltinStream: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
http: boolean;
http2: boolean;
dgram: boolean;
+ dnsPromises: boolean;
watch: boolean;
foreignFfi: boolean;
nodeTest: boolean;
tls: boolean;
cipher: boolean;
+ perfLoop: boolean;
tlsCa: boolean;
webcrypto: boolean;
}
@@ -116,6 +123,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;
@@ -185,12 +195,15 @@ const BOOLEAN_NATIVE_KEYS = [
"bigint",
"searchParams",
"qs",
+ "dynBuiltinClass",
+ "dynBuiltinStream",
"parseArgs",
"stream",
"net",
"http",
"http2",
"dgram",
+ "dnsPromises",
"watch",
"foreignFfi",
"nodeTest",
@@ -198,6 +211,7 @@ const BOOLEAN_NATIVE_KEYS = [
"tlsCa",
"cipher",
"webcrypto",
+ "perfLoop",
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures {
@@ -230,6 +244,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
? ""
@@ -270,6 +285,7 @@ function routeKey(options: Omit = { "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 = {
- 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";
diff --git a/packages/compiler/src/frontend/cjs-syntax.ts b/packages/compiler/src/frontend/cjs-syntax.ts
index 41191432a..99195d994 100644
--- a/packages/compiler/src/frontend/cjs-syntax.ts
+++ b/packages/compiler/src/frontend/cjs-syntax.ts
@@ -1,4 +1,5 @@
import * as ts from "./ts7/syntax.js";
+import { tokenStartOfNode } from "./ts7/node-functions.js";
export interface CjsLexedExports {
/** Names Node's lexer detects in THIS file (no reexport resolution). */
@@ -30,7 +31,7 @@ function identRunEnd(text: string, pos: number): number {
* a `a`-escaped name parses to the same AST as its plain spelling,
* but merve reads bytes and stops at the backslash. */
function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean {
- const ch = sf.text[node.getStart(sf)];
+ const ch = sf.text[tokenStartOfNode(node)];
return ch !== undefined && isIdentStartChar(ch);
}
@@ -39,16 +40,16 @@ function sourceSpellsIdentifier(node: ts.Node, sf: ts.SourceFile): boolean {
* defineProperty getter's `return IDENT.IDENT` because `this` lexes as an
* identifier run — probed), escapes and private names do not. */
function isIdentTokenNode(node: ts.Node, sf: ts.SourceFile): boolean {
- const start = node.getStart(sf);
+ const start = tokenStartOfNode(node);
const ch = sf.text[start];
- return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.getEnd();
+ return ch !== undefined && isIdentStartChar(ch) && identRunEnd(sf.text, start) === node.end;
}
/** The identifier run at the node's START — the whole name when its
* spelling is plain, the byte prefix merve consumed when a unicode escape
* interrupts the run mid-name. */
function identPrefixOf(node: ts.Node, sf: ts.SourceFile): string {
- const start = node.getStart(sf);
+ const start = tokenStartOfNode(node);
return sf.text.slice(start, identRunEnd(sf.text, start));
}
@@ -84,20 +85,17 @@ export function isModuleExports(e: ts.Expression): boolean {
* follows — so the recognizer descends the leftmost-child chain while it
* still starts where `e` starts. A ParenthesizedExpression breaks the
* chain naturally (its child starts after the `(`), matching merve. */
-function leadingRequireOf(
- e: ts.Expression,
- sf: ts.SourceFile,
-): { spec: string; end: number } | null {
- const start = e.getStart(sf);
+function leadingRequireOf(e: ts.Expression): { spec: string; end: number } | null {
+ const start = tokenStartOfNode(e);
let cur: ts.Node = e;
for (;;) {
const spec = bareRequireSpecOf(cur);
- if (spec !== null && cur.getStart(sf) === start) return { spec, end: cur.getEnd() };
+ if (spec !== null && tokenStartOfNode(cur) === start) return { spec, end: cur.end };
let first: ts.Node | undefined;
ts.forEachChild(cur, (c) => {
first ??= c;
});
- if (first === undefined || first.getStart(sf) !== start) return null;
+ if (first === undefined || tokenStartOfNode(first) !== start) return null;
cur = first;
}
}
@@ -179,13 +177,13 @@ function scanTableLiteral(
continue; // whitespace before `,` is skipped on the shorthand path
}
if (ts.isSpreadAssignment(prop)) {
- const req = leadingRequireOf(prop.expression, sf);
+ const req = leadingRequireOf(prop.expression);
let consumedEnd: number;
if (req !== null) {
reexports.push(req.spec);
consumedEnd = req.end;
} else if (sourceSpellsIdentifier(prop.expression, sf)) {
- consumedEnd = identRunEnd(text, prop.expression.getStart(sf));
+ consumedEnd = identRunEnd(text, tokenStartOfNode(prop.expression));
} else {
return; // `...{}` and friends stop the scan
}
@@ -213,7 +211,7 @@ function scanTableLiteral(
}
if (key === null) return; // numeric / computed / template keys stop the scan
const value = prop.initializer;
- const req = leadingRequireOf(value, sf);
+ const req = leadingRequireOf(value);
if (req !== null) {
// A require VALUE adds the key, records the reexport, and stops
// the scan UNCONDITIONALLY — probed: `{ a: require('x'), b: v }`
@@ -223,7 +221,7 @@ function scanTableLiteral(
reexports.push(req.spec);
return;
}
- const start = value.getStart(sf);
+ const start = tokenStartOfNode(value);
const ch0 = text[start];
if (ch0 === undefined || !isIdentStartChar(ch0)) return; // literal values stop WITHOUT adding the key
const consumedEnd = identRunEnd(text, start);
@@ -351,8 +349,8 @@ function starExportSpecOf(call: ts.CallExpression, sf: ts.SourceFile): string |
if (!sourceSpellsIdentifier(nameNode, sf)) return null;
const arg0 = call.arguments.nodes[0];
if (arg0 === undefined) return null;
- if (!byteAdjacentParen(sf, nameNode.getEnd(), arg0.getStart(sf))) return null;
- return leadingRequireOf(arg0, sf)?.spec ?? null;
+ if (!byteAdjacentParen(sf, nameNode.end, tokenStartOfNode(arg0))) return null;
+ return leadingRequireOf(arg0)?.spec ?? null;
}
/** True when the bytes between a callee name ending at `nameEnd` and its
@@ -384,7 +382,7 @@ function starAssignOf(stmt: ts.VariableStatement, sf: ts.SourceFile): [string, s
init.arguments.nodes.length >= 1
) {
const arg0 = init.arguments.nodes[0]!;
- if (!byteAdjacentParen(sf, init.expression.getEnd(), arg0.getStart(sf))) return null;
+ if (!byteAdjacentParen(sf, init.expression.end, tokenStartOfNode(arg0))) return null;
const spec = bareRequireSpecOf(arg0);
if (spec !== null) return [decl.name.text, spec];
}
@@ -717,15 +715,16 @@ export function cjsLexedExportsOfFile(sf: ts.SourceFile): CjsLexedExports {
) {
exports.add(lhs.argumentExpression.text);
} else if (isModuleExports(lhs)) {
- events.push({ pos: n.getStart(sf), kind: "assign", rhs: n.right });
+ events.push({ pos: tokenStartOfNode(n), kind: "assign", rhs: n.right });
}
} else if (ts.isCallExpression(n)) {
scanDefineProperty(n, sf, exports);
if (braceDepth === 0) {
const starSpec = starExportSpecOf(n, sf);
- if (starSpec !== null) events.push({ pos: n.getStart(sf), kind: "spec", spec: starSpec });
+ if (starSpec !== null)
+ events.push({ pos: tokenStartOfNode(n), kind: "spec", spec: starSpec });
const loopId = starLoopIdOf(n, sf);
- if (loopId !== null) events.push({ pos: n.getStart(sf), kind: "loop", id1: loopId });
+ if (loopId !== null) events.push({ pos: tokenStartOfNode(n), kind: "loop", id1: loopId });
}
} else if (ts.isVariableStatement(n) && braceDepth === 0) {
const assign = starAssignOf(n, sf);
@@ -745,7 +744,7 @@ export function cjsLexedExportsOfFile(sf: ts.SourceFile): CjsLexedExports {
for (const spec of starAssigns.get(ev.id1) ?? []) reexports.push(spec);
} else {
reexports = []; // every `module.exports =` clears — stars included (probed)
- const req = leadingRequireOf(ev.rhs, sf);
+ const req = leadingRequireOf(ev.rhs);
if (req !== null) {
reexports.push(req.spec);
} else if (ts.isObjectLiteralExpression(ev.rhs)) {
diff --git a/packages/compiler/src/frontend/contract-types.ts b/packages/compiler/src/frontend/contract-types.ts
index c30788fa1..4ded2a1a0 100644
--- a/packages/compiler/src/frontend/contract-types.ts
+++ b/packages/compiler/src/frontend/contract-types.ts
@@ -130,7 +130,7 @@ function markerNamed(
function nameText(node: ts.Node | undefined): string {
if (node === undefined) return "";
if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) return node.text;
- return node.getText();
+ return ts.getTextOfNode(node);
}
function contractDiag(
diff --git a/packages/compiler/src/frontend/experimental-import.ts b/packages/compiler/src/frontend/experimental-import.ts
index 8fc2238bf..3b35a196a 100644
--- a/packages/compiler/src/frontend/experimental-import.ts
+++ b/packages/compiler/src/frontend/experimental-import.ts
@@ -73,7 +73,7 @@ export function lifetimeRegionMemberOf(
if (
!ts.isClassDeclaration(owner) ||
owner.name?.text !== "LifetimeRegion" ||
- !isExperimentalDeclarationFile(owner.getSourceFile(), known)
+ !isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known)
)
return null;
const name = access.name.text;
@@ -105,7 +105,7 @@ export function internTableMemberOf(
if (
!ts.isClassDeclaration(owner) ||
owner.name?.text !== "InternTable" ||
- !isExperimentalDeclarationFile(owner.getSourceFile(), known)
+ !isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known)
)
return null;
const name = access.name.text;
@@ -135,7 +135,7 @@ export function threadSlotsMemberOf(
if (
!ts.isClassDeclaration(owner) ||
owner.name?.text !== "ThreadSlots" ||
- !isExperimentalDeclarationFile(owner.getSourceFile(), known)
+ !isExperimentalDeclarationFile(ts.getSourceFileOfNode(owner), known)
)
return null;
const name = access.name.text;
diff --git a/packages/compiler/src/frontend/fork-target.test.ts b/packages/compiler/src/frontend/fork-target.test.ts
index 7a3e23c9b..5e56cea5d 100644
--- a/packages/compiler/src/frontend/fork-target.test.ts
+++ b/packages/compiler/src/frontend/fork-target.test.ts
@@ -7,6 +7,7 @@ import { fallbackDtsPath } from "./dts-paths.js";
import { Ts7Host } from "./ts7/program-host.js";
import { Ts7Api } from "./ts7/rpc-api.js";
import { ModuleKind, ModuleResolutionKind } from "./ts7/enums.js";
+import { getTextOfNode } from "./ts7/node-functions.js";
test("fork discovery batches only candidate bindings and preserves aliases and shadowing", () => {
const directory = mkdtempSync(join(tmpdir(), "scriptc-fork-discovery-"));
@@ -49,7 +50,7 @@ test("fork discovery batches only candidate bindings and preserves aliases and s
["first.ts", "second.ts", "third.ts"].map((name) => join(directory, name)),
);
expect(batch).toHaveBeenCalledTimes(1);
- expect(batch.mock.calls[0]![0].map((node) => node.getText())).toEqual([
+ expect(batch.mock.calls[0]![0].map((node) => getTextOfNode(node))).toEqual([
"start",
"child",
"launch",
diff --git a/packages/compiler/src/frontend/fork-target.ts b/packages/compiler/src/frontend/fork-target.ts
index 592bbb2d6..38c8f5f07 100644
--- a/packages/compiler/src/frontend/fork-target.ts
+++ b/packages/compiler/src/frontend/fork-target.ts
@@ -12,7 +12,7 @@ export function isNodeGlobal(program: ts.Program, expression: ts.Identifier): bo
return (
declarations.length > 0 &&
declarations.every((declaration) => {
- const source = declaration.getSourceFile();
+ const source = ts.getSourceFileOfNode(declaration);
return (
source.isDeclarationFile &&
(tsgoPath(source.fileName) === fallbackDtsPath() || isNodeTypesPath(source.fileName))
@@ -61,7 +61,7 @@ export function isBuiltinMemberImport(
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
const target = checker.getAliasedSymbol(symbol);
const targetDecl = target.declarations[0];
- if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) {
+ if (targetDecl !== undefined && ts.getSourceFileOfNode(targetDecl).isDeclarationFile) {
for (
let parent: ts.Node | undefined = targetDecl.parent;
parent !== undefined && !ts.isSourceFile(parent);
@@ -69,7 +69,8 @@ export function isBuiltinMemberImport(
) {
if (ts.isModuleDeclaration(parent) && ts.isStringLiteral(parent.name)) {
return (
- canonicalBuiltinModule(parent.name.text) === moduleName && target.name === memberName
+ canonicalBuiltinModule(parent.name.text) === moduleName &&
+ ts.symbolName(target) === memberName
);
}
}
@@ -188,7 +189,7 @@ function staticString(
): string | null {
const current = strip(expr);
if (ts.isStringLiteralLike(current)) return current.text;
- if (importMetaUrl(current)) return pathToFileURL(current.getSourceFile().fileName).href;
+ if (importMetaUrl(current)) return pathToFileURL(ts.getSourceFileOfNode(current).fileName).href;
if (ts.isTemplateExpression(current)) {
let value = current.head.text;
for (const span of current.templateSpans.nodes) {
diff --git a/packages/compiler/src/frontend/inferred-js-diagnostics.ts b/packages/compiler/src/frontend/inferred-js-diagnostics.ts
index 7095cea5d..b21d19cb0 100644
--- a/packages/compiler/src/frontend/inferred-js-diagnostics.ts
+++ b/packages/compiler/src/frontend/inferred-js-diagnostics.ts
@@ -16,7 +16,7 @@ export function inferredJsDiagnosticSuppressed(
const checker = program.getTypeChecker();
let suppressed = false;
ts.walkPreorder(file, (node) => {
- if (node.getEnd() < diagnostic.pos || node.getStart(file) > diagnostic.end) return "skip";
+ if (node.end < diagnostic.pos || ts.tokenStartOfNode(node) > diagnostic.end) return "skip";
if (
diagnostic.code === 2565 &&
isJsSourceFileName(file.fileName) &&
@@ -30,7 +30,7 @@ export function inferredJsDiagnosticSuppressed(
ts.isPropertyDeclaration(decl) &&
!decl.type &&
!decl.initializer &&
- !/@type\b/.test(file.text.slice(decl.pos, decl.getStart(file))),
+ !/@type\b/.test(file.text.slice(decl.pos, ts.tokenStartOfNode(decl))),
)
) {
// Bare JS fields exist with value undefined before the first write;
@@ -44,14 +44,14 @@ export function inferredJsDiagnosticSuppressed(
const declaration = signature && signature.declaration;
if (
declaration &&
- isJsSourceFileName(declaration.getSourceFile().fileName) &&
+ isJsSourceFileName(ts.getSourceFileOfNode(declaration).fileName) &&
(ts.isFunctionDeclaration(declaration) ||
ts.isFunctionExpression(declaration) ||
ts.isMethodDeclaration(declaration)) &&
node.arguments.nodes.length > declaration.parameters.nodes.length &&
- node.arguments.nodes[declaration.parameters.nodes.length]?.getStart(file) ===
+ ts.tokenStartOfNode(node.arguments.nodes[declaration.parameters.nodes.length]!) ===
diagnostic.pos &&
- node.arguments.nodes[node.arguments.nodes.length - 1]?.getEnd() === diagnostic.end &&
+ node.arguments.nodes[node.arguments.nodes.length - 1]!.end === diagnostic.end &&
bodyReadsArguments(declaration)
) {
suppressed = true;
@@ -68,7 +68,7 @@ export function inferredJsDiagnosticSuppressed(
const iterable = checker.getTypeAtLocation(node.expression);
const iterator = checker
.getPropertiesOfType(iterable)
- .find((property) => property.name.startsWith("__@iterator@"));
+ .find((property) => ts.symbolName(property).startsWith("__@iterator@"));
if (!iterator) return undefined;
const signatures = checker.getCallSignatures(
checker.getTypeOfSymbolAtLocation(iterator, node.expression),
@@ -88,7 +88,7 @@ export function inferredJsDiagnosticSuppressed(
declaration &&
ts.isMethodDeclaration(declaration) &&
declaration.type === undefined &&
- isJsSourceFileName(declaration.getSourceFile().fileName) &&
+ isJsSourceFileName(ts.getSourceFileOfNode(declaration).fileName) &&
declaration.body?.statements?.nodes.length === 1 &&
ts.isThrowStatement(declaration.body.statements.nodes[0]!)
) {
diff --git a/packages/compiler/src/frontend/lib-contract.ts b/packages/compiler/src/frontend/lib-contract.ts
index 0432d06e2..28532ecd8 100644
--- a/packages/compiler/src/frontend/lib-contract.ts
+++ b/packages/compiler/src/frontend/lib-contract.ts
@@ -115,7 +115,7 @@ const CONTRACT_GLOBAL_TYPES = new Set(["Array", "ReadonlyArray", "Uint8Array"]);
type TypeBindings = Map>;
function locOf(file: ts.SourceFile, node: ts.Node): SrcLoc {
- return { file: file.fileName, start: node.getStart(), end: node.end };
+ return { file: file.fileName, start: ts.tokenStartOfNode(node), end: node.end };
}
function isExported(stmt: ts.Node): boolean {
@@ -248,7 +248,7 @@ function typeShape(
const lit = node.literal;
if (ts.isStringLiteral(lit)) return { k: "stringLit", text: lit.text };
if (lit.kind === ts.SyntaxKind.NullKeyword) return { k: "absent", unit: "null" };
- return { k: "unsupported", text: node.getText(file) };
+ return { k: "unsupported", text: ts.getTextOfNode(node) };
}
if (ts.isArrayTypeNode(node))
return { k: "array", elem: typeShape(file, node.elementType, bindings) };
@@ -280,15 +280,19 @@ function typeShape(
if (ts.isConditionalTypeNode(node)) {
return {
k: "unsupported",
- text: `a conditional type (${node.getText(file)})`,
+ text: `a conditional type (${ts.getTextOfNode(node)})`,
computed: "conditional",
};
}
if (ts.isMappedTypeNode(node)) {
- return { k: "unsupported", text: `a mapped type (${node.getText(file)})`, computed: "mapped" };
+ return {
+ k: "unsupported",
+ text: `a mapped type (${ts.getTextOfNode(node)})`,
+ computed: "mapped",
+ };
}
if (ts.isTypeReferenceNode(node)) {
- if (!ts.isIdentifier(node.typeName)) return { k: "unsupported", text: node.getText(file) };
+ if (!ts.isIdentifier(node.typeName)) return { k: "unsupported", text: ts.getTextOfNode(node) };
const name = node.typeName.text;
const global =
CONTRACT_GLOBAL_TYPES.has(name) && isUnshadowedGlobalType(file, node, name, bindings);
@@ -302,10 +306,10 @@ function typeShape(
return { k: "array", elem: typeShape(file, node.typeArguments.nodes[0]!, bindings) };
}
if ((node.typeArguments?.nodes.length ?? 0) > 0)
- return { k: "unsupported", text: node.getText(file) };
+ return { k: "unsupported", text: ts.getTextOfNode(node) };
return { k: "ref", name };
}
- return { k: "unsupported", text: node.getText(file) };
+ return { k: "unsupported", text: ts.getTextOfNode(node) };
}
/** Unwrap `expr as const` / parenthesized initializers. */
@@ -362,7 +366,7 @@ function envMsgArray(expr: ts.Expression): { env: string; msg: string }[] | null
/** One mergeable declaration site, `file:line` (1-based line). */
function siteOf(file: ts.SourceFile, node: ts.Node): string {
- return `${file.fileName}:${ts.getLineAndCharacterOfPosition(file, node.getStart()).line + 1}`;
+ return `${file.fileName}:${ts.getLineAndCharacterOfPosition(file, ts.tokenStartOfNode(node)).line + 1}`;
}
/** Everything the sidecar projection needs from the program's syntax
diff --git a/packages/compiler/src/frontend/lib-exports.ts b/packages/compiler/src/frontend/lib-exports.ts
index 738dd2f19..6a42074bd 100644
--- a/packages/compiler/src/frontend/lib-exports.ts
+++ b/packages/compiler/src/frontend/lib-exports.ts
@@ -35,7 +35,7 @@ export function entryFunctionExports(entry: ts.SourceFile): Map 0,
async: mods?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true,
generator: stmt.asteriskToken !== undefined,
diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts
index 8f2b28a3f..0ca8503df 100644
--- a/packages/compiler/src/frontend/lowering/array-values.ts
+++ b/packages/compiler/src/frontend/lowering/array-values.ts
@@ -8,6 +8,7 @@ import {
type IrType,
isIntegerBytesElem,
type SrcLoc,
+ STRING,
UNDEFINED_T,
typeEquals,
typeKey,
@@ -84,29 +85,54 @@ export function arrayValueRead(
};
}
-/** Read a stabilized union of native arrays without copying either the
- * receiver or its elements. Every arm contributes its missing-value case. */
+/** Read one position of a stabilized union of native arrays and tuples
+ * (`const [a, b] = re.exec(s) || ["", ""]`) without copying the receiver
+ * or its elements. Every array arm contributes its missing-value case; a
+ * tuple arm answers its field, or undefined past its end. Tuple arms need
+ * a constant position. With `stringChars` (the string iterator's code
+ * points as a fresh string[]), a string arm (`re.exec(s) || ""`) answers
+ * its code point at the position, or undefined past its end. */
export function unionArrayValueRead(
lowerer: Lowerer,
value: IrExpr,
index: IrExpr,
loc: SrcLoc,
+ stringChars?: (text: IrExpr) => IrExpr,
): IrExpr | null {
if (value.type.kind !== "union") return null;
const unionId = value.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
if (!arms?.length) return null;
+ const position = index.kind === "numLit" ? String(index.value) : null;
+ // A tuple arm's answer at the position: its field, or undefined past
+ // its end. Null when the arm is neither an array nor a tuple.
+ const tupleField = (arm: IrType): { name: string; type: IrType } | "past" | null => {
+ if (arm.kind !== "record" || position === null) return null;
+ const shape = lowerer.shapes.get(arm.shapeId);
+ if (!shape?.tuple) return null;
+ return shape.fields.find((f) => f.name === position) ?? "past";
+ };
// Keep the collection's IrType union layout and narrow each element at
// its use; an inferred array predicate changes the collection's ABI.
- for (const arm of arms) if (arm.kind !== "array") return null;
+ const stringArm = (arm: IrType): boolean => arm.kind === "string" && stringChars !== undefined;
+ for (const arm of arms)
+ if (arm.kind !== "array" && !stringArm(arm) && tupleField(arm) === null) return null;
const answers = new Map();
for (const arm of arms) {
- if (arm.kind !== "array") return null;
- const answer = arrayValueType(lowerer, arm.elem);
+ const field = arm.kind === "array" || stringArm(arm) ? null : tupleField(arm);
+ const answer =
+ arm.kind === "array"
+ ? arrayValueType(lowerer, arm.elem)
+ : stringArm(arm)
+ ? arrayValueType(lowerer, STRING)
+ : field === "past"
+ ? UNDEFINED_T
+ : field!.type;
const parts = answer.kind === "union" ? lowerer.unions.get(answer.unionId)!.arms : [answer];
for (const part of parts) answers.set(typeKey(part), part);
}
const joined = [...answers.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
+ if (joined.length === 1 && joined[0]!.kind === "undefinedT") return null;
if (
joined.length > 1 &&
(!unionContainerArmsOk(joined) ||
@@ -118,12 +144,39 @@ export function unionArrayValueRead(
let result: IrExpr | null = null;
for (let tag = arms.length - 1; tag >= 0; tag--) {
const arm = arms[tag]!;
- if (arm.kind !== "array") return null;
const receiver: IrExpr = { kind: "unionNarrow", unionId, tag, value, type: arm, loc };
- const read = lowerer.coerceToExpected(
- arrayValueRead(lowerer, receiver, index, arm.elem, loc),
- type,
- );
+ let read: IrExpr;
+ if (arm.kind === "array") {
+ read = lowerer.coerceToExpected(
+ arrayValueRead(lowerer, receiver, index, arm.elem, loc),
+ type,
+ );
+ } else if (stringArm(arm)) {
+ read = lowerer.coerceToExpected(
+ arrayValueRead(lowerer, stringChars!(receiver), index, STRING, loc),
+ type,
+ );
+ } else {
+ const field = tupleField(arm);
+ if (field === null) return null;
+ if (field === "past") {
+ const missing = lowerer.wrappedUndefined(type, loc);
+ if (!missing) return null;
+ read = missing;
+ } else {
+ read = lowerer.coerceToExpected(
+ {
+ kind: "recordGet",
+ obj: receiver,
+ shapeId: (arm as IrType & { kind: "record" }).shapeId,
+ field: field.name,
+ type: field.type,
+ loc,
+ },
+ type,
+ );
+ }
+ }
result =
result === null
? read
diff --git a/packages/compiler/src/frontend/lowering/assert-instances.ts b/packages/compiler/src/frontend/lowering/assert-instances.ts
index ee8a34ea2..1166c6afa 100644
--- a/packages/compiler/src/frontend/lowering/assert-instances.ts
+++ b/packages/compiler/src/frontend/lowering/assert-instances.ts
@@ -194,7 +194,7 @@ function onlyMethodCallReferences(lowerer: Lowerer, name: ts.Identifier): boolea
}
ts.forEachChild(node, visit);
};
- visit(name.getSourceFile());
+ visit(ts.getSourceFileOfNode(name));
return ok;
}
diff --git a/packages/compiler/src/frontend/lowering/assertion-members.ts b/packages/compiler/src/frontend/lowering/assertion-members.ts
index d61c379d6..484963fa8 100644
--- a/packages/compiler/src/frontend/lowering/assertion-members.ts
+++ b/packages/compiler/src/frontend/lowering/assertion-members.ts
@@ -79,7 +79,7 @@ export function extendAssertedRecordMembers(
symbol.declarations.length > 0 &&
symbol.declarations.every(
(d) =>
- !lowerer.isStdlibFile(d.getSourceFile()) &&
+ !lowerer.isStdlibFile(ts.getSourceFileOfNode(d)) &&
(ts.isTypeLiteralNode(d) ||
ts.isInterfaceDeclaration(d) ||
ts.isTypeAliasDeclaration(d)),
diff --git a/packages/compiler/src/frontend/lowering/binding-analysis.ts b/packages/compiler/src/frontend/lowering/binding-analysis.ts
index 88b0f91dc..83b9aa6f9 100644
--- a/packages/compiler/src/frontend/lowering/binding-analysis.ts
+++ b/packages/compiler/src/frontend/lowering/binding-analysis.ts
@@ -16,7 +16,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
let written = false;
// Text pre-check keeps the file walk cheap: only same-named
// identifiers pay a symbol resolution.
- const symText = sym.name;
+ const symText = ts.symbolName(sym);
const namesSym = (e: ts.Node): boolean =>
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
const scanTarget = (t: ts.Expression): void => {
@@ -31,7 +31,7 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) {
const walk = (n: ts.Node): void => {
if (namesSym(n)) written = true;
- else n.forEachChild(walk);
+ else ts.forEachChild(n, walk);
};
walk(e);
}
@@ -54,9 +54,9 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
) {
scanTarget(n.initializer as ts.Expression);
}
- n.forEachChild(visit);
+ ts.forEachChild(n, visit);
};
- decl.getSourceFile().forEachChild(visit);
+ ts.forEachChild(ts.getSourceFileOfNode(decl), visit);
lowerer.neverReassignedCache.set(sym, !written);
return !written;
}
@@ -123,7 +123,7 @@ export function nullishValueUnitOf(
!decl ||
!ts.isVariableDeclaration(decl) ||
!ts.isIdentifier(decl.name) ||
- decl.getSourceFile().isDeclarationFile ||
+ ts.getSourceFileOfNode(decl).isDeclarationFile ||
decl.initializer === undefined ||
!ts.isVariableStatement(decl.parent?.parent)
) {
@@ -154,7 +154,7 @@ export function nullishValueUnitOf(
/** True when any identifier resolving to `sym` sits inside a class
* heritage clause anywhere in the program. */
function usedInHeritageClause(lowerer: Lowerer, sym: ts.Symbol): boolean {
- const symText = sym.name;
+ const symText = ts.symbolName(sym);
let found = false;
const visit = (n: ts.Node): void => {
if (found) return;
@@ -171,12 +171,12 @@ function usedInHeritageClause(lowerer: Lowerer, sym: ts.Symbol): boolean {
}
return;
}
- n.forEachChild(visit);
+ ts.forEachChild(n, visit);
};
for (const file of lowerer.program.getImplementationSourceFiles()) {
if (found) break;
if (file.isDeclarationFile) continue;
- file.forEachChild(visit);
+ ts.forEachChild(file, visit);
}
return found;
}
@@ -236,7 +236,7 @@ export function nullishGenericBindingUnitOf(
* value knowable. Compound assignments, ++/--, for-in/of cursors, and
* destructuring targets all disqualify. */
function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): boolean {
- const symText = sym.name;
+ const symText = ts.symbolName(sym);
const namesSym = (e: ts.Node): boolean =>
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
let ok = true;
@@ -253,7 +253,7 @@ function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): bool
} else if (ts.isArrayLiteralExpression(lhs) || ts.isObjectLiteralExpression(lhs)) {
const walk = (m: ts.Node): void => {
if (namesSym(m)) ok = false;
- else m.forEachChild(walk);
+ else ts.forEachChild(m, walk);
};
walk(lhs);
}
@@ -274,9 +274,9 @@ function allWritesNullish(lowerer: Lowerer, sym: ts.Symbol, decl: ts.Node): bool
t = (t as ts.ParenthesizedExpression).expression;
if (namesSym(t)) ok = false;
}
- n.forEachChild(visit);
+ ts.forEachChild(n, visit);
};
- decl.getSourceFile().forEachChild(visit);
+ ts.forEachChild(ts.getSourceFileOfNode(decl), visit);
return ok;
}
@@ -332,7 +332,7 @@ export function deadUnmappableBinding(
// the no-storage family never claims it (catch bindings sit outside a
// variable statement too and stay out the same way).
if (!ts.isVariableStatement(decl.parent?.parent)) return false;
- const sf = decl.getSourceFile();
+ const sf = ts.getSourceFileOfNode(decl);
if (sf.isDeclarationFile || isJsSourceFile(sf)) return false;
if (decl.initializer !== undefined && !sideEffectFreeValueExpr(lowerer, decl.initializer))
return false;
@@ -344,7 +344,7 @@ export function deadUnmappableBinding(
// bug), so only survivors of the syntactic filters pay it. Mappable
// types keep their real storage.
if (lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(sym)) !== null) return false;
- const symText = sym.name;
+ const symText = ts.symbolName(sym);
const namesSym = (e: ts.Node): boolean =>
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
let dead = true;
@@ -353,7 +353,7 @@ export function deadUnmappableBinding(
if (ts.isIdentifier(n) && n.text === symText) {
// Declaration-name occurrences are not reads.
if (n.parent !== undefined && ts.isVariableDeclaration(n.parent) && n.parent.name === n) {
- n.forEachChild(visit);
+ ts.forEachChild(n, visit);
return;
}
// A plain-assignment LHS is a WRITE — dead only when the RHS
@@ -375,12 +375,12 @@ export function deadUnmappableBinding(
if (namesSym(n)) dead = false;
return;
}
- n.forEachChild(visit);
+ ts.forEachChild(n, visit);
};
for (const file of lowerer.program.getImplementationSourceFiles()) {
if (!dead) break;
if (file.isDeclarationFile) continue;
- file.forEachChild(visit);
+ ts.forEachChild(file, visit);
}
if (dead) lowerer.deadBindings.add(sym);
return dead;
diff --git a/packages/compiler/src/frontend/lowering/builtin-calls.ts b/packages/compiler/src/frontend/lowering/builtin-calls.ts
index 6be798de8..d2e4b53d5 100644
--- a/packages/compiler/src/frontend/lowering/builtin-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts
@@ -1,4 +1,5 @@
import { lowerReadlineCursorCall } from "./builtins/readline.js";
+import { lowerMonitorEventLoopDelayCall } from "./builtins/performance.js";
import {
lowerEnvironmentData,
lowerReceiveMessageOnPort,
@@ -15,6 +16,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 +57,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(
@@ -70,6 +105,9 @@ export function lowerBuiltinCall(
return lowerAddAbortListener(lowerer, expr, loc);
}
if (bi.module === "events" && bi.member === "once") return lowerEventsOnce(lowerer, expr, loc);
+ if (bi.module === "perf_hooks" && bi.member === "monitorEventLoopDelay") {
+ return lowerMonitorEventLoopDelayCall(lowerer, expr);
+ }
if (bi.module === "events") {
const helper = lowerEventsEmitterHelper(lowerer, expr, bi.member, loc);
if (helper) return helper;
@@ -126,6 +164,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(
diff --git a/packages/compiler/src/frontend/lowering/builtin-function-fences.ts b/packages/compiler/src/frontend/lowering/builtin-function-fences.ts
index e6221384b..312596c1f 100644
--- a/packages/compiler/src/frontend/lowering/builtin-function-fences.ts
+++ b/packages/compiler/src/frontend/lowering/builtin-function-fences.ts
@@ -25,7 +25,7 @@ export function builtinFunctionFenceValue(
`library functions as values ('${display}' has no function-value lowering; call it directly)`,
);
lowerer.runtimeFences.push(diag);
- const line = ts.getLineAndCharacterOfPosition(node.getSourceFile(), loc.start).line + 1;
+ const line = ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(node), loc.start).line + 1;
lowerer.liftedFns.push({
name,
params: [],
diff --git a/packages/compiler/src/frontend/lowering/builtins/async-context.ts b/packages/compiler/src/frontend/lowering/builtins/async-context.ts
index e6de99b09..18bd93a86 100644
--- a/packages/compiler/src/frontend/lowering/builtins/async-context.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/async-context.ts
@@ -35,7 +35,7 @@ export function lowerDiagnosticsSubscriber(lowerer: Lowerer, node: ts.Expression
if (ts.isIdentifier(node) && node.text === "setImmediate") {
const sym = lowerer.checker.getSymbolAtLocation(node);
const decls = sym ? sym.declarations : [];
- if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) {
+ if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))) {
return {
kind: "libCall",
fn: "timers.setImmediateFnValue",
diff --git a/packages/compiler/src/frontend/lowering/builtins/blob.ts b/packages/compiler/src/frontend/lowering/builtins/blob.ts
index 3fc4ec173..94c8c7ffe 100644
--- a/packages/compiler/src/frontend/lowering/builtins/blob.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/blob.ts
@@ -52,10 +52,10 @@ export function blobClassOf(lowerer: Lowerer, node: ts.Expression): BlobClass |
function blobReceiverClass(lowerer: Lowerer, node: ts.Expression): BlobClass | null {
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
const sym = type.getAliasSymbol() ?? type.getSymbol();
- if (!sym || (sym.name !== "Blob" && sym.name !== "File") || !lowerer.isStdlibSymbol(sym)) {
- return null;
- }
- return sym.name;
+ if (!sym) return null;
+ const name = ts.symbolName(sym);
+ if ((name !== "Blob" && name !== "File") || !lowerer.isStdlibSymbol(sym)) return null;
+ return name;
}
function memberNameOf(access: ts.PropertyAccessExpression | ts.ElementAccessExpression) {
diff --git a/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts
new file mode 100644
index 000000000..eaa08c9dd
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts
@@ -0,0 +1,159 @@
+import { numLit } from "../../../ir/build.js";
+import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
+import * as ts from "../../ts7/adapter.js";
+import { locOf } from "../../program.js";
+import type { Lowerer } from "../lowerer.js";
+
+/* EventEmitter and the stream classes as untyped constructor values, the
+ * bases of JavaScript constructor functions:
+ *
+ * function Bus() { EventEmitter.call(this); }
+ * util.inherits(Bus, EventEmitter);
+ *
+ * A compiled `class Bus extends EventEmitter` gets a native emitter prefix,
+ * but `new Bus()` on a constructor function builds an untyped object, so
+ * the base is the runtime's dyn constructor (scr_dyn_builtin_class.c): a
+ * function whose prototype carries Node's EventEmitter methods over the
+ * object's own `_events` table. The stream classes have prototype chains
+ * for util.inherits and instanceof; constructing over them refuses where
+ * it runs. These values appear only where a constructor function uses its
+ * base: util.inherits, `Base.call(this, …)` and `Base.apply(this, …)`,
+ * `Base.prototype` and `instanceof Base` on an untyped value. */
+
+/** scr_dyn_builtin_class's kind for a runtime class, or null. */
+export function builtinClassKind(className: string): number | null {
+ switch (className) {
+ case RUNTIME_EMITTER_CLASS:
+ return 0;
+ case "%Readable":
+ return 2;
+ case "%Writable":
+ return 3;
+ case "%Duplex":
+ return 4;
+ case "%Transform":
+ return 5;
+ case "%PassThrough":
+ return 6;
+ default:
+ return null;
+ }
+}
+
+/** The classes by builtin module member. JavaScript sources bind
+ * `require("events").EventEmitter` as a module member rather than a class
+ * value, and `require("stream")` is the legacy Stream constructor. */
+const MODULE_CLASS_KINDS: Readonly>>> = {
+ events: { EventEmitter: 0, default: 0 },
+ stream: { Stream: 1, Readable: 2, Writable: 3, Duplex: 4, Transform: 5, PassThrough: 6 },
+};
+
+/** The kind of a builtin module member that is one of those classes. */
+export function builtinMemberClassKind(module: string, member: string): number | null {
+ return MODULE_CLASS_KINDS[module]?.[member] ?? null;
+}
+
+function unparenthesized(node: ts.Expression): ts.Expression {
+ let current = node;
+ while (ts.isParenthesizedExpression(current)) current = current.expression;
+ return current;
+}
+
+/** The kind of a node that names one of those classes: a class value, a
+ * builtin module member, or the events and stream modules themselves. */
+export function builtinClassKindOf(lowerer: Lowerer, node: ts.Expression): number | null {
+ const expr = unparenthesized(node);
+ const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
+ if (mapped?.kind === "classval") return builtinClassKind(mapped.className);
+ const member = ts.isIdentifier(expr)
+ ? lowerer.builtinImportOf(expr)
+ : ts.isPropertyAccessExpression(expr)
+ ? lowerer.builtinMemberOf(expr)
+ : null;
+ if (member) return MODULE_CLASS_KINDS[member.module]?.[member.member] ?? null;
+ const module = lowerer.builtinNamespaceModuleOf(expr);
+ return module === "events" ? 0 : module === "stream" ? 1 : null;
+}
+
+/** Whether evaluating the node can be skipped: a binding or a member of
+ * one, or of a builtin require. */
+function referenceOnly(node: ts.Expression): boolean {
+ const expr = unparenthesized(node);
+ if (ts.isIdentifier(expr)) return true;
+ if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
+ const base = unparenthesized(expr.expression);
+ return (
+ ts.isIdentifier(base) ||
+ (ts.isCallExpression(base) &&
+ ts.isIdentifier(base.expression) &&
+ base.expression.text === "require" &&
+ base.arguments.nodes.length === 1 &&
+ ts.isStringLiteral(base.arguments.nodes[0]!))
+ );
+ }
+ return false;
+}
+
+/** The untyped constructor named by a node, or null when it names none of
+ * those classes. */
+export function builtinClassDynValueOf(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
+ const kind = builtinClassKindOf(lowerer, node);
+ if (kind === null) return null;
+ const loc = locOf(node);
+ const value = builtinClassValue(kind, loc);
+ if (referenceOnly(node)) return value;
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(node), loc }],
+ result: value,
+ type: DYN,
+ loc,
+ };
+}
+
+/** EventEmitter and the legacy Stream are dyn.builtinClass; the stream
+ * classes, which carry native stream state for constructor functions, are
+ * dyn.builtinStreamClass, so only programs that use them link the stream
+ * half. */
+export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr {
+ return {
+ kind: "libCall",
+ fn: kind <= 1 ? "dyn.builtinClass" : "dyn.builtinStreamClass",
+ args: [numLit(kind, loc)],
+ type: DYN,
+ loc,
+ };
+}
+
+/** `Base.call(this, …)` / `Base.apply(this, args)` on one of those class
+ * values: Function.prototype.call/apply over the untyped constructor. */
+export function lowerBuiltinClassCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
+ const callee = call.expression;
+ if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken) return null;
+ const method = callee.name.text;
+ if (method !== "call" && method !== "apply") return null;
+ if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
+ const receiver = builtinClassDynValueOf(lowerer, callee.expression);
+ if (receiver === null) return null;
+ const loc = locOf(call);
+ return {
+ kind: "dynInvoke",
+ recv: receiver,
+ method,
+ calleeName: ts.getTextOfNode(callee),
+ args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
+ type: DYN,
+ loc,
+ };
+}
+
+/** `value instanceof Base` for an untyped value. */
+export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc): IrExpr {
+ return {
+ kind: "libCall",
+ fn: "dyn.builtinInstanceOf",
+ args: [value, numLit(kind, loc)],
+ type: BOOL,
+ loc,
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts
index afb221d04..661fec528 100644
--- a/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/callable-aliases.ts
@@ -50,7 +50,7 @@ export function registerBuiltinCallableAlias(
// These checked native functions retain a runtime slot in JavaScript so
// aliases can escape without narrowing their arguments to string.
if (
- isJsSourceFile(nameNode.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(nameNode)) &&
target.module === "util" &&
["stripVTControlCharacters", "toUSVString", "isDeepStrictEqual", "styleText"].includes(
target.member,
diff --git a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts
index a435bf740..af7270ea9 100644
--- a/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts
@@ -1,5 +1,6 @@
import { boolLit, strLit } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
+import { requestGeneralStreamSurface } from "../general-streams.js";
import { type Lowerer } from "../lowerer.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { isChildSurfaceMember } from "../surfaces.js";
@@ -121,22 +122,32 @@ export function lowerChildMethodCall(
}
const listenerNode = stripTypeCasts(call.arguments.nodes[1]!);
if (
- isJsSourceFile(listenerNode.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) &&
(ts.isArrowFunction(listenerNode) || ts.isFunctionExpression(listenerNode)) &&
listenerNode.parameters.nodes.some((param) => param.dotDotDotToken) &&
listenerNode.parameters.nodes.every((param) => !param.type) &&
!/@(?:param|type)\b/.test(
- listenerNode.getSourceFile().text.slice(listenerNode.pos, listenerNode.getStart()),
+ ts
+ .getSourceFileOfNode(listenerNode)
+ .text.slice(listenerNode.pos, ts.tokenStartOfNode(listenerNode)),
)
) {
for (const param of listenerNode.parameters.nodes) lowerer.checkedCallbackParams.add(param);
}
- const cb = lowerer.lowerExpr(call.arguments.nodes[1]!);
+ const narrowed =
+ event === "exit" || event === "close"
+ ? narrowedExitListenerParams(lowerer, listenerNode)
+ : [];
+ const cb =
+ narrowed.length > 0
+ ? lowerWithCheckedParams(lowerer, listenerNode, narrowed)
+ : lowerer.lowerExpr(call.arguments.nodes[1]!);
if (
- (isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") ||
+ narrowed.length > 0 ||
+ (isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) && cb.type.kind === "dyn") ||
(cb.type.kind === "func" &&
(cb.type.rest ||
- (isJsSourceFile(listenerNode.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(listenerNode)) &&
cb.type.params.some((param) => param.kind === "dyn"))))
) {
if (
@@ -301,10 +312,12 @@ export function lowerChildMethodCall(
/** Method calls on piped child-output stream receivers (child.stdout /
* child.stderr — the childStream kind): on/once("data" | "end").
- * 'data' listeners take zero parameters, `(chunk: Buffer)`, or a
- * `Buffer | string`-union chunk (the ngrok appendOutput shape — the
- * runtime only ever fires Buffers; the compiler-emitted adapter wraps
- * the chunk at the union's Buffer arm); 'end' listeners take none.
+ * 'data' listeners take zero parameters, `(chunk: Buffer)`, `(chunk:
+ * string)`, an unknown/any chunk (boxed as a checked-dynamic value; see
+ * childStreamEncodedAt), or a `Buffer | string`-union chunk (the ngrok
+ * appendOutput shape — the runtime only ever fires Buffers; the
+ * compiler-emitted adapter wraps the chunk at the union's Buffer arm);
+ * 'end' listeners take none.
* Statement position only (Node returns the stream for chaining; here
* the result is void). Chained receivers (`child.stdout?.on(...)`)
* ride the optional-chain re-dispatch (chainBlocked). Everything else
@@ -335,6 +348,7 @@ export function lowerChildStreamMethodCall(
const evT = lowerer.typeOf(call.arguments.nodes[0]!);
const event = evT.isStringLiteralType() ? evT.value : null;
if (event !== "data" && event !== "end") {
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`stream.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments.nodes[0]!,
@@ -342,6 +356,7 @@ export function lowerChildStreamMethodCall(
);
}
if (!ts.isExpressionStatement(call.parent)) {
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.unsupported(
"SC1090",
call,
@@ -376,23 +391,32 @@ export function lowerChildStreamMethodCall(
if (
param !== undefined &&
param.kind !== "string" &&
+ param.kind !== "dyn" &&
!(param.kind === "bytes" && param.elem === "u8") &&
!unionOk(param)
) {
lowerer.unsupported(
"SC1090",
call.arguments.nodes[1]!,
- `data listeners whose parameter is not 'Buffer', 'string', or a Buffer-armed union (got '${lowerer.fmt(param)}')`,
+ `data listeners whose parameter is not 'Buffer', 'string', 'unknown', or a Buffer-armed union (got '${lowerer.fmt(param)}')`,
);
}
+ // An unknown/any chunk is a Buffer, or a string once setEncoding ran.
+ // The registration takes whichever the enclosing code establishes; a
+ // later setEncoding that contradicts it throws the runtime's
+ // listener-type TypeError instead of delivering the other kind.
+ const strings =
+ param?.kind === "string" ||
+ (param?.kind === "dyn" && childStreamEncodedAt(lowerer, access.expression, call));
return {
kind: "libCall",
- fn: param?.kind === "string" ? "stream.onDataStr" : "stream.onData",
+ fn: strings ? "stream.onDataStr" : "stream.onData",
args: [receiver, cb, once],
type: VOID,
loc,
};
}
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`ReadableStream.${name}`,
call,
@@ -418,6 +442,7 @@ export function lowerChildWriterMethodCall(
if (name === "write") {
if (call.arguments.nodes.length !== 1 || call.arguments.nodes.some(ts.isSpreadElement)) {
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`child stdin write with ${call.arguments.nodes.length} arguments`,
call,
@@ -461,6 +486,7 @@ export function lowerChildWriterMethodCall(
const eventType = lowerer.typeOf(call.arguments.nodes[0]!);
const event = eventType.isStringLiteralType() ? eventType.value : null;
if (event !== "drain" && event !== "finish" && event !== "error") {
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`child stdin ${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
call.arguments.nodes[0]!,
@@ -507,6 +533,7 @@ export function lowerChildWriterMethodCall(
return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc };
}
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`Writable.${name}`,
call,
@@ -514,3 +541,255 @@ export function lowerChildWriterMethodCall(
lowerer.checker.getSymbolAtLocation(access.name),
);
}
+
+/** A receiver spelled as a plain access path (`child.stdout`, `out`): its
+ * root symbol and member names, or null for other forms. A `const` alias
+ * of an access path (`const out = child.stdout`) resolves to that path,
+ * and so does a method call declared to return `this`
+ * (`child.stdout.setEncoding("utf8")`), so every spelling of one stream
+ * gets the same key. */
+function accessPathOf(
+ lowerer: Lowerer,
+ expr: ts.Expression,
+ depth = 0,
+): { root: ts.Symbol; path: string } | null {
+ if (depth > 16) return null;
+ if (ts.isParenthesizedExpression(expr) || ts.isNonNullExpression(expr)) {
+ return accessPathOf(lowerer, expr.expression, depth + 1);
+ }
+ if (ts.isIdentifier(expr)) {
+ const root = lowerer.checker.getSymbolAtLocation(expr);
+ if (!root) return null;
+ const init = constInitializerOf(root);
+ const aliased = init === null ? null : accessPathOf(lowerer, init, depth + 1);
+ return aliased ?? { root, path: "" };
+ }
+ if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
+ const base = accessPathOf(lowerer, expr.expression, depth + 1);
+ return base === null ? null : { root: base.root, path: `${base.path}.${expr.name.text}` };
+ }
+ if (returnsReceiver(lowerer, expr)) {
+ return accessPathOf(
+ lowerer,
+ (expr.expression as ts.PropertyAccessExpression).expression,
+ depth + 1,
+ );
+ }
+ return null;
+}
+
+/** A method call (`recv.m(...)`, not optional) whose declaration returns
+ * `this`, so its value is the receiver itself. */
+function returnsReceiver(lowerer: Lowerer, expr: ts.Expression): expr is ts.CallExpression {
+ if (
+ !ts.isCallExpression(expr) ||
+ expr.questionDotToken ||
+ !ts.isPropertyAccessExpression(expr.expression) ||
+ expr.expression.questionDotToken
+ )
+ return false;
+ const declaration = lowerer.checker.getResolvedSignature(expr)?.declaration;
+ return declaration?.type?.kind === ts.SyntaxKind.ThisType;
+}
+
+/** A call of a child_process factory (spawn, fork, exec, execFile), each
+ * of which returns a new ChildProcess. */
+function createsChildProcess(lowerer: Lowerer, call: ts.CallExpression): boolean {
+ const declaration = lowerer.checker.getResolvedSignature(call)?.declaration;
+ if (
+ declaration === undefined ||
+ !ts.isFunctionDeclaration(declaration) ||
+ declaration.name === undefined ||
+ !["spawn", "fork", "exec", "execFile"].includes(declaration.name.text)
+ )
+ return false;
+ for (let node: ts.Node | undefined = declaration.parent; node; node = node.parent) {
+ if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
+ return node.name.text === "child_process" || node.name.text === "node:child_process";
+ }
+ }
+ return false;
+}
+
+/** The statement that always evaluates `call` when it runs: the call is
+ * the statement's expression, a variable's initializer, or the receiver of
+ * a method chain that is one of those. Null for calls under a condition,
+ * an operator, an argument list or any other construct. */
+function unconditionalStatementOf(call: ts.CallExpression): ts.Statement | null {
+ let node: ts.Node = call;
+ for (;;) {
+ const parent: ts.Node | undefined = node.parent;
+ if (parent === undefined) return null;
+ if (
+ ts.isParenthesizedExpression(parent) ||
+ ts.isNonNullExpression(parent) ||
+ (ts.isPropertyAccessExpression(parent) &&
+ parent.expression === node &&
+ !parent.questionDotToken) ||
+ (ts.isCallExpression(parent) && parent.expression === node && !parent.questionDotToken)
+ ) {
+ node = parent;
+ continue;
+ }
+ if (ts.isExpressionStatement(parent)) return parent;
+ if (ts.isVariableDeclaration(parent) && parent.initializer === node) {
+ const list = parent.parent;
+ const statement = list?.parent;
+ return list !== undefined &&
+ ts.isVariableDeclarationList(list) &&
+ statement !== undefined &&
+ ts.isVariableStatement(statement)
+ ? statement
+ : null;
+ }
+ return null;
+ }
+}
+
+/** The initializer of a symbol declared once as a `const` variable, else null. */
+function constInitializerOf(symbol: ts.Symbol): ts.Expression | null {
+ const decls = symbol.declarations;
+ if (decls.length !== 1) return null;
+ const decl = decls[0]!;
+ if (
+ !ts.isVariableDeclaration(decl) ||
+ !ts.isIdentifier(decl.name) ||
+ decl.initializer === undefined ||
+ (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0
+ )
+ return null;
+ return decl.initializer;
+}
+
+/** Whether two resolved access paths name different objects: the same root
+ * with different members, or two `const` roots each initialized by `new`
+ * or a child_process factory (such as two `spawn()` results). Any other
+ * call may return an object another binding already holds. */
+function distinctPaths(
+ lowerer: Lowerer,
+ a: { root: ts.Symbol; path: string },
+ b: { root: ts.Symbol; path: string },
+): boolean {
+ if (a.root === b.root) return a.path !== b.path;
+ const fresh = (root: ts.Symbol): boolean => {
+ let init = constInitializerOf(root);
+ while (init !== null && ts.isParenthesizedExpression(init)) init = init.expression;
+ if (init === null) return false;
+ return (
+ ts.isNewExpression(init) || (ts.isCallExpression(init) && createsChildProcess(lowerer, init))
+ );
+ };
+ return fresh(a.root) && fresh(b.root);
+}
+
+/** Whether an unknown-typed 'data' listener registered by `call` receives
+ * strings: a `setEncoding` statement on the same receiver path precedes
+ * the registration in its own block or an enclosing block of the same
+ * function. With no setEncoding on that path in the function, chunks are
+ * Buffers. Any other placement leaves the encoding state unclear, which
+ * is reported. */
+function childStreamEncodedAt(
+ lowerer: Lowerer,
+ receiver: ts.Expression,
+ call: ts.CallExpression,
+): boolean {
+ const key = accessPathOf(lowerer, receiver);
+ const statement: ts.Node = call.parent!;
+ let scope: ts.Node | undefined = statement;
+ while (scope && !ts.isFunctionLike(scope) && !ts.isSourceFile(scope)) scope = scope.parent;
+ const unclear = (): never =>
+ lowerer.unsupported(
+ "SC1090",
+ call.arguments.nodes[1]!,
+ "untyped child-stream 'data' listeners whose setEncoding state is unclear at their registration sites (declare the chunk as 'Buffer' or 'string')",
+ );
+ if (key === null || scope === undefined) return unclear();
+ const preceding = new Set();
+ for (let node: ts.Node = statement; node !== scope;) {
+ const parent: ts.Node | undefined = node.parent;
+ if (parent === undefined) break;
+ if (ts.isBlock(parent) || ts.isSourceFile(parent)) {
+ for (const sibling of parent.statements.nodes) {
+ if (sibling === node) break;
+ preceding.add(sibling);
+ }
+ }
+ node = parent;
+ }
+ const receiverType = lowerer.checker.getNonNullableType(lowerer.typeOf(receiver));
+ let before = false;
+ let elsewhere = false;
+ const visit = (node: ts.Node): void => {
+ if (node !== scope && ts.isFunctionLike(node)) return;
+ if (
+ ts.isCallExpression(node) &&
+ ts.isPropertyAccessExpression(node.expression) &&
+ node.expression.name.text === "setEncoding"
+ ) {
+ const target = node.expression.expression;
+ const path = accessPathOf(lowerer, target);
+ if (path !== null && path.root === key.root && path.path === key.path) {
+ const statement = unconditionalStatementOf(node);
+ if (statement !== null && preceding.has(statement)) before = true;
+ else elsewhere = true;
+ } else if (
+ (path === null || !distinctPaths(lowerer, path, key)) &&
+ lowerer.checker.isTypeAssignableTo(receiverType, lowerer.typeOf(target))
+ ) {
+ // Another spelling that can hold this stream (a parameter, a `let`
+ // copy, a call result): its setEncoding may be this stream's.
+ elsewhere = true;
+ }
+ }
+ ts.forEachChild(node, visit);
+ };
+ visit(scope);
+ if (elsewhere) return unclear();
+ return before;
+}
+
+/** An inline 'exit'/'close' listener may declare `(code: number, signal:
+ * string)`: TypeScript accepts it through ChildProcess's catch-all `on`
+ * overload, while Node passes `number | null` and `Signals | null`. Those
+ * parameters bind as checked-dynamic values (the socket 'data' listener
+ * precedent), so the body observes the null Node passes; a typed use of a
+ * null value throws the checked conversion's TypeError. Returns the
+ * narrowed parameter symbols, empty when the declared types already
+ * admit null. */
+function narrowedExitListenerParams(lowerer: Lowerer, listener: ts.Expression): ts.Symbol[] {
+ if (!ts.isArrowFunction(listener) && !ts.isFunctionExpression(listener)) return [];
+ if (isJsSourceFile(ts.getSourceFileOfNode(listener))) return [];
+ const symbols: ts.Symbol[] = [];
+ const wanted: readonly string[] = ["f64", "string"];
+ listener.parameters.nodes.slice(0, 2).forEach((param, i) => {
+ if (!ts.isIdentifier(param.name) || param.dotDotDotToken || param.initializer) return;
+ if (param.questionToken) return;
+ const symbol = lowerer.checker.getSymbolAtLocation(param.name);
+ if (!symbol || lowerer.runtimeOptionalBindingTypes.has(symbol)) return;
+ if (lowerer.paramShape(param).type.kind !== wanted[i]) return;
+ symbols.push(symbol);
+ });
+ return symbols;
+}
+
+/** Lowers an inline listener with the given parameters bound as
+ * checked-dynamic values typed `unknown` for the body. */
+function lowerWithCheckedParams(
+ lowerer: Lowerer,
+ listener: ts.Expression,
+ symbols: readonly ts.Symbol[],
+): IrExpr {
+ const previousImplicit = lowerer.implicitParamTypes;
+ const implicit = new Map(previousImplicit ?? []);
+ for (const symbol of symbols) {
+ implicit.set(symbol, lowerer.checker.getUnknownType());
+ lowerer.runtimeOptionalBindingTypes.set(symbol, DYN);
+ }
+ lowerer.implicitParamTypes = implicit;
+ try {
+ return lowerer.lowerExpr(listener);
+ } finally {
+ for (const symbol of symbols) lowerer.runtimeOptionalBindingTypes.delete(symbol);
+ lowerer.implicitParamTypes = previousImplicit;
+ }
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/class-values.ts b/packages/compiler/src/frontend/lowering/builtins/class-values.ts
index 416217716..4d073c264 100644
--- a/packages/compiler/src/frontend/lowering/builtins/class-values.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/class-values.ts
@@ -97,7 +97,7 @@ export function builtinClassAliasOwnsNoSlot(lowerer: Lowerer, symbol: ts.Symbol)
if (decl === undefined || !ts.isVariableDeclaration(decl)) return false;
if (builtinMemberRequireDecl(decl.name, decl.initializer)) return false;
if (streamClassAliasDecl(lowerer, decl.name, decl.initializer)) return false;
- if (!isJsSourceFile(decl.getSourceFile())) return true;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return true;
let init = decl.initializer;
while (init !== undefined && ts.isParenthesizedExpression(init)) init = init.expression;
if (init === undefined) return true;
@@ -157,9 +157,10 @@ export function runtimeClassValueOfType(lowerer: Lowerer, type: ts.Type): string
const symbol = type.getSymbol();
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
let info = lowerer.builtinEmitterInfoOf(symbol) ?? lowerer.builtinStreamInfoOf(symbol);
- if (!info && symbol.name.endsWith("Constructor")) {
+ if (!info && ts.symbolName(symbol).endsWith("Constructor")) {
for (const [irName, rec] of RUNTIME_ERROR_CLASSES) {
- if (`${rec.lib}Constructor` === symbol.name) info = lowerer.classes.get(irName) ?? null;
+ if (`${rec.lib}Constructor` === ts.symbolName(symbol))
+ info = lowerer.classes.get(irName) ?? null;
}
}
return info?.valueCtorParams !== undefined ? info.def.name : null;
diff --git a/packages/compiler/src/frontend/lowering/builtins/compression.ts b/packages/compiler/src/frontend/lowering/builtins/compression.ts
index e69011522..717e09933 100644
--- a/packages/compiler/src/frontend/lowering/builtins/compression.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/compression.ts
@@ -136,7 +136,7 @@ export function zlibStreamModeOf(
if (alias !== null) return alias.kind === "zlib" ? alias.mode : null;
const target =
symbol.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(symbol) : symbol;
- const mode = ZLIB_STREAM_MODES.get(target.name);
+ const mode = ZLIB_STREAM_MODES.get(ts.symbolName(target));
if (mode === undefined) return null;
return target.declarations.some((d) => ts.isClassDeclaration(d) && isZlibFallbackDecl(lowerer, d))
? mode
@@ -144,7 +144,7 @@ export function zlibStreamModeOf(
}
function isZlibFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
- const file = d.getSourceFile();
+ const file = ts.getSourceFileOfNode(d);
if (!lowerer.isStdlibFile(file) || isNodeTypesPath(file.fileName)) return false;
for (let node: ts.Node | undefined = d.parent; node; node = node.parent) {
if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
diff --git a/packages/compiler/src/frontend/lowering/builtins/constants.ts b/packages/compiler/src/frontend/lowering/builtins/constants.ts
index 611805d94..35f0fc095 100644
--- a/packages/compiler/src/frontend/lowering/builtins/constants.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/constants.ts
@@ -170,7 +170,7 @@ export function fsConstantMemberRead(expr: ts.PropertyAccessExpression): IrExpr
type: DYN,
loc,
};
- return isJsSourceFile(expr.getSourceFile())
+ return isJsSourceFile(ts.getSourceFileOfNode(expr))
? read
: { kind: "dynCheck", value: read, type: F64, loc };
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/crypto.ts b/packages/compiler/src/frontend/lowering/builtins/crypto.ts
index cd490bc87..e6834abd4 100644
--- a/packages/compiler/src/frontend/lowering/builtins/crypto.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/crypto.ts
@@ -240,7 +240,9 @@ function cryptoKdfInput(lowerer: Lowerer, node: ts.Expression): IrExpr {
const symbol = lowerer.typeOf(node).getSymbol();
if (
type?.kind === "bytes" ||
- (symbol?.name === "ArrayBuffer" && lowerer.isStdlibSymbol(symbol))
+ (symbol !== undefined &&
+ ts.symbolName(symbol) === "ArrayBuffer" &&
+ lowerer.isStdlibSymbol(symbol))
) {
const value = lowerer.coerceInto(node, lowerer.lowerExpr(node), DYN);
return { kind: "libCall", fn: "bytes.bufferSource", args: [value], type: BYTES_U8, loc };
@@ -878,8 +880,8 @@ export function isCipherType(lowerer: Lowerer, type: ts.Type): boolean {
const symbol = type.getAliasSymbol() ?? type.getSymbol();
return (
!!symbol &&
- CIPHER_TYPE_NAMES.has(symbol.name) &&
- symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile()))
+ CIPHER_TYPE_NAMES.has(ts.symbolName(symbol)) &&
+ symbol.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))
);
}
@@ -898,7 +900,7 @@ function v8CalleeText(node: ts.Expression): string {
if (ts.isPropertyAccessExpression(node))
return `${v8CalleeText(node.expression)}.${node.name.text}`;
if (ts.isCallExpression(node)) return `${v8CalleeText(node.expression)}(...)`;
- return node.getText();
+ return ts.getTextOfNode(node);
}
/** Calls on native Cipheriv/Decipheriv handles dispatch through the
@@ -959,7 +961,7 @@ export function lowerCryptoHashMethodCall(
return {
kind: "seqExpr",
stmts: evaluated,
- result: nodeThrowExpr(1, "", `${access.getText()} is not a function`, DYN, loc),
+ result: nodeThrowExpr(1, "", `${ts.getTextOfNode(access)} is not a function`, DYN, loc),
type: DYN,
loc,
};
diff --git a/packages/compiler/src/frontend/lowering/builtins/dates.ts b/packages/compiler/src/frontend/lowering/builtins/dates.ts
index 32fe09ab3..31be995e7 100644
--- a/packages/compiler/src/frontend/lowering/builtins/dates.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/dates.ts
@@ -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);
diff --git a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts
index fb1c92257..1b9a9c03a 100644
--- a/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/disposable-stack.ts
@@ -9,7 +9,12 @@ import { varRef } from "../../../ir/build.js";
function isStdlibDisposableStack(lowerer: Lowerer, node: ts.Expression): boolean {
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) {
- if (symbol?.name === "DisposableStack" && lowerer.isStdlibSymbol(symbol)) return true;
+ if (
+ symbol !== undefined &&
+ ts.symbolName(symbol) === "DisposableStack" &&
+ lowerer.isStdlibSymbol(symbol)
+ )
+ return true;
}
return false;
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts
index a8b4dbcd6..d95442829 100644
--- a/packages/compiler/src/frontend/lowering/builtins/errors.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts
@@ -27,9 +27,10 @@ const ASSERTION_ERROR_PROPERTIES: ReadonlySet = new Set([
export function isExecExceptionType(lowerer: Lowerer, t: ts.Type): boolean {
const sym = t.getSymbol();
return (
- sym?.name === "ExecException" &&
+ sym !== undefined &&
+ ts.symbolName(sym) === "ExecException" &&
sym.declarations.some(
- (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
+ (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
)
);
}
@@ -52,6 +53,20 @@ export function refuseExecExceptionCode(lowerer: Lowerer, node: ts.Node): never
* declaration is not stdlib). Reads only: writes keep their fence (no
* compiled program constructs an errno error). Null for non-error
* receivers and non-stdlib members, so the chain keeps trying. */
+/** Error members with slots of their own: never read through the dynamic
+ * node, even when an intersection restates them. */
+const ERROR_SLOT_MEMBERS = new Set([
+ "code",
+ "cause",
+ "stack",
+ "message",
+ "name",
+ "errno",
+ "syscall",
+ "path",
+ "dest",
+]);
+
export function lowerErrorCodeProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
@@ -135,6 +150,33 @@ export function lowerErrorCodeProperty(
declared,
);
}
+ // A member an `Error & { ... }` intersection adds beyond Error's own and
+ // the system-error metadata (`Object.assign(new Error(m), { status })`):
+ // an own property of the error's checked-dynamic node, read at the
+ // declared type (the intersection mapping admits only checkable types).
+ if (
+ recvT.className === "%Error" &&
+ !ERROR_SLOT_MEMBERS.has(expr.name.text) &&
+ !expr.questionDotToken &&
+ !lowerer.isStdlibMember(expr) &&
+ lowerer.typeOf(expr.expression).isIntersectionType()
+ ) {
+ const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
+ if (declared && declared.kind !== "void") {
+ const receiver = lowerer.lowerExpr(expr.expression);
+ const loc = locOf(expr);
+ return lowerer.coerceToExpected(
+ {
+ kind: "dynKeyGet",
+ value: lowerer.coerceToExpected(receiver, DYN),
+ key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
+ type: DYN,
+ loc,
+ },
+ declared,
+ );
+ }
+ }
// A program class under the receiver's class can carry its own `code`
// (a declared field or a property added through an untyped reference),
// which Node reads instead of the errno code: read through the error's
diff --git a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts
index 3fbea04aa..49875e004 100644
--- a/packages/compiler/src/frontend/lowering/builtins/filesystem.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/filesystem.ts
@@ -250,7 +250,7 @@ export function lowerFsLadderCall(
);
}
if (bi.module !== "fs" && bi.module !== "fs/promises") return null;
- if (!isJsSourceFile(expr.getSourceFile())) return null;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
const args = expr.arguments.nodes;
if (args.some(ts.isSpreadElement)) return null;
// Every ladder argument crosses as a dyn value; an argument that
@@ -1564,7 +1564,7 @@ export function lowerDirentMethodCall(
if (call.questionDotToken || access.questionDotToken) return null;
if (!lowerer.isStdlibMember(access)) return null;
const recvSym = lowerer.typeOf(access.expression).getSymbol();
- if (recvSym?.name !== "Dirent") return null;
+ if (recvSym === undefined || ts.symbolName(recvSym) !== "Dirent") return null;
const receiver = lowerer.lowerExpr(access.expression);
if (receiver.type.kind !== "record") return null;
const shape = lowerer.shapes.get(receiver.type.shapeId);
diff --git a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts
index 22e524690..498f9b6d4 100644
--- a/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/fs-streams.ts
@@ -19,7 +19,7 @@ import {
} from "../../../ir/ir.js";
function isFsFallbackDecl(lowerer: Lowerer, d: ts.Node): boolean {
- const file = d.getSourceFile();
+ const file = ts.getSourceFileOfNode(d);
if (!lowerer.isStdlibFile(file) || isNodeTypesPath(file.fileName)) return false;
for (let node: ts.Node | undefined = d.parent; node; node = node.parent) {
if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
@@ -153,11 +153,13 @@ export function fsStreamClassOf(
lowerer: Lowerer,
symbol: ts.Symbol | null | undefined,
): "ReadStream" | "WriteStream" | null {
- if (!symbol || (symbol.name !== "ReadStream" && symbol.name !== "WriteStream")) return null;
+ if (!symbol) return null;
+ const name = ts.symbolName(symbol);
+ if (name !== "ReadStream" && name !== "WriteStream") return null;
const declared = symbol.declarations.some(
(d) => ts.isClassDeclaration(d) && isFsFallbackDecl(lowerer, d),
);
- return declared ? symbol.name : null;
+ return declared ? name : null;
}
/** `new fs.ReadStream(path, options)` / `new fs.WriteStream(...)`: the
diff --git a/packages/compiler/src/frontend/lowering/builtins/json.ts b/packages/compiler/src/frontend/lowering/builtins/json.ts
index b48030ba5..54bcae4b5 100644
--- a/packages/compiler/src/frontend/lowering/builtins/json.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/json.ts
@@ -504,7 +504,7 @@ function jsonNullishArgument(lowerer: Lowerer, node: ts.Expression): boolean {
const symbol = lowerer.checker.getSymbolAtLocation(node);
return (
symbol !== undefined &&
- symbol.declarations.every((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
+ symbol.declarations.every((decl) => lowerer.isStdlibFile(ts.getSourceFileOfNode(decl)))
);
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/key-object.ts b/packages/compiler/src/frontend/lowering/builtins/key-object.ts
index ff6c0b38b..7e680fa20 100644
--- a/packages/compiler/src/frontend/lowering/builtins/key-object.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/key-object.ts
@@ -23,7 +23,7 @@ export function isKeyObjectClass(lowerer: Lowerer, node: ts.Expression): boolean
export function isKeyObjectValue(lowerer: Lowerer, node: ts.Expression): boolean {
const type = lowerer.typeOf(node);
const sym = type.getAliasSymbol() ?? type.getSymbol();
- return !!sym && sym.name === "KeyObject" && lowerer.isStdlibSymbol(sym);
+ return !!sym && ts.symbolName(sym) === "KeyObject" && lowerer.isStdlibSymbol(sym);
}
/** `v instanceof KeyObject`. */
@@ -51,7 +51,7 @@ export function lowerKeyObjectCall(lowerer: Lowerer, call: ts.CallExpression): I
kind: "dynInvoke",
recv: lowerer.lowerExprExpecting(access.expression, DYN),
method: access.name.text,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc: locOf(call),
diff --git a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts
index 86d93de2c..5465aeafa 100644
--- a/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts
@@ -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
@@ -124,7 +135,7 @@ export function builtinImportOf(
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
const target = lowerer.checker.getAliasedSymbol(symbol);
const tdecl = target.declarations[0];
- if (tdecl !== undefined && tdecl.getSourceFile().isDeclarationFile) {
+ if (tdecl !== undefined && ts.getSourceFileOfNode(tdecl).isDeclarationFile) {
for (
let p: ts.Node | undefined = tdecl.parent;
p !== undefined && !ts.isSourceFile(p);
@@ -132,7 +143,7 @@ export function builtinImportOf(
) {
if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) {
const module = canonicalBuiltinModule(p.name.text);
- if (module !== null) return { module, member: target.name };
+ if (module !== null) return { module, member: ts.symbolName(target) };
break;
}
}
@@ -286,14 +297,14 @@ export function createRequireCalleeFileOf(
): ts.SourceFile | null {
const e = stripTypeCasts(callee);
if (ts.isCallExpression(e)) {
- return createRequireBaseCallOf(lowerer, e) !== null ? e.getSourceFile() : null;
+ return createRequireBaseCallOf(lowerer, e) !== null ? ts.getSourceFileOfNode(e) : null;
}
if (!ts.isIdentifier(e)) return null;
const symbol = lowerer.checker.getSymbolAtLocation(e);
const decl = symbol ? symbol.declarations[0] : undefined;
if (!decl || !ts.isVariableDeclaration(decl) || decl.initializer === undefined) return null;
return createRequireBindingDecl(lowerer, decl.name, decl.initializer)
- ? decl.getSourceFile()
+ ? ts.getSourceFileOfNode(decl)
: null;
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
index 956eddb93..c85f2c777 100644
--- a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
@@ -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,
@@ -56,7 +57,7 @@ export function lowerBuiltinModuleCall(
fn: BuiltinModuleFn,
loc: SrcLoc,
): IrExpr {
- const name = expr.expression.getText();
+ const name = ts.getTextOfNode(expr.expression);
if (bi.module === "util/types") {
return lowerUtilTypeCall(lowerer, expr, bi, loc, name);
}
@@ -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);
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts
index afa9dc1ea..6b2e9e589 100644
--- a/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/module-resolution.ts
@@ -292,7 +292,7 @@ export function lowerImportMetaResolveCall(
);
}
const result = resolveImportMetaRuntime(
- call.getSourceFile().fileName,
+ ts.getSourceFileOfNode(call).fileName,
specifier,
lowerer.targetPlatform,
lowerer.frontendServices,
@@ -314,8 +314,8 @@ function requireResolverBaseFile(lowerer: Lowerer, receiver: ts.Expression): ts.
const created = createRequireCalleeFileOf(lowerer, receiver);
if (created !== null) return created;
return lowerer.isStdlibGlobal(receiver, "require") &&
- !isNodeEsmFile(receiver.getSourceFile(), lowerer.program)
- ? receiver.getSourceFile()
+ !isNodeEsmFile(ts.getSourceFileOfNode(receiver), lowerer.program)
+ ? ts.getSourceFileOfNode(receiver)
: null;
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/navigator.ts b/packages/compiler/src/frontend/lowering/builtins/navigator.ts
index 637edaf3c..f90448c1e 100644
--- a/packages/compiler/src/frontend/lowering/builtins/navigator.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/navigator.ts
@@ -20,7 +20,8 @@ export function isNavigatorGlobal(lowerer: Lowerer, node: ts.Expression): boolea
if (!ts.isIdentifier(expr) || expr.text !== "navigator") return false;
if (lowerer.isStdlibGlobal(expr, "navigator")) return true;
return (
- isJsSourceFile(expr.getSourceFile()) && lowerer.checker.getSymbolAtLocation(expr) === undefined
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
+ lowerer.checker.getSymbolAtLocation(expr) === undefined
);
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/net-address.ts b/packages/compiler/src/frontend/lowering/builtins/net-address.ts
index d99d63fd3..9e795bca6 100644
--- a/packages/compiler/src/frontend/lowering/builtins/net-address.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/net-address.ts
@@ -31,8 +31,10 @@ export function netAddressClassOf(lowerer: Lowerer, node: ts.Expression): NetAdd
function netAddressValueClass(lowerer: Lowerer, node: ts.Expression): NetAddressClass | null {
const type = lowerer.typeOf(node);
const sym = type.getAliasSymbol() ?? type.getSymbol();
- if (!sym || (sym.name !== "SocketAddress" && sym.name !== "BlockList")) return null;
- return lowerer.isStdlibSymbol(sym) ? sym.name : null;
+ if (!sym) return null;
+ const name = ts.symbolName(sym);
+ if (name !== "SocketAddress" && name !== "BlockList") return null;
+ return lowerer.isStdlibSymbol(sym) ? name : null;
}
/** `new SocketAddress(options?)` and `new BlockList()`: the argument
@@ -127,7 +129,7 @@ export function lowerNetAddressCall(lowerer: Lowerer, call: ts.CallExpression):
kind: "dynInvoke",
recv: lowerer.lowerExprExpecting(access.expression, DYN),
method: name,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
diff --git a/packages/compiler/src/frontend/lowering/builtins/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts
index 2a9962b0f..72304e6a9 100644
--- a/packages/compiler/src/frontend/lowering/builtins/performance.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/performance.ts
@@ -18,6 +18,7 @@ import {
} from "../../../ir/ir.js";
import { PERF_ENTRY_FIELDS, PERF_ENTRY_MARKER, perfEntryJsonShape } from "../../type-mapper.js";
import { builtinImportOf } from "./module-bindings.js";
+import { lowerOptionalArgument } from "../optional-arguments.js";
import { checkedIterableSpread } from "../checked-iterable-spread.js";
/** True for any spelling of node:perf_hooks' `performance` object — the
@@ -134,6 +135,12 @@ export function lowerPerfHooksCall(
loc,
};
}
+ if (
+ access.name.text === "eventLoopUtilization" &&
+ isPerfHooksPerformanceExpr(lowerer, access.expression)
+ ) {
+ return lowerEventLoopUtilization(lowerer, expr);
+ }
const timeline = PERF_TIMELINE_CALLS.get(access.name.text);
if (timeline && isPerfHooksPerformanceExpr(lowerer, access.expression)) {
return lowerPerfTimelineCall(lowerer, expr, access.name.text, timeline, loc);
@@ -142,7 +149,7 @@ export function lowerPerfHooksCall(
lowerer.noLowering(
`perf_hooks performance.${access.name.text}`,
expr,
- "performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks and clearMeasures are the lowered surface",
+ "performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks, clearMeasures and eventLoopUtilization are the lowered surface",
);
}
return null;
@@ -278,7 +285,7 @@ function lowerPerfArguments(lowerer: Lowerer, call: ts.CallExpression, loc: SrcL
});
continue;
}
- const spelling = arg.expression.getText();
+ const spelling = ts.getTextOfNode(arg.expression);
const items = checkedIterableSpread(lowerer, lowerer.lowerExpr(arg.expression), spelling, loc);
stmts.push({
kind: "exprStmt",
@@ -513,3 +520,309 @@ export function fencePerfEntryKeyWalk(
`${what} over performance entries (Node keeps their fields as prototype getters, so own-key walks see none; read the fields or call toJSON())`,
);
}
+
+/* ── event-loop metrics: eventLoopUtilization and monitorEventLoopDelay ── */
+
+/** The stdlib IntervalHistogram (monitorEventLoopDelay's result), by the
+ * receiver's checker type. Its value is the runtime histogram handle. */
+function isIntervalHistogram(lowerer: Lowerer, node: ts.Expression): boolean {
+ const symbol = lowerer.typeOf(node).getSymbol();
+ return (
+ symbol !== undefined &&
+ ts.symbolName(symbol) === "IntervalHistogram" &&
+ lowerer.isStdlibSymbol(symbol)
+ );
+}
+
+/** IntervalHistogram's numeric members, as perf.eldStat selectors. */
+const HISTOGRAM_STATS: ReadonlyMap = new Map([
+ ["min", 0],
+ ["max", 1],
+ ["mean", 2],
+ ["stddev", 3],
+ ["count", 4],
+ ["exceeds", 5],
+]);
+
+/** `h.min`, `h.max`, … on an IntervalHistogram: the runtime's current
+ * statistic (Node's hdr_histogram values; nanoseconds). Null for every
+ * other read. */
+export function lowerHistogramProperty(
+ lowerer: Lowerer,
+ expr: ts.PropertyAccessExpression,
+): IrExpr | null {
+ const which = HISTOGRAM_STATS.get(expr.name.text);
+ if (
+ which === undefined ||
+ expr.questionDotToken ||
+ !isIntervalHistogram(lowerer, expr.expression)
+ )
+ return null;
+ const loc = locOf(expr);
+ return {
+ kind: "libCall",
+ fn: "perf.eldStat",
+ args: [lowerer.lowerExprExpecting(expr.expression, F64), numLit(which, loc)],
+ type: F64,
+ loc,
+ };
+}
+
+/** enable(), disable(), reset() and percentile(p) on an IntervalHistogram.
+ * Other members (the BigInt twins, the percentiles Map) fence by name. */
+export function lowerHistogramMethodCall(
+ lowerer: Lowerer,
+ expr: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+): IrExpr | null {
+ if (access.questionDotToken || !isIntervalHistogram(lowerer, access.expression)) return null;
+ const loc = locOf(expr);
+ const name = access.name.text;
+ const handle = (): IrExpr => lowerer.lowerExprExpecting(access.expression, F64);
+ const args = expr.arguments.nodes;
+ if ((name === "enable" || name === "disable" || name === "reset") && args.length === 0) {
+ const fn =
+ name === "enable"
+ ? "perf.eldEnable"
+ : name === "disable"
+ ? "perf.eldDisable"
+ : "perf.eldReset";
+ return { kind: "libCall", fn, args: [handle()], type: name === "reset" ? VOID : BOOL, loc };
+ }
+ if (name === "percentile" && args.length === 1 && !ts.isSpreadElement(args[0]!)) {
+ // Node validates first: NaN, p <= 0 and p > 100 throw ERR_OUT_OF_RANGE.
+ const h = lowerer.declareHiddenLocal("%histogram", F64);
+ const p = lowerer.declareHiddenLocal("%percentile", F64);
+ const pRef = (): IrExpr => varRef(p.id, F64, loc);
+ const cmp = (op: "!==" | "<=" | ">", left: IrExpr, right: IrExpr): IrExpr => ({
+ kind: "bin",
+ op,
+ left,
+ right,
+ type: BOOL,
+ loc,
+ });
+ const or = (left: IrExpr, right: IrExpr): IrExpr => ({
+ kind: "logical",
+ op: "||",
+ left,
+ right,
+ type: BOOL,
+ loc,
+ });
+ const outOfRange = or(
+ or(cmp("!==", pRef(), pRef()), cmp("<=", pRef(), numLit(0, loc))),
+ cmp(">", pRef(), numLit(100, loc)),
+ );
+ const message: IrExpr = {
+ kind: "strConcat",
+ left: {
+ kind: "strLit",
+ value: 'The value of "percentile" is out of range. It must be > 0 && <= 100. Received ',
+ type: STRING,
+ loc,
+ },
+ right: { kind: "toString", operand: pRef(), type: STRING, loc },
+ type: STRING,
+ loc,
+ };
+ const thrown: IrExpr = {
+ kind: "libCall",
+ fn: "error.nodeThrow",
+ args: [
+ numLit(2, loc),
+ { kind: "strLit", value: "ERR_OUT_OF_RANGE", type: STRING, loc },
+ message,
+ ],
+ type: F64,
+ loc,
+ };
+ return {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: h.id, init: handle(), loc },
+ { kind: "varDecl", localId: p.id, init: lowerer.lowerExprExpecting(args[0]!, F64), loc },
+ ],
+ result: {
+ kind: "ternary",
+ cond: outOfRange,
+ then: thrown,
+ else_: {
+ kind: "libCall",
+ fn: "perf.eldPercentile",
+ args: [varRef(h.id, F64, loc), pRef()],
+ type: F64,
+ loc,
+ },
+ type: F64,
+ loc,
+ },
+ type: F64,
+ loc,
+ };
+ }
+ lowerer.noLowering(
+ `IntervalHistogram.${name}`,
+ expr,
+ "enable(), disable(), reset(), percentile(p) and the min, max, mean, stddev, count and exceeds readings are the lowered surface",
+ );
+}
+
+/** `monitorEventLoopDelay(options?)` from node:perf_hooks, in any import
+ * spelling (`lowerBuiltinCall`): a new sampling histogram handle. The
+ * options are an inline object literal; the runtime validates the
+ * resolution as Node does (an integer of at least 1). */
+export function lowerMonitorEventLoopDelayCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
+ const loc = locOf(expr);
+ const args = expr.arguments.nodes;
+ let resolution: IrExpr = numLit(10, loc);
+ if (args.length > 1 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering(
+ "this monitorEventLoopDelay call form",
+ expr,
+ "pass at most one options object",
+ );
+ }
+ if (args.length === 1) {
+ const options = args[0]!;
+ if (!ts.isObjectLiteralExpression(options)) {
+ lowerer.noLowering(
+ "monitorEventLoopDelay with a non-literal options object",
+ options,
+ "spell the options inline: monitorEventLoopDelay({ resolution: 20 })",
+ );
+ }
+ for (const property of options.properties.nodes) {
+ const shorthand = ts.isShorthandPropertyAssignment(property);
+ if (
+ (!shorthand && !ts.isPropertyAssignment(property)) ||
+ !ts.isIdentifier(property.name) ||
+ property.name.text !== "resolution"
+ ) {
+ lowerer.noLowering(
+ "monitorEventLoopDelay options other than resolution",
+ property,
+ "resolution is the one option",
+ );
+ }
+ // An undefined resolution takes the default, as Node's destructuring.
+ resolution = lowerOptionalArgument(
+ lowerer,
+ shorthand ? property.name : property.initializer,
+ F64,
+ numLit(10, loc),
+ );
+ }
+ }
+ return { kind: "libCall", fn: "perf.eldNew", args: [resolution], type: F64, loc };
+}
+
+/** performance.eventLoopUtilization(u1?, u2?) — Node's
+ * lib/internal/perf/event_loop_utilization.js over the runtime's loop
+ * start and idle time, one lifted helper per arity. */
+function lowerEventLoopUtilization(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
+ const loc = locOf(expr);
+ const args = expr.arguments.nodes;
+ const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr));
+ if (resultT?.kind !== "record" || args.length > 2 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering("this performance.eventLoopUtilization call form", expr);
+ }
+ const helper = eluHelperOf(lowerer, args.length, resultT);
+ return {
+ kind: "call",
+ callee: helper,
+ args: args.map((arg) => lowerer.lowerExprExpecting(arg, resultT)),
+ type: resultT,
+ loc,
+ };
+}
+
+function eluHelperOf(lowerer: Lowerer, arity: number, recT: IrType & { kind: "record" }): string {
+ const name = `%perf.elu.${arity}`;
+ if (lowerer.liftedFns.some((f) => f.name === name)) return name;
+ const loc: SrcLoc = { file: "", start: 0, end: 0 };
+ const f64Local = (id: string) => ({ id, name: id, type: F64, mutable: false });
+ const params = Array.from({ length: arity }, (_, i) => ({
+ localId: `u${i + 1}.0`,
+ name: `u${i + 1}`,
+ type: recT as IrType,
+ }));
+ const locals = [
+ ...params.map((p) => ({ id: p.localId, name: p.name, type: recT as IrType, mutable: false })),
+ f64Local("ls.0"),
+ f64Local("idle.0"),
+ f64Local("active.0"),
+ ];
+ const ref = (id: string): IrExpr => varRef(id, F64, loc);
+ const field = (param: string, f: string): IrExpr => ({
+ kind: "recordGet",
+ obj: varRef(param, recT, loc),
+ shapeId: recT.shapeId,
+ field: f,
+ type: F64,
+ loc,
+ });
+ const bin = (op: "+" | "-" | "/" | "<=", left: IrExpr, right: IrExpr): IrExpr => ({
+ kind: "bin",
+ op,
+ left,
+ right,
+ type: op === "<=" ? BOOL : F64,
+ loc,
+ });
+ const record = (idle: IrExpr, active: IrExpr, utilization: IrExpr): IrExpr => ({
+ kind: "recordLit",
+ fields: [
+ { name: "idle", value: idle },
+ { name: "active", value: active },
+ { name: "utilization", value: utilization },
+ ],
+ type: recT,
+ loc,
+ });
+ const result = (idle: IrExpr, active: IrExpr): IrStmt => ({
+ kind: "return",
+ value: record(idle, active, bin("/", active, bin("+", idle, active))),
+ loc,
+ });
+ const decl = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc });
+ const body: IrStmt[] = [
+ decl("ls.0", { kind: "libCall", fn: "perf.loopStart", args: [], type: F64, loc }),
+ {
+ kind: "if",
+ cond: bin("<=", ref("ls.0"), numLit(0, loc)),
+ then: [
+ { kind: "return", value: record(numLit(0, loc), numLit(0, loc), numLit(0, loc)), loc },
+ ],
+ else_: null,
+ loc,
+ },
+ ];
+ if (arity === 2) {
+ body.push(
+ decl("idle.0", bin("-", field("u1.0", "idle"), field("u2.0", "idle"))),
+ decl("active.0", bin("-", field("u1.0", "active"), field("u2.0", "active"))),
+ result(ref("idle.0"), ref("active.0")),
+ );
+ } else {
+ body.push(
+ decl("idle.0", { kind: "libCall", fn: "perf.idleTime", args: [], type: F64, loc }),
+ decl(
+ "active.0",
+ bin(
+ "-",
+ bin("-", { kind: "libCall", fn: "perf.now", args: [], type: F64, loc }, ref("ls.0")),
+ ref("idle.0"),
+ ),
+ ),
+ arity === 0
+ ? result(ref("idle.0"), ref("active.0"))
+ : result(
+ bin("-", ref("idle.0"), field("u1.0", "idle")),
+ bin("-", ref("active.0"), field("u1.0", "active")),
+ ),
+ );
+ }
+ lowerer.liftedFns.push({ name, params, returnType: recT, locals, body, loc });
+ return name;
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/process.ts b/packages/compiler/src/frontend/lowering/builtins/process.ts
index dc2f5ee89..78dc6ef14 100644
--- a/packages/compiler/src/frontend/lowering/builtins/process.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/process.ts
@@ -3,6 +3,7 @@ import { envSnapshotHelper, lowerProcessLoadEnvFile } from "./environment.js";
import { dynUndefinedExpr, boolLit, numLit, strLit, varRef } from "../../../ir/build.js";
import { timerStyleCallback } from "../lower-timers.js";
import * as ts from "../../ts7/adapter.js";
+import { requestGeneralStreamSurface } from "../general-streams.js";
import { type Lowerer, own } from "../lowerer.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { isSafeToDiscard } from "../expressions/evaluation-safety.js";
@@ -27,6 +28,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";
@@ -190,14 +192,14 @@ export function lowerProcStreamMethodCall(
if (!lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const loc = locOf(call);
- if (isJsSourceFile(call.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(call))) {
const receiver = lowerer.lowerExpr(access.expression);
if (receiver.type.kind === "dyn") {
return {
kind: "dynInvoke",
recv: receiver,
method: name,
- calleeName: call.expression.getText(),
+ calleeName: ts.getTextOfNode(call.expression),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -212,7 +214,7 @@ export function lowerProcStreamMethodCall(
kind: "dynInvoke",
recv: receiver,
method: name,
- calleeName: call.expression.getText(),
+ calleeName: ts.getTextOfNode(call.expression),
args: [],
type: DYN,
loc,
@@ -224,6 +226,7 @@ export function lowerProcStreamMethodCall(
const receiver = lowerer.lowerExpr(access.expression);
const data = lowerer.lowerExpr(call.arguments.nodes[0]!);
if (data.type.kind !== "string") {
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`write of '${lowerer.fmt(data.type)}' data on a stream value`,
call.arguments.nodes[0]!,
@@ -232,6 +235,7 @@ export function lowerProcStreamMethodCall(
}
return { kind: "libCall", fn: "procStream.write", args: [receiver, data], type: BOOL, loc };
}
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.noLowering(
`WritableStream.${name}`,
call,
@@ -272,7 +276,7 @@ export function lowerProcessStreamProperty(
? lowerer.stdlibGlobalMember(expr.expression, "process")
: null;
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(direct === "stdin" ||
direct === "stdout" ||
direct === "stderr" ||
@@ -578,6 +582,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(ts.getSourceFileOfNode(expr))) {
+ 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
@@ -658,7 +667,7 @@ export function lowerProcessProperty(
// OUTER expressions dispatch first.
if (
member === "stdin" ||
- ((member === "stdout" || member === "stderr") && isJsSourceFile(expr.getSourceFile()))
+ ((member === "stdout" || member === "stderr") && isJsSourceFile(ts.getSourceFileOfNode(expr)))
) {
return {
kind: "libCall",
@@ -688,7 +697,10 @@ export function lowerProcessMethodCall(
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (call.questionDotToken) return null;
- if (isJsSourceFile(call.getSourceFile()) && ts.isPropertyAccessExpression(access.expression)) {
+ if (
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
+ ts.isPropertyAccessExpression(access.expression)
+ ) {
const stream = lowerer.stdlibGlobalMember(access.expression, "process");
if (stream === "stdin" || stream === "stdout" || stream === "stderr") {
if (call.arguments.nodes.some(ts.isSpreadElement))
@@ -697,7 +709,7 @@ export function lowerProcessMethodCall(
kind: "dynInvoke",
recv: lowerer.lowerExpr(access.expression),
method: access.name.text,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc: locOf(call),
@@ -874,7 +886,7 @@ export function lowerProcessMethodCall(
loc,
},
receiver,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: [
lowerer.lowerExprExpecting(call.arguments.nodes[0]!, DYN),
lowerer.lowerExprExpecting(call.arguments.nodes[1]!, DYN),
@@ -1071,7 +1083,7 @@ export function lowerProcessMethodCall(
// `writeOut: (str) => process.stdout.write(str)` — str untyped):
// a runtime string writes; anything else throws the dynCheck's
// honest TypeError at the call.
- if (data.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) {
+ if (data.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(call))) {
data = { kind: "dynCheck", value: data, type: STRING, loc };
}
const isBytes = data.type.kind === "bytes" && data.type.elem === "u8";
@@ -1086,7 +1098,7 @@ export function lowerProcessMethodCall(
let callback: IrExpr | undefined;
if (callbackNode) {
callback = lowerer.lowerExpr(callbackNode);
- if (callback.type.kind === "dyn" && isJsSourceFile(call.getSourceFile())) {
+ if (callback.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(call))) {
callback = {
kind: "dynCheck",
value: callback,
diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts
index 48106d082..668ef01f8 100644
--- a/packages/compiler/src/frontend/lowering/builtins/promises.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts
@@ -1,15 +1,18 @@
-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 {
+ BOOL,
DYN,
type IrExpr,
type IrFunction,
type IrStmt,
type IrType,
+ UNDEFINED_T,
VOID,
arrayOf,
isUnitType,
@@ -17,13 +20,179 @@ 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, 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();
+ 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
* type is the checker's combined promise, and each entry's inner type
* must be that inner type, one of its union arms, or a sub-union of it
* (the backend's interned adapters wrap/re-tag fulfillments; a wider
- * entry would need machinery that doesn't exist and fences).
+ * entry would need machinery that doesn't exist and fences). A
+ * Promise entry fits any result, and an entry whose payload widens
+ * into the result's converts. When the checker's combined type has no
+ * representation, the result takes the type the entries that can fulfill
+ * share, or 'unknown'.
* Promise.all/allSettled/any fence with the sequential-await hint;
* resolve/reject and the rest fall to the member fence. Null for
* non-Promise receivers. */
@@ -50,14 +219,47 @@ export function lowerPromiseStaticCall(
"a non-empty array LITERAL of promises is the supported form: Promise.race([p, q])",
);
}
- const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
- if (resultT?.kind !== "promise") {
- lowerer.noLowering(
- "Promise.race with this combined result type",
- call,
- "the entries' value types must combine into a representable union",
- );
+ // A Promise entry (the deadline idiom: `new Promise((_,
+ // 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 =
+ ts.symbolNameOf(t.getSymbol()) === "Promise"
+ ? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
+ : undefined;
+ return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
+ };
+ const elements = argNode.elements.nodes;
+ let combined = lowerer.mapTypeOf(lowerer.typeOf(call));
+ let lowered: IrExpr[] | null = null;
+ if (combined?.kind !== "promise") {
+ // The checker's combined type has no representation (an 'any'
+ // element array such as events.once's any[], or 'void | any[]'): the
+ // result takes the one type the entries that can fulfill share, or
+ // else 'unknown' when each of them widens into it.
+ lowered = elements.map((el) => lowerer.lowerExpr(el));
+ const fulfilling = lowered.filter((_, i) => !neverEntry(elements[i]!));
+ const first = fulfilling[0]?.type;
+ const dynResult: IrType & { kind: "promise" } = { kind: "promise", inner: DYN };
+ if (first?.kind === "promise" && fulfilling.every((e) => typeEquals(e.type, first))) {
+ combined = first;
+ } else if (
+ fulfilling.length > 0 &&
+ fulfilling.every(
+ (e) => e.type.kind === "promise" && lowerer.promisePayloadWidens(e.type, dynResult),
+ )
+ ) {
+ combined = dynResult;
+ } else {
+ lowerer.noLowering(
+ "Promise.race with this combined result type",
+ call,
+ "the entries' value types must combine into a representable union",
+ );
+ }
}
+ const resultT = combined;
const resultArms =
resultT.inner.kind === "union"
? (lowerer.unions.get(resultT.inner.unionId)?.arms ?? [])
@@ -74,8 +276,8 @@ export function lowerPromiseStaticCall(
}
return resultArms.some((a) => typeEquals(a, inner));
};
- const entries = argNode.elements.nodes.map((el) => {
- const entry = lowerer.lowerExpr(el);
+ const entries = elements.map((el, i) => {
+ const entry = lowered?.[i] ?? lowerer.lowerExpr(el);
if (entry.type.kind !== "promise") {
lowerer.noLowering(
"Promise.race over non-promise entries",
@@ -83,7 +285,11 @@ 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))) {
+ // A payload that widens into the result's (into 'unknown', or a
+ // checked conversion) keeps its promise and settlement turn.
+ if (lowerer.promisePayloadWidens(entry.type, resultT))
+ return lowerer.coerceToExpected(entry, resultT);
lowerer.unsupported(
"SC1090",
el,
@@ -143,8 +349,12 @@ export function lowerPromiseStaticCall(
return { kind: "intrinsic", name: "promise.all", args: [entriesArr], type: resultT, loc };
}
+ if (!isJsSourceFile(ts.getSourceFileOfNode(call))) {
+ const tuple = lowerTuplePromiseAll(lowerer, call, elems);
+ if (tuple) return tuple;
+ }
if (
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
elems.every((entry) => lowerer.dynConvertible(entry.type))
) {
return checkedPromiseAll(
@@ -165,7 +375,7 @@ export function lowerPromiseStaticCall(
);
}
const entries = lowerer.lowerExpr(argNode);
- if (isJsSourceFile(call.getSourceFile()) && entries.type.kind === "dyn")
+ if (isJsSourceFile(ts.getSourceFileOfNode(call)) && entries.type.kind === "dyn")
return checkedPromiseAll(lowerer, entries, loc);
// loadPlugin(p)))` — the loadPlugins shape, where the checker
@@ -224,7 +434,7 @@ export function lowerPromiseStaticCall(
`one iterable of promises is the supported form: Promise.${member}(ps)`,
);
}
- if (isJsSourceFile(call.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(call))) {
// JavaScript entries are checked-dynamic: any iterable, thenables
// and plain values included, exactly like Promise.all.
if (
@@ -491,6 +701,32 @@ export function lowerPromiseStaticCall(
lowerer.noLowering("Promise.resolve at a unit-typed promise", call);
}
const value = lowerer.lowerExprExpecting(argNode, resultT.inner);
+ // In JavaScript, a checked-dynamic argument may itself hold a promise
+ // (a TypeScript async callback's result: `Promise.resolve(fn())`):
+ // Promise.resolve answers THAT promise — the same object, adopted —
+ // and wraps every other value.
+ if (
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
+ value.type.kind === "dyn" &&
+ resultT.inner.kind === "dyn"
+ ) {
+ const local = lowerer.declareHiddenLocal("%resolveValue", DYN);
+ const ref: IrExpr = { kind: "varRef", localId: local.id, type: DYN, loc };
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "varDecl", localId: local.id, init: value, loc }],
+ result: {
+ kind: "ternary",
+ cond: { kind: "dynTest", test: "promise", value: ref, type: BOOL, loc },
+ then: { kind: "dynCheck", value: ref, type: resultT, loc },
+ else_: { kind: "intrinsic", name: "promise.resolve", args: [ref], type: resultT, loc },
+ type: resultT,
+ loc,
+ },
+ type: resultT,
+ loc,
+ };
+ }
return { kind: "intrinsic", name: "promise.resolve", args: [value], type: resultT, loc };
}
return null; // reject lands on lowerPromiseRejectCall / the member fence
diff --git a/packages/compiler/src/frontend/lowering/builtins/properties.ts b/packages/compiler/src/frontend/lowering/builtins/properties.ts
index b0caa0456..70512fed9 100644
--- a/packages/compiler/src/frontend/lowering/builtins/properties.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/properties.ts
@@ -1,5 +1,5 @@
import { isBuiltinClassInstance } from "./receiver-types.js";
-import { strLit } from "../../../ir/build.js";
+import { numLit, strLit } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
@@ -192,6 +192,25 @@ export function lowerBuiltinExtraProperty(
// +1-or-NULL runtime pair.
if (kind === "child" && (name === "stdin" || name === "stdout" || name === "stderr")) {
const receiver = lowerer.lowerExpr(expr.expression);
+ // Under the general stream representation (general-streams.ts) the
+ // declared stream type maps to a runtime stream class: the read
+ // answers the child's bridged stream (one per stdio slot, so repeated
+ // reads keep identity), fed by the same pipe.
+ const general = lowerer.generalNodeStreams ? generalChildStdioType(lowerer, expr, name) : null;
+ if (general) {
+ const type: IrType = {
+ kind: "union",
+ unionId: lowerer.unions.intern([general, { kind: "nullT" }]),
+ };
+ const read: IrExpr = {
+ kind: "libCall",
+ fn: "child.stdioStream",
+ args: [receiver, numLit(name === "stdin" ? 0 : name === "stdout" ? 1 : 2, loc)],
+ type,
+ loc,
+ };
+ return lowerer.maybeNarrow(read, expr);
+ }
const streamType = name === "stdin" ? CHILDWRITER_T : CHILDSTREAM_T;
const type: IrType = {
kind: "union",
@@ -259,3 +278,24 @@ export function lowerBuiltinExtraProperty(
}
return null;
}
+
+/** The runtime stream class a child stdio read produces under the general
+ * stream representation, or null when the declared type keeps the stdio
+ * handle (the fallback declarations' ChildStdin). */
+function generalChildStdioType(
+ lowerer: Lowerer,
+ expr: ts.PropertyAccessExpression,
+ name: "stdin" | "stdout" | "stderr",
+): IrType | null {
+ const className = name === "stdin" ? "%Writable" : "%Readable";
+ const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
+ if (!mapped) return null;
+ // Checked-dynamic stream slots: the declared stream type maps to dyn
+ // (dyn-streams.ts); the read still produces the bridged stream.
+ if (lowerer.dynNodeStreams && mapped.kind === "dyn") return { kind: "object", className };
+ const arms =
+ mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
+ return arms.some((arm) => arm.kind === "object" && arm.className === className)
+ ? { kind: "object", className }
+ : null;
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/readline.ts b/packages/compiler/src/frontend/lowering/builtins/readline.ts
index bcf77a39b..ede65d788 100644
--- a/packages/compiler/src/frontend/lowering/builtins/readline.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/readline.ts
@@ -46,10 +46,10 @@ function isReadlineModule(spec: string | null): boolean {
export function isReadlineTyped(lowerer: Lowerer, node: ts.Expression): boolean {
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getAliasSymbol() ?? t.getSymbol();
- if (sym?.name !== "Interface") return false;
+ if (sym === undefined || ts.symbolName(sym) !== "Interface") return false;
return sym.declarations.some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
- if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
+ if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false;
return isReadlineModule(ambientModuleOf(d));
});
}
@@ -74,7 +74,7 @@ const RL_EVENT_LIST =
* readline/promises question. */
function returnsPromise(lowerer: Lowerer, call: ts.CallExpression): boolean {
const t = lowerer.checker.getTypeAtLocation(call);
- return (t.getSymbol()?.name ?? "") === "Promise";
+ return (ts.symbolNameOf(t.getSymbol()) ?? "") === "Promise";
}
/** The `{ signal }` options argument of the promise question: the signal
@@ -107,10 +107,10 @@ function questionSignal(lowerer: Lowerer, node: ts.Expression | undefined): ts.E
function isReadlineUiTyped(lowerer: Lowerer, node: ts.Expression): boolean {
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getAliasSymbol() ?? t.getSymbol();
- if (sym?.name !== "Readline") return false;
+ if (sym === undefined || ts.symbolName(sym) !== "Readline") return false;
return sym.declarations.some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
- if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
+ if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false;
const spec = ambientModuleOf(d);
return spec === "readline/promises" || spec === "node:readline/promises";
});
diff --git a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts
index fc507e2bd..230441d31 100644
--- a/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/receiver-types.ts
@@ -10,10 +10,10 @@ export function isBuiltinClassInstance(
): boolean {
const type = lowerer.checker.getTypeAtLocation(node);
const symbol = type.getAliasSymbol() ?? type.getSymbol();
- if (symbol?.name !== name) return false;
+ if (symbol === undefined || ts.symbolName(symbol) !== name) return false;
return symbol.declarations.some(
(declaration) =>
(ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) &&
- lowerer.isStdlibFile(declaration.getSourceFile()),
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration)),
);
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts b/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts
index 4b61f98c1..7db3f8dcc 100644
--- a/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/runtime-constructor.ts
@@ -17,7 +17,7 @@ export function runtimeConstructorReceiver(
let e = expr;
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (!ts.isPropertyAccessExpression(e) || e.questionDotToken) return null;
- if (e.name.text !== "constructor" || isJsSourceFile(e.getSourceFile())) return null;
+ if (e.name.text !== "constructor" || isJsSourceFile(ts.getSourceFileOfNode(e))) return null;
const mapped = lowerer.mapTypeOf(lowerer.typeOf(e.expression));
if (mapped?.kind !== "object") return null;
const info = lowerer.classes.get(mapped.className);
diff --git a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts
index e89b1f07e..7229d5b84 100644
--- a/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/text-codecs.ts
@@ -360,13 +360,15 @@ export function storedTextCodecClassOf(
expr: ts.Expression,
): TextCodecCtor["cls"] | null {
const sym = lowerer.typeOf(expr).getSymbol();
- if (!sym || (sym.name !== "TextEncoder" && sym.name !== "TextDecoder")) return null;
+ if (!sym) return null;
+ const name = ts.symbolName(sym);
+ if (name !== "TextEncoder" && name !== "TextDecoder") return null;
return sym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
- lowerer.isStdlibFile(d.getSourceFile()),
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
)
- ? sym.name
+ ? name
: null;
}
@@ -517,8 +519,9 @@ function directTextCodecCtorOf(lowerer: Lowerer, expr: ts.Expression): TextCodec
if (callee.text !== "TextDecoder" && callee.text !== "TextEncoder") return null;
const sym = lowerer.resolveValueSymbol(callee);
if (!sym || !lowerer.isStdlibSymbol(sym)) return null;
- if (sym.name !== "TextDecoder" && sym.name !== "TextEncoder") return null;
- return { cls: sym.name, ctor };
+ const name = ts.symbolName(sym);
+ if (name !== "TextDecoder" && name !== "TextEncoder") return null;
+ return { cls: name, ctor };
}
/** Inline codec calls avoid allocating a receiver; stored receivers use
diff --git a/packages/compiler/src/frontend/lowering/builtins/timers.ts b/packages/compiler/src/frontend/lowering/builtins/timers.ts
index 9f950cddc..7230d18cd 100644
--- a/packages/compiler/src/frontend/lowering/builtins/timers.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/timers.ts
@@ -154,9 +154,9 @@ function isTimerHandleTyped(
): boolean {
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getAliasSymbol() ?? t.getSymbol();
- if (sym?.name !== name) return false;
+ if (sym === undefined || ts.symbolName(sym) !== name) return false;
return sym.declarations.some(
- (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
+ (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
);
}
function isTimeoutTyped(lowerer: Lowerer, node: ts.Expression): boolean {
diff --git a/packages/compiler/src/frontend/lowering/builtins/url.ts b/packages/compiler/src/frontend/lowering/builtins/url.ts
index 3de973335..dca8a7cc0 100644
--- a/packages/compiler/src/frontend/lowering/builtins/url.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/url.ts
@@ -842,7 +842,7 @@ export function lowerLegacyUrlCall(
if (bi.member === "domainToASCII" || bi.member === "domainToUnicode") {
args.push({ kind: "boolLit", value: expr.arguments.nodes.length === 0, type: BOOL, loc });
} else {
- const warn = !insideNodeModules(expr.getSourceFile().fileName);
+ const warn = !insideNodeModules(ts.getSourceFileOfNode(expr).fileName);
args.push({ kind: "boolLit", value: warn, type: BOOL, loc });
}
return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
diff --git a/packages/compiler/src/frontend/lowering/builtins/v8.ts b/packages/compiler/src/frontend/lowering/builtins/v8.ts
new file mode 100644
index 000000000..3582ce8ae
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/v8.ts
@@ -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 };
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts
index dc61853f8..56347e437 100644
--- a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts
@@ -2,17 +2,44 @@ import * as ts from "../../ts7/adapter.js";
import { type Lowerer, PoisonError } from "../lowerer.js";
import { locOf } from "../../program.js";
import { webAssemblyDiag } from "../../../diagnostics/diagnostic.js";
-import { STRING, type IrExpr } from "../../../ir/ir.js";
+import { BOOL, DYN, F64, STRING, type IrExpr } from "../../../ir/ir.js";
+import { dynUndefinedExpr } from "../../../ir/build.js";
+import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
+import { isWebAssemblyType } from "../../type-mapper.js";
/* The WebAssembly API. scriptc.d.ts declares its types (lib.dom's
- * namespace) so that packages whose declarations mention them typecheck,
- * and the type mapper gives those types the checked-dynamic representation.
- * No module compiles to native code yet, so every reached use of the API
- * value fences at its expression with webAssemblyDiag: a build error in
- * TypeScript sources, a located runtime fence in JavaScript ones. typeof
- * needs no value and folds to Node's answer. */
+ * namespace) and the type mapper gives them the checked-dynamic
+ * representation. Modules whose bytes the build sees are translated to
+ * native code (wasm/translate.ts) and found at run time by the SHA-256 of
+ * their bytes (scr_wasm.c):
+ * - WebAssembly.compile, instantiate and validate calls, new
+ * WebAssembly.Module, Instance, Memory, Table and Global, and
+ * WebAssembly.Module.imports, exports and customSections lower to
+ * wasm.* libCalls; members of the values they produce (exports, memory
+ * buffers, globals, tables) dispatch through the native handles;
+ * - the classes called without `new` throw Node's TypeError
+ * (wasm.withoutNew);
+ * - `v instanceof WebAssembly.` lowers to wasm.is;
+ * - the error classes construct with or without `new` (wasm.errorNew).
+ * Every other reached use of the API value (the classes as values, the
+ * streaming functions, Tag and Exception) still fences at its expression
+ * with webAssemblyDiag: a build error in TypeScript sources, a located
+ * runtime fence in JavaScript ones. typeof needs no value and folds to Node's
+ * answer. A lowering marks the lowerer, so the build translates the
+ * modules it discovers (lowerToIr). */
+/** The classes `wasm.is` answers, by kind. */
+const WASM_CLASSES = [
+ "Module",
+ "Instance",
+ "Memory",
+ "Table",
+ "Global",
+ "CompileError",
+ "LinkError",
+ "RuntimeError",
+] as const;
/** Strips the spellings that leave a value unchanged: parentheses, type
* assertions, `satisfies` and non-null assertions. */
function bare(expr: ts.Expression): ts.Expression {
@@ -130,3 +157,248 @@ export function lowerWebAssemblyTypeof(lowerer: Lowerer, operand: ts.Expression)
}
return null;
}
+
+/** The member `WebAssembly.` names (any spelling of the namespace),
+ * or null. */
+function webAssemblyMember(lowerer: Lowerer, expr: ts.Expression): string | null {
+ const surface = webAssemblySurface(lowerer, expr);
+ if (surface === null || !surface.startsWith("WebAssembly.")) return null;
+ const member = surface.slice("WebAssembly.".length);
+ return member.includes(".") ? null : member;
+}
+
+function lowerArg(lowerer: Lowerer, arg: ts.Expression): IrExpr {
+ return ts.isObjectLiteralExpression(arg)
+ ? lowerDynObjectLiteral(lowerer, arg)
+ : lowerer.lowerExprExpecting(arg, DYN);
+}
+
+/** An argument whose evaluation has no effect: a literal, a plain
+ * identifier, or an empty object or array literal. */
+function isInert(arg: ts.Expression): boolean {
+ const node = bare(arg);
+ if (ts.isObjectLiteralExpression(node)) return node.properties.nodes.length === 0;
+ if (ts.isArrayLiteralExpression(node)) return node.elements.nodes.length === 0;
+ return (
+ ts.isIdentifier(node) ||
+ ts.isNumericLiteral(node) ||
+ node.kind === ts.SyntaxKind.BigIntLiteral ||
+ ts.isStringLiteral(node) ||
+ ts.isNoSubstitutionTemplateLiteral(node) ||
+ node.kind === ts.SyntaxKind.NullKeyword ||
+ node.kind === ts.SyntaxKind.TrueKeyword ||
+ node.kind === ts.SyntaxKind.FalseKeyword
+ );
+}
+
+/** The arguments, padded with undefined to `count`. Extra arguments are
+ * ignored, as the API ignores them, when evaluating them has no effect;
+ * null for spreads or other extra arguments (they stay refused). */
+function lowerArgs(
+ lowerer: Lowerer,
+ args: readonly ts.Expression[],
+ count: number,
+ loc: IrExpr["loc"],
+): IrExpr[] | null {
+ if (args.some(ts.isSpreadElement)) return null;
+ if (args.length > count) {
+ if (!args.slice(count).every(isInert)) return null;
+ args = args.slice(0, count);
+ }
+ const out = args.map((arg) => lowerArg(lowerer, arg));
+ while (out.length < count) out.push(dynUndefinedExpr(loc));
+ return out;
+}
+
+function errorNew(member: string, args: IrExpr[], loc: IrExpr["loc"]): IrExpr {
+ const kind = (WASM_CLASSES as readonly string[]).indexOf(member);
+ return {
+ kind: "libCall",
+ fn: "wasm.errorNew",
+ args: [{ kind: "numLit", value: kind, type: F64, loc }, args[0]!, args[1]!],
+ type: DYN,
+ loc,
+ };
+}
+
+/** The classes that require `new`, in wasm.withoutNew's numbering. */
+const NEW_ONLY = ["Module", "Instance", "Memory", "Table", "Global"];
+
+/** `WebAssembly.Module.imports(module)`, `exports(module)` and
+ * `customSections(module, name)`. Their results are arrays of descriptors
+ * or ArrayBuffers built as checked-dynamic values, then taken into the
+ * call's own type. Null for anything else. */
+function lowerModuleStatic(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
+ const surface = webAssemblySurface(lowerer, call.expression);
+ const name = surface?.startsWith("WebAssembly.Module.")
+ ? surface.slice("WebAssembly.Module.".length)
+ : null;
+ if (name !== "imports" && name !== "exports" && name !== "customSections") return null;
+ const loc = locOf(call);
+ const args = lowerArgs(lowerer, call.arguments.nodes, name === "customSections" ? 2 : 1, loc);
+ if (args === null) return null;
+ lowerer.usesWebAssembly = true;
+ if (name === "customSections") lowerer.usesWasmCustomSections = true;
+ const fn =
+ name === "imports"
+ ? "wasm.moduleImports"
+ : name === "exports"
+ ? "wasm.moduleExports"
+ : "wasm.moduleCustomSections";
+ const result: IrExpr = { kind: "libCall", fn, args, type: DYN, loc };
+ return lowerer.coerceInto(call, result, lowerer.irTypeOf(call));
+}
+
+/** `WebAssembly.compile(bytes)`, `WebAssembly.instantiate(source,
+ * imports?)`, `WebAssembly.validate(bytes)`, the error classes called
+ * without `new`, the other classes called without `new` (a TypeError), and
+ * Module's static functions. Null for anything else. */
+export function lowerWebAssemblyCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
+ if (call.questionDotToken) return null;
+ const member = webAssemblyMember(lowerer, call.expression);
+ if (member === null) return lowerModuleStatic(lowerer, call);
+ const loc = locOf(call);
+ const newOnly = NEW_ONLY.indexOf(member);
+ if (newOnly >= 0) {
+ // The arguments evaluate, then the call throws.
+ const args = lowerArgs(lowerer, call.arguments.nodes, 2, loc);
+ if (args === null) return null;
+ lowerer.usesWebAssembly = true;
+ return {
+ kind: "libCall",
+ fn: "wasm.withoutNew",
+ args: [{ kind: "numLit", value: newOnly, type: F64, loc }, args[0]!, args[1]!],
+ type: DYN,
+ loc,
+ };
+ }
+ const isError = member === "CompileError" || member === "LinkError" || member === "RuntimeError";
+ if (member !== "compile" && member !== "instantiate" && member !== "validate" && !isError)
+ return null;
+ const used = member === "instantiate" || isError ? 2 : 1;
+ const args = lowerArgs(lowerer, call.arguments.nodes, used, loc);
+ if (args === null) return null;
+ lowerer.usesWebAssembly = true;
+ let result: IrExpr;
+ if (isError) result = errorNew(member, args, loc);
+ else if (member === "validate")
+ result = { kind: "libCall", fn: "wasm.validate", args: [args[0]!], type: BOOL, loc };
+ else if (member === "compile")
+ result = {
+ kind: "libCall",
+ fn: "wasm.compile",
+ args: [args[0]!],
+ type: { kind: "promise", inner: DYN },
+ loc,
+ };
+ else
+ result = {
+ kind: "libCall",
+ fn: "wasm.instantiate",
+ args: [args[0]!, args[1]!],
+ type: { kind: "promise", inner: DYN },
+ loc,
+ };
+ return result;
+}
+
+/** `new WebAssembly.Module(bytes)`, `new WebAssembly.Instance(module,
+ * imports?)`, `new WebAssembly.Memory(descriptor)`, `new
+ * WebAssembly.Table(descriptor, value?)`, `new
+ * WebAssembly.Global(descriptor, value?)` and the error classes. Null for
+ * anything else. */
+export function lowerWebAssemblyNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
+ const member = webAssemblyMember(lowerer, expr.expression);
+ if (member === null) return null;
+ const isError = member === "CompileError" || member === "LinkError" || member === "RuntimeError";
+ if (!NEW_ONLY.includes(member) && !isError) return null;
+ const loc = locOf(expr);
+ const used = member === "Module" || member === "Memory" ? 1 : 2;
+ const args = lowerArgs(lowerer, expr.arguments?.nodes ?? [], used, loc);
+ if (args === null) return null;
+ lowerer.usesWebAssembly = true;
+ let result: IrExpr;
+ if (isError) result = errorNew(member, args, loc);
+ else if (member === "Module")
+ result = { kind: "libCall", fn: "wasm.moduleNew", args: [args[0]!], type: DYN, loc };
+ else if (member === "Memory")
+ result = { kind: "libCall", fn: "wasm.memoryNew", args: [args[0]!], type: DYN, loc };
+ else if (member === "Table")
+ result = { kind: "libCall", fn: "wasm.tableNew", args: [args[0]!, args[1]!], type: DYN, loc };
+ else if (member === "Global")
+ result = { kind: "libCall", fn: "wasm.globalNew", args: [args[0]!, args[1]!], type: DYN, loc };
+ else
+ result = {
+ kind: "libCall",
+ fn: "wasm.instanceNew",
+ args: [args[0]!, args[1]!],
+ type: DYN,
+ loc,
+ };
+ return result;
+}
+
+/** `v instanceof WebAssembly.`. Null for anything else. */
+export function lowerWebAssemblyInstanceof(
+ lowerer: Lowerer,
+ expr: ts.BinaryExpression,
+): IrExpr | null {
+ const member = webAssemblyMember(lowerer, expr.right);
+ if (member === null) return null;
+ const kind = (WASM_CLASSES as readonly string[]).indexOf(member);
+ if (kind < 0) return null;
+ const loc = locOf(expr);
+ lowerer.usesWebAssembly = true;
+ const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
+ return {
+ kind: "libCall",
+ fn: "wasm.is",
+ args: [value, { kind: "numLit", value: kind, type: F64, loc }],
+ type: BOOL,
+ loc,
+ };
+}
+
+/** WebAssembly API values: `WebAssembly.compile`, `instantiate` and
+ * `validate` read as functions, the namespace itself (`WebAssembly`,
+ * `globalThis.WebAssembly`), and the global object cast to a type with a
+ * `WebAssembly` member (pi codemode's spelling). Null for anything else,
+ * which refuseWebAssemblyUse then fences. */
+export function lowerWebAssemblyValue(lowerer: Lowerer, expr: ts.Expression): IrExpr | null {
+ const surface = webAssemblySurface(lowerer, expr);
+ if (surface === null) return null;
+ const loc = locOf(expr);
+ const node = bare(expr);
+ const fn = (name: string, args: IrExpr[]): IrExpr => {
+ lowerer.usesWebAssembly = true;
+ return { kind: "libCall", fn: name as "wasm.namespace", args, type: DYN, loc };
+ };
+ if (surface === "globalThis.WebAssembly") {
+ // The cast spelling names the global object, not its member.
+ const cast =
+ (ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) &&
+ isGlobalThis(lowerer, expr.expression);
+ return cast ? fn("wasm.globalView", []) : fn("wasm.namespace", []);
+ }
+ if (surface === "WebAssembly") return fn("wasm.namespace", []);
+ const index = ["WebAssembly.compile", "WebAssembly.instantiate", "WebAssembly.validate"].indexOf(
+ surface,
+ );
+ if (index < 0 || !ts.isPropertyAccessExpression(node)) return null;
+ return fn("wasm.function", [{ kind: "numLit", value: index, type: F64, loc }]);
+}
+
+/** `global.valueOf()` on a receiver typed as a WebAssembly value: a
+ * Global answers its value (the generic dyn.valueOf answers the object
+ * itself for native handles other than Date). Null for other receivers. */
+export function lowerWebAssemblyValueOf(
+ lowerer: Lowerer,
+ receiver: ts.Expression,
+ recv: IrExpr,
+ loc: IrExpr["loc"],
+): IrExpr | null {
+ const ctx = { checker: lowerer.checker, isStdlibFile: lowerer.isStdlibFile };
+ if (!isWebAssemblyType(lowerer.typeOf(receiver), ctx)) return null;
+ lowerer.usesWebAssembly = true;
+ return { kind: "libCall", fn: "wasm.valueOf", args: [recv], type: DYN, loc };
+}
diff --git a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts
index 2a9ff9eef..93cba84c5 100644
--- a/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/webcrypto.ts
@@ -103,7 +103,7 @@ export function fenceWebCryptoMember(
use: "read" | "call",
): IrExpr | null {
if (use !== "read" || !ts.isPropertyAccessExpression(access)) return null;
- if (isJsSourceFile(access.getSourceFile())) return null;
+ if (isJsSourceFile(ts.getSourceFileOfNode(access))) return null;
if (isSubtleReceiver(lowerer, access)) {
lowerer.noLowering("crypto.subtle as a value", access, SUBTLE_HINT);
}
@@ -123,8 +123,8 @@ export function isWebCryptoMemberType(lowerer: Lowerer, type: ts.Type): boolean
const symbol = type.getAliasSymbol() ?? type.getSymbol();
return (
symbol !== undefined &&
- symbol.name !== "Crypto" &&
- WEBCRYPTO_DYN_TYPES.has(symbol.name) &&
+ ts.symbolName(symbol) !== "Crypto" &&
+ WEBCRYPTO_DYN_TYPES.has(ts.symbolName(symbol)) &&
lowerer.isStdlibSymbol(symbol)
);
}
diff --git a/packages/compiler/src/frontend/lowering/builtins/workers.ts b/packages/compiler/src/frontend/lowering/builtins/workers.ts
index 335fdb969..1426041a2 100644
--- a/packages/compiler/src/frontend/lowering/builtins/workers.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/workers.ts
@@ -174,10 +174,10 @@ function propertyNameText(name: ts.PropertyName): string | undefined {
function isWorkerThreadsClass(lowerer: Lowerer, node: ts.Expression, name: string): boolean {
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getSymbol();
- if (sym?.name !== name) return false;
+ if (sym === undefined || ts.symbolName(sym) !== name) return false;
return sym.declarations.some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
- if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
+ if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false;
let up: ts.Node | undefined = d.parent;
while (up) {
if (ts.isModuleDeclaration(up) && ts.isStringLiteral(up.name))
diff --git a/packages/compiler/src/frontend/lowering/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts
index f5600b033..49f7c8ec0 100644
--- a/packages/compiler/src/frontend/lowering/call-arguments.ts
+++ b/packages/compiler/src/frontend/lowering/call-arguments.ts
@@ -1,3 +1,4 @@
+import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js";
import { dynUndefinedExpr } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -26,13 +27,13 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js";
import { type ParamShape } from "./call-signatures.js";
import { arrayValueRead, arrayValueType } from "./array-values.js";
import { buildArrayConversion } from "./coercions/builders.js";
-import { typeKey } from "../type-mapper.js";
+import { restTupleElementTypes, typeKey } from "../type-mapper.js";
function fixedTupleSpreadInfo(
lowerer: Lowerer,
node: ts.Expression,
): { type: IrType & { kind: "record" }; fields: { name: string; type: IrType }[] } | null {
- const type = lowerer.mapTypeOf(lowerer.typeOf(node));
+ const type = forwardedTupleType(lowerer, node) ?? lowerer.mapTypeOf(lowerer.typeOf(node));
if (type?.kind !== "record") return null;
const shape = lowerer.shapes.get(type.shapeId);
if (!shape?.tuple || shape.fields.length === 0) return null;
@@ -42,6 +43,18 @@ function fixedTupleSpreadInfo(
};
}
+/** `...t` over a provably EMPTY tuple bound to a plain name (`[]`, a rest
+ * tuple whose type parameter or `Parameters` resolved to no positions):
+ * it contributes no arguments, and reading the name has no effect. */
+function emptyTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean {
+ let operand = node;
+ while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
+ if (!ts.isIdentifier(operand)) return false;
+ const forwarded = forwardedTupleType(lowerer, operand);
+ if (forwarded) return forwarded.kind === "array";
+ return restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx)?.length === 0;
+}
+
export function completeArgs(
lowerer: Lowerer,
argNodes: readonly ts.Expression[],
@@ -59,7 +72,9 @@ export function completeArgs(
// graph. Ordinary calls and fixed tuples never need that analysis.
const runtimeSpreads = argNodes.filter(
(arg): arg is ts.SpreadElement =>
- ts.isSpreadElement(arg) && !fixedTupleSpreadInfo(lowerer, arg.expression),
+ ts.isSpreadElement(arg) &&
+ !fixedTupleSpreadInfo(lowerer, arg.expression) &&
+ !emptyTupleSpread(lowerer, arg.expression),
);
// Spread operands lowered while choosing the completion, each exactly once.
const loweredSpreads = new Map();
@@ -128,7 +143,7 @@ export function completeArgs(
loc: value.loc,
};
}
- value = checkedIterableSpread(lowerer, value, arg.expression.getText(), loc);
+ value = checkedIterableSpread(lowerer, value, ts.getTextOfNode(arg.expression), loc);
if (spreads === 1 && index === argNodes.length - 1) {
stmts.push({
kind: "exprStmt",
@@ -138,7 +153,7 @@ export function completeArgs(
args: [
ref(),
value,
- { kind: "strLit", value: arg.expression.getText(), type: STRING, loc },
+ { kind: "strLit", value: ts.getTextOfNode(arg.expression), type: STRING, loc },
],
type: VOID,
loc,
@@ -190,6 +205,12 @@ export function completeArgs(
sources.push(arg);
continue;
}
+ if (emptyTupleSpread(lowerer, arg.expression)) continue;
+ const pure = pureForwardedValues(lowerer, arg.expression);
+ if (pure) {
+ for (const value of pure) sources.push({ ir: value });
+ continue;
+ }
const tuple = fixedTupleSpreadInfo(lowerer, arg.expression);
if (!tuple) {
sources.push(arg);
@@ -204,7 +225,13 @@ export function completeArgs(
// values as separately written arguments while the saved tuple stays
// alive through the call.
const spreadLoc = locOf(arg);
- const value = lowerer.coerceInto(arg.expression, lowerer.lowerExpr(arg.expression), tuple.type);
+ // A forwarded tuple seen through type assertions (`...(arguments as
+ // unknown as Parameters)`) spreads the tuple itself.
+ const operand =
+ forwardedTupleType(lowerer, arg.expression) !== null
+ ? stripTypeWrappers(arg.expression)
+ : arg.expression;
+ const value = lowerer.coerceInto(arg.expression, lowerer.lowerExpr(operand), tuple.type);
const saved = lowerer.declareHiddenLocal("%callSpread", tuple.type);
const savedRef = (): IrExpr => ({
kind: "varRef",
@@ -506,9 +533,38 @@ export function completeFuncValueArgs(
shapes.push({ type: rest, mode: "rest" });
return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call);
}
+ // A fixed-tuple spread (`fn(...args)` over a tuple-typed rest binding)
+ // has a compile-time arity: completion flattens it into the positional
+ // slots exactly like a direct call's.
+ if (
+ funcType.rest !== true &&
+ call.arguments.nodes.some(ts.isSpreadElement) &&
+ call.arguments.nodes.every(
+ (arg) =>
+ !ts.isSpreadElement(arg) ||
+ fixedTupleSpreadInfo(lowerer, arg.expression) !== null ||
+ emptyTupleSpread(lowerer, arg.expression),
+ )
+ ) {
+ const shapes: ParamShape[] = funcType.params.map((type) => ({
+ type,
+ mode: omittedArgFor(lowerer, type, loc) ? "omittable" : "required",
+ }));
+ return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call);
+ }
const args = call.arguments.nodes.map((arg, index) =>
lowerer.lowerExprExpecting(arg, funcType.params[index]),
);
+ // Effect-free surplus arguments of a fixed-arity value (`(r: void) =>
+ // void` drops its void slot) evaluate to nothing observable: drop them,
+ // as the callee ignores them.
+ while (
+ funcType.rest !== true &&
+ args.length > funcType.params.length &&
+ isSafeToDiscard(args[args.length - 1]!)
+ ) {
+ args.pop();
+ }
for (let i = args.length; i < funcType.params.length; i++) {
const absent = omittedArgFor(lowerer, funcType.params[i]!, loc);
if (!absent) {
@@ -549,7 +605,7 @@ export function lowerSpreadArgsCall(
// TypeScript reaches this lane only for checked-dynamic variadic callees
// (`(...args: unknown[]) => R`), whose ABI is the JS one.
if (
- !isJsSourceFile(expr.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
!(callee.type.kind === "func" && callee.type.rest === true && callee.type.restAbi === undefined)
)
return null;
@@ -557,7 +613,7 @@ export function lowerSpreadArgsCall(
if (callee.type.kind !== "dyn" && callee.type.kind !== "func") return null;
const calleeName =
ts.isPropertyAccessExpression(expr.expression) || ts.isElementAccessExpression(expr.expression)
- ? expr.expression.getText()
+ ? ts.getTextOfNode(expr.expression)
: ts.isIdentifier(expr.expression)
? expr.expression.text
: "value";
@@ -584,8 +640,8 @@ export function lowerSpreadArgsCall(
if (p.spreadOf !== null) {
// The spelling rides along for V8's nullish spread-call
// TypeError ("v is not iterable (cannot read property ...)").
- spreads.push({ arg: args.length, what: p.spreadOf.getText() });
- args.push(checkedIterableSpread(lowerer, p.v, p.spreadOf.getText(), loc));
+ spreads.push({ arg: args.length, what: ts.getTextOfNode(p.spreadOf) });
+ args.push(checkedIterableSpread(lowerer, p.v, ts.getTextOfNode(p.spreadOf), loc));
} else {
args.push(lowerer.coerceInto(p.node!, p.v, DYN));
}
diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts
index 7570fff88..47cdd834f 100644
--- a/packages/compiler/src/frontend/lowering/call-signatures.ts
+++ b/packages/compiler/src/frontend/lowering/call-signatures.ts
@@ -1,3 +1,8 @@
+import {
+ typedArgumentsCandidate,
+ typedArgumentsShapes,
+ typedArgumentsTarget,
+} from "./typed-arguments.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
@@ -15,6 +20,7 @@ import {
import { isJsSourceFile, isNodeEsmFile } from "../program.js";
import {
genResultRecord,
+ restTupleParamTypes,
isAnyArrayType,
isUnitOnlyTsType,
jsOpenObjectType,
@@ -56,6 +62,10 @@ export interface ParamShape {
bodyType?: IrType;
callDefault?: IrExpr;
entryNarrow?: IrType;
+ /** One flattened position of a fixed-tuple rest parameter
+ * (restTupleShapes): the declaration's last parameter owns every shape
+ * from its index on, and the body binds the tuple (declareParams). */
+ restTuple?: true;
}
export interface FnSig {
@@ -138,7 +148,9 @@ export function documentedParameter(param: ts.ParameterDeclaration): boolean {
return (
param.type !== undefined ||
/@(?:param|type)\b/.test(
- param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()),
+ ts
+ .getSourceFileOfNode(param)
+ .text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)),
)
);
}
@@ -148,7 +160,7 @@ export function checkedJsDocumentedParameter(
lowerer: Lowerer,
param: ts.ParameterDeclaration,
): boolean {
- return lowerer.checkedJsFile(param.getSourceFile()) && documentedParameter(param);
+ return lowerer.checkedJsFile(ts.getSourceFileOfNode(param)) && documentedParameter(param);
}
/** One parameter's ParamShape — the shared signature-shaped collection
@@ -170,10 +182,10 @@ export function checkedJsDocumentedParameter(
export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): ParamShape {
const stage = lowerer.stageSourceType(param.name);
if (stage) return { type: stage, mode: "required" };
- if (isJsSourceFile(param.getSourceFile()) && ts.isSetAccessorDeclaration(param.parent))
+ if (isJsSourceFile(ts.getSourceFileOfNode(param)) && ts.isSetAccessorDeclaration(param.parent))
return { type: DYN, mode: "required" };
if (
- isJsSourceFile(param.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(param)) &&
ts.isConstructorDeclaration(param.parent) &&
!param.dotDotDotToken &&
!checkedJsDocumentedParameter(lowerer, param)
@@ -197,12 +209,14 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
lowerer.isStdlibGlobal(construction.expression, "Promise");
if (
!nativeExecutor &&
- isJsSourceFile(param.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(param)) &&
!param.type &&
!param.dotDotDotToken &&
!ts.isSetAccessorDeclaration(param.parent) &&
!/@(?:param|type)\b/.test(
- param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()),
+ ts
+ .getSourceFileOfNode(param)
+ .text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)),
)
) {
return param.initializer
@@ -211,10 +225,12 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
}
if (
param.initializer &&
- isJsSourceFile(param.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(param)) &&
!param.type &&
!/@(?:param|type)\b/.test(
- param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()),
+ ts
+ .getSourceFileOfNode(param)
+ .text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)),
)
) {
return { type: DYN, mode: "omittable", bodyType: DYN };
@@ -223,10 +239,12 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
// contract. Consume the actual iterable, including surplus elements.
if (
ts.isArrayBindingPattern(param.name) &&
- isJsSourceFile(param.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(param)) &&
!param.type &&
!/@(?:param|type)\b/.test(
- param.getSourceFile().text.slice(param.parent?.pos ?? param.pos, param.getStart()),
+ ts
+ .getSourceFileOfNode(param)
+ .text.slice(param.parent?.pos ?? param.pos, ts.tokenStartOfNode(param)),
)
) {
if (param.dotDotDotToken) return { type: DYN, mode: "dynRest" };
@@ -288,7 +306,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
// ARRAY param the dyn call thunk fills with the call's surplus
// arguments; the binding is that array (dynRest — funcType marks
// `rest`, and the value only ever calls through the boxed thunk).
- if (isJsSourceFile(param.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(param))) {
const restMapped = lowerer.mapTypeOf(lowerer.typeOf(param.name));
// Unmapped JS rest parameters use the native variadic checked-value ABI.
@@ -315,7 +333,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
// including typed arrays passed through checked callbacks. Extracting
// a native array here would copy it and discard element writes.
if (
- isJsSourceFile(param.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(param)) &&
!param.type &&
(raw.kind === "array" ||
(raw.kind === "union" &&
@@ -327,7 +345,7 @@ export function paramShape(lowerer: Lowerer, param: ts.ParameterDeclaration): Pa
}
let type = lowerer.runtimeOptionalBindingType(param.name, lowerer.irTypeOf(param.name));
const body = param.parent?.body;
- if (isJsSourceFile(param.getSourceFile()) && body && type.kind !== "dyn") {
+ if (isJsSourceFile(ts.getSourceFileOfNode(param)) && body && type.kind !== "dyn") {
const symbol = lowerer.checker.getSymbolAtLocation(param.name);
let acceptsUndefined = false;
ts.walkPreorder(body, (node) => {
@@ -373,6 +391,28 @@ export function isThisParameter(param: ts.ParameterDeclaration): boolean {
return ts.isIdentifier(param.name) && param.name.text === "this";
}
+/** A TypeScript rest parameter bound to a name whose type is a fixed tuple
+ * (`...args: [A, B]`, `...args: A` with A bound to a tuple, `...args:
+ * Parameters` with F bound): one required shape per position. Call
+ * sites and function values see the equivalent fixed signature; the body
+ * rebuilds the tuple (declareParams) unless it only forwards it. Null for
+ * every other parameter. */
+export function restTupleShapes(
+ lowerer: Lowerer,
+ param: ts.ParameterDeclaration,
+ tsType?: ts.Type,
+): ParamShape[] | null {
+ if (
+ param.dotDotDotToken === undefined ||
+ !ts.isIdentifier(param.name) ||
+ isJsSourceFile(ts.getSourceFileOfNode(param))
+ ) {
+ return null;
+ }
+ const elems = restTupleParamTypes(tsType ?? lowerer.typeOf(param.name), lowerer.typeCtx);
+ return elems ? elems.map((type) => ({ type, mode: "required", restTuple: true })) : null;
+}
+
/** ParamShapes for a whole parameter list. */
export function paramShapes(
lowerer: Lowerer,
@@ -380,21 +420,48 @@ export function paramShapes(
signature?: ts.Signature,
blameOf?: (param: ts.ParameterDeclaration, index: number) => ts.Node,
): ParamShape[] {
- if (!signature)
- return params.filter((p) => !isThisParameter(p)).map((param) => lowerer.paramShape(param));
+ const out: ParamShape[] = [];
+ if (!signature) {
+ for (const param of params) {
+ if (isThisParameter(param)) continue;
+ const flattened = restTupleShapes(lowerer, param);
+ if (flattened) out.push(...flattened);
+ else out.push(lowerer.paramShape(param));
+ }
+ return out;
+ }
const symbols = signature.getParameters();
- return params.map((declParam, i) => {
+ const shapeAt = (declParam: ts.ParameterDeclaration, i: number): ParamShape => {
const stage = lowerer.stageSourceType(declParam.name);
if (stage) return { type: stage, mode: "required" as const };
const symbol = symbols[i];
const tsType = symbol
? lowerer.checker.getTypeOfSymbol(symbol)
: lowerer.typeOf(declParam.name);
+ // A call-site instantiation whose concrete argument type does not map
+ // is that argument's blocker: name it there, with its concrete type,
+ // rather than as the declaration's unbound type parameter.
+ if (
+ blameOf !== undefined &&
+ !isJsSourceFile(ts.getSourceFileOfNode(declParam)) &&
+ (tsType.flags & ts.TypeFlags.Any) === 0 &&
+ declParam.type !== undefined &&
+ mentionsTypeParameter(lowerer, lowerer.checker.getTypeFromTypeNode(declParam.type)) &&
+ lowerer.mapTypeOf(tsType) === null
+ ) {
+ lowerer.badType(blameOf(declParam, i), tsType);
+ }
let mapped = jsOpenObjectType(
declParam,
lowerer.runtimeOptionalBindingType(
declParam.name,
- lowerer.mapTypeOf(tsType) ?? lowerer.irTypeOf(declParam.name),
+ lowerer.mapTypeOf(tsType) ??
+ // A TypeScript instantiation at `any` (a type argument inferred
+ // from an untyped value) takes the checked-dynamic slot.
+ ((tsType.flags & ts.TypeFlags.Any) !== 0 &&
+ !isJsSourceFile(ts.getSourceFileOfNode(declParam))
+ ? DYN
+ : lowerer.irTypeOf(declParam.name)),
),
lowerer.shapes,
lowerer.unions,
@@ -428,7 +495,32 @@ export function paramShapes(
);
}
return { type: mapped, mode: declParam.questionToken ? "omittable" : "required" };
+ };
+ params.forEach((declParam, i) => {
+ const symbol = symbols[i];
+ const flattened = restTupleShapes(
+ lowerer,
+ declParam,
+ symbol ? lowerer.checker.getTypeOfSymbol(symbol) : undefined,
+ );
+ if (flattened) out.push(...flattened);
+ else out.push(shapeAt(declParam, i));
});
+ return out;
+}
+
+/** A declared type that is (or is a union over) a bare type parameter
+ * or an instantiation of a generic alias/interface over type arguments. */
+function mentionsTypeParameter(lowerer: Lowerer, t: ts.Type): boolean {
+ if (t.flags & ts.TypeFlags.TypeParameter) return true;
+ const mentions = (part: ts.Type): boolean => mentionsTypeParameter(lowerer, part);
+ if (t.isUnionType()) return ts.constituentTypes(t).some(mentions);
+ if (t.getAliasTypeArguments().some(mentions)) return true;
+ return (
+ (t.flags & ts.TypeFlags.Object) !== 0 &&
+ ((t as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) !== 0 &&
+ lowerer.checker.getTypeArguments(t as ts.TypeReference).some(mentions)
+ );
}
/** A default expression may itself yield undefined. Keep that arm in the
@@ -585,7 +677,7 @@ export function requireExactArityValue(
const ownType = lowerer.typeOf(contextual);
mappedFn =
lowerer.mapTypeOf(ownType) ??
- (isJsSourceFile(contextual.getSourceFile())
+ (isJsSourceFile(ts.getSourceFileOfNode(contextual))
? dynFallbackType(lowerer, contextual, ownType)
: null);
}
@@ -623,7 +715,7 @@ export function requireExactArityValue(
target === undefined &&
contextual !== null &&
(ts.isArrowFunction(contextual) || ts.isFunctionExpression(contextual)) &&
- npmStaticPackageOfPath(contextual.getSourceFile().fileName) !== null
+ npmStaticPackageOfPath(ts.getSourceFileOfNode(contextual).fileName) !== null
) {
return;
}
@@ -688,7 +780,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara
// routes generic declarations (direct calls, namespace/CJS member
// calls, value references) resolves it through genericFnsBySymbol
// unchanged.
- if (implicitMonoFile(decl.getSourceFile()) && !isAsync && !isGenerator) {
+ if (implicitMonoFile(ts.getSourceFileOfNode(decl)) && !isAsync && !isGenerator) {
const implicit = implicitAnyParamSymbolsOf(lowerer, decl);
if (implicit) {
const nameText = decl.name?.text ?? "%default";
@@ -697,7 +789,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara
lowerer.genericFnsBySymbol.set(symbol, {
decl,
baseName: nameText,
- qualifiedName: lowerer.qualify(decl.getSourceFile(), nsPathPrefix(decl) + nameText),
+ qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText),
typeParams: [],
instances: new Map(),
implicitParams: implicit,
@@ -732,7 +824,7 @@ export function collectSignatureInner(lowerer: Lowerer, decl: ts.FunctionDeclara
// collide. The anonymous default export takes the synthetic
// "%default" spelling ('%' cannot appear in a user identifier).
name: lowerer.qualify(
- decl.getSourceFile(),
+ ts.getSourceFileOfNode(decl),
nsPathPrefix(decl) + (decl.name?.text ?? "%default"),
),
params,
@@ -761,7 +853,7 @@ function appendArgumentsParameter(
ts.isFunctionDeclaration(node) ||
ts.isMethodDeclaration(node)
) ||
- !isJsSourceFile(node.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(node)) ||
!bodyReadsArguments(node)
)
return;
@@ -771,7 +863,7 @@ function appendArgumentsParameter(
(ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent));
if (
!classMethod &&
- !isNodeEsmFile(node.getSourceFile(), lowerer.program) &&
+ !isNodeEsmFile(ts.getSourceFileOfNode(node), lowerer.program) &&
!unmappedArguments(node)
) {
lowerer.unsupported(
@@ -828,11 +920,21 @@ export function lambdaSignature(
| ts.SetAccessorDeclaration,
): { shapes: ParamShape[]; funcType: IrType & { kind: "func" } } {
if (!node.body) lowerer.unsupported("SC1090", node, "function overload signatures");
+ // A parameterless function expression forwarding its `arguments`,
+ // asserted to a fixed signature: that signature is its ABI.
+ if (typedArgumentsCandidate(node) && !node.typeParameters?.nodes.length) {
+ const target = typedArgumentsTarget(lowerer, node);
+ if (target) {
+ const shapes = typedArgumentsShapes(target);
+ lowerer.typedArgumentsLambdas.add(node);
+ return { shapes, funcType: funcTypeFromParamShapes(shapes, target.ret) };
+ }
+ }
// A JS replacement of a native stream's write method receives the
// actual call tuple, including overloads and omitted arguments. Node's
// contextual overload signature cannot define this closure's ABI.
if (
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
!node.type &&
(ts.isFunctionExpression(node) || ts.isArrowFunction(node)) &&
node.parameters.nodes.every(
@@ -950,7 +1052,7 @@ export function lambdaSignature(
: [];
const ctxDecl = ctxSigs.length === 1 ? ctxSigs[0]!.declaration : undefined;
const ctxRetRaw =
- ctxDecl && !ctxDecl.getSourceFile().isDeclarationFile
+ ctxDecl && !ts.getSourceFileOfNode(ctxDecl).isDeclarationFile
? lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(ctxSigs[0]!))
: null;
// An async lambda always produces a promise, so a slot returning a
@@ -972,7 +1074,7 @@ export function lambdaSignature(
// must match the slot's ABI exactly.
const dynVariadicSlot =
ctxRet?.kind === "dyn" &&
- !isJsSourceFile(node.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(node)) &&
shapes.some((shape) => shape.mode === "dynRest");
if (
dynVariadicSlot ||
diff --git a/packages/compiler/src/frontend/lowering/checked-promise-all.ts b/packages/compiler/src/frontend/lowering/checked-promise-all.ts
index c3be91833..6c24a8b2c 100644
--- a/packages/compiler/src/frontend/lowering/checked-promise-all.ts
+++ b/packages/compiler/src/frontend/lowering/checked-promise-all.ts
@@ -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)) {
diff --git a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts
index f06dde5a2..30030b18f 100644
--- a/packages/compiler/src/frontend/lowering/cjs-exports-object.ts
+++ b/packages/compiler/src/frontend/lowering/cjs-exports-object.ts
@@ -40,12 +40,12 @@ import { DYN, type IrExpr, type IrGlobal } from "../../ir/ir.js";
* source declaration) in a CommonJS JS file. */
export function isCjsExportsIdentifier(lowerer: Lowerer, node: ts.Expression): boolean {
if (!ts.isIdentifier(node) || node.text !== "exports") return false;
- if (!isCjsJsFile(node.getSourceFile(), lowerer.program)) return false;
+ if (!isCjsJsFile(ts.getSourceFileOfNode(node), lowerer.program)) return false;
const symbol = lowerer.checker.getSymbolAtLocation(node);
const decls = symbol ? symbol.declarations : [];
return !decls.some(
(d) =>
- !d.getSourceFile().isDeclarationFile &&
+ !ts.getSourceFileOfNode(d).isDeclarationFile &&
(ts.isVariableDeclaration(d) ||
ts.isParameter(d) ||
ts.isBindingElement(d) ||
@@ -62,7 +62,7 @@ export function isCjsModuleExports(lowerer: Lowerer, node: ts.Expression): boole
return (
isModuleExportsAccess(node) &&
isCjsModuleGlobal(lowerer, node.expression) &&
- isCjsJsFile(node.getSourceFile(), lowerer.program)
+ isCjsJsFile(ts.getSourceFileOfNode(node), lowerer.program)
);
}
@@ -75,7 +75,7 @@ export function cjsInFileExportGlobalOf(
access: ts.PropertyAccessExpression,
): IrGlobal | null {
if (access.questionDotToken || !ts.isIdentifier(access.name)) return null;
- const sf = access.getSourceFile();
+ const sf = ts.getSourceFileOfNode(access);
const recv =
isModuleExportsAccess(access.expression) ||
(ts.isIdentifier(access.expression) &&
@@ -186,7 +186,7 @@ function exportsAssignments(
* it runs: `exports` unless the file rebinds it; `module.exports` unless the
* file rebinds `exports` or replaces `module.exports`. */
function namesOriginalExports(lowerer: Lowerer, node: ts.Expression): boolean {
- const assigned = exportsAssignments(lowerer, node.getSourceFile());
+ const assigned = exportsAssignments(lowerer, ts.getSourceFileOfNode(node));
if (assigned.exports) return false;
return ts.isIdentifier(node) || !assigned.moduleExports;
}
@@ -252,7 +252,7 @@ export function usesCjsExportsIdentity(lowerer: Lowerer, sf: ts.SourceFile): boo
* refusal. Null when `node` is not such a use. */
export function lowerCjsExportsObjectValue(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
if (!isCjsExportsObjectValue(lowerer, node)) return null;
- const sf = node.getSourceFile();
+ const sf = ts.getSourceFileOfNode(node);
// A module that replaces module.exports answers `module.exports` reads
// through the replacement's own machinery (the class-expression whole
// export, the replaced-exports reads).
@@ -328,7 +328,7 @@ export function cjsExportMemberWriteGlobalOf(
lowerer: Lowerer,
target: ts.PropertyAccessExpression,
): IrGlobal | null {
- const sf = target.getSourceFile();
+ const sf = ts.getSourceFileOfNode(target);
if (!isCjsJsFile(sf, lowerer.program)) return null;
const assigned = exportsAssignments(lowerer, sf);
if (assigned.exports || assigned.moduleExports) return null;
@@ -458,7 +458,7 @@ export function cjsMemberAliasValueOf(lowerer: Lowerer, ref: ts.Identifier): ts.
)
return null;
const spec = importDecl.moduleSpecifier.text;
- const from = importDecl.getSourceFile();
+ const from = ts.getSourceFileOfNode(importDecl);
const dep =
resolveImport(lowerer.program, from, spec, "import") ??
npmStaticDepSf7(lowerer.program, from, spec, "import");
@@ -552,7 +552,7 @@ function rereadableMemberValue(
const sym = lowerer.resolveValueSymbol(value);
if (sym === null || sym.declarations.length !== 1) return false;
const decl = sym.declarations[0]!;
- if (decl.getSourceFile() !== stmt.getSourceFile()) return false;
+ if (ts.getSourceFileOfNode(decl) !== ts.getSourceFileOfNode(stmt)) return false;
// A function declaration is initialized when the module starts; any
// other binding must be initialized before the write stores it.
if (!ts.isFunctionDeclaration(decl) && decl.end > stmt.pos) return false;
diff --git a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts
index 174f9dc7c..2b28c6533 100644
--- a/packages/compiler/src/frontend/lowering/cjs-inline-require.ts
+++ b/packages/compiler/src/frontend/lowering/cjs-inline-require.ts
@@ -56,7 +56,7 @@ export function inlineProgramRequireOf(
if (!ts.isCallExpression(e) || !ts.isIdentifier(e.expression)) return null;
const spec = requireSpecOf(e);
if (spec === null || canonicalBuiltinModule(spec) !== null) return null;
- const sf = e.getSourceFile();
+ const sf = ts.getSourceFileOfNode(e);
if (!isCjsJsFile(sf, lowerer.program)) return null;
if (!isCjsRequireGlobal(lowerer, e.expression)) return null;
if (createRequireCalleeFileOf(lowerer, e.expression) !== null) return null;
@@ -201,7 +201,7 @@ export function lowerUncompiledRequireCall(
call: ts.CallExpression,
): IrExpr | null {
if (!ts.isIdentifier(call.expression) || call.expression.text !== "require") return null;
- const sf = call.getSourceFile();
+ const sf = ts.getSourceFileOfNode(call);
if (!isCjsJsFile(sf, lowerer.program) || !isCjsRequireGlobal(lowerer, call.expression))
return null;
if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null;
@@ -240,7 +240,7 @@ export function requireNestedPatternsOf(decl: ts.VariableDeclaration): ts.Bindin
if (!ts.isObjectBindingPattern(decl.name) || decl.initializer === undefined) return [];
const spec = requireSpecOf(decl.initializer);
if (spec === null || canonicalBuiltinModule(spec) !== null) return [];
- if (!isJsSourceFile(decl.getSourceFile())) return [];
+ if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) return [];
return decl.name.elements.nodes.filter(
(e) =>
e.name !== undefined &&
diff --git a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts
index cd83c8709..aae221b30 100644
--- a/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts
+++ b/packages/compiler/src/frontend/lowering/cjs-tsc-interop.ts
@@ -175,7 +175,7 @@ export function tscInteropDefaultValueOf(
export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boolean {
const name = tscHelperDeclName(stmt);
if (name === null) return false;
- const sf = stmt.getSourceFile();
+ const sf = ts.getSourceFileOfNode(stmt);
const helperSpans = new Map();
for (const s of sf.statements.nodes) {
const n = tscHelperDeclName(s);
@@ -192,11 +192,11 @@ export function isDeadTscHelperDecl(lowerer: Lowerer, stmt: ts.Statement): boole
const parent = node.parent;
if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node)
return undefined;
- const at = node.getStart(sf);
- if (at >= decl.getStart(sf) && at < decl.getEnd()) return undefined;
+ const at = ts.tokenStartOfNode(node);
+ if (at >= ts.tokenStartOfNode(decl) && at < decl.end) return undefined;
for (const [other, otherDecl] of helperSpans) {
if (other !== helper && dead.has(other)) {
- if (at >= otherDecl.getStart(sf) && at < otherDecl.getEnd()) return undefined;
+ if (at >= ts.tokenStartOfNode(otherDecl) && at < otherDecl.end) return undefined;
}
}
const call = parent;
diff --git a/packages/compiler/src/frontend/lowering/class-callbacks.ts b/packages/compiler/src/frontend/lowering/class-callbacks.ts
index 859938845..6ca685684 100644
--- a/packages/compiler/src/frontend/lowering/class-callbacks.ts
+++ b/packages/compiler/src/frontend/lowering/class-callbacks.ts
@@ -39,8 +39,8 @@ export function isClassCallback(lowerer: Lowerer, info: ClassInfo, name: string)
!found.sig.abstract &&
(lowerer.prototypeMethodAccesses.has(name) ||
(info.decl !== null &&
- isJsSourceFile(info.decl.getSourceFile()) &&
- !lowerer.checkedJsFile(info.decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
+ !lowerer.checkedJsFile(ts.getSourceFileOfNode(info.decl)) &&
found.sig.params.length === 0 &&
found.sig.ret.kind === "void"))
);
@@ -171,7 +171,7 @@ export function classCallbackCall(
const spreads: { arg: number; what: string }[] = [];
const args = argumentNodes.map((argument, index) => {
if (!ts.isSpreadElement(argument)) return lowerer.lowerExprExpecting(argument, DYN);
- const spelling = argument.expression.getText();
+ const spelling = ts.getTextOfNode(argument.expression);
spreads.push({ arg: index, what: spelling });
return checkedIterableSpread(lowerer, lowerer.lowerExpr(argument.expression), spelling, loc);
});
diff --git a/packages/compiler/src/frontend/lowering/class-construction.ts b/packages/compiler/src/frontend/lowering/class-construction.ts
index 7f4a003c9..30f504b4a 100644
--- a/packages/compiler/src/frontend/lowering/class-construction.ts
+++ b/packages/compiler/src/frontend/lowering/class-construction.ts
@@ -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: {
diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
index 2587a4b65..de84e3fed 100644
--- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
+++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts
@@ -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";
@@ -1157,7 +1158,7 @@ export class ClassDynamicDispatch {
if (this.bagClasses.has(plan.key)) continue;
this.bagClasses.add(plan.key);
this.ensurePropertyBag(info);
- if (info.decl && isJsSourceFile(info.decl.getSourceFile())) {
+ if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) {
let root = info;
while (root.base && !root.base.def.runtime) root = root.base;
const track = (current: ClassInfo): void => {
@@ -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,
};
@@ -3195,7 +3246,7 @@ export class ClassDynamicDispatch {
for (const name of this.boxedConstructors) {
if (this.staticBagClasses.has(name)) continue;
const info = lowerer.classes.get(name);
- if (!info?.decl || !isJsSourceFile(info.decl.getSourceFile())) continue;
+ if (!info?.decl || !isJsSourceFile(ts.getSourceFileOfNode(info.decl))) continue;
const loc = this.staticPropertyBag.loc;
const bag = classStaticData(lowerer, info, loc);
if (!bag) continue;
@@ -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.` 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 = new Set([
+ "valueOf",
+ "toString",
+ "hasOwnProperty",
+ "propertyIsEnumerable",
+]);
diff --git a/packages/compiler/src/frontend/lowering/class-iterables.ts b/packages/compiler/src/frontend/lowering/class-iterables.ts
new file mode 100644
index 000000000..8c58fc0bd
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/class-iterables.ts
@@ -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
+ );
+}
diff --git a/packages/compiler/src/frontend/lowering/class-method-mutations.ts b/packages/compiler/src/frontend/lowering/class-method-mutations.ts
index c07ec9136..cc8700469 100644
--- a/packages/compiler/src/frontend/lowering/class-method-mutations.ts
+++ b/packages/compiler/src/frontend/lowering/class-method-mutations.ts
@@ -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();
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
@@ -142,10 +152,11 @@ export function collectClassMethodMutations(
const required = properties.filter(
(property) => (property.flags & ts.SymbolFlags.Optional) === 0,
);
- if (required.some((property) => !FUNCTION_MEMBER_NAMES.has(property.name))) return true;
+ if (required.some((property) => !FUNCTION_MEMBER_NAMES.has(ts.symbolName(property))))
+ return true;
return (
required.length === 0 &&
- properties.every((property) => !FUNCTION_MEMBER_NAMES.has(property.name))
+ properties.every((property) => !FUNCTION_MEMBER_NAMES.has(ts.symbolName(property)))
);
};
// A string index whose values cannot be functions excludes callable
@@ -157,7 +168,7 @@ export function collectClassMethodMutations(
.getIndexInfosOfType(lowerer.typeOf(value))
.some(
(index) =>
- (index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.valueType),
+ (index.keyType.flags & ts.TypeFlags.StringLike) !== 0 && functionFree(index.type),
);
const literalLoopKeys = (key: ts.Expression): (string | number)[] | null => {
if (!ts.isIdentifier(key)) return null;
@@ -555,6 +566,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 +609,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);
diff --git a/packages/compiler/src/frontend/lowering/class-method-values.ts b/packages/compiler/src/frontend/lowering/class-method-values.ts
index 795307c93..1197d9bdd 100644
--- a/packages/compiler/src/frontend/lowering/class-method-values.ts
+++ b/packages/compiler/src/frontend/lowering/class-method-values.ts
@@ -92,7 +92,8 @@ export function lowerClassMethodValue(
loc,
};
};
- const checked = isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(value.type);
+ const checked =
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(value.type);
if (checked) value = lowerer.coerceToExpected(value, DYN);
// Select the declaration when extracting the value. Calling the value
// later must not redispatch the method name on a different receiver.
@@ -374,7 +375,7 @@ function reflectedMethodValue(
if (!member || (!ts.isMethodDeclaration(member) && !ts.isAccessor(member)) || !member.body)
return null;
let needsNativeReceiver =
- !isJsSourceFile(declaration.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(declaration)) ||
info.localClass !== undefined ||
lowerer.errorHierarchyClassOf(info.def.name);
ts.walkPreorder(member, (node) => {
diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts
index 0c9a24f24..33515bf2c 100644
--- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts
+++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts
@@ -87,7 +87,7 @@ function codeOwner(node: ts.Node): ts.Node {
!ts.isPropertyDeclaration(current)
)
current = current.parent;
- return current ?? node.getSourceFile();
+ return current ?? ts.getSourceFileOfNode(node);
}
/** A local variable or parameter that only its own function's code
@@ -102,21 +102,21 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean {
const decl = symbol.valueDeclaration;
let owned =
decl !== undefined &&
- decl.getSourceFile() === id.getSourceFile() &&
+ ts.getSourceFileOfNode(decl) === ts.getSourceFileOfNode(id) &&
((ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) ||
(ts.isParameter(decl) && ts.isIdentifier(decl.name)));
if (owned && decl) {
const owner = codeOwner(decl);
const names = (node: ts.Node): boolean =>
ts.isIdentifier(node) &&
- node.text === symbol.name &&
+ node.text === ts.symbolName(symbol) &&
lowerer.resolveValueSymbol(node) === symbol;
const written = (target: ts.Node): boolean => {
let found = false;
const walk = (node: ts.Node): void => {
if (found) return;
if (names(node)) found = true;
- else node.forEachChild(walk);
+ else ts.forEachChild(node, walk);
};
walk(target);
return found;
@@ -144,9 +144,9 @@ function ownedBinding(lowerer: Lowerer, id: ts.Identifier): boolean {
)
target = node.initializer;
if (target && codeOwner(node) !== owner && written(target)) owned = false;
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- owner.forEachChild(visit);
+ ts.forEachChild(owner, visit);
}
lowerer.protocolOwnedBindings.set(symbol, owned);
return owned;
@@ -188,7 +188,8 @@ function protocolSignature(
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): ProtocolSignature | null {
- const recvTs = lowerer.typeOf(access.expression);
+ // A guarded optional step's receiver still carries its nullish arms.
+ const recvTs = lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression));
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
if (!property) return null;
const declarations = property.declarations;
@@ -240,10 +241,11 @@ export function lowerProtocolMethodCall(
access: ts.PropertyAccessExpression,
recv: IrExpr,
): IrExpr | null {
+ // An optional chain that already guarded this step (`s?.m(x)`, the chain
+ // re-dispatching with the narrowed receiver) calls like a plain one.
if (
recv.type.kind !== "dyn" ||
- call.questionDotToken ||
- access.questionDotToken ||
+ lowerer.chainBlocked(call, access) ||
call.arguments.nodes.some(ts.isSpreadElement)
)
return null;
@@ -300,7 +302,7 @@ export function lowerProtocolMethodCall(
kind: "dynCall",
callee: probed ? varRef("p.callee", DYN, loc) : read(),
receiver: varRef("p.0", DYN, loc),
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
calleeNameValue: varRef("p.calleeName", STRING, loc),
args: params.map((type, i) => lowerer.coerceToExpected(varRef(`p.${i + 1}`, type, loc), DYN)),
type: DYN,
@@ -376,7 +378,7 @@ export function lowerProtocolMethodCall(
receiver,
...(callee ? [varRef(callee.id, DYN, loc)] : []),
...args,
- { kind: "strLit", value: access.getText(), type: STRING, loc },
+ { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc },
],
type: result,
loc,
@@ -485,7 +487,7 @@ export function lowerProtocolPropertyRead(
lowerer: Lowerer,
access: ts.PropertyAccessExpression,
): IrExpr | null {
- if (isJsSourceFile(access.getSourceFile())) return null;
+ if (isJsSourceFile(ts.getSourceFileOfNode(access))) return null;
const receiverType = lowerer.typeOf(access.expression);
if (!lowerer.classImplementedProtocol(receiverType)) return null;
const name = access.name.text;
diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts
index 5a27f0815..4de7f03ad 100644
--- a/packages/compiler/src/frontend/lowering/class-protocols.ts
+++ b/packages/compiler/src/frontend/lowering/class-protocols.ts
@@ -46,9 +46,9 @@ export class ClassProtocols {
)
)
continue;
- const entries = this.methods.get(property.name) ?? [];
+ const entries = this.methods.get(ts.symbolName(property)) ?? [];
entries.push(node);
- this.methods.set(property.name, entries);
+ this.methods.set(ts.symbolName(property), entries);
}
}
if (
@@ -472,7 +472,7 @@ export class ClassProtocols {
if (facts.observed.has(symbol) || facts.written.has(symbol)) return true;
const target = facts.targets.get(symbol);
if (target === undefined) return true;
- const names = new Set(this.checker.getPropertiesOfType(target).map((p) => p.name));
+ const names = new Set(this.checker.getPropertiesOfType(target).map((p) => ts.symbolName(p)));
const origins = facts.origins.get(symbol) ?? new Set();
const writesNames = (written: Set | "all"): boolean =>
written === "all" || [...written].some((name) => names.has(name));
@@ -517,7 +517,7 @@ export class ClassProtocols {
.getPropertiesOfType(declared)
.some(
(property) =>
- names.has(property.name) &&
+ names.has(ts.symbolName(property)) &&
property.declarations.some(
(declaration) =>
ts.isGetAccessorDeclaration(declaration) || ts.isSetAccessorDeclaration(declaration),
@@ -575,7 +575,9 @@ export class ClassProtocols {
return (
symbol !== undefined &&
(symbol.flags & ts.SymbolFlags.Class) !== 0 &&
- symbol.declarations.some((declaration) => !declaration.getSourceFile().isDeclarationFile) &&
+ symbol.declarations.some(
+ (declaration) => !ts.getSourceFileOfNode(declaration).isDeclarationFile,
+ ) &&
// `typeof C` (the class value) shares the class symbol.
this.checker.getConstructSignatures(type).length === 0
);
@@ -598,7 +600,8 @@ export class ClassProtocols {
this.methods.size > 0 &&
symbol.declarations.some(
(declaration) =>
- ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile,
+ ts.isInterfaceDeclaration(declaration) &&
+ !ts.getSourceFileOfNode(declaration).isDeclarationFile,
) &&
this.protocol(type)
);
@@ -617,7 +620,7 @@ export class ClassProtocols {
const declarations = symbol.declarations;
return (
declarations.length > 0 &&
- declarations.every((declaration) => !declaration.getSourceFile().isDeclarationFile)
+ declarations.every((declaration) => !ts.getSourceFileOfNode(declaration).isDeclarationFile)
);
}
@@ -704,14 +707,15 @@ export class ClassProtocols {
!symbol ||
!symbol.declarations.some(
(declaration) =>
- ts.isInterfaceDeclaration(declaration) && !declaration.getSourceFile().isDeclarationFile,
+ ts.isInterfaceDeclaration(declaration) &&
+ !ts.getSourceFileOfNode(declaration).isDeclarationFile,
)
)
return false;
for (const property of properties) {
const memberType = this.checker.getTypeOfSymbol(property);
if (this.checker.getCallSignatures(memberType).length === 0) continue;
- for (const declaration of this.methods.get(property.name) ?? []) {
+ for (const declaration of this.methods.get(ts.symbolName(property)) ?? []) {
const classSymbol = declaration.name
? this.checker.getSymbolAtLocation(declaration.name)
: undefined;
diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts
index d337dc802..6c6c7811d 100644
--- a/packages/compiler/src/frontend/lowering/class-prototypes.ts
+++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
+import { builtinClassKind, builtinClassValue } from "./builtins/builtin-class-dyn.js";
import * as ts from "../ts7/adapter.js";
import {
BOOL,
@@ -450,6 +451,19 @@ export function lowerClassPrototypeData(
exactClassOfReceiver(lowerer, expr.expression) ??
(stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined);
if (!info) return null;
+ // EventEmitter.prototype and the stream prototypes: the untyped
+ // constructor's prototype object (builtin-class-dyn.ts).
+ const builtinKind = builtinClassKind(info.def.name);
+ if (builtinKind !== null) {
+ const loc = locOf(expr);
+ return {
+ kind: "dynKeyGet",
+ value: builtinClassValue(builtinKind, loc),
+ key: { kind: "strLit", value: "prototype", type: STRING, loc },
+ type: DYN,
+ loc,
+ };
+ }
const parent = expr.parent;
const descriptorKey =
ts.isCallExpression(parent) &&
diff --git a/packages/compiler/src/frontend/lowering/class-static-data.ts b/packages/compiler/src/frontend/lowering/class-static-data.ts
index af5425727..dd6927d18 100644
--- a/packages/compiler/src/frontend/lowering/class-static-data.ts
+++ b/packages/compiler/src/frontend/lowering/class-static-data.ts
@@ -38,7 +38,7 @@ export function classStaticDataFor(
): IrExpr | null {
if (
!info.decl ||
- !isJsSourceFile(info.decl.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(info.decl)) ||
name.startsWith("#") ||
constructorMembers.has(name)
)
diff --git a/packages/compiler/src/frontend/lowering/code-generation.ts b/packages/compiler/src/frontend/lowering/code-generation.ts
new file mode 100644
index 000000000..c3a5c0ea7
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/code-generation.ts
@@ -0,0 +1,146 @@
+/* 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 !== undefined &&
+ ts.symbolName(symbol) === "Function" &&
+ lowerer.isStdlibSymbol(symbol)
+ ) {
+ return isJsSourceFile(ts.getSourceFileOfNode(expr)) ? 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;
+}
diff --git a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts
index 445014b88..3d8817ab1 100644
--- a/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts
+++ b/packages/compiler/src/frontend/lowering/coercions/function-adapters.ts
@@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../../errors.js";
import type { IrExpr, IrParam, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js";
import {
canAdaptDynFuncTo,
+ canDynCheckTo,
DYN,
F64,
isUnitType,
@@ -341,6 +342,19 @@ export function funcCoerceAdapter(
voidRet = toT.ret.kind === "dyn" ? "dyn" : "strand";
}
}
+ // An untyped result (a generic wrapper's `any` handed back `as F`) takes
+ // the checked-dynamic extraction to the slot's result: a record checks
+ // its shape, a Promise keeps its identity and checks its payload when it
+ // settles.
+ const checkedRet =
+ strandRet &&
+ fromT.ret.kind === "dyn" &&
+ canDynCheckTo(
+ toT.ret,
+ (id) => lowerer.shapes.get(id),
+ (id) => lowerer.unions.get(id),
+ );
+ if (checkedRet) strandRet = false;
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`;
const existing = lowerer.coercions.retags.get(key);
if (existing) return existing;
@@ -440,6 +454,10 @@ export function funcCoerceAdapter(
`a '${lowerer.fmt(fromT)}' function invoked through a '${lowerer.fmt(toT)}' slot (the result cannot convert to '${lowerer.fmt(toT.ret)}' — the checker's loose function compatibility admitted the assignment, but the call has no exact lowering)`,
),
];
+ } else if (checkedRet) {
+ body = [
+ { kind: "return", value: { kind: "dynCheck", value: call, type: toT.ret, loc }, loc },
+ ];
} else {
const result = lowerer.coerceToExpected(call, toT.ret);
if (!typeEquals(result.type, toT.ret))
diff --git a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts
index 2bc991c14..53b0748b4 100644
--- a/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts
+++ b/packages/compiler/src/frontend/lowering/coercions/structural-plans.ts
@@ -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).
diff --git a/packages/compiler/src/frontend/lowering/coercions/structural.ts b/packages/compiler/src/frontend/lowering/coercions/structural.ts
index c85b9868d..496a29607 100644
--- a/packages/compiler/src/frontend/lowering/coercions/structural.ts
+++ b/packages/compiler/src/frontend/lowering/coercions/structural.ts
@@ -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();
+ (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");
diff --git a/packages/compiler/src/frontend/lowering/collection-ingestion.ts b/packages/compiler/src/frontend/lowering/collection-ingestion.ts
index 5449b0943..5d14d5492 100644
--- a/packages/compiler/src/frontend/lowering/collection-ingestion.ts
+++ b/packages/compiler/src/frontend/lowering/collection-ingestion.ts
@@ -91,7 +91,7 @@ export function collectionInput(
const symbol = type.getSymbol();
if (
!symbol ||
- (symbol.name !== "MapIterator" && symbol.name !== "SetIterator") ||
+ (ts.symbolName(symbol) !== "MapIterator" && ts.symbolName(symbol) !== "SetIterator") ||
!lowerer.isStdlibSymbol(symbol)
)
return null;
diff --git a/packages/compiler/src/frontend/lowering/collection-narrowing.ts b/packages/compiler/src/frontend/lowering/collection-narrowing.ts
index 29b358e99..6c1ca80f4 100644
--- a/packages/compiler/src/frontend/lowering/collection-narrowing.ts
+++ b/packages/compiler/src/frontend/lowering/collection-narrowing.ts
@@ -11,9 +11,12 @@ export function concreteCollectionNarrow(
): ts.Type | null {
if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return null;
const symbol = narrowed.getSymbol();
- const name = symbol?.name;
+ const name = ts.symbolNameOf(symbol);
if (name !== "Map" && name !== "Set") return null;
- if (!symbol || !symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile())))
+ if (
+ !symbol ||
+ !symbol.declarations.some((decl) => lowerer.isStdlibFile(ts.getSourceFileOfNode(decl)))
+ )
return null;
const args = lowerer.checker.getTypeArguments(narrowed as ts.TypeReference);
if (args.length === 0 || !args.every((arg) => (arg.flags & ts.TypeFlags.Any) !== 0)) return null;
diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts
index 77f72dc15..6807946d0 100644
--- a/packages/compiler/src/frontend/lowering/construction-escapes.test.ts
+++ b/packages/compiler/src/frontend/lowering/construction-escapes.test.ts
@@ -16,7 +16,7 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo
const ctor = cls.members.nodes.find(ts.isConstructorDeclaration) ?? null;
const fields = cls.members.nodes
.filter(ts.isPropertyDeclaration)
- .map((member) => ({ name: member.name.getText(), initializer: member.initializer }));
+ .map((member) => ({ name: ts.getTextOfNode(member.name), initializer: member.initializer }));
const fieldNames = new Set(fields.map((field) => field.name));
return constructionEscape({
ctor,
@@ -30,7 +30,7 @@ function analyze(source: string, options: { derived?: boolean; baseEscapes?: boo
const name = call.expression.name.text;
const method = cls.members.nodes.find(
(member): member is ts.MethodDeclaration =>
- ts.isMethodDeclaration(member) && member.name.getText() === name,
+ ts.isMethodDeclaration(member) && ts.getTextOfNode(member.name) === name,
);
const isPrivate =
method !== undefined &&
diff --git a/packages/compiler/src/frontend/lowering/construction-escapes.ts b/packages/compiler/src/frontend/lowering/construction-escapes.ts
index b9c8d7e07..3fdae4ab4 100644
--- a/packages/compiler/src/frontend/lowering/construction-escapes.ts
+++ b/packages/compiler/src/frontend/lowering/construction-escapes.ts
@@ -23,7 +23,7 @@ export function mentionsReceiver(node: ts.Node): boolean {
found = true;
return;
}
- child.forEachChild(visit);
+ ts.forEachChild(child, visit);
};
visit(node);
return found;
@@ -68,9 +68,9 @@ function containsReturn(node: ts.Node): boolean {
found = true;
return;
}
- child.forEachChild(visit);
+ ts.forEachChild(child, visit);
};
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
return found;
}
@@ -100,7 +100,7 @@ export function constructionEscape(inputs: ConstructionInputs): ConstructionEsca
return !(ts.isIdentifier(node.name) && assigned.has(name) && inputs.isField(name));
}
let found = false;
- node.forEachChild((child) => {
+ ts.forEachChild(node, (child) => {
if (!found && mentionsReceiver(child)) found = exposes(child, depth);
});
return found;
diff --git a/packages/compiler/src/frontend/lowering/constructor-returns.ts b/packages/compiler/src/frontend/lowering/constructor-returns.ts
index bee2e7567..0e600e700 100644
--- a/packages/compiler/src/frontend/lowering/constructor-returns.ts
+++ b/packages/compiler/src/frontend/lowering/constructor-returns.ts
@@ -86,9 +86,9 @@ function ownValueReturns(ctor: ts.ConstructorDeclaration): ts.ReturnStatement[]
const visit = (node: ts.Node): void => {
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return;
if (ts.isReturnStatement(node) && node.expression !== undefined) out.push(node);
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- ctor.body?.forEachChild(visit);
+ if (ctor.body !== undefined) ts.forEachChild(ctor.body, visit);
return out;
}
@@ -188,9 +188,9 @@ function ownReturns(body: ts.Node): ts.ReturnStatement[] {
const visit = (node: ts.Node): void => {
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return;
if (ts.isReturnStatement(node)) out.push(node);
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- body.forEachChild(visit);
+ ts.forEachChild(body, visit);
return out;
}
@@ -225,7 +225,7 @@ export function constructorReturnPlan(
// the class too (`if (C.instance) return C.instance;`).
const runtimeInstance =
info.localClass === undefined &&
- isJsSourceFile(ctor.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(ctor)) &&
[...returns].some(([stmt, kind]) => kind === "mixed" && !insideFinallyTry(stmt, ctor));
if ([...returns.values()].includes("instance") || runtimeInstance) {
slot = {
@@ -277,7 +277,7 @@ export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatemen
};
}
const feature = foreignObjectFeature(info);
- if (kind === "object" || !isJsSourceFile(stmt.getSourceFile()))
+ if (kind === "object" || !isJsSourceFile(ts.getSourceFileOfNode(stmt)))
lowerer.unsupported("SC1090", stmt, feature);
// A JavaScript value that is an object only sometimes: a primitive
// still answers the constructed instance, an object refuses.
@@ -296,7 +296,7 @@ export function lowerConstructorReturn(lowerer: Lowerer, stmt: ts.ReturnStatemen
});
const diag = unsupportedDiag("SC1090", loc, feature);
lowerer.runtimeFences.push(diag);
- const sf = stmt.getSourceFile();
+ const sf = ts.getSourceFileOfNode(stmt);
const line = ts.getLineAndCharacterOfPosition(sf, loc.start).line + 1;
return {
kind: "block",
@@ -460,7 +460,7 @@ export function superIntoResultSlot(
const slot = plan.slot;
const display = base.def.name.replace(/^%|^%m\d+\./, "");
const feature = `extending a class whose constructor can return an existing instance ('super()' would rebind 'this' to the instance '${display}' returns)`;
- if (!isJsSourceFile(blame.getSourceFile())) {
+ if (!isJsSourceFile(ts.getSourceFileOfNode(blame))) {
lowerer.unsupported("SC1090", blame, feature);
return call;
}
@@ -470,7 +470,8 @@ export function superIntoResultSlot(
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
const diag = unsupportedDiag("SC1090", loc, feature);
lowerer.runtimeFences.push(diag);
- const line = ts.getLineAndCharacterOfPosition(blame.getSourceFile(), loc.start).line + 1;
+ const line =
+ ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(blame), loc.start).line + 1;
const instance = { id: "instance.0", name: "instance", type, mutable: false };
const returned = { id: "returned.0", name: "returned", type: slot.type, mutable: false };
lowerer.liftedFns.push({
diff --git a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts
index 2e93e2fd5..5406c3ec1 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts
@@ -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
@@ -328,7 +304,7 @@ function lowerFastCallback(
fastLoweringDepth > 0 ||
slow.arity <= elemIndex ||
!ts.isArrowFunction(argNode) ||
- isJsSourceFile(argNode.getSourceFile()) ||
+ isJsSourceFile(ts.getSourceFileOfNode(argNode)) ||
valueT?.kind !== "union" ||
typeEquals(valueT, elem) ||
elem.kind === "union" ||
@@ -1513,7 +1489,7 @@ export function lowerArrayFlatMapCall(
kind: "dynInvoke",
recv: { kind: "dynFrom", value: receiver, liveRef: true, type: DYN, loc },
method: "flatMap",
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: [{ kind: "dynFrom", value: fnArg, type: DYN, loc }],
type: DYN,
loc,
@@ -2109,7 +2085,7 @@ export function lowerArraySortCall(
// Arithmetic on inferred JS values retains a checked result because
// it can produce BigInt. The sort callback still requires a number.
const result =
- fnArg.type.ret.kind === "dyn" && isJsSourceFile(argNode.getSourceFile())
+ fnArg.type.ret.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(argNode))
? F64
: fnArg.type.ret;
const expected = funcOf(
@@ -2247,7 +2223,7 @@ export function lowerDynArrayFilterCall(
recv,
fnArg,
{ kind: "strLit", value: access.name.text, type: STRING, loc },
- { kind: "strLit", value: access.getText(), type: STRING, loc },
+ { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc },
],
type: arrayOf(elemT),
loc,
@@ -2288,7 +2264,7 @@ export function lowerDynArrayFlatMapCall(
kind: "dynInvoke",
recv,
method: "flatMap",
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: [boxed],
type: DYN,
loc,
@@ -2310,7 +2286,7 @@ export function lowerDynArrayFlatMapCall(
recv,
fnArg,
{ kind: "strLit", value: access.name.text, type: STRING, loc },
- { kind: "strLit", value: access.getText(), type: STRING, loc },
+ { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc },
],
type: arrayOf(ret.elem),
loc,
diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
index 6072b58b7..53d46d00d 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
@@ -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,
@@ -279,7 +296,7 @@ export function lowerArrayConstructor(
// reference. Known element forms retain native arrays, including their
// existing sparse storage and indexed property contracts.
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(result?.kind !== "array" || result.elem.kind === "dyn")
) {
return {
@@ -295,7 +312,16 @@ export function lowerArrayConstructor(
const contextual = lowerer.checker.getContextualType(expr);
if (contextual) result = lowerer.mapTypeOf(contextual);
}
- if (isJsSourceFile(expr.getSourceFile()) && (result === null || result.kind === "dyn"))
+ // `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(ts.getSourceFileOfNode(expr)) && (result === null || result.kind === "dyn"))
result = arrayOf(DYN);
if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) {
lowerer.badType(expr, lowerer.typeOf(expr));
@@ -797,7 +823,7 @@ export function lowerArrayFromCall(
const loc = locOf(call);
const args = call.arguments.nodes;
if (
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
args.length >= 1 &&
args.length <= 3 &&
!args.some(ts.isSpreadElement)
@@ -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));
diff --git a/packages/compiler/src/frontend/lowering/containers/array-methods.ts b/packages/compiler/src/frontend/lowering/containers/array-methods.ts
index 7ef96457c..12078720e 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-methods.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-methods.ts
@@ -353,7 +353,7 @@ export function lowerArrayMethodCall(
// machinery answered string[]): same rule, any-typed spelling.
((lowerer.typeOf(access.expression).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !==
0 &&
- isJsSourceFile(access.getSourceFile())))
+ isJsSourceFile(ts.getSourceFileOfNode(access))))
) {
const probe = lowerer.lowerExpr(access.expression);
if (probe.type.kind === "array") {
@@ -1039,7 +1039,7 @@ export function lowerArrayMethodCall(
kind: "dynInvoke",
recv: receiver,
method: "join",
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args,
type: DYN,
loc,
@@ -2096,7 +2096,7 @@ export function lowerTupleReadMethodCall(
kind: "dynInvoke",
recv: { kind: "dynFrom", value: receiver, type: DYN, loc },
method: "flatMap",
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: [{ kind: "dynFrom", value: fnArg, type: DYN, loc }],
type: DYN,
loc,
diff --git a/packages/compiler/src/frontend/lowering/containers/bytes.ts b/packages/compiler/src/frontend/lowering/containers/bytes.ts
index c5a41cf32..f8e49a516 100644
--- a/packages/compiler/src/frontend/lowering/containers/bytes.ts
+++ b/packages/compiler/src/frontend/lowering/containers/bytes.ts
@@ -120,7 +120,7 @@ function lowerBytesSortCall(
fnArg.type.params.every((param) => lowerer.coercibleValue(F64, param))
) {
const result =
- fnArg.type.ret.kind === "dyn" && isJsSourceFile(argNode.getSourceFile())
+ fnArg.type.ret.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(argNode))
? F64
: fnArg.type.ret;
const expected = funcOf(
@@ -187,7 +187,7 @@ export function lowerBytesNew(
): IrExpr | null {
if (
symbol != null &&
- (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer") &&
+ (ts.symbolName(symbol) === "ArrayBuffer" || ts.symbolName(symbol) === "SharedArrayBuffer") &&
lowerer.isStdlibSymbol(symbol)
) {
const args = expr.arguments?.nodes ?? [];
@@ -203,18 +203,19 @@ export function lowerBytesNew(
: dynUndefinedExpr(locOf(expr));
return {
kind: "libCall",
- fn: symbol.name === "SharedArrayBuffer" ? "sharedArrayBuffer.new" : "arrayBuffer.new",
+ fn:
+ ts.symbolName(symbol) === "SharedArrayBuffer" ? "sharedArrayBuffer.new" : "arrayBuffer.new",
args: [length],
type: DYN,
loc: locOf(expr),
};
}
- if (symbol && symbol.name === "DataView" && lowerer.isStdlibSymbol(symbol)) {
+ if (symbol && ts.symbolName(symbol) === "DataView" && lowerer.isStdlibSymbol(symbol)) {
return lowerDataViewNew(lowerer, expr);
}
- const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined;
+ const elem = symbol ? own(BYTES_CTORS, ts.symbolName(symbol)) : undefined;
if (!elem || !symbol || !lowerer.isStdlibSymbol(symbol)) return null;
- const name = symbol.name;
+ const name = ts.symbolName(symbol);
const type = bytesOf(elem);
const loc = locOf(expr);
const args = expr.arguments?.nodes ?? [];
@@ -291,7 +292,7 @@ export function lowerBytesStaticCall(
}
if (access.name.text !== "from" || !ts.isIdentifier(access.expression)) return null;
const symbol = lowerer.resolveValueSymbol(access.expression);
- const elem = symbol ? own(BYTES_CTORS, symbol.name) : undefined;
+ const elem = symbol ? own(BYTES_CTORS, ts.symbolName(symbol)) : undefined;
if (!elem || !lowerer.isStdlibSymbol(symbol ?? undefined)) return null;
if (
call.arguments.nodes.length < 1 ||
@@ -442,7 +443,7 @@ export function lowerBytesMethodCall(
const nArgs = call.arguments.nodes.length;
// Encoded stream callbacks can receive strings even when JavaScript's
// contextual inference still describes their parameter as a Buffer.
- if (name === "toString" && nArgs <= 1 && isJsSourceFile(call.getSourceFile())) {
+ if (name === "toString" && nArgs <= 1 && isJsSourceFile(ts.getSourceFileOfNode(call))) {
const lowered = lowerer.lowerExpr(access.expression);
const receiver =
lowered.kind === "dynCheck" && lowered.value.type.kind === "dyn" ? lowered.value : lowered;
@@ -455,7 +456,7 @@ export function lowerBytesMethodCall(
call.arguments.nodes[0]
? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN)
: dynUndefinedExpr(loc),
- { kind: "strLit", value: access.getText(), type: STRING, loc },
+ { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc },
],
type: STRING,
loc,
@@ -1334,7 +1335,8 @@ function lowerBufferStaticValue(
args.length <= 3 &&
!args.some(ts.isSpreadElement) &&
((symbol) =>
- symbol != null && (symbol.name === "ArrayBuffer" || symbol.name === "SharedArrayBuffer"))(
+ symbol != null &&
+ (ts.symbolName(symbol) === "ArrayBuffer" || ts.symbolName(symbol) === "SharedArrayBuffer"))(
lowerer.typeOf(args[0]!).getSymbol(),
)
) {
@@ -1650,7 +1652,9 @@ function lowerBufferStaticValue(
// flavor. Plain bytes are Uint8Arrays at this boundary; inspecting
// only the bytes tag would incorrectly identify them as Buffers.
const checked =
- isJsSourceFile(call.getSourceFile()) && v.kind === "dynCheck" && v.value.type.kind === "dyn"
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
+ v.kind === "dynCheck" &&
+ v.value.type.kind === "dyn"
? v.value
: v;
if (checked.type.kind === "dyn") {
diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts
index e87a5e1bc..18754f088 100644
--- a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts
+++ b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts
@@ -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(ts.getSourceFileOfNode(argNode)) &&
+ 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 };
+}
diff --git a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts
index 7c4ddf220..0bf126830 100644
--- a/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts
+++ b/packages/compiler/src/frontend/lowering/containers/collection-grouping.ts
@@ -67,7 +67,7 @@ export function lowerGroupByStaticCall(
const fNode = call.arguments.nodes[1]!;
// An unannotated JavaScript callback parameter takes the element.
const f =
- isJsSourceFile(fNode.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(fNode)) &&
(ts.isArrowFunction(fNode) || ts.isFunctionExpression(fNode))
? lowerArrayCallback(lowerer, fNode, [elem], groupT, true).fnArg
: lowerer.lowerExpr(fNode);
diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
index a8b782b51..6aa6d6faf 100644
--- a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
+++ b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
@@ -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
@@ -58,7 +60,7 @@ export function lowerMapMethodCall(
if (!MAP_METHODS.has(name)) return null;
let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
let probedUntyped = false;
- if (receiverIr?.kind !== "map" && isJsSourceFile(access.getSourceFile())) {
+ if (receiverIr?.kind !== "map" && isJsSourceFile(ts.getSourceFileOfNode(access))) {
const probed = tryLowerExpression(lowerer, access.expression);
if (probed?.type.kind === "map") {
receiverIr = probed.type;
@@ -425,7 +427,7 @@ export function lowerCollectionSpread(
loc,
},
then: entries,
- else_: nodeThrowExpr(1, "", `${node.getText()} is not iterable`, entries.type, loc),
+ else_: nodeThrowExpr(1, "", `${ts.getTextOfNode(node)} is not iterable`, entries.type, loc),
type: entries.type,
loc,
},
@@ -542,7 +544,7 @@ export function lowerSetMethodCall(
// Set, but the VALUE lowered as a real Set of
// identity tokens (the new-Set probe) — the lowered receiver's type
// is the honest dispatch key.
- if (receiverIr?.kind !== "set" && isJsSourceFile(access.getSourceFile())) {
+ if (receiverIr?.kind !== "set" && isJsSourceFile(ts.getSourceFileOfNode(access))) {
const probed = tryLowerExpression(lowerer, access.expression);
if (probed?.type.kind === "set") {
receiverIr = probed.type;
@@ -971,7 +973,7 @@ export function lowerSetSeedNew(
{
kind: "libCall",
fn: "dyn.iterPack",
- args: [source, strLit(node.getText(), loc)],
+ args: [source, strLit(ts.getTextOfNode(node), loc)],
type: DYN,
loc,
},
@@ -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 };
@@ -1155,7 +1165,7 @@ export function lowerMapSeedNew(
}
if (ts.isArrayLiteralExpression(argNode)) {
if (
- isJsSourceFile(argNode.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(argNode)) &&
argNode.elements.nodes.some((entry) => !ts.isArrayLiteralExpression(entry))
) {
return mapFromSeedValue(lowerer, lowerer.lowerExprExpecting(argNode, DYN), mapT);
@@ -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 };
diff --git a/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts b/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts
index 523fda46e..32f90bfe7 100644
--- a/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts
+++ b/packages/compiler/src/frontend/lowering/containers/dynamic-receivers.ts
@@ -142,7 +142,7 @@ export function dynStringReceiver(
args: [
recv,
{ kind: "strLit", value: access.name.text, type: STRING, loc },
- { kind: "strLit", value: access.getText(), type: STRING, loc },
+ { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc },
],
type: STRING,
loc,
diff --git a/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts b/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts
index f2967c298..397bf6ec8 100644
--- a/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts
+++ b/packages/compiler/src/frontend/lowering/containers/exec-result-uses.ts
@@ -29,7 +29,7 @@ export function execResultCaptureMask(lowerer: Lowerer, call: ts.CallExpression)
const symbol = lowerer.resolveValueSymbol(decl.name);
if (!symbol) return null;
const read = new Set();
- for (const use of identifiersNamed(lowerer, decl.getSourceFile(), decl.name.text)) {
+ for (const use of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), decl.name.text)) {
if (use === decl.name || lowerer.resolveValueSymbol(use) !== symbol) continue;
const ref = outerValue(use);
const parent = ref.parent;
diff --git a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts
index 450e64142..efc173ffc 100644
--- a/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts
+++ b/packages/compiler/src/frontend/lowering/containers/indexed-objects.ts
@@ -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)` 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) {
diff --git a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts
index 46074573d..c23e6931c 100644
--- a/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts
+++ b/packages/compiler/src/frontend/lowering/containers/iterator-helpers.ts
@@ -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")
@@ -678,7 +684,7 @@ function isHelperReceiver(lowerer: Lowerer, expr: ts.Expression): boolean {
return (
mapped?.kind === "dyn" &&
symbol !== undefined &&
- ADAPTED_ITERATOR_TYPES.has(symbol.name) &&
+ ADAPTED_ITERATOR_TYPES.has(ts.symbolName(symbol)) &&
lowerer.isStdlibSymbol(symbol)
);
}
diff --git a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts
index f936e27cb..794bd27c2 100644
--- a/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts
+++ b/packages/compiler/src/frontend/lowering/containers/match-row-uses.ts
@@ -242,7 +242,7 @@ function constBindingReadsByElements(
}
function usesOf(lowerer: Lowerer, name: ts.Identifier, symbol: ts.Symbol): ts.Identifier[] {
- return identifiersNamed(lowerer, name.getSourceFile(), name.text).filter(
+ return identifiersNamed(lowerer, ts.getSourceFileOfNode(name), name.text).filter(
(use) => use !== name && lowerer.resolveValueSymbol(use) === symbol,
);
}
diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts
index 39154a820..50693e5e4 100644
--- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts
+++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts
@@ -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")
diff --git a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts
index e3ca2270d..b80eb7ac0 100644
--- a/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts
+++ b/packages/compiler/src/frontend/lowering/deferred-module-initializers.ts
@@ -62,8 +62,9 @@ export class DeferredModuleInitializers {
const info = exactClassOfReceiver(lowerer, owner);
if (
info &&
- info.decl?.getSourceFile() === statement.getSourceFile() &&
- info.decl.getStart() < statement.getStart() &&
+ info.decl !== null &&
+ ts.getSourceFileOfNode(info.decl) === ts.getSourceFileOfNode(statement) &&
+ ts.tokenStartOfNode(info.decl) < ts.tokenStartOfNode(statement) &&
!info.localClass &&
!info.classDecorators &&
!info.generic &&
@@ -101,7 +102,7 @@ export class DeferredModuleInitializers {
loc: locOf(statement),
};
const context = lowerer.ctx;
- const moduleIndex = [...lowerer.initNameOf.keys()].indexOf(statement.getSourceFile());
+ const moduleIndex = [...lowerer.initNameOf.keys()].indexOf(ts.getSourceFileOfNode(statement));
const initializers = this.pending.get(key) ?? [];
initializers.push({
loc: block.loc,
@@ -269,14 +270,14 @@ function registryInitializer(
return false;
expression = expression.arguments.nodes[0]!;
}
- const packageSource = npmStaticPackageOfPath(statement.getSourceFile().fileName) !== null;
+ const packageSource = npmStaticPackageOfPath(ts.getSourceFileOfNode(statement).fileName) !== null;
const initialized = (declaration: ts.Node): boolean => {
- const source = declaration.getSourceFile();
- return source === statement.getSourceFile()
- ? declaration.getStart() < statement.getStart()
+ const source = ts.getSourceFileOfNode(declaration);
+ return source === ts.getSourceFileOfNode(statement)
+ ? ts.tokenStartOfNode(declaration) < ts.tokenStartOfNode(statement)
: [...lowerer.initNameOf.keys()].indexOf(source) >= 0 &&
[...lowerer.initNameOf.keys()].indexOf(source) <
- [...lowerer.initNameOf.keys()].indexOf(statement.getSourceFile());
+ [...lowerer.initNameOf.keys()].indexOf(ts.getSourceFileOfNode(statement));
};
const callable = (callee: ts.Expression): boolean => {
if (!ts.isIdentifier(callee)) return false;
@@ -324,7 +325,7 @@ function registryInitializer(
// bundlers. Arguments still need to be safe to omit: an annotation
// never licenses dropping argument effects or a throwing property read.
if (packageSource && (ts.isCallExpression(node) || ts.isNewExpression(node))) {
- const trivia = node.getSourceFile().text.slice(node.pos, node.getStart());
+ const trivia = ts.getSourceFileOfNode(node).text.slice(node.pos, ts.tokenStartOfNode(node));
if (!/[@#]__PURE__/.test(trivia)) return false;
if (!callable(node.expression)) return false;
return (node.arguments?.nodes ?? []).every((argument) => value(argument, depth + 1));
@@ -337,8 +338,8 @@ function registryInitializer(
const info = exactClassOfReceiver(lowerer, node.expression.expression);
return (
!!info?.decl &&
- info.decl.getSourceFile() === statement.getSourceFile() &&
- info.decl.getStart() < statement.getStart() &&
+ ts.getSourceFileOfNode(info.decl) === ts.getSourceFileOfNode(statement) &&
+ ts.tokenStartOfNode(info.decl) < ts.tokenStartOfNode(statement) &&
classWriteIsUnobserved(lowerer, info, statement) &&
(findMethodOn(lowerer, info, node.name.text) !== null ||
findGenericMethodOn(lowerer, info, node.name.text) !== null)
@@ -398,7 +399,7 @@ function registryInitializer(
if (
functionDeclaration &&
ts.isFunctionDeclaration(functionDeclaration) &&
- functionDeclaration.getSourceFile() === statement.getSourceFile()
+ ts.getSourceFileOfNode(functionDeclaration) === ts.getSourceFileOfNode(statement)
)
return true;
// Reading an earlier initialized binding executes none of its
@@ -410,8 +411,8 @@ function registryInitializer(
functionDeclaration &&
ts.isVariableDeclaration(functionDeclaration) &&
functionDeclaration.initializer &&
- functionDeclaration.getSourceFile() === statement.getSourceFile() &&
- functionDeclaration.getStart() < statement.getStart()
+ ts.getSourceFileOfNode(functionDeclaration) === ts.getSourceFileOfNode(statement) &&
+ ts.tokenStartOfNode(functionDeclaration) < ts.tokenStartOfNode(statement)
)
return true;
const info = symbol && lowerer.classBySymbol.get(symbol);
@@ -421,8 +422,8 @@ function registryInitializer(
// decorated classes and local factories keep ordinary initialization.
return (
!!declaration &&
- declaration.getSourceFile() === statement.getSourceFile() &&
- declaration.getStart() < statement.getStart() &&
+ ts.getSourceFileOfNode(declaration) === ts.getSourceFileOfNode(statement) &&
+ ts.tokenStartOfNode(declaration) < ts.tokenStartOfNode(statement) &&
ts.isClassDeclaration(declaration) &&
!info!.classDecorators &&
!info!.generic &&
@@ -476,19 +477,20 @@ function registryOwnerIsData(
)
)
return false;
- if (declaration.getSourceFile() !== statement.getSourceFile()) {
+ if (ts.getSourceFileOfNode(declaration) !== ts.getSourceFileOfNode(statement)) {
const modules = [...lowerer.initNameOf.keys()];
return (
!!frozen &&
- modules.indexOf(declaration.getSourceFile()) >= 0 &&
- modules.indexOf(declaration.getSourceFile()) < modules.indexOf(statement.getSourceFile())
+ modules.indexOf(ts.getSourceFileOfNode(declaration)) >= 0 &&
+ modules.indexOf(ts.getSourceFileOfNode(declaration)) <
+ modules.indexOf(ts.getSourceFileOfNode(statement))
);
}
- if (declaration.getStart() >= statement.getStart()) return false;
+ if (ts.tokenStartOfNode(declaration) >= ts.tokenStartOfNode(statement)) return false;
if (frozen) return true;
let safe = true;
const visit = (node: ts.Node): void => {
- if (!safe || node.getStart() >= statement.getStart()) return;
+ if (!safe || ts.tokenStartOfNode(node) >= ts.tokenStartOfNode(statement)) return;
if (
ts.isIdentifier(node) &&
node !== declaration.name &&
@@ -515,7 +517,7 @@ function registryOwnerIsData(
}
ts.forEachChild(node, visit);
};
- visit(statement.getSourceFile());
+ visit(ts.getSourceFileOfNode(statement));
return safe;
}
@@ -527,7 +529,7 @@ function classWriteIsUnobserved(
info: ClassInfo,
statement: ts.Statement,
): boolean {
- const source = statement.getSourceFile();
+ const source = ts.getSourceFileOfNode(statement);
const declaration = info.decl!;
if (
declaration.members.nodes.some(
@@ -543,7 +545,10 @@ function classWriteIsUnobserved(
)
return false;
for (const earlier of source.statements.nodes) {
- if (earlier.getStart() <= declaration.getStart() || earlier.getStart() >= statement.getStart())
+ if (
+ ts.tokenStartOfNode(earlier) <= ts.tokenStartOfNode(declaration) ||
+ ts.tokenStartOfNode(earlier) >= ts.tokenStartOfNode(statement)
+ )
continue;
if (
ts.isFunctionDeclaration(earlier) ||
diff --git a/packages/compiler/src/frontend/lowering/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts
index 36fbf48b9..1c362cb09 100644
--- a/packages/compiler/src/frontend/lowering/divergences.ts
+++ b/packages/compiler/src/frontend/lowering/divergences.ts
@@ -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>();
@@ -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)) {
@@ -294,7 +281,7 @@ export function scanDivergences(
ts.isVariableDeclaration(decl) &&
decl.initializer !== undefined &&
ts.isObjectLiteralExpression(decl.initializer) &&
- decl.getSourceFile() === inner.getSourceFile()
+ ts.getSourceFileOfNode(decl) === ts.getSourceFileOfNode(inner)
) {
lit = decl.initializer;
binding = inner.text;
@@ -304,7 +291,9 @@ export function scanDivergences(
const contextual = safely(() => checker.getContextualType(lit!));
if (contextual === undefined) continue;
if (namedObjectDeclaration(checker, contextual) === null) continue;
- const declared = safely(() => checker.getPropertiesOfType(contextual).map((p) => p.name));
+ const declared = safely(() =>
+ checker.getPropertiesOfType(contextual).map((p) => ts.symbolName(p)),
+ );
if (declared === undefined) continue;
const written = lit.properties.nodes.map((p) => propertyNameText(p)!);
const assigned =
@@ -399,55 +388,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(f: () => T): T | undefined {
try {
@@ -522,7 +465,7 @@ function identityUse(root: ts.Node, name: string, from = -1): IdentityUse | null
return cur;
};
const textOf = (node: ts.Node): string => {
- const text = node.getText();
+ const text = ts.getTextOfNode(node);
return text.length > 60 ? `${text.slice(0, 57)}...` : text;
};
const visit = (node: ts.Node): "skip" | "stop" | undefined => {
@@ -650,7 +593,7 @@ function elementWrite(root: ts.Node, name: string, from: number): string | null
)
target = node.operand;
if (target !== undefined && throughElement(target)) {
- found = target.getText();
+ found = ts.getTextOfNode(target);
return "stop";
}
if (
@@ -729,8 +672,8 @@ function widthCopyBetween(
// still observes the same writes: no divergence to report.
const accessor = ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor;
if (sourceProps.some((p) => (p.flags & accessor) !== 0)) return null;
- const sourceNames = new Set(sourceProps.map((p) => p.name));
- const targetNames = checker.getPropertiesOfType(t).map((p) => p.name);
+ const sourceNames = new Set(sourceProps.map((p) => ts.symbolName(p)));
+ const targetNames = checker.getPropertiesOfType(t).map((p) => ts.symbolName(p));
if (targetNames.length === sourceNames.size && targetNames.every((n) => sourceNames.has(n)))
return null;
}
@@ -743,7 +686,7 @@ function isProgramClassType(checker: ts.TypeChecker, type: ts.Type): boolean {
return (
symbol !== undefined &&
(symbol.flags & ts.SymbolFlags.Class) !== 0 &&
- symbol.declarations.some((d) => !d.getSourceFile().isDeclarationFile)
+ symbol.declarations.some((d) => !ts.getSourceFileOfNode(d).isDeclarationFile)
);
}
@@ -757,7 +700,7 @@ function isPlainRecordType(checker: ts.TypeChecker, type: ts.Type): boolean {
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Class) !== 0) return false;
// Library-declared object types (Map, Date, Promise, ...) are not records.
const decls = symbol === undefined ? [] : symbol.declarations;
- if (decls.some((d) => d.getSourceFile().isDeclarationFile)) return false;
+ if (decls.some((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return false;
return true;
}
@@ -770,7 +713,7 @@ function namedObjectDeclaration(checker: ts.TypeChecker, type: ts.Type): ts.Node
if (symbol === undefined) return null;
if ((symbol.flags & ts.SymbolFlags.Class) !== 0) return null;
const decl = symbol.declarations[0];
- if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null;
+ if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null;
return decl;
}
@@ -824,7 +767,7 @@ function classDeclarationOfTypeNode(
const symbol = lowerer.checker.getSymbolAtLocation(node.typeName);
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null;
const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
- if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null;
+ if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null;
return decl as ts.ClassLikeDeclaration;
}
@@ -835,7 +778,7 @@ function classDeclarationOfType(
const symbol = type.getSymbol();
if (symbol === undefined || (symbol.flags & ts.SymbolFlags.Class) === 0) return null;
const decl = symbol.declarations.find((d) => ts.isClassDeclaration(d) || ts.isClassExpression(d));
- if (decl === undefined || decl.getSourceFile().isDeclarationFile) return null;
+ if (decl === undefined || ts.getSourceFileOfNode(decl).isDeclarationFile) return null;
return decl as ts.ClassLikeDeclaration;
}
@@ -870,20 +813,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());
-}
diff --git a/packages/compiler/src/frontend/lowering/dyn-streams.ts b/packages/compiler/src/frontend/lowering/dyn-streams.ts
new file mode 100644
index 000000000..51bcf0ea8
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/dyn-streams.ts
@@ -0,0 +1,318 @@
+/* Checked-dynamic stream slots (TypeMapperCtx.dynNodeStreams).
+ *
+ * Under the general stream representation (general-streams.ts),
+ * NodeJS.ReadableStream and NodeJS.WritableStream map to the runtime stream
+ * classes. JavaScript code also hands such slots its own stream objects:
+ * constructor-function streams over EventEmitter or the legacy Stream, plain
+ * objects implementing on/pipe/write/end, streams of bundled npm code. A
+ * program that moves an untyped value into a stream slot therefore lowers
+ * again with checked-dynamic slots: those two interfaces map to dyn, so a
+ * slot holds either a boxed native stream (its typed capsule, which unboxes
+ * to the same stream) or the JavaScript object itself, and a value returned
+ * to JavaScript is the object that came in.
+ *
+ * A member call on such a slot dispatches per value at run time. A native
+ * stream unboxes and takes the ordinary class-method lowering (the same
+ * stream surface a typed stream has); any other value calls its own member
+ * with JavaScript's dynamic semantics (dynInvoke). Programs without such a
+ * flow never take this pass, so their IR is unchanged. */
+import type { IrExpr, IrStmt, IrType } from "../../ir/ir.js";
+import { dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js";
+import { DYN } from "../../ir/ir.js";
+import * as ts from "../ts7/adapter.js";
+import { locOf } from "../program.js";
+import { isDeclaredInAmbientNamespace } from "../type-mapper.js";
+import { resultIsDiscarded } from "./call-position.js";
+import { lowerObjectMethodCall } from "./lower-calls.js";
+import type { Lowerer } from "./lowerer.js";
+
+/** The side a NodeJS stream interface names: "r" for NodeJS.ReadableStream,
+ * "w" for NodeJS.WritableStream (through null/undefined and intersections),
+ * or null for any other type. */
+export function nodeStreamInterfaceSide(lowerer: Lowerer, type: ts.Type): "r" | "w" | null {
+ const parts = (t: ts.Type): ts.Type[] =>
+ (t.flags & ts.TypeFlags.Intersection) !== 0 ? ts.constituentTypes(t).flatMap(parts) : [t];
+ for (const part of parts(lowerer.checker.getNonNullableType(type))) {
+ const psym = part.getSymbol() ?? part.getAliasSymbol();
+ if (
+ (psym === undefined || ts.symbolName(psym) !== "ReadableStream") &&
+ (psym === undefined || ts.symbolName(psym) !== "WritableStream")
+ )
+ continue;
+ const declared = psym.declarations.some(
+ (d) =>
+ ts.isInterfaceDeclaration(d) &&
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(d)) &&
+ isDeclaredInAmbientNamespace(d, "NodeJS"),
+ );
+ if (declared) return ts.symbolName(psym) === "ReadableStream" ? "r" : "w";
+ }
+ return null;
+}
+
+/** The runtime class a native value of the side unboxes to. */
+export function nativeStreamClass(side: "r" | "w"): IrType {
+ return { kind: "object", className: side === "r" ? "%Readable" : "%Writable" };
+}
+
+/** The native stream of a checked-dynamic value (+1, typed as the side's
+ * class) or null: the stream.dynNative libCall, which tests the value's
+ * typed capsule without throwing. */
+function dynNativeStream(lowerer: Lowerer, value: IrExpr, side: "r" | "w"): IrExpr {
+ const native = nativeStreamClass(side);
+ return {
+ kind: "libCall",
+ fn: "stream.dynNative",
+ args: [value, numLit(side === "r" ? 1 : 2, value.loc)],
+ type: { kind: "union", unionId: lowerer.unions.intern([native, { kind: "nullT" }]) },
+ loc: value.loc,
+ };
+}
+
+/** Runs `lower` with `node` lowering to `value` and its member calls taking
+ * `type` as the receiver representation. */
+function withStreamReceiver(
+ lowerer: Lowerer,
+ node: ts.Expression,
+ value: IrExpr,
+ type: IrType,
+ lower: () => T,
+): T {
+ const previous = lowerer.receiverTypeOverrides.get(node);
+ lowerer.receiverTypeOverrides.set(node, type);
+ try {
+ return lowerer.withExpressionOverride(node, value, lower);
+ } finally {
+ if (previous) lowerer.receiverTypeOverrides.set(node, previous);
+ else lowerer.receiverTypeOverrides.delete(node);
+ }
+}
+
+/** `stream.member(args)` on a NodeJS.ReadableStream / NodeJS.WritableStream
+ * receiver under checked-dynamic stream slots, or null when the call is not
+ * one. */
+export function lowerDynStreamMemberCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+): IrExpr | null {
+ if (!lowerer.dynNodeStreams) return null;
+ // The native arm re-enters the call lowering with the receiver bound.
+ if (lowerer.chainRecvByNode.has(access.expression)) return null;
+ if (access.questionDotToken || call.questionDotToken || !ts.isIdentifier(access.name))
+ return null;
+ if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
+ const side = nodeStreamInterfaceSide(lowerer, lowerer.typeOf(access.expression));
+ if (side === null) return null;
+ const loc = locOf(call);
+ const receiverValue = lowerer.lowerExpr(access.expression);
+ const receiver = lowerer.declareHiddenLocal("%streamReceiver", receiverValue.type);
+ const receiverRef = varRef(receiver.id, receiver.type, locOf(access.expression));
+ const stmts: IrStmt[] = [{ kind: "varDecl", localId: receiver.id, init: receiverValue, loc }];
+ const seq = (result: IrExpr): IrExpr => ({
+ kind: "seqExpr",
+ stmts,
+ result,
+ type: result.type,
+ loc,
+ });
+ // A typed stream whose declared type is one of the interfaces (a child
+ // stdio read): the class-method path on its own representation.
+ if (receiverValue.type.kind !== "dyn") {
+ const typed =
+ receiverValue.type.kind === "object"
+ ? withStreamReceiver(lowerer, access.expression, receiverRef, receiverValue.type, () =>
+ lowerObjectMethodCall(lowerer, call, access),
+ )
+ : lowerer.withExpressionOverride(access.expression, receiverRef, () =>
+ lowerer.lowerCall(call),
+ );
+ if (typed === null) lowerer.noLowering(`${access.name.text} on this stream`, call);
+ return seq(typed);
+ }
+ const nativeType = nativeStreamClass(side);
+ const nullable = dynNativeStream(lowerer, receiverRef, side);
+ const nativeOrNull = lowerer.declareHiddenLocal("%streamNative", nullable.type);
+ stmts.push({ kind: "varDecl", localId: nativeOrNull.id, init: nullable, loc });
+ const unionId = nullable.type.kind === "union" ? nullable.type.unionId : "";
+ const tag = lowerer.armTag(unionId, nativeType);
+ const native = lowerer.declareHiddenLocal("%streamNativeValue", nativeType);
+ const nativeRef = varRef(native.id, nativeType, locOf(access.expression));
+ const nativeDecl: IrStmt = {
+ kind: "varDecl",
+ localId: native.id,
+ init: {
+ kind: "unionNarrow",
+ unionId,
+ tag,
+ value: varRef(nativeOrNull.id, nullable.type, loc),
+ type: nativeType,
+ loc,
+ },
+ loc,
+ };
+ const typed = withStreamReceiver(lowerer, access.expression, nativeRef, nativeType, () =>
+ lowerObjectMethodCall(lowerer, call, access),
+ );
+ if (typed === null)
+ lowerer.noLowering(
+ `${side === "r" ? "NodeJS.ReadableStream" : "NodeJS.WritableStream"}.${access.name.text}`,
+ call,
+ );
+ // Any other value calls its own member, with the arguments as untyped
+ // values (typed callbacks box behind checked thunks).
+ const invoked: IrExpr = {
+ kind: "dynInvoke",
+ recv: receiverRef,
+ method: access.name.text,
+ calleeName: ts.getTextOfNode(access),
+ args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
+ type: DYN,
+ loc,
+ };
+ const cond: IrExpr = {
+ kind: "unionIsTag",
+ unionId,
+ tag,
+ negated: false,
+ value: varRef(nativeOrNull.id, nullable.type, loc),
+ type: { kind: "bool" },
+ loc,
+ };
+ const resultType = resultIsDiscarded(call) ? null : lowerer.irTypeOf(call);
+ if (resultType === null || resultType.kind === "void") {
+ stmts.push({
+ kind: "if",
+ cond,
+ then: [nativeDecl, { kind: "exprStmt", expr: typed, loc }],
+ else_: [{ kind: "exprStmt", expr: invoked, loc }],
+ loc,
+ });
+ return seq(dynUndefinedExpr(loc));
+ }
+ const result = lowerer.declareHiddenLocal("%streamResult", resultType);
+ result.mutable = true;
+ stmts.push(
+ { kind: "varDecl", localId: result.id, init: null, loc },
+ {
+ kind: "if",
+ cond,
+ then: [
+ nativeDecl,
+ {
+ kind: "assign",
+ localId: result.id,
+ value: lowerer.coerceInto(call, typed, resultType),
+ loc,
+ },
+ ],
+ else_: [
+ {
+ kind: "assign",
+ localId: result.id,
+ value: lowerer.coerceInto(call, invoked, resultType),
+ loc,
+ },
+ ],
+ loc,
+ },
+ );
+ return seq(varRef(result.id, resultType, loc));
+}
+
+/** `for await (const chunk of stream)` over a checked-dynamic
+ * NodeJS.ReadableStream: a native stream iterates through the Readable
+ * lowering; any other value throws the not-supported Error (async
+ * iteration of JavaScript stream objects has no lowering). */
+export function lowerForAwaitDynStream(
+ lowerer: Lowerer,
+ stmt: ts.ForOfStatement,
+ lowerLoop: () => IrStmt | null,
+): IrStmt | null {
+ if (!lowerer.dynNodeStreams || lowerer.chainRecvByNode.has(stmt.expression)) return null;
+ if (nodeStreamInterfaceSide(lowerer, lowerer.typeOf(stmt.expression)) !== "r") return null;
+ const loc = locOf(stmt.expression);
+ const value = lowerer.lowerExpr(stmt.expression);
+ const native: IrType =
+ value.type.kind === "dyn" ? { kind: "object", className: "%Duplex" } : value.type;
+ const local = lowerer.declareHiddenLocal("%streamIterated", native);
+ const init: IrExpr =
+ value.type.kind === "dyn"
+ ? {
+ kind: "libCall",
+ fn: "stream.unboxDyn",
+ args: [
+ value,
+ strLit(
+ "for await over stream objects other than native streams is not supported yet",
+ loc,
+ ),
+ ],
+ type: native,
+ loc,
+ }
+ : value;
+ const loop = withStreamReceiver(
+ lowerer,
+ stmt.expression,
+ varRef(local.id, native, loc),
+ native,
+ lowerLoop,
+ );
+ if (loop === null) return null;
+ return {
+ kind: "block",
+ body: [{ kind: "varDecl", localId: local.id, init, loc }, loop],
+ loc: locOf(stmt),
+ };
+}
+
+/** A statement that runs `onNative` with the native stream a
+ * checked-dynamic value boxes (typed as the side's class), or `otherwise`
+ * for any other value. */
+export function dynNativeBranch(
+ lowerer: Lowerer,
+ value: IrExpr,
+ side: "r" | "w",
+ onNative: (native: IrExpr) => IrStmt,
+ otherwise: IrStmt,
+): IrStmt {
+ const loc = value.loc;
+ const nativeType = nativeStreamClass(side);
+ const nullable = dynNativeStream(lowerer, value, side);
+ const nativeOrNull = lowerer.declareHiddenLocal("%streamNative", nullable.type);
+ const unionId = nullable.type.kind === "union" ? nullable.type.unionId : "";
+ const tag = lowerer.armTag(unionId, nativeType);
+ const native = lowerer.declareHiddenLocal("%streamNativeValue", nativeType);
+ const nullableRef = varRef(nativeOrNull.id, nullable.type, loc);
+ return {
+ kind: "block",
+ body: [
+ { kind: "varDecl", localId: nativeOrNull.id, init: nullable, loc },
+ {
+ kind: "if",
+ cond: {
+ kind: "unionIsTag",
+ unionId,
+ tag,
+ negated: false,
+ value: nullableRef,
+ type: { kind: "bool" },
+ loc,
+ },
+ then: [
+ {
+ kind: "varDecl",
+ localId: native.id,
+ init: { kind: "unionNarrow", unionId, tag, value: nullableRef, type: nativeType, loc },
+ loc,
+ },
+ onNative(varRef(native.id, nativeType, loc)),
+ ],
+ else_: [otherwise],
+ loc,
+ },
+ ],
+ loc,
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/error-name-order.ts b/packages/compiler/src/frontend/lowering/error-name-order.ts
new file mode 100644
index 000000000..7453d123e
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/error-name-order.ts
@@ -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 & {
+ 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.` or `this[""]`,
+ * 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 | 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();
+ 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 {
+ return new Set(
+ fields
+ .map((field) => field.name)
+ .filter(
+ (name) => isClassOwnEnumerableFieldName(name) && name !== "name" && name !== "message",
+ ),
+ );
+}
diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts
index ef2aa9778..a80367dd0 100644
--- a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts
@@ -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
@@ -82,7 +83,7 @@ export function lowerArrayLiteral(
// structurally equal checker types. Their native class ABIs still differ.
if (
!expected &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
mapped?.kind === "array" &&
mapped.elem.kind === "classval"
)
@@ -201,7 +202,7 @@ export function lowerArrayLiteral(
// keep the checked-dynamic fallback below.
if (
lowerer.implicitParamTypes !== null &&
- npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null &&
+ npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null &&
(mapped === null ||
mapped.kind === "dyn" ||
(mapped.kind === "array" && mapped.elem.kind === "dyn")) &&
@@ -221,7 +222,7 @@ export function lowerArrayLiteral(
mapped = arrayOf(first.type);
}
}
- if (!expected && isJsSourceFile(expr.getSourceFile()) && mapped?.kind === "array") {
+ if (!expected && isJsSourceFile(ts.getSourceFileOfNode(expr)) && mapped?.kind === "array") {
const elem = mapped.elem;
const arms = elem.kind === "union" ? lowerer.unions.get(elem.unionId)?.arms : [elem];
if (arms?.length && arms.every((arm) => arm.kind === "classval")) mapped = DYN;
@@ -233,7 +234,7 @@ export function lowerArrayLiteral(
}
if (
!expected &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
expr.elements.nodes.some(
(element) =>
ts.isSpreadElement(element) &&
@@ -246,7 +247,7 @@ export function lowerArrayLiteral(
// only in a checked-dynamic array: a static element type would copy it.
if (
!expected &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
mapped?.kind !== "dyn" &&
expr.elements.nodes.some(
(element) => !ts.isSpreadElement(element) && jsHoldsDynObject(lowerer, element),
@@ -268,7 +269,7 @@ export function lowerArrayLiteral(
// real keys. A structural union can otherwise drop descriptor fields.
if (
!expected &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
mapped?.kind === "array" &&
mapped.elem.kind === "union" &&
expr.elements.nodes.some(ts.isObjectLiteralExpression)
@@ -315,7 +316,7 @@ export function lowerArrayLiteral(
// to DYN wholesale now (mapType's dyn-element array rule), so the
// literal IS the dyn array.
if (
- isJsSourceFile(expr.getSourceFile()) ||
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) ||
mapped?.kind === "dyn" ||
lowerer.checkerAnyArray(expr)
) {
@@ -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") {
diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts
index cecbde717..07d983cc0 100644
--- a/packages/compiler/src/frontend/lowering/expressions/binary.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts
@@ -1,5 +1,6 @@
/* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */
import * as ts from "../../ts7/adapter.js";
+import { cleanFuncAdaptable } from "../coercions/function-adapters.js";
import { type Lowerer, jsFuncNameOf } from "../lowerer.js";
import {
BIGINT_T,
@@ -481,6 +482,22 @@ export function lowerNullishPair(
);
}
}
+ // An optional callback defaulted to a function of another signature
+ // (`o.tags ?? (() => ({}))`, fewer parameters or a narrower result): the
+ // result is the callback's own function type, and the default adapts into
+ // it (JavaScript ignores surplus arguments). The checker's merged union
+ // signature would lose the callback's result shape.
+ if (present?.kind === "func" && present.rest !== true) {
+ const raw = lowerer.lowerExpr(expr.right);
+ if (
+ raw.type.kind === "func" &&
+ (typeEquals(raw.type, present) || cleanFuncAdaptable(lowerer, raw.type, present))
+ ) {
+ const right = lowerer.coerceInto(expr.right, raw, present);
+ lowerer.nullishFuncResults.add(expr);
+ return { kind: "nullish", left, right, type: present, loc };
+ }
+ }
lowerer.unsupported(
"SC1090",
expr,
@@ -514,7 +531,7 @@ export function lowerNativeFunctionAssignment(
): IrExpr | null {
const target = expr.left;
if (
- !isJsSourceFile(expr.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(expr)) ||
(!ts.isPropertyAccessExpression(target) && !ts.isElementAccessExpression(target)) ||
target.questionDotToken
)
@@ -886,7 +903,7 @@ export function lowerCompoundValueToTarget(
// catchable TypeError, as the expanded assignment does).
if (
(target.type.kind === "dyn" || rhs.type.kind === "dyn") &&
- (isJsSourceFile(expr.getSourceFile()) ||
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) ||
(lowerer.dynConvertible(read.type) && lowerer.dynConvertible(rhs.type)))
) {
const left = lowerer.coerceInto(expr.left, read, DYN);
@@ -1125,7 +1142,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
recv &&
(recv.type.kind === "dyn" ||
(recv.type.kind === "array" &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
lowerer.dynConvertible(recv.type)))
) {
return lowerDynMemberAssignment(
@@ -1517,7 +1534,7 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
// Numeric operators dispatch on ToNumeric's actual Number/BigInt
// result, including values flowing through native callable properties.
if (
- isJsSourceFile(expr.getSourceFile()) ||
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) ||
op === ts.SyntaxKind.PlusToken ||
[
ts.SyntaxKind.LessThanToken,
@@ -2392,7 +2409,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
}
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
op === ts.SyntaxKind.AmpersandAmpersandToken &&
left.type.kind === "union" &&
lowerer.unions.get(left.type.unionId)?.arms.some((arm) => arm.kind === "bool") &&
@@ -2440,7 +2457,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
op === ts.SyntaxKind.AmpersandAmpersandToken &&
target === null &&
lowerer.implicitParamTypes !== null &&
- npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null &&
+ npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null &&
(lowerer.typeOf(expr).flags & ts.TypeFlags.Any) !== 0 &&
left.type.kind === "union"
) {
@@ -2601,7 +2618,7 @@ function lowerLogicalPair(lowerer: Lowerer, expr: ts.BinaryExpression, left: IrE
// When both operands have native checked representations, preserve
// the deciding value and short-circuit evaluation in that domain.
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
lowerer.dynConvertible(left.type) &&
lowerer.dynConvertible(right.type)
) {
diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
index 144742257..7ef4b9f25 100644
--- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
@@ -63,6 +63,8 @@ import { npmStaticPackageOfPath } from "../../npm-static.js";
import { isSafeToRepeat } from "./evaluation-safety.js";
import { lowerClassPrototypeComputedAssignment } from "../class-prototypes.js";
import { isClassCallback } from "../class-callbacks.js";
+import { isKeyedFieldAccess } from "./keyed-field-targets.js";
+import { lowerFieldCompoundValue } from "./updates.js";
/** `a[i]` reads. Only f64 indices into array receivers are modeled; JS
* string-key element access (`a["length"]`) and string indexing (`s[0]`
@@ -229,7 +231,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
let receiverIr = neverTaintedJsType(lowerer, expr.expression, lowerer.typeOf(expr.expression))
? null
: lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
- if (isJsSourceFile(expr.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const receiver = tryLowerExpression(lowerer, expr.expression);
if (receiver?.type.kind === "dyn") {
const key = lowerer.lowerExprExpecting(expr.argumentExpression, DYN);
@@ -314,7 +316,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
locOf(expr.argumentExpression),
expr.argumentExpression,
);
- if (isJsSourceFile(expr.getSourceFile()) && recv.type.kind === "bytes") {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && recv.type.kind === "bytes") {
return {
kind: "dynKeyGet",
value: lowerer.coerceInto(expr.expression, recv, DYN),
@@ -563,7 +565,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
if (
receiver?.type.kind === "dyn" ||
(receiver &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(receiver.type.kind === "union" || receiver.type.kind === "bytes") &&
lowerer.dynConvertible(receiver.type))
) {
@@ -767,7 +769,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
if (
arr.type.kind === "array" &&
(index.type.kind === "dyn" ||
- (isJsSourceFile(expr.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
index.type.kind !== "f64" &&
lowerer.dynConvertible(index.type)))
) {
@@ -820,7 +822,7 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre
arr.type.kind === "array" &&
arr.kind === "arrIntrinsic" &&
arr.method === "slice" &&
- npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null
+ npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null
) {
const safe = lowerSafeIndexRead(lowerer, arr, index, locOf(expr));
if (safe) return safe;
@@ -1304,7 +1306,11 @@ export function lowerRecordKeyRead(
) {
declared = shape.fields[0]!.type;
}
- if (!declared && isJsSourceFile(expr.getSourceFile()) && recordKeyResultOk(lowerer, shape, DYN))
+ if (
+ !declared &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
+ recordKeyResultOk(lowerer, shape, DYN)
+ )
declared = DYN;
if (!declared) {
lowerer.unsupported(
@@ -1417,8 +1423,13 @@ export function lowerElementCompoundParts(
const target = expr.left as ts.ElementAccessExpression;
fenceNodeModuleMutation(lowerer, target, "assignment");
const loc = locOf(expr);
+ // A record through a computed key (`totals[side] += n` with a literal
+ // union key, `counts[name] += 1` on an index-signature record): the
+ // member compound's receiver, key, old value, rhs, write sequence.
+ if (isKeyedFieldAccess(lowerer, target))
+ return { expr: lowerFieldCompoundValue(lowerer, target, op, expr.right, loc), stmts: null };
const checked = lowerer.lowerExpr(target.expression);
- if (checked?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) {
+ if (checked?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const stmts: IrStmt[] = [];
const save = (value: IrExpr, name: string): IrExpr => {
const local = lowerer.declareHiddenLocal(name, value.type);
@@ -1509,7 +1520,7 @@ export function lowerElementCompoundParts(
const rhs = lowerer.lowerExpr(expr.right);
const numericRhs =
rhs.type.kind === "dyn" &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
receiver.type.kind === "bytes"
? { kind: "libCall" as const, fn: "dyn.toNumberCoerce" as const, args: [rhs], type: F64, loc }
: lowerOptionalNumber(lowerer, rhs, loc);
@@ -1559,7 +1570,7 @@ export function lowerElementCompoundParts(
type: F64,
loc,
};
- } else if (valueType.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) {
+ } else if (valueType.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const left = lowerer.coerceToExpected(oldRef(), DYN),
right = lowerer.coerceToExpected(rhs, DYN);
computed = dynCompoundValue(op, left, right, loc);
@@ -1693,7 +1704,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
loc: locOf(expr),
};
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
receiver.type.kind === "object" &&
lowerer.dynConvertible(receiver.type)
) {
@@ -1717,7 +1728,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
loc: locOf(expr),
};
}
- if (checkedReceiver?.type.kind === "object" && isJsSourceFile(expr.getSourceFile())) {
+ if (checkedReceiver?.type.kind === "object" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const info = lowerer.classes.get(checkedReceiver.type.className);
const key =
recordKeyLiteralText(target.argumentExpression) ??
@@ -1732,7 +1743,7 @@ export function lowerElementWrite(lowerer: Lowerer, expr: ts.BinaryExpression):
}
}
let receiverIr = lowerer.mapTypeOf(lowerer.typeOf(target.expression));
- if (isJsSourceFile(expr.getSourceFile()) && checkedReceiver) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && checkedReceiver) {
const represented =
checkedReceiver.type.kind === "union"
? lowerer.stripUndefinedArm(checkedReceiver.type)
diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
index d7a58800e..b5f99d682 100644
--- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
@@ -1,6 +1,8 @@
/* Identifier references in expression position: lowerExprInner's identifier branch and its helpers. */
import { cjsMemberAliasValueOf } from "../cjs-exports-object.js";
+import { builtinClassValue, builtinMemberClassKind } from "../builtins/builtin-class-dyn.js";
import * as ts from "../../ts7/adapter.js";
+import { InternalCompilerError } from "../../../errors.js";
import * as posix from "node:path/posix";
import { type Lowerer, PoisonError } from "../lowerer.js";
import {
@@ -21,21 +23,31 @@ 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 { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.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";
@@ -87,7 +99,18 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// type and lowerLambda declared the local.
if (expr.text === "arguments") {
const binding = captureContextArguments(lowerer.fnStack);
- if (binding) return { kind: "varRef", localId: binding.id, type: DYN, loc };
+ if (binding && lowerer.ctx.pureTuples.has(binding.id)) {
+ throw new InternalCompilerError("lowerer bug: forwarded-only 'arguments' read as a value");
+ }
+ if (binding) return { kind: "varRef", localId: binding.id, type: binding.type, loc };
+ const owner = argumentsOwner(expr);
+ if (owner && typedArgumentsCandidate(owner)) {
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ "'arguments' in a function expression whose value is not asserted to one fixed-arity signature (assert it, as in `function () { … } as F`, and the function lowers against that signature)",
+ );
+ }
}
// A compiler-projected adapter as a VALUE: the adapter has no
// runtime function object (its calls rewrite directly) — call it
@@ -110,7 +133,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
);
}
if (projection?.kind === "builtin-function") {
- if (!isJsSourceFile(expr.getSourceFile())) {
+ if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const callable = lowerer.lowerBuiltinCallableValue(
{ module: projection.module, member: projection.member },
loc,
@@ -126,7 +149,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// `EventEmitter`, `Readable` or `TypeError` (or a const alias of
// one) as a value in a TypeScript file: the runtime class's class
// object. JavaScript files keep the identity token.
- if (!isJsSourceFile(expr.getSourceFile())) {
+ if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const runtimeClass = runtimeClassValueOf(lowerer, expr);
if (runtimeClass) return lowerer.classValueRef(runtimeClass, expr);
}
@@ -164,7 +187,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
const clause = decl.parent.parent.parent;
if (
(ts.isCaseClause(clause) || ts.isDefaultClause(clause)) &&
- !(expr.getStart() >= clause.getStart() && expr.end <= clause.end)
+ !(ts.tokenStartOfNode(expr) >= ts.tokenStartOfNode(clause) && expr.end <= clause.end)
) {
lowerer.unsupported(
"SC1090",
@@ -362,7 +385,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
(expr.text === "__dirname" || expr.text === "__filename") &&
lowerer.isStdlibGlobal(expr, expr.text)
) {
- const sf = expr.getSourceFile();
+ const sf = ts.getSourceFileOfNode(expr);
if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName)) {
return {
kind: "libCall",
@@ -529,13 +552,22 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// JavaScript sources: an otherwise unsupported builtin VALUE is
// the same identity-token story as stdlib globals above (the
// harness adds worker_threads.Worker to its identity Set).
- if (isJsSourceFile(expr.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
+ // EventEmitter and the stream classes are the runtime's untyped
+ // constructors, so constructor functions can inherit from them.
+ const classKind = builtinMemberClassKind(bi.module, bi.member);
+ if (classKind !== null) return builtinClassValue(classKind, loc);
// A function member is a function value that fences when called.
if (builtinModuleFnOf(lowerer, bi.module, bi.member))
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,
};
@@ -620,7 +652,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// type-mapper.ts pins the one concrete signature `(value: string) =>
// primitive`; direct calls `String(x)` never reach here — the call
// lowering intercepts them with the wider static coercions).
- const jsPrimitiveCtorSelector = isJsSourceFile(expr.getSourceFile());
+ const jsPrimitiveCtorSelector = isJsSourceFile(ts.getSourceFileOfNode(expr));
const primitiveName = stdlibGlobalNameOf(lowerer, expr);
if (
primitiveName === "String" ||
@@ -632,7 +664,7 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
}
if (lowerer.isStdlibGlobal(expr, "ArrayBuffer"))
return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc };
- if (isJsSourceFile(expr.getSourceFile()) && lowerer.isStdlibGlobal(expr, "Array"))
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.isStdlibGlobal(expr, "Array"))
return { kind: "libCall", fn: "dyn.arrayConstructor", args: [], type: DYN, loc };
if (
Object.values(BYTES_ELEMENT_NAME).includes(expr.text) &&
@@ -715,9 +747,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") {
- if (isJsSourceFile(expr.getSourceFile())) {
- const canonical = stdlibGlobalNameOf(lowerer, expr) ?? expr.text;
+ 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(ts.getSourceFileOfNode(expr)),
+ );
+ if (builtinValue !== null) return builtinValue;
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc };
}
// The families with a WHY: each hint states what makes the
@@ -930,5 +979,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(ts.getSourceFileOfNode(binding), 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));
}
diff --git a/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts
new file mode 100644
index 000000000..e53d27aa9
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/expressions/keyed-field-targets.ts
@@ -0,0 +1,271 @@
+/* Read-modify-write targets for computed record keys: `r[k] op= v`, `r[k]++` and `r[k] ??= v`. */
+import * as ts from "../../ts7/adapter.js";
+import {
+ BOOL,
+ F64,
+ STRING,
+ typeEquals,
+ type IrExpr,
+ type IrStmt,
+ type SrcLoc,
+} from "../../../ir/ir.js";
+import { isJsSourceFile } from "../../program.js";
+import { constituentTypes } from "../../ts7/checker.js";
+import type { FieldTarget } from "../lower-exprs.js";
+import type { Lowerer } from "../lowerer.js";
+
+export type KeyedFieldTarget = FieldTarget & { container: "recordKeyed" };
+
+/** The property names a computed key's type allows: one or more string
+ * literals (a literal union such as `"sent" | "received"`, enum string
+ * members, or a type parameter bound to one inside a generic instance).
+ * Null for any other key type. */
+export function literalFieldKeys(lowerer: Lowerer, keyNode: ts.Expression): string[] | null {
+ let t: ts.Type = lowerer.typeOf(keyNode);
+ if (t.flags & ts.TypeFlags.TypeParameter) {
+ const bound = lowerer.typeParamTsResolver(t);
+ if (!bound) return null;
+ t = bound;
+ }
+ const parts = t.flags & ts.TypeFlags.Union ? constituentTypes(t) : [t];
+ const keys: string[] = [];
+ for (const part of parts) {
+ if (!part.isStringLiteralType()) return null;
+ if (!keys.includes(part.value)) keys.push(part.value);
+ }
+ return keys.length > 0 ? keys : null;
+}
+
+/** Does `r[k]` read and write through declared record storage by key? A
+ * checker-only test (nothing lowers), so a caller can route before it
+ * evaluates the receiver: TypeScript sources only, a record whose declared
+ * fields include every name the key's literal type allows, or a pure
+ * index-signature record with a string key. */
+export function isKeyedFieldAccess(lowerer: Lowerer, access: ts.ElementAccessExpression): boolean {
+ if (
+ access.questionDotToken ||
+ isJsSourceFile(ts.getSourceFileOfNode(access)) ||
+ lowerer.isProcessEnv(access.expression)
+ )
+ return false;
+ const keys = literalFieldKeys(lowerer, access.argumentExpression);
+ if (!keys && !(lowerer.typeOf(access.argumentExpression).flags & ts.TypeFlags.StringLike))
+ return false;
+ const receiver = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
+ if (receiver?.kind !== "record") return false;
+ const shape = lowerer.shapes.get(receiver.shapeId);
+ if (!shape || shape.tuple) return false;
+ if (shape.fields.length === 0) return shape.indexValue !== undefined;
+ return keys !== null && keys.every((k) => shape.fields.some((f) => f.name === k));
+}
+
+/** The target of a read-modify-write through `r[k]`, or null when
+ * isKeyedFieldAccess declines. A literal-union key dispatches over the
+ * declared fields it names, which must share one type; a string key on a
+ * pure index-signature record reads and writes its entries. The receiver
+ * and key may arrive already lowered (and saved) by the caller; otherwise
+ * both lower here, unsaved. A caller that evaluates the target more than
+ * once saves `obj` and then `key`, the JavaScript evaluation order. With
+ * `numeric`, a missing index-signature entry reads as NaN (every numeric
+ * update applies ToNumber to it) instead of a value armed with undefined. */
+export function keyedFieldTarget(
+ lowerer: Lowerer,
+ access: ts.ElementAccessExpression,
+ numeric: boolean,
+ lowered?: { obj: IrExpr; key: IrExpr },
+): KeyedFieldTarget | null {
+ if (!isKeyedFieldAccess(lowerer, access)) return null;
+ const keyNode = access.argumentExpression;
+ const keys = literalFieldKeys(lowerer, keyNode);
+ const obj = lowered?.obj ?? lowerer.lowerExpr(access.expression);
+ if (obj.type.kind !== "record") {
+ lowerer.unsupported(
+ "SC1090",
+ access.expression,
+ `updating a computed key of a '${lowerer.fmt(obj.type)}' value (narrow the receiver to its record type first)`,
+ );
+ }
+ const shapeId = obj.type.shapeId;
+ const shape = lowerer.shapes.get(shapeId)!;
+ // A single literal spelled at the site (or an identifier whose type is
+ // that one literal) names the field statically: identifier evaluation
+ // is pure, so skipping it matches JavaScript.
+ const indexed = shape.fields.length === 0;
+ const staticKey =
+ !indexed && keys?.length === 1 && (ts.isStringLiteralLike(keyNode) || ts.isIdentifier(keyNode));
+ const key = staticKey ? null : (lowered?.key ?? lowerer.lowerExpr(keyNode));
+ if (key && key.type.kind !== "string") {
+ lowerer.unsupported(
+ "SC1090",
+ keyNode,
+ `updating a record through a '${lowerer.fmt(key.type)}' key (convert the key to a string first)`,
+ );
+ }
+ if (keys && !indexed) {
+ const fields = keys.map((k) => shape.fields.find((f) => f.name === k));
+ const first = fields[0];
+ if (!first || fields.some((f) => !f || !typeEquals(f.type, first.type))) {
+ lowerer.unsupported(
+ "SC1090",
+ access,
+ `updating '${lowerer.fmt(obj.type)}' through a key whose fields are not all stored with one type (${keys.map((k) => `'${k}'`).join(", ")}; switch over the key and update each field)`,
+ );
+ }
+ return {
+ container: "recordKeyed",
+ obj,
+ shapeId,
+ field: keys[0]!,
+ keys,
+ key,
+ fieldType: first.type,
+ };
+ }
+ if (!indexed || !shape.indexValue) {
+ lowerer.unsupported(
+ "SC1090",
+ access,
+ `updating '${lowerer.fmt(obj.type)}' through a runtime key (the stored record declares fields)`,
+ );
+ }
+ return {
+ container: "recordKeyed",
+ obj,
+ shapeId,
+ field: "",
+ keys: null,
+ key,
+ fieldType: shape.indexValue,
+ ...(numeric && shape.indexValue.kind === "f64" ? { missNaN: true as const } : {}),
+ };
+}
+
+/** Does `key` name `field`? */
+function keyIs(key: IrExpr, field: string, loc: SrcLoc): IrExpr {
+ return {
+ kind: "strEq",
+ negated: false,
+ left: key,
+ right: { kind: "strLit", value: field, type: STRING, loc },
+ type: BOOL,
+ loc,
+ };
+}
+
+/** A key outside its literal type (smuggled through a cast) names no
+ * declared field: a fixed-shape record cannot add the property, so the
+ * update throws the TypeError of a keyed write miss. */
+function keyMiss(key: IrExpr, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
+ const message: IrExpr = {
+ kind: "strConcat",
+ left: {
+ kind: "strConcat",
+ left: { kind: "strLit", value: "Cannot add property '", type: STRING, loc },
+ right: key,
+ type: STRING,
+ loc,
+ },
+ right: { kind: "strLit", value: "' to a fixed-shape object", type: STRING, loc },
+ type: STRING,
+ loc,
+ };
+ return {
+ kind: "libCall",
+ fn: "error.nodeThrow",
+ args: [
+ { kind: "numLit", value: 1, type: F64, loc },
+ { kind: "strLit", value: "", type: STRING, loc },
+ message,
+ ],
+ type: target.fieldType,
+ loc,
+ };
+}
+
+/** The current value of a keyed target. */
+export function keyedFieldRead(lowerer: Lowerer, target: KeyedFieldTarget, loc: SrcLoc): IrExpr {
+ const { obj, shapeId, key, fieldType } = target;
+ if (!target.keys) {
+ const type = target.missNaN
+ ? fieldType
+ : fieldType.kind === "dyn"
+ ? fieldType
+ : lowerer.withUndefinedArmOf(fieldType);
+ if (!type || !key) throw new Error("keyed index-signature target without a key or read type");
+ return {
+ kind: "recordKeyGet",
+ obj,
+ shapeId,
+ key,
+ ...(target.missNaN ? { missNaN: true as const } : {}),
+ type,
+ loc,
+ };
+ }
+ const get = (field: string): IrExpr => ({
+ kind: "recordGet",
+ obj,
+ shapeId,
+ field,
+ type: fieldType,
+ loc,
+ });
+ if (!key) return get(target.keys[0]!);
+ return target.keys.reduceRight(
+ (otherwise, field) => ({
+ kind: "ternary",
+ cond: keyIs(key, field, loc),
+ then: get(field),
+ else_: otherwise,
+ type: fieldType,
+ loc,
+ }),
+ keyMiss(key, target, loc),
+ );
+}
+
+/** Store `value` (already of the target's field type) through a keyed
+ * target. A multi-key dispatch references the value once per field, so a
+ * value that is not a plain local is saved first. */
+export function keyedFieldWrite(
+ lowerer: Lowerer,
+ target: KeyedFieldTarget,
+ value: IrExpr,
+ loc: SrcLoc,
+): IrStmt {
+ const { obj, shapeId, key } = target;
+ if (!target.keys) {
+ if (!key) throw new Error("keyed index-signature target without a key");
+ return { kind: "recordKeySet", obj, shapeId, key, value, loc };
+ }
+ if (!key) return { kind: "recordSet", obj, shapeId, field: target.keys[0]!, value, loc };
+ const prefix: IrStmt[] = [];
+ let stored = value;
+ if (value.kind !== "varRef") {
+ const local = lowerer.declareHiddenLocal("%keyedValue", value.type);
+ prefix.push({ kind: "varDecl", localId: local.id, init: value, loc });
+ stored = { kind: "varRef", localId: local.id, type: value.type, loc };
+ }
+ const chain = target.keys.reduceRight(
+ (otherwise, field) => [
+ {
+ kind: "if",
+ cond: keyIs(key, field, loc),
+ then: [{ kind: "recordSet", obj, shapeId, field, value: stored, loc }],
+ else_: otherwise,
+ loc,
+ },
+ ],
+ [{ kind: "exprStmt", expr: keyMiss(key, target, loc), loc }],
+ );
+ return { kind: "block", body: [...prefix, ...chain], loc };
+}
+
+/** Save a keyed target's key after its receiver, so a multi-key dispatch
+ * reads and writes the field the key named when it was evaluated. */
+export function saveKeyedFieldKey(
+ target: FieldTarget,
+ save: (value: IrExpr, name: string) => IrExpr,
+): void {
+ if (target.container === "recordKeyed" && target.key) target.key = save(target.key, "%keyedKey");
+}
diff --git a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts
index b54da8180..dec06fdd9 100644
--- a/packages/compiler/src/frontend/lowering/expressions/narrowing.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/narrowing.ts
@@ -26,6 +26,7 @@ import { jsBindingHasOpenWrites } from "../lower-stmts.js";
import { narrowClassUnion, narrowStoredClassValue, structuralNarrowing } from "../class-unions.js";
import { checkedClassAssertion, checkedClassUnionAssertion } from "../class-assertions.js";
import { isOptionalProcessStreamProperty } from "../builtins/process.js";
+import { requestGeneralStreamSurface } from "../general-streams.js";
import { literalValues } from "../../literal-values.js";
import { literalUnionArm } from "../../union-discriminants.js";
import { checkedArmExtraction } from "../coercions/unions.js";
@@ -91,7 +92,7 @@ export function lowerTernary(
) {
falseArmNarrows.push(n);
}
- n.forEachChild(collect);
+ ts.forEachChild(n, collect);
};
collect(expr.whenFalse);
}
@@ -332,7 +333,7 @@ export function lowerTernary(
// call ABI. A checker join can erase a rest/arguments pack even though
// a later call supplies additional observable arguments.
const callableJoin =
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
thenRaw.type.kind === "func" &&
elseRaw.type.kind === "func" &&
!typeEquals(thenRaw.type, elseRaw.type) &&
@@ -342,7 +343,7 @@ export function lowerTernary(
// a conditional's type even after that field acquired other values.
// A checked arm must retain its actual representation at the join.
const checkedJsJoin =
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(thenRaw.type.kind === "dyn" || elseRaw.type.kind === "dyn") &&
lowerer.dynConvertible(thenRaw.type) &&
lowerer.dynConvertible(elseRaw.type);
@@ -384,7 +385,7 @@ export function lowerTernary(
export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrExpr {
if (
(expr.type.kind === "union" || expr.type.kind === "dyn") &&
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
ts.isPropertyAccessExpression(node)
) {
const symbol = lowerer.checker.getSymbolAtLocation(node.name);
@@ -395,7 +396,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
ts.isPropertyDeclaration(decl) &&
!decl.initializer &&
!decl.type &&
- isJsSourceFile(decl.getSourceFile()),
+ isJsSourceFile(ts.getSourceFileOfNode(decl)),
)
) {
// Inference describes later writes, not the initial undefined.
@@ -415,7 +416,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
// An inferred JS member's storage may be deliberately wider than its
// declaration (prototype-selected buffers, null-initialized scratch
// objects). Extract at the consuming operation, not at the read.
- if (isJsSourceFile(node.getSourceFile()) && ts.isPropertyAccessExpression(node)) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isPropertyAccessExpression(node)) {
const receiver = lowerer.mapTypeOf(lowerer.typeOf(node.expression));
if (receiver?.kind === "object") {
const info = lowerer.classes.get(receiver.className);
@@ -431,7 +432,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
}
}
if (
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
(ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node))
) {
let receiver = node.expression;
@@ -460,7 +461,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
if (bound && lowerer.mapTypeOf(bound)?.kind === "dyn") return expr;
}
}
- if (isJsSourceFile(node.getSourceFile()) && ts.isIdentifier(node)) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(node)) && ts.isIdentifier(node)) {
const symbol = lowerer.resolveValueSymbol(node);
if (
symbol &&
@@ -494,7 +495,9 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
decl.initializer &&
!decl.type &&
!/@(?:param|type)\b/.test(
- decl.getSourceFile().text.slice(decl.parent?.pos ?? decl.pos, decl.getStart()),
+ ts
+ .getSourceFileOfNode(decl)
+ .text.slice(decl.parent?.pos ?? decl.pos, ts.tokenStartOfNode(decl)),
),
)
)
@@ -521,7 +524,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
// has no such property. Preserve the checked read for JS capability
// probes instead of treating a declaration as a runtime type guard.
if (
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
ts.isPropertyAccessExpression(node) &&
["isTTY", "columns", "rows"].includes(node.name.text) &&
lowerer.isStdlibMember(node)
@@ -531,7 +534,7 @@ export function maybeNarrow(lowerer: Lowerer, expr: IrExpr, node: ts.Node): IrEx
// checker flow calls its non-null branch never. The live checked
// value remains authoritative; never is not a numeric guard.
if (
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
(lowerer.typeOf(node).flags & ts.TypeFlags.Never) !== 0
)
return expr;
@@ -838,11 +841,11 @@ export function v8CalleeText(node: ts.Node): string {
const object = v8CalleeText(node.expression);
return ts.isStringLiteral(key) || ts.isNoSubstitutionTemplateLiteral(key)
? `${object}.${key.text}`
- : `${object}[${key.getText()}]`;
+ : `${object}[${ts.getTextOfNode(key)}]`;
}
if (ts.isPropertyAccessExpression(node) && !node.questionDotToken)
return `${v8CalleeText(node.expression)}.${node.name.text}`;
- return node.getText();
+ return ts.getTextOfNode(node);
}
/** Node's TypeError for the operation consuming `node` when it holds a
@@ -1186,6 +1189,56 @@ function classInstanceOrAbsent(lowerer: Lowerer, type: IrType): boolean {
);
}
+/** `Object.fromEntries(...) as T` with T a declared record (the generic
+ * `transform>(t: T): T` idiom): the
+ * entries build a pure index-signature record, which has no layout in
+ * common with T's struct. Node keeps the object itself, with exactly the
+ * keys and the order the entries gave it. The result of fromEntries is a
+ * fresh object nothing else references, so the checked-dynamic bridge,
+ * which keeps keys and their order the same way `JSON.parse(text) as T`
+ * does, cannot be told apart from the original object; a value outside
+ * T throws the catchable TypeError of every checked cast. The entries'
+ * values must be scalars or checked-dynamic values, which the bridge
+ * passes by value or by identity; native arrays and records would be
+ * copied. Other sources may be aliased, where a rebuilt object would sever
+ * mutation, so they keep the exact-shape fence. */
+function freshEntriesRecordAssertion(
+ lowerer: Lowerer,
+ expr: ts.AsExpression | ts.TypeAssertion,
+ inner: IrExpr,
+ target: IrType | null,
+): IrExpr | null {
+ if (inner.type.kind !== "record" || target?.kind !== "record") return null;
+ const source = lowerer.shapes.get(inner.type.shapeId);
+ const dest = lowerer.shapes.get(target.shapeId);
+ if (!source?.indexValue || source.tuple || source.fields.length > 0) return null;
+ if (!dest || dest.tuple || dest.indexValue || shapeHasAccessorSlots(dest)) return null;
+ // Native arrays and records in the entries would be copied on the way
+ // through the checked-dynamic bridge, severing their identity: only
+ // slots of scalars or checked-dynamic values pass.
+ if (extractionCopiesReference(lowerer, source.indexValue)) return null;
+ let operand: ts.Expression = expr.expression;
+ while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
+ if (
+ !ts.isCallExpression(operand) ||
+ !ts.isPropertyAccessExpression(operand.expression) ||
+ operand.expression.name.text !== "fromEntries" ||
+ !lowerer.isStdlibGlobal(operand.expression.expression, "Object")
+ )
+ return null;
+ if (!lowerer.dynConvertible(inner.type)) return null;
+ if (
+ !canDynCheckTo(
+ target,
+ (id) => lowerer.shapes.get(id),
+ (id) => lowerer.unions.get(id),
+ )
+ )
+ return null;
+ const loc = locOf(expr);
+ return { kind: "dynCheck", value: lowerer.coerceToExpected(inner, DYN), type: target, loc };
+}
+
/** `e as T` and the old-style assertion `e` — one node shape (both
* carry `.type` and `.expression`), one lowering. */
export function lowerAsExpression(
@@ -1237,12 +1290,14 @@ export function lowerAsExpression(
// the requested shape; an assertion does not bypass runtime checks.
const narrowed = lowerer.mapTypeOf(lowerer.typeOf(expr.expression));
const bridged =
- narrowed &&
- (narrowed.kind === "f64" ||
- narrowed.kind === "bool" ||
- narrowed.kind === "string" ||
- narrowed.kind === "object" ||
- narrowed.kind === "dyn");
+ (narrowed &&
+ (narrowed.kind === "f64" ||
+ narrowed.kind === "bool" ||
+ narrowed.kind === "string" ||
+ narrowed.kind === "object" ||
+ narrowed.kind === "dyn")) ||
+ // `catch (e: any)`: the binding reads as its checked-dynamic value.
+ (narrowed === null && (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Any) !== 0);
if (!bridged) lowerer.unsupported("SC1063", expr);
}
const inner = lowerer.lowerExpr(expr.expression);
@@ -1323,6 +1378,8 @@ export function lowerAsExpression(
) {
return lowerer.coerceInto(expr, inner, target);
}
+ const rebuilt = freshEntriesRecordAssertion(lowerer, expr, inner, target);
+ if (rebuilt) return rebuilt;
// `job as Job & { startedAt: number }` on a record: the asserted
// record refines the stored one, so the assertion keeps the object
// (a width copy would sever writes through the asserted view).
@@ -1376,6 +1433,12 @@ export function lowerAsExpression(
return inner; // erasure, unchanged
}
+ // `value as never` is the escape hatch that makes an unknown value fit
+ // any slot (`tagSuccess(value as never)`). No value has type never, so
+ // checking against its placeholder layout would reject every value.
+ // Erase it instead: the consuming slot applies the same validated
+ // extraction an unknown value gets wherever the checker admits it.
+ if (expr.type.kind === ts.SyntaxKind.NeverKeyword) return inner;
const targetTs = lowerer.checker.getTypeFromTypeNode(expr.type);
const target = lowerer.mapTypeOf(targetTs) ?? dynFallbackType(lowerer, expr.type, targetTs);
if (!target) lowerer.badType(expr.type, targetTs);
@@ -1450,6 +1513,9 @@ export function lowerAsExpression(
if (DYN_HANDLE_KINDS.has(target.kind)) {
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
}
+ // `value as NodeJS.ReadableStream`: the general stream classes take
+ // the checked cast (general-streams.ts).
+ requestGeneralStreamSurface(lowerer, expr);
lowerer.unsupported(
"SC1090",
expr,
diff --git a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts
index e3b9bf025..51b7f4028 100644
--- a/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/native-proxy.ts
@@ -25,7 +25,7 @@ export function lowerNativeProxy(lowerer: Lowerer, expr: ts.NewExpression): IrEx
if (args.length !== 2 || args.some(ts.isSpreadElement))
lowerer.noLowering("new Proxy with this argument list", expr);
const handler = args[1]!;
- if (isJsSourceFile(expr.getSourceFile()) && ts.isObjectLiteralExpression(handler)) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && ts.isObjectLiteralExpression(handler)) {
for (const prop of handler.properties.nodes) {
const callback = ts.isMethodDeclaration(prop)
? prop
diff --git a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts
index 4366b8d6b..81b3b9d69 100644
--- a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts
@@ -18,6 +18,11 @@ import { arrayNumberRead, arrayValueRead, arrayValueStore } from "../array-value
import { fenceNodeModuleMutation } from "../lower-node-module.js";
import { globalSymbolKey } from "./global-symbols.js";
import { tryLowerExpression } from "./try-lower-expression.js";
+import {
+ type KeyedFieldTarget,
+ keyedFieldTarget,
+ saveKeyedFieldKey,
+} from "./keyed-field-targets.js";
import {
eventAccessorRead,
eventAccessorWrite,
@@ -147,6 +152,7 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
} else {
const receiver = save(lowerer.lowerExpr(target.expression), "%shortCircuitReceiver");
let key = lowerer.lowerExpr(target.argumentExpression);
+ let keyed: KeyedFieldTarget | null = null;
if (receiver.type.kind === "dyn") {
key = save(
{
@@ -245,6 +251,17 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp
value: lowerer.coerceToExpected(value, indexValue),
loc,
});
+ } else if (
+ receiver.type.kind === "record" &&
+ (keyed = keyedFieldTarget(lowerer, target, false, { obj: receiver, key }))
+ ) {
+ // `totals[side] ??= v` through a literal-union key: the read and
+ // the write dispatch over the declared fields the key can name.
+ const field = keyed;
+ saveKeyedFieldKey(field, save);
+ storage = field.fieldType;
+ read = lowerer.fieldGetExpr(field, locOf(target), target);
+ write = (value) => lowerer.fieldSetStmt(field, value, loc, target);
} else {
lowerer.unsupported(
"SC1090",
diff --git a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
index 6238479e0..303b6ed55 100644
--- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
@@ -276,7 +276,7 @@ export function lowerDynObjectLiteral(
const raw =
builtin === "path" || builtin === "path/posix" || builtin === "path/win32"
? pathModuleValue(lowerer, builtin === "path" ? "path/posix" : builtin, locOf(prop))
- : isJsSourceFile(expr.getSourceFile())
+ : isJsSourceFile(ts.getSourceFileOfNode(expr))
? lowerer.lowerExprExpecting(prop.expression, DYN)
: lowerer.lowerExpr(prop.expression);
fenceSymbolFieldCopy(lowerer, prop.expression, raw.type);
@@ -410,7 +410,7 @@ export function lowerDynObjectLiteral(
// Repeated references must compare equal and mutations remain shared.
if (
!boxValue &&
- isJsSourceFile(prop.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(prop)) &&
v.kind === "dynFrom" &&
v.value.type.kind === "array"
) {
@@ -495,7 +495,7 @@ function fenceClosureProbe(
slotType: IrType | undefined,
attempt: () => IrExpr,
): IrExpr | null {
- if (!isJsSourceFile(node.getSourceFile())) return null;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(node))) return null;
let inner: ts.Node = node;
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
if (
@@ -580,7 +580,10 @@ function literalUnionArmOf(
if (own.isUnionType()) return null;
props.length = 0;
for (const property of lowerer.checker.getPropertiesOfType(own)) {
- props.push({ name: property.name, type: lowerer.checker.getTypeOfSymbol(property) });
+ props.push({
+ name: ts.symbolName(property),
+ type: lowerer.checker.getTypeOfSymbol(property),
+ });
}
}
/** Cheap literal discriminants avoid checker traffic for unrelated arms.
@@ -640,6 +643,74 @@ function literalUnionArmOf(
/** Object spread treats nullish sources as empty. Preserve evaluation once,
* then turn `Record | 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,
@@ -698,7 +769,7 @@ export function lowerObjectLiteral(
// Preserve their checked arrays instead of assigning never[] a numeric ABI.
if (
!expected &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
expr.properties.nodes.some(
(prop) =>
ts.isPropertyAssignment(prop) &&
@@ -711,7 +782,7 @@ export function lowerObjectLiteral(
}
if (
!expected &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
expr.properties.nodes.some(
(prop) =>
ts.isPropertyAssignment(prop) &&
@@ -726,7 +797,7 @@ export function lowerObjectLiteral(
// the represented binding rather than checking it back into that type.
if (
!expected &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
expr.properties.nodes.some((prop) => {
let value = ts.isPropertyAssignment(prop)
? prop.initializer
@@ -754,7 +825,7 @@ export function lowerObjectLiteral(
// JavaScript object methods/accessors carry a live receiver. Keep the
// literal in checked native storage instead of erasing its prototype ABI.
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(expr.properties.nodes.length === 0 ||
expr.properties.nodes.some(
(prop) =>
@@ -770,7 +841,7 @@ export function lowerObjectLiteral(
// binding. Construct in that representation: an intermediate record would
// check nested aliases back into inferred shapes and lose their identity.
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
ts.isVariableDeclaration(expr.parent) &&
expr.parent.initializer === expr &&
ts.isIdentifier(expr.parent.name) &&
@@ -782,7 +853,7 @@ export function lowerObjectLiteral(
// methods absent from its inferred record shape. Copy the real properties.
if (
!expected &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
expr.properties.nodes.some(
(property) =>
ts.isSpreadAssignment(property) &&
@@ -802,7 +873,7 @@ export function lowerObjectLiteral(
// values. TypeScript keeps the record world and its fence: this shape
// only arises in checked-dynamic JS.
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
expr.properties.nodes.some((p) => {
const n = ts.isSpreadAssignment(p) ? undefined : p.name;
return n !== undefined && ts.isComputedPropertyName(n);
@@ -849,7 +920,7 @@ export function lowerObjectLiteral(
// receiver (capturing the record under construction would be an
// RC cycle), and the generic lexical-this walk would incorrectly
// capture an ENCLOSING method's `this` — the object-method rule.
- if (!isJsSourceFile(expr.getSourceFile())) {
+ if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) {
rejectThisInObjectAccessor(lowerer, prop.body ?? prop);
}
}
@@ -925,7 +996,7 @@ export function lowerObjectLiteral(
// `null`, which maps to nothing on its own).
if (
tsType.isUnionType() &&
- ts.constituentTypes(tsType).some((t) => t.getSymbol()?.name === "PromiseLike")
+ ts.constituentTypes(tsType).some((t) => ts.symbolNameOf(t.getSymbol()) === "PromiseLike")
) {
tsType = lowerer.checker.getAwaitedType(tsType) ?? tsType;
}
@@ -946,7 +1017,7 @@ export function lowerObjectLiteral(
// A JavaScript call can contextually type an object literal as string even
// though the literal itself is a record. Keep its own shape so a caller
// performing ToString can convert it after the value is built.
- if (mapped?.kind === "string" && isJsSourceFile(expr.getSourceFile())) {
+ if (mapped?.kind === "string" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const own = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (own?.kind === "record") mapped = own;
}
@@ -1079,7 +1150,7 @@ export function lowerObjectLiteral(
// (rest-param function types) narrow away. SEMANTICS.md documents the
// enumeration divergence.
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(!mapped ||
expr.properties.nodes.some(
(p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p),
@@ -1119,7 +1190,7 @@ export function lowerObjectLiteral(
// consumers ride the keyed-dyn paths. TypeScript keeps the fence.
if (
mapped?.kind === "dyn" ||
- (!mapped && isJsSourceFile(expr.getSourceFile())) ||
+ (!mapped && isJsSourceFile(ts.getSourceFileOfNode(expr))) ||
(!expected &&
mapped &&
jsOpenObjectType(expr, mapped, lowerer.shapes, lowerer.unions).kind === "dyn")
@@ -1135,7 +1206,7 @@ export function lowerObjectLiteral(
// extra fields, spreads, accessors, and keyed shapes retain their paths.
if (
expected &&
- !isJsSourceFile(expr.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
expr.properties.nodes.every(
(p) => ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p),
)
@@ -1187,7 +1258,7 @@ export function lowerObjectLiteral(
// JavaScript Dates carry native object identity in checked storage. An
// inferred scalar Date field would erase that identity before boxing.
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
shape.fields.some((field) => field.type.kind === "date")
) {
return lowerDynObjectLiteral(lowerer, expr);
@@ -1197,7 +1268,7 @@ export function lowerObjectLiteral(
// so the literal's shape NARROWS to its plain fields (reads resolve
// through the lifted accessors; Object.keys over the value omits
// accessor names — the documented divergence).
- if (isJsSourceFile(expr.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const accessorNames = new Set(
expr.properties.nodes
.filter((p) => ts.isGetAccessorDeclaration(p) || ts.isSetAccessorDeclaration(p))
@@ -1233,7 +1304,7 @@ export function lowerObjectLiteral(
);
if (
topLevelJsDecl &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
assignedProperties.length === expr.properties.nodes.length
) {
const provided = new Set(assignedProperties.map((prop) => propNameText(lowerer, prop.name)));
@@ -1260,7 +1331,7 @@ export function lowerObjectLiteral(
// multiple spreads, spread-after-explicit order, and shape-changing width
// copies retain their existing lowering and diagnostics.
if (
- !isJsSourceFile(expr.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
!shape.indexValue &&
!shape.tuple &&
!shapeHasAccessorSlots(shape) &&
@@ -1372,7 +1443,7 @@ export function lowerObjectLiteral(
vNode,
);
contributors.push({
- kind: "field",
+ kind: "condField",
name: csProp.name.text,
value: {
kind: "ternary",
@@ -1387,6 +1458,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 +1703,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",
@@ -2146,7 +2222,7 @@ export function lowerObjectLiteral(
// closures is side-effect-free, so their position among the data
// fields' source-order evaluation is unobservable — exactly JS,
// where accessor definitions evaluate nothing.
- if (isJsSourceFile(expr.getSourceFile())) continue;
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) continue;
const name = propNameText(lowerer, prop.name); // key-form-checked above
const slotName = `${ts.isGetAccessorDeclaration(prop) ? "%get" : "%set"}:${name}`;
const slotT = fieldTypes.get(slotName);
@@ -2268,7 +2344,7 @@ export function lowerObjectLiteral(
if (
!(err instanceof PoisonError) ||
!pureMember ||
- !isJsSourceFile(expr.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(expr)) ||
lowerer.diagSink !== null ||
lowerer.diags.length <= propDiagsBefore ||
lowerer.diags.slice(propDiagsBefore).some((d) => d.code === "SC9001")
@@ -2363,7 +2439,7 @@ export function lowerObjectLiteral(
* their identity. Changing a discriminator or adding variant-specific
* fields needs a different result layout and stays on the ordinary path. */
function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpression): IrExpr | null {
- if (isJsSourceFile(expr.getSourceFile())) return null;
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
const spread = expr.properties.nodes[0];
if (!spread || !ts.isSpreadAssignment(spread)) return null;
let sourceNode = spread.expression;
@@ -2559,6 +2635,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 +2674,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) & {
diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts
index 930e1a7cb..5dc512a6c 100644
--- a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts
@@ -1,3 +1,5 @@
+import { isSafeToDiscard } from "./evaluation-safety.js";
+import { completeFuncValueArgs } from "../call-arguments.js";
import { dynUndefinedExpr } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { DYN, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../../ir/ir.js";
@@ -90,7 +92,7 @@ export function isRequireMainFilename(lowerer: Lowerer, expr: ts.Expression): bo
const main = expr.expression;
if (!ts.isPropertyAccessExpression(main) || main.name.text !== "main") return false;
if (!lowerer.isStdlibGlobal(main.expression, "require")) return false;
- return !isNodeEsmFile(expr.getSourceFile(), lowerer.program);
+ return !isNodeEsmFile(ts.getSourceFileOfNode(expr), lowerer.program);
}
export function isOptionalChainTail(lowerer: Lowerer, expr: ts.Expression): boolean {
@@ -255,14 +257,14 @@ export function lowerOptionalChain(
const spreads: { arg: number; what: string }[] = [];
const args = expr.arguments.nodes.map((arg, index) => {
if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN);
- spreads.push({ arg: index, what: arg.expression.getText() });
+ spreads.push({ arg: index, what: ts.getTextOfNode(arg.expression) });
return lowerer.lowerExprExpecting(arg.expression, DYN);
});
const body: IrExpr = {
kind: "dynCall",
callee,
...(thisValue ? { receiver: thisValue } : {}),
- calleeName: recvNode.getText(),
+ calleeName: ts.getTextOfNode(recvNode),
args,
...(spreads.length ? { spreads } : {}),
type: DYN,
@@ -429,7 +431,22 @@ export function lowerOptionalChain(
if (dotNode === expr && ts.isCallExpression(expr)) {
if (narrowed.kind !== "func") lowerer.badType(recvNode, lowerer.typeOf(recvNode));
const params = narrowed.params;
+ // Forwarded fixed tuples (`f?.(...args)`) complete into the slots.
+ if (expr.arguments.nodes.some(ts.isSpreadElement)) {
+ const completed = completeFuncValueArgs(lowerer, expr, narrowed, loc);
+ const call: IrExpr = {
+ kind: "callValue",
+ callee: recvRef,
+ args: completed,
+ type: narrowed.ret,
+ loc,
+ };
+ return lowerer.finishOptionalChain(expr, id, receiver, call, loc);
+ }
const args = expr.arguments.nodes.map((a, i) => lowerer.lowerExprExpecting(a, params[i]));
+ // Effect-free surplus arguments (a dropped `void` slot) are ignored.
+ while (narrowed.rest !== true && args.length > params.length && isSafeToDiscard(args.at(-1)!))
+ args.pop();
const body: IrExpr = { kind: "callValue", callee: recvRef, args, type: narrowed.ret, loc };
return lowerer.finishOptionalChain(expr, id, receiver, body, loc);
}
diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
index a3154c45a..36ea0e8c2 100644
--- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
@@ -1,5 +1,9 @@
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
import * as ts from "../../ts7/adapter.js";
+import { lowerHistogramProperty } from "../builtins/performance.js";
+import { staticFunctionNameRead } from "../static-function-names.js";
+import { typedArgumentsArity } from "../typed-arguments.js";
+import { isThisParameter } from "../call-signatures.js";
import { localPrivateBrandClass } from "../private-brands.js";
import { type Lowerer, PoisonError, jsFuncNameOf, neverTaintedJsType, own } from "../lowerer.js";
import {
@@ -96,6 +100,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,
@@ -154,6 +159,7 @@ import { errorPropertyRead } from "../error-methods.js";
import { isSafeToRepeat } from "./evaluation-safety.js";
import { isZlibStreamMember, lowerZlibStreamBytesWritten } from "../builtins/compression.js";
import { isFsStreamMember, lowerFsStreamProperty } from "../builtins/fs-streams.js";
+import { keyedFieldRead } from "./keyed-field-targets.js";
/** True when a property access names an ABSTRACT property declaration
* (`abstract p: number` — a PropertyDeclaration, not an accessor).
@@ -199,7 +205,7 @@ function isParseArgsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpress
const sym = lowerer.checker.getSymbolAtLocation(expr.name);
if (!sym) return false;
return sym.declarations.some((d) => {
- if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
+ if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false;
let inParseArgsType = false;
for (let node: ts.Node | undefined = d.parent; node; node = node.parent) {
if (
@@ -227,7 +233,7 @@ function isStatFsDynProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression
return (
!!symbol &&
symbol.declarations.some((declaration) => {
- if (!lowerer.isStdlibFile(declaration.getSourceFile())) return false;
+ if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration))) return false;
const parent = declaration.parent;
if (!ts.isInterfaceDeclaration(parent)) return false;
if (
@@ -254,6 +260,23 @@ export function lowerPropertyAccessExpr(
const navigator = lowerNavigatorMember(lowerer, expr, NODE_COMPAT_VERSION);
if (navigator) return navigator;
}
+ // `fn.name` on a function parameter of a generic instance keyed by
+ // its argument's static name (call-site specialization).
+ {
+ const staticName = staticFunctionNameRead(lowerer, expr);
+ if (staticName !== null) return { kind: "strLit", value: staticName, type: STRING, loc };
+ }
+ // An IntervalHistogram's readings (min, max, mean, stddev, count, exceeds).
+ {
+ const reading = lowerHistogramProperty(lowerer, expr);
+ if (reading) return reading;
+ }
+ // `.length` of a forwarded fixed tuple (a typed `arguments`, a tuple
+ // rest parameter, an alias of either): the signature's arity.
+ if (expr.name.text === "length" && !expr.questionDotToken) {
+ const arity = typedArgumentsArity(lowerer, expr.expression);
+ if (arity !== null) return { kind: "numLit", value: arity, type: F64, loc };
+ }
// `require("./x").member` over a program module's export table: the
// module's %init at this position, then the member read exactly like
// a `const m = require("./x")` binding's (cjs-inline-require.ts).
@@ -513,7 +536,7 @@ export function lowerPropertyAccessExpr(
if (
represented &&
isMatchSliceType(lowerer, represented.type) &&
- (lowerer.isStdlibMember(expr) || isJsSourceFile(expr.getSourceFile()))
+ (lowerer.isStdlibMember(expr) || isJsSourceFile(ts.getSourceFileOfNode(expr)))
) {
const recv = represented;
const key: IrExpr = { kind: "strLit", value: expr.name.text, type: STRING, loc };
@@ -546,7 +569,7 @@ export function lowerPropertyAccessExpr(
ts.isIdentifier(expr.expression) &&
expr.expression.text === "arguments" &&
!lowerer.ctx.argumentsLocal &&
- lowerer.typeOf(expr.expression).getSymbol()?.name === "IArguments" &&
+ ts.symbolNameOf(lowerer.typeOf(expr.expression).getSymbol()) === "IArguments" &&
lowerer.isStdlibSymbol(lowerer.typeOf(expr.expression).getSymbol())
) {
let fn: ts.Node | undefined = expr.parent;
@@ -569,9 +592,12 @@ export function lowerPropertyAccessExpr(
p.dotDotDotToken === undefined,
);
if (fixedArity) {
+ // A `this` parameter is type-only: callers never pass it.
return {
kind: "numLit",
- value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.length,
+ value: (fn as ts.FunctionLikeDeclaration).parameters.nodes.filter(
+ (p) => !isThisParameter(p),
+ ).length,
type: F64,
loc,
};
@@ -614,9 +640,9 @@ export function lowerPropertyAccessExpr(
if (
!expr.questionDotToken &&
isModuleExportsAccess(expr) &&
- isCjsJsFile(expr.getSourceFile(), lowerer.program)
+ isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program)
) {
- const whole = cjsClassExprWholeExportOf(expr.getSourceFile());
+ const whole = cjsClassExprWholeExportOf(ts.getSourceFileOfNode(expr));
if (whole) {
let inBody = false;
for (
@@ -629,7 +655,7 @@ export function lowerPropertyAccessExpr(
break;
}
}
- if (inBody || expr.getStart() > whole.stmt.getEnd()) {
+ if (inBody || ts.tokenStartOfNode(expr) > whole.stmt.end) {
return lowerer.classValueRef(lowerer.lowerClassExpressionInfo(whole.classExpr), expr);
}
}
@@ -741,6 +767,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.
@@ -770,11 +802,13 @@ export function lowerPropertyAccessExpr(
if (
!expr.questionDotToken &&
stdlibGlobalNameOf(lowerer, expr.expression) === "globalThis" &&
- isJsSourceFile(expr.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(expr))
) {
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 +821,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
@@ -809,7 +850,7 @@ export function lowerPropertyAccessExpr(
// them, and calls dispatch through the dyn method machinery
// instead.
if (
- (isJsSourceFile(expr.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(lowerer.mapTypeOf(lowerer.typeOf(expr.expression)) === null ||
(lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Never) !== 0 ||
// A never-tainted receiver type maps (never rides as f64) but
@@ -850,7 +891,7 @@ export function lowerPropertyAccessExpr(
: { kind: "strIntrinsic", method: "length", receiver: recv, args: [], type: F64, loc };
}
}
- if (isJsSourceFile(expr.getSourceFile()) && expr.name.text === "constructor") {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.name.text === "constructor") {
const receiver = tryLowerExpression(lowerer, expr.expression);
if (
receiver?.type.kind === "dyn" ||
@@ -866,6 +907,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
@@ -877,7 +955,7 @@ export function lowerPropertyAccessExpr(
// storage can expose its own name and length in both source languages.
if (
!["apply", "bind", "call", "toString", "caller", "arguments"].includes(expr.name.text) &&
- (isJsSourceFile(expr.getSourceFile()) ||
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) ||
expr.name.text === "name" ||
expr.name.text === "length" ||
expr.name.text === "constructor")
@@ -890,7 +968,9 @@ export function lowerPropertyAccessExpr(
const receiverSymbol = receiverType.getSymbol();
const checkedFunction =
lowerer.checker.getCallSignatures(receiverType).length > 0 ||
- (receiverSymbol?.name === "Function" && lowerer.isStdlibSymbol(receiverSymbol));
+ (receiverSymbol !== undefined &&
+ ts.symbolName(receiverSymbol) === "Function" &&
+ lowerer.isStdlibSymbol(receiverSymbol));
if (probed?.type.kind === "dyn" && checkedFunction) {
const key: IrExpr = {
kind: "strLit",
@@ -912,7 +992,7 @@ export function lowerPropertyAccessExpr(
);
}
if (
- (isJsSourceFile(expr.getSourceFile()) || expr.name.text === "constructor") &&
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) || expr.name.text === "constructor") &&
probed?.type.kind === "func" &&
canBoxFuncIntoDyn(
probed.type,
@@ -946,17 +1026,17 @@ export function lowerPropertyAccessExpr(
if (expr.name.text === "done" || expr.name.text === "value") {
const receiverType = lowerer.typeOf(expr.expression);
const recvSym =
- receiverType.getAliasSymbol()?.name === "IteratorResult"
+ ts.symbolNameOf(receiverType.getAliasSymbol()) === "IteratorResult"
? receiverType.getAliasSymbol()
: receiverType.getSymbol();
if (
- (recvSym?.name === "IteratorResult" ||
- recvSym?.name === "IteratorYieldResult" ||
- recvSym?.name === "IteratorReturnResult") &&
+ ((recvSym !== undefined && ts.symbolName(recvSym) === "IteratorResult") ||
+ (recvSym !== undefined && ts.symbolName(recvSym) === "IteratorYieldResult") ||
+ (recvSym !== undefined && ts.symbolName(recvSym) === "IteratorReturnResult")) &&
recvSym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) &&
- lowerer.isStdlibFile(d.getSourceFile()),
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
)
) {
const recv = lowerer.lowerExpr(expr.expression);
@@ -1007,7 +1087,8 @@ export function lowerPropertyAccessExpr(
// members owned by these implemented families bypass the stdlib fence.
const segmentDataSymbol = lowerer.typeOf(expr.expression).getSymbol();
const segmentDataProperty =
- segmentDataSymbol?.name === "SegmentData" &&
+ segmentDataSymbol !== undefined &&
+ ts.symbolName(segmentDataSymbol) === "SegmentData" &&
lowerer.isStdlibSymbol(segmentDataSymbol) &&
["segment", "index", "input", "isWordLike"].includes(expr.name.text);
// os.constants members vary by platform (the declared groups list
@@ -1064,6 +1145,23 @@ export function lowerPropertyAccessExpr(
);
}
}
+ // A stdlib-declared member (`message` of an interface extending Error)
+ // on a catch binding narrowed to a structural type: the binding reads
+ // as its checked-dynamic value, so the member does too.
+ if (!expr.questionDotToken && lowerer.caughtLocalOf(expr.expression)) {
+ const recv = lowerer.lowerExpr(expr.expression);
+ if (recv.kind === "caughtToDyn") {
+ const read: IrExpr = {
+ kind: "dynKeyGet",
+ key: { kind: "strLit", value: expr.name.text, type: STRING, loc: locOf(expr.name) },
+ value: recv,
+ type: DYN,
+ loc,
+ };
+ const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
+ return declared && declared.kind !== "void" ? lowerer.coerceToExpected(read, declared) : read;
+ }
+ }
// The lib fence's PROPERTY chokepoint: a stdlib-declared member that
// no lowering above claimed ([1,2].entries, Math.SQRT2, Promise.all,
// re.exec as a value, ...) reports SC2020 here.
@@ -1097,7 +1195,7 @@ export function lowerPropertyAccessExpr(
// validated dynCheck as usual.
if (
recvLowered.type.kind === "dyn" ||
- (isJsSourceFile(expr.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(["f64", "bool", "bigint", "string", "symbol"].includes(recvLowered.type.kind) ||
(recvLowered.type.kind === "union" && lowerer.dynConvertible(recvLowered.type))))
) {
@@ -1165,8 +1263,8 @@ export function lowerPropertyAccessExpr(
recvShape &&
!recvShape.tuple &&
lowerer.implicitParamTypes !== null &&
- isJsSourceFile(expr.getSourceFile()) &&
- npmStaticPackageOfPath(expr.getSourceFile().fileName) !== null
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
+ npmStaticPackageOfPath(ts.getSourceFileOfNode(expr).fileName) !== null
) {
return {
kind: "seqExpr",
@@ -1255,6 +1353,29 @@ 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;
+ // A checker-`any` receiver whose value is a FRESH typed result of an
+ // earlier step in the chain (`out.split(" ").length`, where split
+ // extracted the string and answered a native string[]): the member read
+ // goes through the boxed checked-dynamic value, as the method-call twin
+ // does. The box copies; a call result has no other holder to observe it.
+ if (
+ (lowerer.typeOf(expr.expression).flags & ts.TypeFlags.Any) !== 0 &&
+ isFreshCallResult(expr.expression) &&
+ lowerer.dynConvertible(recvLowered.type)
+ ) {
+ return lowerer.maybeNarrow(
+ {
+ kind: "dynKeyGet",
+ value: lowerer.coerceToExpected(recvLowered, DYN),
+ key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
+ type: DYN,
+ loc,
+ },
+ expr,
+ );
+ }
lowerer.unsupported(
"SC1090",
expr,
@@ -1262,6 +1383,108 @@ export function lowerPropertyAccessExpr(
);
}
+/** A call expression, through parentheses: its value has no other holder. */
+function isFreshCallResult(node: ts.Expression): boolean {
+ let e = node;
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ return ts.isCallExpression(e);
+}
+
+/** `(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;
}
@@ -1288,7 +1511,12 @@ export function lowerIntrinsicProperty(
| "BigInt"
)[]) {
if (!lowerer.chainBlocked(expr) && lowerer.isStdlibGlobal(expr, name)) {
- return primitiveCtorClosure(lowerer, name, locOf(expr), isJsSourceFile(expr.getSourceFile()));
+ return primitiveCtorClosure(
+ lowerer,
+ name,
+ locOf(expr),
+ isJsSourceFile(ts.getSourceFileOfNode(expr)),
+ );
}
}
if (lowerer.stdlibGlobalMember(expr, "Reflect") === "ownKeys") {
@@ -1305,6 +1533,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 {
@@ -1459,7 +1694,7 @@ export function lowerIntrinsicProperty(
)
return null;
const name = expr.name.text;
- if (kind === "array" && name === "constructor" && isJsSourceFile(expr.getSourceFile())) {
+ if (kind === "array" && name === "constructor" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const receiver = lowerer.lowerExpr(expr.expression);
if (receiver.type.kind === "dyn")
return {
@@ -1487,7 +1722,7 @@ export function lowerIntrinsicProperty(
const strictReceiver = (expected: IrType): IrExpr => {
const value = lowerer.lowerExpr(expr.expression);
if (value.type.kind === "dyn") {
- if (isJsSourceFile(expr.getSourceFile()) && name === "length") return value;
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && name === "length") return value;
return lowerer.coerceInto(expr.expression, value, expected);
}
return lowerer.runtimeOptionalPropertyReceiver(expr.expression, value, expected, name) ?? value;
@@ -2007,7 +2242,7 @@ function lowerReplacedExportsRead(
if (expr.questionDotToken) return null;
const recv = expr.expression;
if (!ts.isIdentifier(recv) || recv.text !== "exports") return null;
- const sf = expr.getSourceFile();
+ const sf = ts.getSourceFileOfNode(expr);
if (!isJsSourceFile(sf) || isNodeEsmFile(sf, lowerer.program)) return null;
if (lowerer.peekLocal(recv) || lowerer.globalOf(recv)) return null; // a user binding shadows
// Write position is the export-assignment machinery's territory.
@@ -2044,7 +2279,7 @@ function lowerCjsExportTableThisMember(
expr: ts.PropertyAccessExpression,
): IrExpr | null {
if (expr.expression.kind !== ts.SyntaxKind.ThisKeyword || expr.questionDotToken) return null;
- if (!isJsSourceFile(expr.getSourceFile())) return null;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
// The enclosing accessor, crossing only arrow boundaries (arrows
// inherit `this`; any other function form rebinds it).
let n: ts.Node | undefined = expr.parent;
@@ -2244,7 +2479,8 @@ function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressio
const groups = matchResultNamedGroupsOf(lowerer, expr.expression);
// An untraced regex: TypeScript reaches the member fence; JavaScript
// would otherwise read the slice's absent expando as undefined.
- if (groups === null && isJsSourceFile(expr.getSourceFile())) lowerer.stdlibMemberFence(expr);
+ if (groups === null && isJsSourceFile(ts.getSourceFileOfNode(expr)))
+ lowerer.stdlibMemberFence(expr);
if (groups === null) return null;
if (groups.length === 0) {
// Node: a match result of a group-less regex answers undefined —
@@ -2261,7 +2497,7 @@ function lowerMatchGroupsRead(lowerer: Lowerer, expr: ts.PropertyAccessExpressio
}
const recv = lowerer.lowerExpr(expr.expression);
// A JavaScript callback parameter holds the row as a dynamic array.
- const dynRow = recv.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile());
+ const dynRow = recv.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr));
if (!dynRow && !isMatchSliceType(lowerer, recv.type)) return null;
return lowerGroupsProjection(lowerer, recv, groups, loc);
}
@@ -2584,6 +2820,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,
@@ -2817,7 +3074,7 @@ export function lowerUnionProperty(
// instance argument (e.g. Matrix4.makeTranslation). Preserve primitive
// missing-property semantics and dispatch class data through its native
// capsule instead of demanding a field shared by every union arm.
- if (isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(value.type)) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(value.type)) {
return {
kind: "dynKeyGet",
value: lowerer.coerceToExpected(value, DYN),
@@ -2969,7 +3226,7 @@ export function classFieldTarget(
const represented = saved ?? lowerer.lowerExpr(receiverNode);
if (
represented.type.kind === "dyn" &&
- isJsSourceFile(receiverNode.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(receiverNode)) &&
!fieldName.startsWith("#")
) {
return { container: "dynamic", obj: represented, field: fieldName, fieldType: DYN };
@@ -3016,7 +3273,7 @@ export function classFieldTarget(
if (fieldType) {
const obj = lowerObjectReceiver();
if (
- isJsSourceFile(receiverNode.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(receiverNode)) &&
!fieldName.startsWith("#") &&
[...lowerer.classes.values()].some(
(candidate) =>
@@ -3054,7 +3311,7 @@ export function classFieldTarget(
fieldType: getF ? getF.sig.ret : setF!.sig.params[0]!.type,
};
}
- if (info.decl && isJsSourceFile(info.decl.getSourceFile())) {
+ if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) {
const property = lowerer.checker.getPropertyOfType(
lowerer.checker.getTypeAtLocation(receiverNode),
fieldName,
@@ -3273,7 +3530,7 @@ export function fieldTarget(
: [declared];
return declaredArms.every((a) => ivArms.some((b) => typeEquals(a, b)));
};
- if (!nameSym || nameSym.name === ts.InternalSymbolName.Index || canonicalized()) {
+ if (!nameSym || ts.symbolName(nameSym) === ts.InternalSymbolName.Index || canonicalized()) {
let obj = lowerer.lowerExpr(access.expression);
if (obj.type.kind === "union" && lowerer.armTag(obj.type.unionId, UNDEFINED_T) >= 0) {
const present = lowerer.stripUndefinedArm(obj.type);
@@ -3330,6 +3587,7 @@ export function fieldGetExpr(
);
if (checked) target = { ...target, obj: checked };
}
+ if (target.container === "recordKeyed") return keyedFieldRead(lowerer, target, loc);
if (target.container === "dynamic")
return lowerer.coerceInto(
blame,
@@ -3384,7 +3642,7 @@ export function fieldGetExpr(
}
if (
!target.field.startsWith("#") &&
- isJsSourceFile(blame.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(blame)) &&
lowerer.dynConvertible(getF.sig.ret)
) {
const value: IrExpr = {
@@ -3557,6 +3815,225 @@ 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 = 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 = 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 = 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 = 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}`
+ : ts.getTextOfNode(callee).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 &&
+ ts.symbolName(symbol).endsWith("Constructor") &&
+ GLOBAL_CONSTRUCTOR_VALUES.has(ts.symbolName(symbol).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 = 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
@@ -3570,7 +4047,7 @@ function jsFunctionExpandoMember(lowerer: Lowerer, access: ts.PropertyAccessExpr
return (
decls.length > 0 &&
decls.every((d) => {
- if (!isJsSourceFile(d.getSourceFile())) return false;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(d))) return false;
const assign = ts.isBinaryExpression(d) ? d : d.parent;
return (
assign !== undefined &&
@@ -3581,3 +4058,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;
+}
diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts
index 4aa62534e..f8b8c7604 100644
--- a/packages/compiler/src/frontend/lowering/expressions/relational.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts
@@ -1,5 +1,6 @@
/* instanceof and in, including private-name and runtime-key in. */
import * as ts from "../../ts7/adapter.js";
+import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js";
import {
checkedPrivateIn,
localPrivateBrandClass,
@@ -25,7 +26,7 @@ import {
typeEquals,
typeKey,
} from "../../../ir/ir.js";
-import { refuseWebAssemblyUse } from "../builtins/webassembly.js";
+import { lowerWebAssemblyInstanceof, refuseWebAssemblyUse } from "../builtins/webassembly.js";
import { lowerSocketInstanceOf } from "../lower-server.js";
import { builtinClassRefOf } from "../builtins/class-values.js";
import { lowerZlibInstanceOf } from "../builtins/compression.js";
@@ -62,6 +63,8 @@ import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"
* Folding is limited to side-effect-free operands — dropping a computed
* operand would skip its effects, so those are rejected instead. */
export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
+ const webAssembly = lowerWebAssemblyInstanceof(lowerer, expr);
+ if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(lowerer, expr.right);
// `x instanceof net.Socket` over a union with a netSocket arm — the
// h2 compat 'connect' narrowing (lower-server.ts): a union tag test.
@@ -467,10 +470,26 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
);
}
const constructor = tryLowerExpression(lowerer, expr.right);
- if (constructor?.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) {
+ if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return checkedClassInstanceOf(lowerer, value, constructor, loc);
}
+ // `this instanceof Glob` in a JavaScript constructor function: the
+ // function's prototype against the value's prototype chain.
+ if (
+ constructor?.type.kind === "func" &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
+ lowerer.dynConvertible(constructor.type)
+ ) {
+ const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
+ return {
+ kind: "libCall",
+ fn: "dyn.functionInstanceOf",
+ args: [value, { kind: "dynFrom", value: constructor, type: DYN, loc }],
+ type: BOOL,
+ loc,
+ };
+ }
lowerer.unsupported(
"SC1090",
expr.right,
@@ -579,6 +598,14 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
loc,
};
}
+ // `u instanceof EventEmitter` (or a stream class): an untyped object
+ // built by a constructor function inherits the untyped constructor's
+ // prototype (builtin-class-dyn.ts). Compiled emitters and streams have
+ // no untyped form, so the prototype chain answers for every value.
+ const builtinKind = builtinClassKind(target.def.name);
+ if (left.type.kind === "dyn" && builtinKind !== null) {
+ return builtinClassInstanceOf(left, builtinKind, loc);
+ }
if (left.type.kind === "dyn") {
lowerer.unsupported(
"SC1090",
@@ -1053,7 +1080,11 @@ function lowerPrivateIn(
// when the value is a class instance at or below the declarer. A typed
// reference carries the class it was boxed under: the declarer or a
// descendant answers true; an ancestor answers per instance.
- if (recv.type.kind === "dyn" && recv.kind === "varRef" && isJsSourceFile(expr.getSourceFile())) {
+ if (
+ recv.type.kind === "dyn" &&
+ recv.kind === "varRef" &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr))
+ ) {
// A function-local class has one brand per evaluation.
if (info.localClass) {
const owner = localPrivateBrandClass(lowerer, priv);
@@ -1144,6 +1175,24 @@ function lowerPrivateIn(
}
/** The string keys every record inherits from Object.prototype. */
+/** Keys built-in errors carry outside their class fields: Node's system
+ * error properties, and AggregateError's, SuppressedError's and
+ * AssertionError's own members. */
+const BUILTIN_ERROR_SLOT_KEYS: ReadonlySet = new Set([
+ "errno",
+ "syscall",
+ "path",
+ "dest",
+ "info",
+ "errors",
+ "error",
+ "suppressed",
+ "actual",
+ "expected",
+ "operator",
+ "generatedMessage",
+]);
+
const OBJECT_PROTOTYPE_KEYS: ReadonlySet = new Set([
"constructor",
"__defineGetter__",
@@ -1336,7 +1385,7 @@ export function lowerInExpression(
recv.type.kind === "classval" ? lowerer.classes.get(recv.type.className) : undefined;
if (
(classInfo && (classInfo.callableBase || hasRuntimeStatics(classInfo))) ||
- (isJsSourceFile(expr.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
recv.type.kind === "func" &&
lowerer.dynConvertible(recv.type))
) {
@@ -1360,9 +1409,10 @@ export function lowerInExpression(
// predicate's `err instanceof Error && "code" in err` shape): `code`
// answers from the runtime error's code slot (stamped by fs/system/
// event/spawn errors, absent on plain constructions — exactly Node's
- // own-property answer), and the always-declared members fold true.
- // Everything else fences: the runtime object carries no other dynamic
- // properties to ask (`stack` would be a lie — Node has one, we don't).
+ // own-property answer), and the always-present members (message, name,
+ // stack, Object.prototype's keys, members the static class declares)
+ // fold true. Other keys test the declaring subclasses and the error's
+ // own runtime properties; keys of built-in errors' extra slots fence.
if (recv.type.kind === "object") {
// %DOMException first: `'cause' in e` answers the options form's
// own-property record (the runtime slot), and code/name/message are
@@ -1384,34 +1434,77 @@ export function lowerInExpression(
if (key === "cause") {
return { kind: "libCall", fn: "error.hasCause", args: [recv], type: BOOL, loc };
}
+ // The code slot, or an own `code` with another value
+ // (`err.code = undefined` is present).
if (key === "code") {
- const codeType = lowerer.envValueType();
- if (codeType.kind !== "union")
- throw new InternalCompilerError("lowerer bug: error code type is not a union");
- const undefTag = lowerer.armTag(codeType.unionId, UNDEFINED_T);
- const read: IrExpr = {
+ return { kind: "libCall", fn: "error.hasCode", args: [recv], type: BOOL, loc };
+ }
+ // The receiver evaluates for its effects when the answer is static.
+ const always = (): IrExpr =>
+ recv.kind === "varRef" || recv.kind === "caughtNarrow"
+ ? { kind: "boolLit", value: true, type: BOOL, loc }
+ : {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: recv, loc }],
+ result: { kind: "boolLit", value: true, type: BOOL, loc },
+ type: BOOL,
+ loc,
+ };
+ if (key === "message" || key === "name") return always();
+ // Every Error has its own `stack`, and Error.prototype's `toString`
+ // and `constructor` are among Object.prototype's keys.
+ if (key === "stack" || OBJECT_PROTOTYPE_KEYS.has(key)) return always();
+ // A member the static class declares is always present.
+ const declares = (c: ClassInfo): boolean =>
+ c.fields.has(key) ||
+ lowerer.findMethodOn(c, key) !== null ||
+ lowerer.findMethodOn(c, `get:${key}`) !== null ||
+ lowerer.findMethodOn(c, `set:${key}`) !== null;
+ const staticInfo = lowerer.classes.get(recv.type.className);
+ if (staticInfo && declares(staticInfo)) return always();
+ // Any other key: a descendant class that declares it (instanceof), or
+ // an own property set at runtime (a throw site's extra fields,
+ // Object.assign, writes through a dynamic view). Keys built-in errors
+ // keep outside their class fields depend on the runtime error and
+ // stay fenced.
+ if (!BUILTIN_ERROR_SLOT_KEYS.has(key)) {
+ const stable = recv.kind === "varRef" || recv.kind === "caughtNarrow";
+ const slot = stable ? null : lowerer.declareHiddenLocal("%inRecv", recv.type);
+ const ref = (): IrExpr => (slot ? varRef(slot.id, recv.type, loc) : recv);
+ const tests: IrExpr[] = [];
+ const staticName = recv.type.className;
+ for (const c of lowerer.classes.values()) {
+ const name = c.def.name;
+ if (name === staticName || !lowerer.isSubclassOf(name, staticName) || !declares(c))
+ continue;
+ // A declaring ancestor below the static class already covers it.
+ if (c.base && c.base.def.name !== staticName && declares(c.base)) continue;
+ tests.push({ kind: "instanceOf", value: ref(), className: name, type: BOOL, loc });
+ }
+ tests.push({
kind: "libCall",
- fn: "error.code",
- args: [recv],
- type: codeType,
+ fn: "error.hasKey",
+ args: [ref(), { kind: "strLit", value: key, type: STRING, loc }],
+ type: BOOL,
loc,
- };
+ });
+ const answer = tests.reduce((acc, test) => ({
+ kind: "logical",
+ op: "||",
+ left: acc,
+ right: test,
+ type: BOOL,
+ loc,
+ }));
+ if (!slot) return answer;
return {
- kind: "unionIsTag",
- unionId: codeType.unionId,
- tag: undefTag,
- negated: true,
- value: read,
+ kind: "seqExpr",
+ stmts: [{ kind: "varDecl", localId: slot.id, init: recv, loc }],
+ result: answer,
type: BOOL,
loc,
};
}
- if (
- (key === "message" || key === "name") &&
- (recv.kind === "varRef" || recv.kind === "caughtNarrow")
- ) {
- return { kind: "boolLit", value: true, type: BOOL, loc };
- }
lowerer.unsupported(
"SC1090",
expr,
diff --git a/packages/compiler/src/frontend/lowering/expressions/typeof.ts b/packages/compiler/src/frontend/lowering/expressions/typeof.ts
index c56ef4097..46005e228 100644
--- a/packages/compiler/src/frontend/lowering/expressions/typeof.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/typeof.ts
@@ -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);
@@ -69,7 +71,7 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
(operand.text === "__dirname" || operand.text === "__filename") &&
lowerer.isStdlibGlobal(operand, operand.text)
) {
- const sf = operand.getSourceFile();
+ const sf = ts.getSourceFileOfNode(operand);
if (isNodeEsmFile(sf, lowerer.program) && !/\.tsx?$/.test(sf.fileName))
return { kind: "strLit", value: "undefined", type: STRING, loc };
}
@@ -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.`: 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,13 +193,18 @@ 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
// its own lowering as an identity token (a string), so the operand's
// representation cannot answer. Node's answer follows the export's
// kind, and the binding read has no effects.
- const imported = isJsSourceFile(tested.getSourceFile())
+ const imported = isJsSourceFile(ts.getSourceFileOfNode(tested))
? lowerer.builtinImportOf(tested)
: null;
if (imported && builtinModuleConstOf(lowerer, imported.module, imported.member) === undefined) {
diff --git a/packages/compiler/src/frontend/lowering/expressions/union-fields.ts b/packages/compiler/src/frontend/lowering/expressions/union-fields.ts
index f2dd0d0f0..6454bc1fd 100644
--- a/packages/compiler/src/frontend/lowering/expressions/union-fields.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/union-fields.ts
@@ -117,7 +117,7 @@ export function lowerUnionFieldRead(
access: ts.PropertyAccessExpression,
receiver: IrExpr,
): IrExpr | null {
- if (receiver.type.kind !== "union" || isJsSourceFile(access.getSourceFile())) return null;
+ if (receiver.type.kind !== "union" || isJsSourceFile(ts.getSourceFileOfNode(access))) return null;
const unionId = receiver.type.unionId;
const arms = lowerer.unions.get(unionId)?.arms;
if (!arms || arms.length < 2) return null;
diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts
index 2270f6043..104057906 100644
--- a/packages/compiler/src/frontend/lowering/expressions/updates.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/updates.ts
@@ -42,6 +42,7 @@ import type { CompoundOp } from "../surfaces.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import { hasRuntimeStatics } from "../class-runtime-statics.js";
import { declareContextLocal } from "../function-context.js";
+import { keyedFieldTarget, keyedFieldWrite, saveKeyedFieldKey } from "./keyed-field-targets.js";
/** Read-modify-write updates read the member first: an unassigned
* deferred-init receiver throws Node's member-read TypeError. */
@@ -109,7 +110,7 @@ export function lowerPrefixUnary(lowerer: Lowerer, expr: ts.PrefixUnaryExpressio
}
// Match the checked numeric boundary used by JS binary arithmetic.
const operand =
- raw.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())
+ raw.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
? { kind: "dynCheck" as const, value: raw, type: F64, loc }
: lowerOptionalNumber(lowerer, raw, loc, expr.operand);
if (operand.type.kind !== "f64") operatorUnsupported(lowerer, expr, "-", [raw.type]);
@@ -262,7 +263,7 @@ export function lowerIncDec(
const receiver = save(raw, "%incrementArray");
const rawIndex = lowerer.lowerExpr(access.argumentExpression);
const index = save(
- rawIndex.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())
+ rawIndex.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))
? { kind: "libCall", fn: "dyn.toNumberCoerce", args: [rawIndex], type: F64, loc }
: rawIndex,
"%incrementIndex",
@@ -307,10 +308,13 @@ export function lowerIncDec(
// class-backed protocol view): the expanded form `o.v = o.v + 1`, its
// read checked into number like any other read of the member.
const checkedNumber =
- !isJsSourceFile(expr.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
raw.type.kind === "dyn" &&
lowerer.mapTypeOf(lowerer.typeOf(access))?.kind === "f64";
- if ((isJsSourceFile(expr.getSourceFile()) || checkedNumber) && raw.type.kind === "dyn") {
+ if (
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) || checkedNumber) &&
+ raw.type.kind === "dyn"
+ ) {
const receiver = save(raw, "%incrementReceiver");
const key = save(
ts.isElementAccessExpression(access)
@@ -337,10 +341,11 @@ export function lowerIncDec(
}
const target = ts.isPropertyAccessExpression(access)
? lowerer.fieldTarget(access)
- : symbolFieldTarget(lowerer, access);
+ : (symbolFieldTarget(lowerer, access) ?? keyedFieldTarget(lowerer, access, true));
if (target && lowerer.dynConvertible(target.fieldType)) {
checkDeferredMemberReceiver(lowerer, target, access);
target.obj = save(target.obj, "%incrementReceiver");
+ saveKeyedFieldKey(target, save);
return finish(lowerer.fieldGetExpr(target, loc, access), (value) =>
lowerer.fieldSetStmt(
target,
@@ -350,7 +355,7 @@ export function lowerIncDec(
),
);
}
- if (isJsSourceFile(expr.getSourceFile()) && lowerer.dynConvertible(raw.type)) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && lowerer.dynConvertible(raw.type)) {
const receiver = save(lowerer.coerceInto(access.expression, raw, DYN), "%incrementReceiver");
const key = save(
ts.isElementAccessExpression(access)
@@ -617,6 +622,7 @@ export function fieldSetStmt(
};
}
}
+ if (target.container === "recordKeyed") return keyedFieldWrite(lowerer, target, value, loc);
if (target.container === "dynamic")
return {
kind: "exprStmt",
@@ -815,7 +821,7 @@ export function lowerFieldCompoundValue(
rhsNode ? lowerer.lowerExpr(rhsNode) : { kind: "numLit", value: 1, type: F64, loc },
"%compoundRhs",
);
- if (isJsSourceFile(access.getSourceFile()) && lowerer.dynConvertible(rhs.type)) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(access)) && lowerer.dynConvertible(rhs.type)) {
const numeric = ["-", "*", "/", "%", "**"].includes(op);
const right = lowerer.coerceToExpected(rhs, DYN);
const value = save(
@@ -872,12 +878,13 @@ export function lowerFieldCompoundValue(
const targetOf = (): FieldTarget | null =>
ts.isPropertyAccessExpression(access)
? lowerer.fieldTarget(access)
- : symbolFieldTarget(lowerer, access);
+ : (symbolFieldTarget(lowerer, access) ?? keyedFieldTarget(lowerer, access, true));
const target = targetOf();
if (!target)
lowerer.unsupported("SC1090", access, "compound assignment to unsupported field targets");
checkDeferredMemberReceiver(lowerer, target, access);
target.obj = save(target.obj, "%compoundReceiver");
+ saveKeyedFieldKey(target, save);
// Accessors observe getter, RHS side effects, then setter, all through
// the saved receiver.
const fieldRead = lowerer.fieldGetExpr(target, locOf(access), access);
@@ -908,7 +915,7 @@ export function lowerFieldCompoundValue(
"%compoundRhs",
);
const numericRhs: IrExpr =
- rhs.type.kind === "dyn" && isJsSourceFile(access.getSourceFile())
+ rhs.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(access))
? { kind: "dynCheck", value: rhs, type: F64, loc: rhs.loc }
: lowerOptionalNumber(lowerer, rhs, loc);
let value: IrExpr;
@@ -923,7 +930,7 @@ export function lowerFieldCompoundValue(
} else if (bigintValue) {
value = bigintValue;
} else if (
- (target.fieldType.kind === "dyn" && isJsSourceFile(access.getSourceFile())) ||
+ (target.fieldType.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(access))) ||
// An untyped right operand: the expanded `o.f = o.f op rhs`; the field
// write below checks the result into the declared field type.
(rhs.type.kind === "dyn" &&
diff --git a/packages/compiler/src/frontend/lowering/function-context.ts b/packages/compiler/src/frontend/lowering/function-context.ts
index e31e194c5..92042b7d7 100644
--- a/packages/compiler/src/frontend/lowering/function-context.ts
+++ b/packages/compiler/src/frontend/lowering/function-context.ts
@@ -48,6 +48,16 @@ export interface FnCtx {
argumentsLocal?: IrLocal | null;
/** Arrows inherit the nearest ordinary function's arguments binding. */
inheritsArguments?: boolean;
+ /** A function lowered on demand on top of an unrelated caller's frames
+ * (a generic instance): lexical `this` resolution stops here, so the
+ * body sees what it sees when lowered alone. */
+ thisBoundary?: true;
+ /** Tuple bindings of this function that are only forwarded
+ * (typed-arguments.ts onlyForwarded): binding local id → the native
+ * slots forwarding reads. Such a binding is never materialized. Local ids
+ * are unique within one function only, and nested functions never read
+ * these bindings (a read there needs the tuple). */
+ pureTuples: Map;
/** Declared return type — lets `return` detect record-shape mismatches
* (SC2002) before the validator would ICE on them. */
returnType: IrType;
@@ -81,6 +91,7 @@ export function newFnCtx(
captures: lifted ? [] : null,
captureSources: [],
captureBySymbol: new Map(),
+ pureTuples: new Map(),
selfSymbol,
selfType,
returnType,
@@ -154,7 +165,16 @@ export function captureContextBinding(
throw new InternalCompilerError("lowerer bug: no active function context");
const direct = bindingInContext(current, symbol);
if (direct !== null) return { local: direct, origin: direct, error: null };
- for (let depth = stack.length - 2; depth >= 0; depth--) {
+ let floor = 0;
+ if (symbol === undefined) {
+ for (let depth = stack.length - 1; depth >= 0; depth--) {
+ if (stack[depth]!.thisBoundary) {
+ floor = depth;
+ break;
+ }
+ }
+ }
+ for (let depth = stack.length - 2; depth >= floor; depth--) {
const origin = bindingInContext(stack[depth]!, symbol);
if (origin === null) continue;
diff --git a/packages/compiler/src/frontend/lowering/function-returns.ts b/packages/compiler/src/frontend/lowering/function-returns.ts
index c0e70013c..9500d6d2d 100644
--- a/packages/compiler/src/frontend/lowering/function-returns.ts
+++ b/packages/compiler/src/frontend/lowering/function-returns.ts
@@ -98,8 +98,8 @@ export function appendImplicitUndefinedReturn(
/** Whether the source explicitly documents a return contract. */
export function hasExplicitJsDocReturn(decl: ts.SignatureDeclaration): boolean {
- const sourceFile = decl.getSourceFile();
- return /@returns?\b/.test(sourceFile.text.slice(decl.pos, decl.getStart(sourceFile)));
+ const sourceFile = ts.getSourceFileOfNode(decl);
+ return /@returns?\b/.test(sourceFile.text.slice(decl.pos, ts.tokenStartOfNode(decl)));
}
export function promiseCarriesDyn(lowerer: Lowerer, type: IrType): boolean {
@@ -130,14 +130,14 @@ export function declaredReturnType(
decl: ts.SignatureDeclaration,
blame: ts.Node,
): IrType {
- if (isJsSourceFile(decl.getSourceFile()) && ts.isGetAccessorDeclaration(decl)) return DYN;
+ if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && ts.isGetAccessorDeclaration(decl)) return DYN;
const sig = lowerer.checker.getSignatureFromDeclaration(decl);
if (!sig) lowerer.unsupported("SC1090", decl, "this function form");
const retTsType = lowerer.checker.getReturnTypeOfSignature(sig);
// An inferred null/undefined return in JS can read a mutable checked-value
// field. Keep the actual value instead of restricting it to the initializer.
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
(retTsType.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0
)
return DYN;
@@ -154,7 +154,7 @@ export function declaredReturnType(
)
mappedReturn = mapAsyncGeneratorAnnotation(retTsType, lowerer.typeCtx);
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
decl.modifiers?.nodes.some((modifier) => modifier.kind === ts.SyntaxKind.AsyncKeyword) &&
!decl.asteriskToken &&
mappedReturn?.kind !== "promise"
@@ -162,7 +162,11 @@ export function declaredReturnType(
const fallback = dynFallbackType(lowerer, decl, retTsType);
return fallback?.kind === "promise" ? fallback : { kind: "promise", inner: DYN };
}
- if (mappedReturn && isJsSourceFile(decl.getSourceFile()) && functionCanReturnUndefined(decl))
+ if (
+ mappedReturn &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ functionCanReturnUndefined(decl)
+ )
mappedReturn = lowerer.withUndefinedArmOf(mappedReturn) ?? mappedReturn;
// A generic-class method body that can return an indexed read that
// misses returns undefined there, whatever the declared result says.
@@ -171,33 +175,33 @@ export function declaredReturnType(
mappedReturn.kind === "promise"
? { kind: "promise", inner: lowerer.runtimeOptionalType(mappedReturn.inner) }
: lowerer.runtimeOptionalType(mappedReturn);
- if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "void") {
+ if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && mappedReturn?.kind === "void") {
let returnsValue = false;
const scan = (node: ts.Node): void => {
if (node !== decl && ts.isFunctionLike(node)) return;
if (ts.isReturnStatement(node) && node.expression) returnsValue = true;
- node.forEachChild(scan);
+ ts.forEachChild(node, scan);
};
scan(decl);
if (returnsValue) return DYN;
}
- if (isJsSourceFile(decl.getSourceFile()) && mappedReturn?.kind === "array") return DYN;
+ if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && mappedReturn?.kind === "array") return DYN;
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!hasExplicitJsDocReturn(decl) &&
mappedReturn?.kind === "record"
)
return DYN;
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!hasExplicitJsDocReturn(decl) &&
mappedReturn?.kind === "date"
)
return DYN;
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!hasExplicitJsDocReturn(decl) &&
mappedReturn?.kind === "union" &&
@@ -207,7 +211,7 @@ export function declaredReturnType(
// JS inference assigns Number to arithmetic over any, although the
// runtime operands can both be BigInts. Preserve that native result.
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!hasExplicitJsDocReturn(decl) &&
mappedReturn?.kind === "f64"
@@ -237,13 +241,13 @@ export function declaredReturnType(
)
)
numericAnyReturn = true;
- node.forEachChild(check);
+ ts.forEachChild(node, check);
};
check(decl);
if (numericAnyReturn) return DYN;
}
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!hasExplicitJsDocReturn(decl) &&
mappedReturn !== null &&
promiseCarriesDyn(lowerer, mappedReturn) &&
@@ -264,7 +268,7 @@ export function declaredReturnType(
// value rides its own box, calls go through the boxed thunk (JS
// arity), and typed slots re-check with dynCheck as usual.
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
decl.type === undefined &&
lowerer.mapTypeOf(retTsType)?.kind === "func"
) {
@@ -304,7 +308,7 @@ export function declaredReturnType(
(arm.kind === "object" && !hasExplicitJsDocReturn(decl)),
);
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
decl.type === undefined &&
recordReturn !== null &&
recordReturn !== undefined &&
@@ -468,5 +472,5 @@ export function blockBodyOf(decl: ts.FunctionLikeDeclaration): ts.Block | null {
const body = decl.body;
if (body === undefined) return null;
if (ts.isBlock(body)) return body;
- return decl.forEachChild((c) => (ts.isBlock(c) ? c : undefined)) ?? null;
+ return ts.forEachChild(decl, (c) => (ts.isBlock(c) ? c : undefined)) ?? null;
}
diff --git a/packages/compiler/src/frontend/lowering/general-streams.ts b/packages/compiler/src/frontend/lowering/general-streams.ts
new file mode 100644
index 000000000..d31439465
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/general-streams.ts
@@ -0,0 +1,136 @@
+/* The general stream representation (TypeMapperCtx.generalNodeStreams).
+ *
+ * NodeJS.ReadableStream and NodeJS.WritableStream (and @types/node's
+ * stream.Readable / stream.Writable, which also type child stdio) name the
+ * interface every Node stream implements. By default they map to the
+ * child/process stdio handles: the specialized representation of the
+ * values that commonly fill them (child.stdout, process.stdout). A program
+ * that moves another stream — fs.createReadStream(), a zlib Transform, a
+ * Readable subclass — into one of those slots needs the general
+ * representation instead: the runtime stream classes, with the stdio
+ * handles converted into them where they are read or flow.
+ *
+ * The decision is per program. Reachable emit lowers without it; a flow
+ * that needs it marks the lowerer (needsGeneralNodeStreams) and lowerToIr
+ * lowers the whole program again with it. Programs without such a flow
+ * never take the second pass, so their IR is unchanged. */
+import type { IrExpr, IrType } from "../../ir/ir.js";
+import { everyTypeChild } from "../../ir/traverse.js";
+import * as ts from "../ts7/adapter.js";
+import { isGeneralNodeStreamInterface } from "../type-mapper.js";
+import type { Lowerer } from "./lowerer.js";
+import { streamSidesOf } from "./lower-stream.js";
+
+const STDIO_STREAM_KINDS: ReadonlySet = new Set([
+ "childStream",
+ "childWriter",
+ "procStream",
+]);
+
+/** Whether `type` (through union arms, record fields and the structural
+ * type children) contains a part satisfying `test`. */
+function containsPart(lowerer: Lowerer, type: IrType, test: (part: IrType) => boolean): boolean {
+ const seen = new Set();
+ const walk = (part: IrType): boolean => {
+ if (test(part)) return true;
+ if (part.kind === "union") {
+ if (seen.has(`u:${part.unionId}`)) return false;
+ seen.add(`u:${part.unionId}`);
+ return (lowerer.unions.get(part.unionId)?.arms ?? []).some(walk);
+ }
+ if (part.kind === "record") {
+ if (seen.has(`r:${part.shapeId}`)) return false;
+ seen.add(`r:${part.shapeId}`);
+ return (lowerer.shapes.get(part.shapeId)?.fields ?? []).some((f) => walk(f.type));
+ }
+ let found = false;
+ everyTypeChild(part, (child) => {
+ found = walk(child);
+ return !found;
+ });
+ return found;
+ };
+ return walk(type);
+}
+
+function isGeneralStream(lowerer: Lowerer, part: IrType): boolean {
+ return (
+ part.kind === "object" && streamSidesOf(lowerer, lowerer.classes.get(part.className)) !== null
+ );
+}
+
+/** A mismatched flow (requireExactShape's input) that the general stream
+ * representation resolves: a general stream into a stdio-represented
+ * slot, a child pipe handle into an 'unknown' slot, or an 'unknown' value
+ * into a child-pipe slot (the general classes take a checked cast). */
+export function needsGeneralNodeStreams(
+ lowerer: Lowerer,
+ actual: IrType,
+ expected: IrType,
+): boolean {
+ const stdio = (part: IrType): boolean => STDIO_STREAM_KINDS.has(part.kind);
+ const pipeHandle = (part: IrType): boolean =>
+ part.kind === "childStream" || part.kind === "childWriter";
+ if (actual.kind === "dyn") return containsPart(lowerer, expected, pipeHandle);
+ if (containsPart(lowerer, expected, stdio))
+ return containsPart(lowerer, actual, (p) => isGeneralStream(lowerer, p));
+ return expected.kind === "dyn" && containsPart(lowerer, actual, pipeHandle);
+}
+
+/** Under the general representation, process.stdout / process.stderr (the
+ * procStream fd scalar) flowing into a runtime Writable slot: an
+ * fd-backed Writable whose writes take the same synchronous path as
+ * process.stdout.write. Null when the flow is not that one. */
+export function stdioIntoGeneralStream(
+ lowerer: Lowerer,
+ expr: IrExpr,
+ expected: IrType,
+): IrExpr | null {
+ if (!lowerer.generalNodeStreams || expr.type.kind !== "procStream") return null;
+ const writable: IrType = { kind: "object", className: "%Writable" };
+ // Checked-dynamic stream slots (dyn-streams.ts) hold the Writable boxed.
+ const accepts =
+ (expected.kind === "object" && expected.className === "%Writable") ||
+ (expected.kind === "union" && lowerer.armTag(expected.unionId, writable) >= 0) ||
+ (expected.kind === "dyn" && lowerer.dynNodeStreams);
+ if (!accepts) return null;
+ return {
+ kind: "libCall",
+ fn: "stream.stdioWritable",
+ args: [expr],
+ type: writable,
+ loc: expr.loc,
+ };
+}
+
+/** A stdio-handle receiver whose member has no lowering on the handle
+ * (`out.on("error", ...)`, `child.stdout.pipe(...)`, `output.end()`): when
+ * its declared type is one the general representation maps to a runtime
+ * stream class, the program lowers again with it, where the full stream
+ * surface applies. The caller still reports its fence for this pass. */
+export function requestGeneralStreamSurface(lowerer: Lowerer, receiver: ts.Expression): void {
+ if (lowerer.generalNodeStreams) return;
+ const type = lowerer.checker.getNonNullableType(lowerer.typeOf(receiver));
+ if (isGeneralNodeStreamInterface(type, lowerer.typeCtx))
+ lowerer.requiresGeneralNodeStreams = true;
+}
+
+/** Under the general representation, an untyped value reaching a runtime
+ * stream class slot (the checked unwrap) or an untyped pipe/pipeline
+ * stage: the program lowers again with checked-dynamic stream slots
+ * (dyn-streams.ts), where NodeJS.ReadableStream / NodeJS.WritableStream
+ * hold JavaScript stream objects too. */
+export function noteUntypedStreamFlow(lowerer: Lowerer, expected: IrType | null): void {
+ if (lowerer.dynNodeStreams) return;
+ if (!lowerer.generalNodeStreams) {
+ // An untyped pipe or pipeline stage needs both: the next pass lowers
+ // with the general representation and checked-dynamic slots at once.
+ if (expected === null) {
+ lowerer.requiresGeneralNodeStreams = true;
+ lowerer.requiresDynNodeStreams = true;
+ }
+ return;
+ }
+ if (expected === null || containsPart(lowerer, expected, (p) => isGeneralStream(lowerer, p)))
+ lowerer.requiresDynNodeStreams = true;
+}
diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts
index f588faf6d..880cccc8b 100644
--- a/packages/compiler/src/frontend/lowering/generic-functions.ts
+++ b/packages/compiler/src/frontend/lowering/generic-functions.ts
@@ -1,3 +1,4 @@
+import { staticNameBindings } from "./static-function-names.js";
import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
import type { Lowerer } from "./lowerer.js";
@@ -100,6 +101,9 @@ export interface GenericFnInfo {
* names where the slot binds the call site's object type. Computed on
* first use. */
implicitPatterns?: (ImplicitPattern | null)[];
+ /** Parameter index → symbol for the function parameters whose `.name`
+ * the body reads (static-function-names.ts); computed on first use. */
+ nameReadParams?: Map;
}
export interface GenericInstance {
@@ -140,6 +144,9 @@ export interface GenericInstance {
implicitInferReturn?: true;
/** An erased instance (every type parameter bound to `unknown`). */
erased?: true;
+ /** Name-keyed instances: function parameter → its call site's static
+ * name (null when only known at run time), folded at `.name` reads. */
+ staticFnNames?: Map;
}
/** Defaults bind during inference; collection only resolves symbols in
@@ -218,7 +225,7 @@ export function collectGenericSignature(lowerer: Lowerer, decl: ts.FunctionDecla
lowerer.genericFnsBySymbol.set(symbol, {
decl,
baseName: nameText,
- qualifiedName: lowerer.qualify(decl.getSourceFile(), nsPathPrefix(decl) + nameText),
+ qualifiedName: lowerer.qualify(ts.getSourceFileOfNode(decl), nsPathPrefix(decl) + nameText),
typeParams,
instances: new Map(),
});
@@ -295,7 +302,13 @@ export function genericCallInstance(
const retTs = lowerer.checker.getReturnTypeOfSignature(rsig);
const local = localClassResultInstance(lowerer, expr, info, rsig, params, retTs);
if (local) return local;
- const mappedReturn = lowerer.mapTypeOf(retTs);
+ // A result instantiated at `any` (a type argument inferred from an
+ // untyped value, such as an untyped forwarding call) is checked-dynamic.
+ const mappedReturn =
+ lowerer.mapTypeOf(retTs) ??
+ ((retTs.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(ts.getSourceFileOfNode(info.decl))
+ ? DYN
+ : null);
if (!mappedReturn) {
fenceGenericSignatureResult(lowerer, expr, retTs);
lowerer.badType(expr, retTs);
@@ -317,6 +330,7 @@ export function genericCallInstance(
// different fields). Non-literal bindings (a key union, plain string)
// share one runtime-keyed instance per IR signature, exactly the
// widened discipline.
+ const named = staticNameBindings(lowerer, expr, info);
const keyofTps = keyofConstrainedTypeParams(info);
if (keyofTps.size > 0) {
const tsBindings = new Map();
@@ -333,12 +347,19 @@ export function genericCallInstance(
})
.join(",");
return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, {
- extraKey: `@${litKey}`,
+ extraKey: `@${litKey}${named?.key ?? ""}`,
tsBindings,
+ ...(named ? { staticFnNames: named.names } : {}),
});
}
- return internGenericInstance(lowerer, expr, info, params, returnType, (tsBindings) =>
- lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings),
+ return internGenericInstance(
+ lowerer,
+ expr,
+ info,
+ params,
+ returnType,
+ (tsBindings) => lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings),
+ named ? { extraKey: named.key, staticFnNames: named.names } : undefined,
);
}
@@ -498,7 +519,11 @@ function internGenericInstance(
params: ParamShape[],
returnType: IrType,
makeBindings: (tsBindings: Map) => Map,
- opts?: { extraKey?: string; tsBindings?: Map },
+ opts?: {
+ extraKey?: string;
+ tsBindings?: Map;
+ staticFnNames?: Map;
+ },
): GenericInstance {
// keyof-constrained instantiations append their literal keys
// (extraKey): the IR signature alone under-discriminates there — two
@@ -527,7 +552,7 @@ function internGenericInstance(
return JSON.stringify(tsBound.value);
}
const bound = bindings.get(tp);
- return bound ? lowerer.fmt(bound) : tp.name;
+ return bound ? lowerer.fmt(bound) : ts.symbolName(tp);
})
.join(", ");
// Deep polymorphic recursion renders unbounded types — keep messages sane.
@@ -541,6 +566,7 @@ function internGenericInstance(
bindings,
tsBindings,
typeArgsText,
+ ...(opts?.staticFnNames ? { staticFnNames: opts.staticFnNames } : {}),
};
info.instances.set(key, inst);
lowerer.instantiationQueue.push({ info, inst });
@@ -671,7 +697,12 @@ function unifySignatureBindings(
tsBindings.set(sym, lowerer.typeParamTsResolver(inst) ?? inst);
}
if (!bindings.has(sym)) {
- const mapped = lowerer.mapTypeOf(inst);
+ const mapped =
+ lowerer.mapTypeOf(inst) ??
+ ((inst.flags & ts.TypeFlags.Any) !== 0 &&
+ !isJsSourceFile(ts.getSourceFileOfNode(info.decl))
+ ? DYN
+ : null);
// A visitor's inferred T can be void. It still binds the
// return convention of `(node) => T`; discarding it leaves
// the callback's return type spuriously uninstantiated.
@@ -781,7 +812,7 @@ function unifySignatureBindings(
}
if (declared.flags & ts.TypeFlags.Object) {
for (const dp of lowerer.checker.getPropertiesOfType(declared)) {
- const ip = lowerer.checker.getPropertyOfType(inst, dp.name);
+ const ip = lowerer.checker.getPropertyOfType(inst, ts.symbolName(dp));
if (ip)
unify(
lowerer.checker.getTypeOfSymbol(dp),
@@ -824,6 +855,8 @@ export function lowerGenericInstance(
const prevSuppress = lowerer.suppressStats;
const prevClass = lowerer.currentClass;
const prevImplicit = lowerer.implicitParamTypes;
+ const prevStaticFnNames = lowerer.staticFnNames;
+ lowerer.staticFnNames = inst.staticFnNames ?? null;
// A generic METHOD of a generic-class INSTANTIATION lowers under BOTH
// binding sets: the receiver instantiation's class type parameters
// underneath, the method instantiation's own on top (disjoint symbol
@@ -873,6 +906,8 @@ export function lowerGenericInstance(
: lowerer.bodyReturnType(isAsync, inst.returnType);
const fnCtx = newFnCtx(false, null, null, bodyReturn);
fnCtx.isAsync = isAsync;
+ // Instances lower on demand, on top of whichever caller reached them.
+ fnCtx.thisBoundary = true;
// Implicit-any instances whose declared return did not map lower in
// return-INFERENCE mode: `return` statements record here bare, and the
// post-pass (resolveInferredReturn) settles the type and wraps them.
@@ -984,6 +1019,7 @@ export function lowerGenericInstance(
lowerer.typeParamBindings = prevBindings;
lowerer.typeParamTsBindings = prevTsBindings;
lowerer.implicitParamTypes = prevImplicit;
+ lowerer.staticFnNames = prevStaticFnNames;
lowerer.instantiationContext = prevContext;
lowerer.localClassInstantiations.pop();
lowerer.suppressStats = prevSuppress;
@@ -1136,7 +1172,7 @@ function genericValueInstance(
const rendered = info.typeParams
.map((tp) => {
const bound = bindings.get(tp);
- return bound ? lowerer.fmt(bound) : tp.name;
+ return bound ? lowerer.fmt(bound) : ts.symbolName(tp);
})
.join(", ");
lowerer.typeParamBindings = bindings;
@@ -1235,7 +1271,7 @@ function paramWrittenInBody(
hit = true;
return;
}
- m.forEachChild(scan);
+ ts.forEachChild(m, scan);
};
scan(n);
return hit;
@@ -1270,7 +1306,7 @@ function paramWrittenInBody(
written = true;
return;
}
- n.forEachChild(walk);
+ ts.forEachChild(n, walk);
};
walk(body);
return written;
@@ -1445,7 +1481,7 @@ export function implicitAnyParamSymbolsOf(
});
// Destructured object parameters monomorphize too (implicitPatternsOf);
// their slots stay null here.
- if (!hasImplicitParams && isJsSourceFile(decl.getSourceFile()))
+ if (!hasImplicitParams && isJsSourceFile(ts.getSourceFileOfNode(decl)))
hasImplicitParams = implicitPatternsOf(lowerer, decl).some((pattern) => pattern !== null);
return hasImplicitParams ? out : null;
}
@@ -1595,7 +1631,7 @@ function implicitDeclaredReturn(lowerer: Lowerer, info: GenericFnInfo): IrType |
// declaredReturnType widens non-generic JavaScript functions.
if (
mapped !== null &&
- isJsSourceFile(info.decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
functionCanReturnUndefined(info.decl)
)
return lowerer.withUndefinedArmOf(mapped) ?? mapped;
@@ -1643,7 +1679,7 @@ function bindableImplicitIr(t: IrType | null): t is IrType {
function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrType | null {
while (ts.isParenthesizedExpression(arg)) arg = arg.expression;
if (
- isJsSourceFile(arg.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
(ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))
) {
const saved = lowerer.diagSink;
@@ -1666,7 +1702,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
}
if (
ts.isObjectLiteralExpression(arg) &&
- isJsSourceFile(arg.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
(arg.properties.nodes.length === 0 ||
arg.properties.nodes.some(
(property) =>
@@ -1681,7 +1717,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
if (ts.isIdentifier(arg))
return lowerer.peekLocal(arg)?.type ?? lowerer.globalOf(arg)?.type ?? null;
if (
- isJsSourceFile(arg.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
(ts.isPropertyAccessExpression(arg) || ts.isElementAccessExpression(arg))
) {
// Mutable JS members can retain an initializer-only checker type (for
@@ -1691,7 +1727,7 @@ function storedImplicitArgumentType(lowerer: Lowerer, arg: ts.Expression): IrTyp
if (represented) return represented.type;
}
if (
- isJsSourceFile(arg.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
(ts.isCallExpression(arg) ||
ts.isConditionalExpression(arg) ||
(ts.isBinaryExpression(arg) &&
@@ -1808,7 +1844,7 @@ export function implicitCallInstance(
(neverTaintedJsType(lowerer, arg, t) ? null : lowerer.mapTypeOf(t)) ??
dynFallbackType(lowerer, arg, t);
if (
- isJsSourceFile(arg.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
ts.isArrayLiteralExpression(arg) &&
mapped?.kind === "array" &&
(mapped.elem.kind === "classval" ||
@@ -1833,7 +1869,7 @@ export function implicitCallInstance(
// preserve represented arms through the checked boundary instead
// of specializing a call to a union that would discard them.
if (
- isJsSourceFile(arg.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(arg)) &&
mapped?.kind === "func" &&
!lowerer.dynConvertible(mapped)
) {
@@ -2031,7 +2067,7 @@ export function implicitLocalFnNodeOf(
if (cached !== undefined)
return cached ? (cached.decl as ts.FunctionExpression | ts.ArrowFunction) : null;
const probe = (): ts.FunctionExpression | ts.ArrowFunction | null => {
- if (!implicitMonoFile(decl.getSourceFile())) return null;
+ if (!implicitMonoFile(ts.getSourceFileOfNode(decl))) return null;
if (!ts.isIdentifier(decl.name) || decl.initializer === undefined) return null;
let init: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(init)) init = init.expression;
@@ -2062,7 +2098,7 @@ export function implicitLocalFnNodeOf(
const d = s ? s.valueDeclaration : undefined;
if (
d &&
- d.getSourceFile() === decl.getSourceFile() &&
+ ts.getSourceFileOfNode(d) === ts.getSourceFileOfNode(decl) &&
!(d.pos >= init.pos && d.end <= init.end)
) {
// Declared outside the initializer, in this file: a capture
@@ -2081,7 +2117,7 @@ export function implicitLocalFnNodeOf(
}
}
}
- n.forEachChild(scan);
+ ts.forEachChild(n, scan);
};
scan(init.body);
return captures ? null : init;
@@ -2119,8 +2155,8 @@ export function implicitLocalFnInfoOf(
decl: fnNode,
baseName: name,
qualifiedName: lowerer.qualify(
- decl.getSourceFile(),
- nsPathPrefix(stmt ?? decl, decl) + `${name}%l${decl.getStart()}`,
+ ts.getSourceFileOfNode(decl),
+ nsPathPrefix(stmt ?? decl, decl) + `${name}%l${ts.tokenStartOfNode(decl)}`,
),
typeParams: [],
instances: new Map(),
@@ -2214,7 +2250,10 @@ export function objLitGenericFnInfoOf(
const info: GenericFnInfo = {
decl: fnNode,
baseName: name,
- qualifiedName: lowerer.qualify(fnNode.getSourceFile(), `%ol${fnNode.getStart()}.${name}`),
+ qualifiedName: lowerer.qualify(
+ ts.getSourceFileOfNode(fnNode),
+ `%ol${ts.tokenStartOfNode(fnNode)}.${name}`,
+ ),
typeParams,
instances: new Map(),
objectLiteral: true,
@@ -2346,7 +2385,10 @@ export function bindingGenericFnInfoOf(
const info: GenericFnInfo = {
decl: fnNode,
baseName: name,
- qualifiedName: lowerer.qualify(decl.getSourceFile(), nsPathPrefix(stmt ?? decl, decl) + name),
+ qualifiedName: lowerer.qualify(
+ ts.getSourceFileOfNode(decl),
+ nsPathPrefix(stmt ?? decl, decl) + name,
+ ),
typeParams,
instances: new Map(),
};
diff --git a/packages/compiler/src/frontend/lowering/import-meta.ts b/packages/compiler/src/frontend/lowering/import-meta.ts
index baf550e6d..796e64b69 100644
--- a/packages/compiler/src/frontend/lowering/import-meta.ts
+++ b/packages/compiler/src/frontend/lowering/import-meta.ts
@@ -20,7 +20,7 @@ export function isImportMeta(expr: ts.Expression): boolean {
/** File-backed metadata is fixed by the compiled module graph. Reads share
* one implementation across dot access, folded keys and selected bindings. */
export function importMetaField(lowerer: Lowerer, node: ts.Node, name: string): IrExpr {
- const sf = node.getSourceFile();
+ const sf = ts.getSourceFileOfNode(node);
const fileName =
lowerer.targetPlatform === "wasi"
? (wasiGuestPath(sf.fileName) ?? sf.fileName.replace(/\\/g, "/"))
diff --git a/packages/compiler/src/frontend/lowering/intern-tables.ts b/packages/compiler/src/frontend/lowering/intern-tables.ts
index 32bb71858..fc69237db 100644
--- a/packages/compiler/src/frontend/lowering/intern-tables.ts
+++ b/packages/compiler/src/frontend/lowering/intern-tables.ts
@@ -142,7 +142,7 @@ interface TableTypes {
/** The key and value types of an InternTable checker type. */
function tableTypes(lowerer: Lowerer, type: ts.Type, blame: ts.Node): TableTypes {
const parts = type.isUnionType() ? ts.constituentTypes(type) : [type];
- const table = parts.find((p) => p.getSymbol()?.name === "InternTable");
+ const table = parts.find((p) => ts.symbolNameOf(p.getSymbol()) === "InternTable");
const args = table ? lowerer.checker.getTypeArguments(table as ts.TypeReference) : [];
if (args.length !== 2) lowerer.noLowering("InternTable without type arguments", blame);
const key = lowerer.mapTypeOf(args[0]!);
diff --git a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts
index d39e5cceb..b9bbf6216 100644
--- a/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts
+++ b/packages/compiler/src/frontend/lowering/js-dynamic-objects.ts
@@ -81,10 +81,10 @@ export function isValueReturningConstructorClass(decl: ts.ClassDeclaration): boo
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return;
if (ts.isReturnStatement(node) && node.expression !== undefined && !insideArrow(node, ctor!))
returnsValue = true;
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- ctor.body.forEachChild(visit);
- for (const parameter of ctor.parameters.nodes) parameter.forEachChild(visit);
+ ts.forEachChild(ctor.body, visit);
+ for (const parameter of ctor.parameters.nodes) ts.forEachChild(parameter, visit);
return returnsValue && !observesThis;
}
@@ -215,7 +215,7 @@ export function lowerValueReturningConstructorNew(
"spread arguments when constructing a class whose constructor returns a value",
);
}
- const key = `ctorValue:${decl.getSourceFile().fileName}:${decl.getStart()}`;
+ const key = `ctorValue:${ts.getSourceFileOfNode(decl).fileName}:${ts.tokenStartOfNode(decl)}`;
let name = lowerer.valueHelpers.get(key);
if (name === undefined) {
name = `%ctorValue.${lowerer.valueHelpers.size}`;
@@ -235,7 +235,7 @@ export function lowerValueReturningConstructorNew(
{
kind: "runtimeFence",
code: "SC1090",
- message: `constructors that return no object (the bare instance of '${decl.name?.text ?? "class"}' has no representation here) are not supported yet [SC1090 at ${decl.getSourceFile().fileName}:${ts.getLineAndCharacterOfPosition(decl.getSourceFile(), ctor.getStart()).line + 1}]`,
+ message: `constructors that return no object (the bare instance of '${decl.name?.text ?? "class"}' has no representation here) are not supported yet [SC1090 at ${ts.getSourceFileOfNode(decl).fileName}:${ts.getLineAndCharacterOfPosition(ts.getSourceFileOfNode(decl), ts.tokenStartOfNode(ctor)).line + 1}]`,
loc: locOf(ctor),
},
];
@@ -261,8 +261,8 @@ export function lowerValueReturningConstructorNew(
.map((arg) => ({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }));
const result = lowerer.declareHiddenLocal("%constructed", DYN);
const value: IrExpr = { kind: "varRef", localId: result.id, type: DYN, loc };
- const sf = expr.getSourceFile();
- const line = ts.getLineAndCharacterOfPosition(sf, expr.getStart()).line + 1;
+ const sf = ts.getSourceFileOfNode(expr);
+ const line = ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(expr)).line + 1;
const objectLike: IrExpr = {
kind: "logical",
op: "||",
diff --git a/packages/compiler/src/frontend/lowering/json-require.ts b/packages/compiler/src/frontend/lowering/json-require.ts
index 7c688dcda..927745253 100644
--- a/packages/compiler/src/frontend/lowering/json-require.ts
+++ b/packages/compiler/src/frontend/lowering/json-require.ts
@@ -36,7 +36,7 @@ export function jsonRequireTarget(
if (call === undefined || !ts.isCallExpression(call)) return null;
const spec = requireSpecOf(call);
if (spec === null || !ts.isIdentifier(call.expression)) return null;
- const sf = call.getSourceFile();
+ const sf = ts.getSourceFileOfNode(call);
if (!isCjsJsFile(sf, lowerer.program)) return null;
if (createRequireCalleeFileOf(lowerer, call.expression) !== null) return null;
const target = resolveImport(lowerer.program, sf, spec, "require");
diff --git a/packages/compiler/src/frontend/lowering/lower-assert.ts b/packages/compiler/src/frontend/lowering/lower-assert.ts
index cb31ecc79..460f4d53a 100644
--- a/packages/compiler/src/frontend/lowering/lower-assert.ts
+++ b/packages/compiler/src/frontend/lowering/lower-assert.ts
@@ -887,10 +887,10 @@ export function lowerAssertDirectCall(
* call's SOURCE TEXT (Node re-reads the file at runtime — the frontend has
* the same bytes at compile time). */
function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
- requireStatementPosition(lowerer, expr, expr.expression.getText());
+ requireStatementPosition(lowerer, expr, ts.getTextOfNode(expr.expression));
if (expr.arguments.nodes.length < 1 || expr.arguments.nodes.length > 2) {
lowerer.noLowering(
- `${expr.expression.getText()} with ${expr.arguments.nodes.length} arguments`,
+ `${ts.getTextOfNode(expr.expression)} with ${expr.arguments.nodes.length} arguments`,
expr,
"the supported forms are assert(value) and assert(value, message)",
);
@@ -912,7 +912,7 @@ function lowerAssertOk(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc):
// Node reads the CALLSITE LINE and slices the expression out of it
// (internal/errors/error_source.js) — a call spanning lines truncates
// at the first newline, so the compile-time text does too.
- const source = expr.getText().split("\n", 1)[0]!;
+ const source = ts.getTextOfNode(expr).split("\n", 1)[0]!;
const text = `The expression evaluated to a falsy value:\n\n ${source}\n`;
msg = { kind: "strLit", value: text, type: STRING, loc };
generated = true;
diff --git a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts
index f095c5b9c..0e48f2040 100644
--- a/packages/compiler/src/frontend/lowering/lower-async-iteration.ts
+++ b/packages/compiler/src/frontend/lowering/lower-async-iteration.ts
@@ -137,7 +137,9 @@ function literalBooleanOption(
function nodeReadablePlan(lowerer: Lowerer, expression: ts.Expression): NodeReadablePlan | null {
const direct =
- lowerer.stageSourceType(expression) ?? lowerer.mapTypeOf(lowerer.typeOf(expression));
+ lowerer.receiverTypeOverrides.get(expression) ??
+ lowerer.stageSourceType(expression) ??
+ lowerer.mapTypeOf(lowerer.typeOf(expression));
if (direct?.kind === "object") {
const sides = streamSidesOf(lowerer, lowerer.classes.get(direct.className));
if (sides === "r" || sides === "rw") {
diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts
index ae864c2cb..b1b4086cd 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -1,7 +1,11 @@
+import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js";
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
+import { lowerDynStreamMemberCall } from "./dyn-streams.js";
+import { lowerBuiltinClassCall } from "./builtins/builtin-class-dyn.js";
import { refuseModuleMemberThis } from "./module-member-this.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
+import { lowerWebAssemblyValueOf } from "./builtins/webassembly.js";
import { lowerResponseStaticJson } from "./lower-stdlib.js";
import { lowerArrayCallbackCall } from "./containers/array-callback-call.js";
/** Dispatch source calls, builtin members and receiver-specific operations.
@@ -149,6 +153,7 @@ import {
import {
fencePerfEntryKeyWalk,
isPerfEntryType,
+ lowerHistogramMethodCall,
lowerPerfHooksCall,
perfEntryTag,
} from "./builtins/performance.js";
@@ -248,11 +253,12 @@ import {
genericCallInstance,
} from "./generic-functions.js";
+import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js";
+
const ZLIB_STREAM_METHODS: ReadonlySet = new Set(["flush", "params", "close", "reset"]);
export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
-
// Object's checker return type is `any`. Claim known object inputs
// before generic checked-dynamic dispatch tries to load the constructor.
if (
@@ -264,10 +270,21 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
if (object) return object;
}
+ // `EventEmitter.call(this)` in a constructor function: the untyped
+ // EventEmitter constructor initializes the receiver.
+ const builtinClassCall = lowerBuiltinClassCall(lowerer, expr);
+ if (builtinClassCall) return builtinClassCall;
+
// require() of a JSON document: the shared parsed object.
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
@@ -661,7 +678,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const isStorage =
parts.length > 0 &&
parts.every((part) => {
- const name = part.getSymbol()?.name;
+ const name = ts.symbolNameOf(part.getSymbol());
return name === "ArrayBuffer" || name === "SharedArrayBuffer";
});
if (isStorage) {
@@ -835,7 +852,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
(source.type.kind === "dyn" ||
source.type.kind === "func" ||
(source.type.kind === "union" &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
lowerer.dynConvertible(source.type)) ||
((source.type.kind === "map" || source.type.kind === "set") &&
lowerer.dynConvertible(source.type)) ||
@@ -851,7 +868,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.lowerExprExpecting(ts.isSpreadElement(arg) ? arg.expression : arg, DYN),
);
const spreads = expr.arguments.nodes.flatMap((arg, index) =>
- ts.isSpreadElement(arg) ? [{ arg: index, what: arg.expression.getText() }] : [],
+ ts.isSpreadElement(arg) ? [{ arg: index, what: ts.getTextOfNode(arg.expression) }] : [],
);
return {
kind: "seqExpr",
@@ -862,7 +879,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
kind: "dynCall",
callee,
receiver,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args,
...(spreads.length > 0 ? { spreads } : {}),
type: DYN,
@@ -1167,7 +1184,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// argument list. Typed .ts spreads keep completeArgs' rest packing.
const jsSpreadArgs =
expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) &&
- isJsSourceFile(expr.getSourceFile());
+ isJsSourceFile(ts.getSourceFileOfNode(expr));
if (lowerer.isTopLevelFnSymbol(expr.expression) && !lowerer.peekLocal(expr.expression)) {
// `import g = N.f; g()` — the alias's own source-order guards
// (a no-op for every non-import= binding).
@@ -1377,12 +1394,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
if (
g &&
(g.type.kind === "func" || g.type.kind === "dyn") &&
- !cjsExportCallIgnoresThis(lowerer, expr.getSourceFile(), expr.expression.name.text)
+ !cjsExportCallIgnoresThis(lowerer, ts.getSourceFileOfNode(expr), expr.expression.name.text)
) {
lowerer.unsupported(
"SC1090",
expr,
- `calling '${expr.expression.getText()}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`,
+ `calling '${ts.getTextOfNode(expr.expression)}' through the module's exports object when the function may read 'this' (Node binds it to the exports object, which compiles to a static table — call a local function instead)`,
);
}
if (g && (g.type.kind === "func" || g.type.kind === "dyn")) {
@@ -1397,7 +1414,12 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
}
if (ts.isPropertyAccessExpression(expr.expression)) {
const intrinsic =
+ // Checked-dynamic stream slots dispatch per value (dyn-streams.ts).
+ lowerDynStreamMemberCall(lowerer, expr, expr.expression) ??
fenceNodeModuleMutationCall(lowerer, expr, expr.expression) ??
+ // `f.apply(thisArg, args)` forwarding a fixed argument tuple (typed
+ // `arguments`, a tuple rest parameter): a direct call.
+ lowerForwardingApply(lowerer, expr, expr.expression) ??
// Builtin namespace imports first (`fs.readFileSync(...)` where fs
// is `import * as fs from "node:fs"`): the same tables and fences
// as named builtin imports — before anything below tries to lower
@@ -1409,6 +1431,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// .bind(performance) function value over the runtime's
// process-start-anchored monotonic clock.
lowerPerfHooksCall(lowerer, expr, expr.expression) ??
+ lowerHistogramMethodCall(lowerer, expr, expr.expression) ??
// The composed crypto pattern (randomBytes(n).toString(enc))
// — its receiver is a Buffer-typed CALL no other lowering claims.
lowerer.lowerCryptoComposedCall(expr, expr.expression) ??
@@ -1571,7 +1594,11 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// monomorphized against the defining literal's declaration.
lowerObjLitGenericMethodCall(lowerer, expr, expr.expression);
if (intrinsic) return intrinsic;
- if (isJsSourceFile(expr.getSourceFile())) {
+ {
+ const boxed = lowerDynReceiverMethodCall(lowerer, expr, expr.expression, true);
+ if (boxed) return boxed;
+ }
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const staticField = lowerStaticFieldRead(lowerer, expr.expression);
if (staticField?.type.kind === "dyn" && !expr.arguments.nodes.some(ts.isSpreadElement)) {
return {
@@ -1639,7 +1666,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerer.unsupported(
"SC1090",
expr,
- `calls through function-with-properties values ('${expr.expression.expression.getText()}' is callable AND carries members — the chalk shape; no hybrid representation exists yet: export the base and the property as separate functions)`,
+ `calls through function-with-properties values ('${ts.getTextOfNode(expr.expression.expression)}' is callable AND carries members — the chalk shape; no hybrid representation exists yet: export the base and the property as separate functions)`,
);
}
}
@@ -1664,7 +1691,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
);
}
}
- lowerer.unsupported("SC1090", expr, `method calls like '${expr.expression.getText()}'`);
+ lowerer.unsupported("SC1090", expr, `method calls like '${ts.getTextOfNode(expr.expression)}'`);
}
// `s[Symbol.iterator]()` on a string: the string iterator as a value.
@@ -1703,7 +1730,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
) {
const access = expr.expression;
const value = tryLowerExpression(lowerer, access.expression);
- if (value?.type.kind === "array" && !isJsSourceFile(expr.getSourceFile())) {
+ if (value?.type.kind === "array" && !isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const callback = lowerArrayCallbackCall(lowerer, expr, access, value);
if (callback) return callback;
}
@@ -1738,7 +1765,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
kind: "dynCall",
callee,
receiver,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: expr.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -1857,7 +1884,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const calleeName =
ts.isPropertyAccessExpression(expr.expression) ||
ts.isElementAccessExpression(expr.expression)
- ? expr.expression.getText()
+ ? ts.getTextOfNode(expr.expression)
: ts.isIdentifier(expr.expression)
? expr.expression.text
: "value";
@@ -1893,6 +1920,19 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// or marshals the callee and applies through a runtime-built argument
// list (lowerSpreadArgsCall). Shapes outside its lanes fall through
// to the historical fences.
+ // Forwarded fixed tuples (`fn(...args)` over a tuple rest parameter or a
+ // typed `arguments`) have a compile-time arity: complete them into the
+ // callee's positional slots directly, without the runtime-arity lane.
+ if (
+ callee.type.rest !== true &&
+ expr.arguments.nodes.some(ts.isSpreadElement) &&
+ expr.arguments.nodes.every(
+ (a) => !ts.isSpreadElement(a) || isForwardedTupleSpread(lowerer, a.expression),
+ )
+ ) {
+ const args = completeFuncValueArgs(lowerer, expr, callee.type, loc);
+ return { kind: "callValue", callee, args, type: callee.type.ret, loc };
+ }
if (expr.arguments.nodes.some((a) => ts.isSpreadElement(a))) {
const spreadServed = lowerSpreadArgsCall(lowerer, expr, callee, loc);
if (spreadServed) return spreadServed;
@@ -1906,7 +1946,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
// per use like every any-origin value.
if (
(expr.arguments.nodes.length > callee.type.params.length || callee.type.rest === true) &&
- (isJsSourceFile(expr.getSourceFile()) ||
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) ||
(callee.type.rest === true && callee.type.restAbi === undefined)) &&
!expr.arguments.nodes.some((a) => ts.isSpreadElement(a)) &&
canBoxFuncIntoDyn(
@@ -1938,7 +1978,7 @@ function lowerCheckedCallableValue(
callee.type.kind !== "func" ||
!(
(callee.type.rest === true && callee.type.restAbi === undefined) ||
- (isJsSourceFile(call.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(call)) &&
call.arguments.nodes.length > callee.type.params.length)
) ||
!lowerer.dynConvertible(callee.type)
@@ -2503,7 +2543,7 @@ function lowerArrayPrototypeBorrowCall(
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (
- !isJsSourceFile(call.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(call)) ||
call.questionDotToken ||
access.questionDotToken ||
access.name.text !== "call" ||
@@ -2715,7 +2755,7 @@ function lowerObjectOwnCall(
result = guardNullProto(
receiverRef,
type.shapeId,
- `${methodAccess.getText()} is not a function`,
+ `${ts.getTextOfNode(methodAccess)} is not a function`,
result,
loc,
);
@@ -3278,6 +3318,13 @@ const ITERATOR_HELPER_METHODS = new Set([
"find",
]);
+/** A call expression, through parentheses: its value has no other holder. */
+function isFreshCallResult(node: ts.Expression): boolean {
+ let e = node;
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ return ts.isCallExpression(e);
+}
+
/** METHOD calls on dyn receivers (`pkg.name.replace(...)`, `rawName.split`,
* `ws.packages.filter(...)` — JSON.parse-derived values): validate the
* receiver's dyn kind, extract, and ride the STATIC method machinery — the
@@ -3295,6 +3342,7 @@ function lowerDynReceiverMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
+ boxTypedTemporary = false,
): IrExpr | null {
if (lowerer.chainBlocked(call, access)) return null;
if (stdlibGlobalNameOf(lowerer, access.expression) !== null) {
@@ -3311,7 +3359,13 @@ function lowerDynReceiverMethodCall(
// array — scalar narrowings bridge through maybeNarrow's dynCheck and
// take the ordinary typed paths, but there is no static home for an
// any-elemented array). Typed receivers keep their own lowerings.
- const recvTs = lowerer.typeOf(access.expression);
+ // An optional step the chain already guarded (`s?.m(x)`) has a receiver
+ // proven non-nullish.
+ const rawRecvTs = lowerer.typeOf(access.expression);
+ const recvTs =
+ access.questionDotToken !== undefined && lowerer.chainHandled.has(access)
+ ? lowerer.checker.getNonNullableType(rawRecvTs)
+ : rawRecvTs;
if (lowerer.classImplementedProtocol(recvTs)) {
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
if (
@@ -3323,7 +3377,7 @@ function lowerDynReceiverMethodCall(
const recvSymbol = recvTs.getSymbol();
if (
recvSymbol &&
- CHECKED_ITERATOR_TYPES.has(recvSymbol.name) &&
+ CHECKED_ITERATOR_TYPES.has(ts.symbolName(recvSymbol)) &&
lowerer.isStdlibSymbol(recvSymbol) &&
ITERATOR_HELPER_METHODS.has(access.name.text) &&
lowerer.isStdlibMember(access)
@@ -3391,7 +3445,7 @@ function lowerDynReceiverMethodCall(
kind: "dynCall",
callee: member,
receiver,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -3413,7 +3467,7 @@ function lowerDynReceiverMethodCall(
return null;
if (
probed?.type.kind === "union" &&
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
lowerer.unions
.get(probed.type.unionId)
?.arms.every(
@@ -3463,7 +3517,7 @@ function lowerDynReceiverMethodCall(
if (
checkerUntyped &&
recv.type.kind === "union" &&
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
isStringMethodName(access.name.text) &&
lowerer.unions
.get(recv.type.unionId)
@@ -3477,6 +3531,22 @@ function lowerDynReceiverMethodCall(
// 'unknown' slot does.
if (checkerUntyped && DYN_HANDLE_KINDS.has(recv.type.kind))
recv = lowerer.coerceToExpected(recv, DYN);
+ // The last-resort retry (boxTypedTemporary — every other lowering
+ // already declined): a checker-`any` receiver whose value is a FRESH
+ // typed result of an earlier step in the chain (`out.split("\n")`
+ // extracted the string and answered a native string[]) has no typed
+ // home under its `any` spelling, so it boxes into the checked-dynamic
+ // tree and the call dispatches on the runtime kind — `.map`, `.pop`,
+ // `.filter` on that array run JS-exact. Only call results qualify:
+ // the box copies, and a temporary has no other holder to observe it.
+ if (
+ boxTypedTemporary &&
+ checkerUntyped &&
+ recv.type.kind !== "dyn" &&
+ isFreshCallResult(access.expression) &&
+ lowerer.dynConvertible(recv.type)
+ )
+ recv = lowerer.coerceToExpected(recv, DYN);
if (recv.type.kind !== "dyn") return null;
if (
access.name.text === "normalize" &&
@@ -3533,7 +3603,7 @@ function lowerDynReceiverMethodCall(
kind: "dynInvoke",
recv: value,
method: "normalize",
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: hasArgument ? [argument] : [],
type: DYN,
loc,
@@ -3569,7 +3639,7 @@ function lowerDynReceiverMethodCall(
// dynKeyGet cannot stand in for JavaScript's prototype lookup. Only
// an authored declaration can justify the own-member path.
const authored = prop.declarations.some(
- (decl) => !lowerer.isStdlibFile(decl.getSourceFile()),
+ (decl) => !lowerer.isStdlibFile(ts.getSourceFileOfNode(decl)),
);
return (
authored &&
@@ -3611,7 +3681,7 @@ function lowerDynReceiverMethodCall(
lowerer,
call,
access.name.text,
- access.getText(),
+ ts.getTextOfNode(access),
recv,
);
if (primitive) return primitive;
@@ -3626,7 +3696,7 @@ function lowerDynReceiverMethodCall(
args: [
recv,
argument ? lowerer.lowerExprExpecting(argument, DYN) : dynUndefinedExpr(locOf(call)),
- { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) },
+ { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc: locOf(call) },
],
type: STRING,
loc: locOf(call),
@@ -3636,10 +3706,15 @@ function lowerDynReceiverMethodCall(
// answers each kind's own: primitives and plain objects themselves, a
// Date its time value, an own or inherited valueOf member its result.
if (access.name.text === "valueOf" && call.arguments.nodes.length === 0) {
+ const webAssembly = lowerWebAssemblyValueOf(lowerer, access.expression, recv, locOf(call));
+ if (webAssembly !== null) return webAssembly;
return {
kind: "libCall",
fn: "dyn.valueOf",
- args: [recv, { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) }],
+ args: [
+ recv,
+ { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc: locOf(call) },
+ ],
type: DYN,
loc: locOf(call),
};
@@ -3727,15 +3802,15 @@ function lowerDynReceiverMethodCall(
// statement cannot leave the expression).
const stringRefusal = (read: IrExpr): IrExpr => {
if (!stringOnlyName) return read;
- const sf = call.getSourceFile();
- const line = ts.getLineAndCharacterOfPosition(sf, call.getStart(sf)).line + 1;
+ const sf = ts.getSourceFileOfNode(call);
+ const line = ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(call)).line + 1;
return {
kind: "ternary",
cond: { kind: "dynTest", test: "string", value: receiver, type: BOOL, loc },
then: nodeThrowExpr(
0,
"SC1090",
- `method calls like '${access.getText()}' on a string are not supported yet [SC1090 at ${sf.fileName}:${line}]`,
+ `method calls like '${ts.getTextOfNode(access)}' on a string are not supported yet [SC1090 at ${sf.fileName}:${line}]`,
DYN,
loc,
),
@@ -3771,7 +3846,7 @@ function lowerDynReceiverMethodCall(
kind: "dynCall",
callee,
receiver,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args,
type: DYN,
loc,
@@ -4061,7 +4136,7 @@ export function lowerDynDispatchMethodCall(
): IrExpr | null {
const method = access.name.text;
if (
- (!DYN_DISPATCH_METHODS.has(method) && !isJsSourceFile(call.getSourceFile())) ||
+ (!DYN_DISPATCH_METHODS.has(method) && !isJsSourceFile(ts.getSourceFileOfNode(call))) ||
lowerer.chainBlocked(call, access)
)
return null;
@@ -4170,7 +4245,7 @@ export function lowerDynDispatchMethodCall(
kind: "dynInvoke",
recv: { kind: "varRef", localId: saved.id, type: DYN, loc: locOf(access.expression) },
method,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: [dynUndefinedExpr(locOf(call.arguments.nodes[0]!))],
type: DYN,
loc: locOf(call),
@@ -4192,7 +4267,7 @@ export function lowerDynDispatchMethodCall(
kind: "dynInvoke",
recv,
method,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args,
type: DYN,
loc: locOf(call),
@@ -4343,6 +4418,14 @@ function lowerCaughtPredicateCall(
const paramSymbol = lowerer.checker.getSymbolAtLocation(param.name);
if (!paramSymbol) return null;
const ret = decl.body.statements.nodes.length === 1 ? decl.body.statements.nodes[0] : undefined;
+ // A longer guard whose parameter is `unknown` (or `any`) takes the caught
+ // value as an ordinary checked-dynamic argument: the call path converts
+ // the binding, and the narrowed binding reads through the same value.
+ if (
+ (!ret || !ts.isReturnStatement(ret) || !ret.expression) &&
+ lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(paramSymbol))?.kind === "dyn"
+ )
+ return null;
if (!ret || !ts.isReturnStatement(ret) || !ret.expression) {
lowerer.unsupported(
"SC1090",
@@ -4404,7 +4487,7 @@ function lowerNumberToStringCall(
call.arguments.nodes[0]
? lowerer.lowerExprExpecting(call.arguments.nodes[0], DYN)
: dynUndefinedExpr(locOf(call)),
- { kind: "strLit", value: access.getText(), type: STRING, loc: locOf(call) },
+ { kind: "strLit", value: ts.getTextOfNode(access), type: STRING, loc: locOf(call) },
],
type: STRING,
loc: locOf(call),
@@ -4527,7 +4610,7 @@ function lowerDefaultToStringCall(
// A null-prototype record (Object.groupBy) has no toString: Node
// throws " is not a function".
const nullProtoT = nullProtoRecordShape(lowerer, recvT);
- const missing = nullProtoT ? `${access.getText()} is not a function` : null;
+ const missing = nullProtoT ? `${ts.getTextOfNode(access)} is not a function` : null;
if (!missing && pureObjectToStringReceiver(access.expression)) return constant;
const recv = lowerer.lowerExpr(access.expression);
const key = `objToStr:${typeKey(recv.type)}${missing ? `:${missing}` : ""}`;
@@ -4678,7 +4761,7 @@ function lowerPrimitiveProtoCall(
(name === "toFixed" || name === "toExponential") &&
call.arguments.nodes.length <= 1 &&
!call.arguments.nodes.some(ts.isSpreadElement) &&
- isJsSourceFile(call.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(call))
) {
const operand = lowerer.lowerExpr(recv);
if (
@@ -4691,7 +4774,7 @@ function lowerPrimitiveProtoCall(
lowerer,
call,
name,
- `${recv.getText()}.${name}`,
+ `${ts.getTextOfNode(recv)}.${name}`,
lowerer.coerceToExpected(operand, DYN),
);
if (dispatched) return dispatched;
@@ -4704,7 +4787,7 @@ function lowerPrimitiveProtoCall(
kind: "dynInvoke",
recv: lowerer.coerceToExpected(operand, DYN),
method: name,
- calleeName: `${recv.getText()}.${name}`,
+ calleeName: `${ts.getTextOfNode(recv)}.${name}`,
args: call.arguments.nodes.map((argument) => lowerer.lowerExprExpecting(argument, DYN)),
type: DYN,
loc: locOf(call),
@@ -4714,7 +4797,7 @@ function lowerPrimitiveProtoCall(
name === "toPrecision" &&
call.arguments.nodes.length <= 1 &&
!call.arguments.nodes.some(ts.isSpreadElement) &&
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
recvKind === "union"
) {
const operand = lowerer.lowerExpr(recv);
@@ -4723,7 +4806,7 @@ function lowerPrimitiveProtoCall(
lowerer,
call,
name,
- `${recv.getText()}.${name}`,
+ `${ts.getTextOfNode(recv)}.${name}`,
lowerer.coerceToExpected(operand, DYN),
);
if (dispatched) return dispatched;
@@ -4943,9 +5026,10 @@ function isSoleAbstractMethod(decl: ts.FunctionDeclaration | ts.MethodDeclaratio
if (!ts.getModifiers(decl)?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword)) return false;
const owner = decl.parent;
if (!ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) return false;
- const name = decl.name.getText();
+ const name = ts.getTextOfNode(decl.name);
return !owner.members.nodes.some(
- (member) => member !== decl && ts.isMethodDeclaration(member) && member.name.getText() === name,
+ (member) =>
+ member !== decl && ts.isMethodDeclaration(member) && ts.getTextOfNode(member.name) === name,
);
}
@@ -4963,7 +5047,7 @@ function reconcileOverloadReturn(
call.type.kind === "object" &&
rdecl &&
ts.isMethodDeclaration(rdecl) &&
- (!isJsSourceFile(rdecl.getSourceFile()) || returnsOnlyThis(rdecl))
+ (!isJsSourceFile(ts.getSourceFileOfNode(rdecl)) || returnsOnlyThis(rdecl))
) {
call = lowerer.maybeNarrow(call, expr);
}
@@ -5125,7 +5209,7 @@ export function lowerTaggedTemplate(lowerer: Lowerer, expr: ts.TaggedTemplateExp
let callee = lowerer.lowerExpr(expr.tag);
if (
callee.type.kind === "func" &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
lowerer.dynConvertible(callee.type)
) {
callee = { kind: "dynFrom", value: callee, type: DYN, loc };
@@ -5143,7 +5227,7 @@ export function lowerTaggedTemplate(lowerer: Lowerer, expr: ts.TaggedTemplateExp
];
const calleeName =
ts.isPropertyAccessExpression(expr.tag) || ts.isElementAccessExpression(expr.tag)
- ? expr.tag.getText()
+ ? ts.getTextOfNode(expr.tag)
: ts.isIdentifier(expr.tag)
? expr.tag.text
: "value";
@@ -5213,7 +5297,7 @@ function loweredTemplateStrings(
loc,
})
: null;
- const key = `templateStringsGlobal:${loc.file}:${expr.template.getStart()}:${raw ? "checked" : "typed"}`;
+ const key = `templateStringsGlobal:${loc.file}:${ts.tokenStartOfNode(expr.template)}:${raw ? "checked" : "typed"}`;
let getter = lowerer.valueHelpers.get(key);
if (!getter) {
const site = lowerer.globalsList.length;
@@ -5928,7 +6012,7 @@ export function lowerPromiseMethodCall(
kind: "dynInvoke",
recv: lowerer.coerceToExpected(receiver, DYN),
method: "finally",
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: [lowerer.coerceToExpected(cb, DYN)],
type: DYN,
loc,
@@ -6029,7 +6113,7 @@ export function lowerPromiseMethodCall(
if (
member === "catch" &&
(inner.kind === "dyn" ||
- (isJsSourceFile(call.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(call)) &&
(inner.kind === "void" || lowerer.dynConvertible(inner))))
) {
let cb = lowerer.lowerExpr(call.arguments.nodes[0]!);
@@ -6884,6 +6968,7 @@ function recordAssignHelper(
const helper = `%obj.assign.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, helper);
const sShape = lowerer.shapes.get(srcShapeId)!;
+ const tShape = lowerer.shapes.get(targetShapeId)!;
const tT: IrType = { kind: "record", shapeId: targetShapeId };
const sT: IrType = { kind: "record", shapeId: srcShapeId };
const tRef: IrExpr = { kind: "varRef", localId: "t.0", type: tT, loc };
@@ -6898,12 +6983,13 @@ function recordAssignHelper(
type: f.type,
loc,
};
+ const targetType = tShape.fields.find((x) => x.name === f.name)!.type;
const set: IrStmt = {
kind: "recordSet",
obj: tRef,
shapeId: targetShapeId,
field: f.name,
- value: get,
+ value: typeEquals(targetType, f.type) ? get : lowerer.coerceToExpected(get, targetType),
loc,
};
const present = lowerer.recordFieldPresent(sRef, srcShapeId, f.name, loc);
@@ -7421,7 +7507,7 @@ function lowerObjectStaticCall(
const prototype = lowerClassPrototypeAssign(lowerer, call);
if (prototype) return prototype;
if (
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
call.arguments.nodes.length > 0 &&
!call.arguments.nodes.some(ts.isSpreadElement)
) {
@@ -7451,6 +7537,8 @@ function lowerObjectStaticCall(
};
}
}
+ const onError = lowerObjectAssignOntoError(lowerer, call);
+ if (onError) return onError;
const hybrid = lowerObjectAssignHybrid(lowerer, call);
if (hybrid) return hybrid;
// `Object.assign({}, lit)` — an EMPTY fresh-literal target and one
@@ -7543,7 +7631,12 @@ function lowerObjectStaticCall(
!shapeHasAccessorSlots(sShape) &&
sShape.fields.every((sf) => {
const tf = tShape.fields.find((x) => x.name === sf.name);
- return tf !== undefined && typeEquals(tf.type, sf.type);
+ // A source value converts into a wider target field (`{ bail:
+ // true }` into `bail?: boolean`).
+ return (
+ tf !== undefined &&
+ (typeEquals(tf.type, sf.type) || lowerer.coercibleValue(sf.type, tf.type))
+ );
});
if (ok) {
const loc = locOf(call);
@@ -7561,6 +7654,37 @@ function lowerObjectStaticCall(
}
}
}
+ // `throw Object.assign(record, { extra })`: an error-shaped record given
+ // a member its type lacks, thrown at once. A fixed record cannot grow
+ // the member, so the throw carries a checked-dynamic copy of the target
+ // with the source's members added. The copy is unobservable: control
+ // leaves with the throw, so nothing on that path reads the target again.
+ {
+ let parent: ts.Node | undefined = call.parent;
+ while (parent && ts.isParenthesizedExpression(parent)) parent = parent.parent;
+ const argNodes = call.arguments.nodes;
+ if (
+ parent &&
+ ts.isThrowStatement(parent) &&
+ argNodes.length === 2 &&
+ !argNodes.some((a) => ts.isSpreadElement(a))
+ ) {
+ const tProbe = tryLowerExpression(lowerer, argNodes[0]!);
+ const sProbe = tryLowerExpression(lowerer, argNodes[1]!);
+ if (
+ tProbe?.type.kind === "record" &&
+ lowerer.dynConvertible(tProbe.type) &&
+ sProbe &&
+ (sProbe.type.kind === "dyn" || lowerer.dynConvertible(sProbe.type))
+ ) {
+ const loc = locOf(call);
+ const target = lowerer.coerceToExpected(lowerer.lowerExpr(argNodes[0]!), DYN);
+ const source = lowerer.coerceToExpected(lowerer.lowerExpr(argNodes[1]!), DYN);
+ if (target.type.kind === "dyn" && source.type.kind === "dyn")
+ return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc };
+ }
+ }
+ }
// `Object.assign(target, ...sources)` over a CHECKED-DYNAMIC target
// — the n-ary/spread form (`Object.assign({}, ...plugins.map(p =>
// p.options), coreOptions)`, support.js's option-table merge). The
@@ -7655,7 +7779,12 @@ function lowerObjectStaticCall(
args: [
pRef(),
src,
- { kind: "strLit", value: srcNode.getText(), type: STRING, loc: argLoc },
+ {
+ kind: "strLit",
+ value: ts.getTextOfNode(srcNode),
+ type: STRING,
+ loc: argLoc,
+ },
],
type: VOID,
loc: argLoc,
@@ -7712,7 +7841,9 @@ function lowerObjectStaticCall(
(exactClassOfReceiver(lowerer, targetNode.expression) !== null ||
storedClassValueType(lowerer, targetNode.expression)?.kind === "classval");
const declaredTarget =
- target?.type.kind === "dyn" && !prototypeTarget && !isJsSourceFile(targetNode.getSourceFile())
+ target?.type.kind === "dyn" &&
+ !prototypeTarget &&
+ !isJsSourceFile(ts.getSourceFileOfNode(targetNode))
? lowerer.mapTypeOf(lowerer.typeOf(targetNode))
: null;
if (target && declaredTarget && isDynTypedRefType(declaredTarget))
@@ -7911,7 +8042,7 @@ function lowerObjectStaticCall(
while (ts.isParenthesizedExpression(inner) || ts.isAsExpression(inner))
inner = inner.expression;
const value =
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
(ts.isObjectLiteralExpression(inner) || ts.isArrayLiteralExpression(inner))
? lowerer.lowerExprExpecting(argNode, DYN)
: lowerer.lowerExpr(argNode);
@@ -8132,7 +8263,7 @@ function lowerObjectStaticCall(
probed.type.kind === "bool");
const isJsUnion =
probed?.type.kind === "union" &&
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
lowerer.dynConvertible(probed.type);
if (
isDyn ||
@@ -8154,7 +8285,7 @@ function lowerObjectStaticCall(
// write to the arrays well typed. JavaScript may store anything into
// them, so it keeps the checked-dynamic arrays.
const resultT =
- member !== "values" && !isJsSourceFile(call.getSourceFile())
+ member !== "values" && !isJsSourceFile(ts.getSourceFileOfNode(call))
? lowerer.mapTypeOf(lowerer.typeOf(call))
: null;
if (member === "keys" && resultT?.kind === "array" && resultT.elem.kind === "string")
@@ -8177,7 +8308,11 @@ function lowerObjectStaticCall(
// program only iterates or indexes the result, or passes it to a
// read-only array method, a native vector of the checked values skips
// building that array and, for for-of, the iterator protocol.
- if (member === "values" && !isJsSourceFile(call.getSourceFile()) && consumesValueList(call))
+ if (
+ member === "values" &&
+ !isJsSourceFile(ts.getSourceFileOfNode(call)) &&
+ consumesValueList(call)
+ )
return {
kind: "libCall",
fn: "dyn.objValueList",
@@ -8215,7 +8350,7 @@ function lowerObjectStaticCall(
if (
(member === "values" || member === "entries") &&
(enumerationType?.kind === "dyn" ||
- (enumerationType === null && isJsSourceFile(call.getSourceFile())))
+ (enumerationType === null && isJsSourceFile(ts.getSourceFileOfNode(call))))
) {
let receiver = lowerer.coerceToExpected(lowerer.lowerExpr(argNode), DYN);
if (receiver.type.kind !== "dyn") lowerer.badType(argNode, lowerer.typeOf(argNode));
@@ -8746,7 +8881,7 @@ export function lowerRecordFieldCall(
const target = lowerer.fieldTarget(access);
const loc = locOf(call);
const receiverLocal =
- target && isJsSourceFile(call.getSourceFile())
+ target && isJsSourceFile(ts.getSourceFileOfNode(call))
? lowerer.declareHiddenLocal("%callReceiver", target.obj.type)
: null;
const init: IrStmt | null =
@@ -8766,7 +8901,7 @@ export function lowerRecordFieldCall(
// values (util.DebugLogger).
if (
callee.type.kind === "dyn" &&
- (isJsSourceFile(call.getSourceFile()) ||
+ (isJsSourceFile(ts.getSourceFileOfNode(call)) ||
lowerer.checker.getCallSignatures(lowerer.typeOf(access)).length > 0)
) {
const receiver =
@@ -8784,13 +8919,16 @@ export function lowerRecordFieldCall(
kind: "dynCall",
callee,
...(receiver ? { receiver } : {}),
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
};
return init ? { kind: "seqExpr", stmts: [init], result, type: result.type, loc } : result;
}
+ // An optional callback field the checker narrowed at this call
+ // (`o.cb ? o.cb(x) : …`, `if (o.cb) o.cb(x)`): extract the func arm.
+ if (callee.type.kind === "union") callee = lowerer.maybeNarrow(callee, access);
if (callee.type.kind !== "func") lowerer.badType(access, lowerer.typeOf(access));
// JavaScript passes the record as `this`. A record with no dynamic form
// cannot be that receiver; the library's own function fields (the
@@ -8852,7 +8990,7 @@ function lowerUrlStaticCall(
if (!ts.isPropertyAccessExpression(callee) || callee.questionDotToken !== undefined) return null;
if (!ts.isIdentifier(callee.expression)) return null;
const sym = lowerer.resolveValueSymbol(callee.expression);
- if (!sym || !lowerer.isStdlibSymbol(sym) || sym.name !== "URL") return null;
+ if (!sym || !lowerer.isStdlibSymbol(sym) || ts.symbolName(sym) !== "URL") return null;
if (callee.name.text === "canParse" || callee.name.text === "parse") {
return lowerUrlFactory(lowerer, call, callee.name.text);
}
@@ -9210,7 +9348,7 @@ function lowerObjLitGenericMethodCall(
kind: "dynInvoke",
recv: { kind: "dynFrom", value, type: DYN, loc },
method: name,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -9219,7 +9357,7 @@ function lowerObjLitGenericMethodCall(
lowerer.unsupported(
"SC1090",
call,
- `Function.prototype.${name} on a function whose signature cannot cross the checked-value boundary (call '${access.expression.getText()}(...)' directly)`,
+ `Function.prototype.${name} on a function whose signature cannot cross the checked-value boundary (call '${ts.getTextOfNode(access.expression)}(...)' directly)`,
);
}
// STANDARD-LIBRARY generic members (Promise.then, Object.
@@ -9247,7 +9385,7 @@ function lowerObjLitGenericMethodCall(
kind: "dynInvoke",
recv: receiver,
method: name,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -9289,6 +9427,7 @@ export function lowerObjectMethodCall(
): IrExpr | null {
if (lowerer.chainBlocked(access, call)) return null;
let mappedReceiver =
+ lowerer.receiverTypeOverrides.get(access.expression) ??
structuralReadType(lowerer, access.expression) ??
lowerer.mapTypeOf(lowerer.typeOf(access.expression));
// EventEmitter's static side (the class, a value of it, or the events
@@ -9307,7 +9446,7 @@ export function lowerObjectMethodCall(
(lowerer.implicitParamTypes !== null ||
lowerer.classImplementedProtocol(lowerer.typeOf(access.expression)) ||
ts.isNewExpression(access.expression) ||
- isJsSourceFile(call.getSourceFile()))
+ isJsSourceFile(ts.getSourceFileOfNode(call)))
? ts.isNewExpression(access.expression)
? lowerer.lowerExpr(access.expression)
: tryLowerExpression(lowerer, access.expression)
@@ -9326,7 +9465,7 @@ export function lowerObjectMethodCall(
if (dispatched) return dispatched;
const arms = lowerer.unions.get(mappedReceiver.unionId)?.arms;
if (
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
arms?.every(
(arm) => arm.kind === "object" && !lowerer.classes.get(arm.className)?.def.runtime,
) &&
@@ -9480,7 +9619,7 @@ export function lowerObjectMethodCall(
callee,
lowerer.armTag(callee.type.unionId, callableType),
callableType,
- () => `${access.getText()} is not a function`,
+ () => `${ts.getTextOfNode(access)} is not a function`,
);
const helper = inPlace
? null
@@ -9494,7 +9633,7 @@ export function lowerObjectMethodCall(
if (
((callee.type.rest === true && callee.type.restAbi === undefined) ||
call.arguments.nodes.length > callee.type.params.length) &&
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
lowerer.dynConvertible(callee.type)
) {
const boxed: IrExpr = { kind: "dynFrom", value: callee, type: DYN, loc };
@@ -9505,7 +9644,7 @@ export function lowerObjectMethodCall(
kind: "dynCall",
callee: boxed,
...receiver,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args,
type: DYN,
loc,
@@ -9523,7 +9662,7 @@ export function lowerObjectMethodCall(
kind: "dynCall",
callee,
...receiver,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args,
type: DYN,
loc,
@@ -9532,7 +9671,7 @@ export function lowerObjectMethodCall(
}
if (
!fieldType &&
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
!info.def.runtime &&
!info.builtinError &&
!info.builtinEmitter &&
@@ -9562,7 +9701,7 @@ export function lowerObjectMethodCall(
kind: "dynCall",
callee,
receiver,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -9611,7 +9750,7 @@ export function lowerObjectMethodCall(
call.arguments.nodes,
fallback,
loc,
- access.getText(),
+ ts.getTextOfNode(access),
);
return {
kind: "seqExpr",
@@ -9637,6 +9776,30 @@ export function lowerObjectMethodCall(
`calls of the abstract method '${method}' with no concrete implementation below the receiver's static class`,
);
}
+ // The static receiver's nearest declaration is an override adapted to
+ // the inherited ABI, and nothing below overrides it: call its body
+ // directly under its declared signature (exact parameters and result).
+ // The slot function stays reachable; it lifts the body.
+ const adapted = found.declarer.returnAdapters?.get(method);
+ // A TypeScript override's own entry answers its declared result. A
+ // JavaScript body's result is inferred, so its callers keep the slot.
+ const declarerDecl = found.declarer.decl;
+ if (
+ adapted &&
+ !(declarerDecl && isJsSourceFile(ts.getSourceFileOfNode(declarerDecl))) &&
+ !lowerer.overrideBelow(info, method)
+ ) {
+ lowerer.noteEdge(`%${found.declarer.def.name}.${method}`);
+ const receiver = lowerReceiver();
+ const args = lowerer.completeArgs(call.arguments.nodes, adapted.params, locOf(call), call);
+ return {
+ kind: "call",
+ callee: `%${found.declarer.def.name}.${method}$own`,
+ args: [lowerer.upcastTo(receiver, found.declarer.def.name), ...args],
+ type: adapted.ret,
+ loc: locOf(call),
+ };
+ }
if (lowerer.overrideBelow(info, method)) lowerer.noteVirtualEdge(info, method);
else lowerer.noteEdge(`%${found.declarer.def.name}.${method}`);
const receiver = lowerReceiver();
@@ -9713,7 +9876,7 @@ function lowerUnionObjectMethodCall(
kind: "dynCall",
callee,
receiver,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -9875,7 +10038,7 @@ function lowerUnionObjectDynFieldCall(
kind: "dynCall",
callee,
receiver: lowerer.coerceToExpected(receiverRef, DYN),
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args,
type: DYN,
loc,
@@ -10247,3 +10410,60 @@ function maybePromiseHandlerResult(
}
return stmt;
}
+
+/** `Object.assign(err, ...sources)` in TypeScript onto an error-hierarchy
+ * target (`Object.assign(new Error(message), details)`): the sources'
+ * own members land on the error — the runtime copy over its
+ * checked-dynamic node, whose name, message, stack, cause and code feed
+ * back into the typed slots — and the call answers the error itself.
+ * Null for every other target. */
+function lowerObjectAssignOntoError(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
+ const argNodes = call.arguments.nodes;
+ if (
+ isJsSourceFile(ts.getSourceFileOfNode(call)) ||
+ argNodes.length < 2 ||
+ argNodes.some((a) => ts.isSpreadElement(a))
+ )
+ return null;
+ const probe = tryLowerExpression(lowerer, argNodes[0]!);
+ if (probe?.type.kind !== "object" || !lowerer.isErrorRootedClass(probe.type.className))
+ return null;
+ const loc = locOf(call);
+ const target = lowerer.lowerExpr(argNodes[0]!);
+ const slot = lowerer.declareHiddenLocal("%assignErr", target.type);
+ const sources: IrExpr[] = [];
+ for (const node of argNodes.slice(1)) {
+ const raw = lowerer.lowerExpr(node);
+ fenceSymbolFieldCopy(lowerer, node, raw.type);
+ const source = lowerer.coerceToExpected(raw, DYN);
+ if (source.type.kind !== "dyn") {
+ lowerer.unsupported(
+ "SC1090",
+ node,
+ `Object.assign onto an Error from a '${lowerer.fmt(raw.type)}' source (sources enter the error's dynamic properties)`,
+ );
+ }
+ sources.push(source);
+ }
+ const ref = (): IrExpr => ({ kind: "varRef", localId: slot.id, type: target.type, loc });
+ return {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: slot.id, init: target, loc },
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "error.assign",
+ args: [ref(), { kind: "dynArrLit", elems: sources, type: DYN, loc }],
+ type: VOID,
+ loc,
+ },
+ loc,
+ },
+ ],
+ result: ref(),
+ type: target.type,
+ loc,
+ };
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index a31a62e61..49e7ad9fc 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -1,4 +1,5 @@
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
+import { adaptPromiseExecutor } from "./promise-executor-adapter.js";
import { dynUndefinedExpr, nodeThrowExpr, varRef } from "../../ir/build.js";
import {
constructedFromValue,
@@ -16,6 +17,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";
@@ -28,6 +31,7 @@ import type { FnCtx, Lowerer } from "./lowerer.js";
import { inlineNamespaceRequireOf, lowerWithInlineRequireInit } from "./cjs-inline-require.js";
import {
BOOL,
+ canDynCheckTo,
DATE_T,
DYN,
F64,
@@ -70,6 +74,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,
@@ -112,6 +117,7 @@ import {
import {
STREAM_API_MEMBERS,
STREAM_PROP_MEMBERS,
+ STREAM_COMPLETION_CALLBACK_INDEX,
UNDERSCORE_METHODS,
lowerStreamNew,
lowerStreamSuperCall,
@@ -144,6 +150,7 @@ import {
ambientUndefReadType,
ambientUndefVarRootOf,
ambientUndefinedFnSymbolOf,
+ contextualUndefReadType,
fenceEarlyAliasUse,
fenceEarlyNsMemberRef,
nsMemberIdentOf,
@@ -176,9 +183,9 @@ export function returnsOnlyThis(member: ts.MethodDeclaration): boolean {
while (value && ts.isParenthesizedExpression(value)) value = value.expression;
if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false;
}
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- body.forEachChild(visit);
+ ts.forEachChild(body, visit);
return valid;
}
@@ -209,9 +216,9 @@ export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean {
}
if (!value || value.kind !== ts.SyntaxKind.ThisKeyword) valid = false;
}
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- body.forEachChild(visit);
+ ts.forEachChild(body, visit);
return valid;
}
@@ -334,7 +341,7 @@ export function storedClassValueType(
}
if (
(ts.isArrowFunction(callee) || ts.isFunctionExpression(callee)) &&
- isJsSourceFile(callee.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(callee)) &&
!callee.typeParameters?.nodes.length &&
!lowerer.preparingClassFactories.has(callee)
) {
@@ -403,7 +410,7 @@ export function storedClassValueType(
}
if (
!factory ||
- !isJsSourceFile(factory.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(factory)) ||
factory.typeParameters?.nodes.length ||
!ts.isVariableDeclaration(factory.parent) ||
factory.parent.initializer !== factory ||
@@ -495,6 +502,12 @@ export interface ClassInfo {
/** Own JS overrides whose signature cannot use the inherited ABI. The slot
* retains that ABI, but its implementation throws before executing. */
methodEntryFences?: Map;
+ /** Own TypeScript overrides whose declared result differs from the
+ * inherited ABI's but converts into it (`Promise` over `any`, a
+ * union narrowed to its inherited arm): the slot function `%C.m` keeps the
+ * inherited ABI and converts the result of the body, which lowers as
+ * `%C.m$own` with the declared result (async when declared so). */
+ returnAdapters?: Map;
/** OWN GENERIC instance methods (own type parameters — `m(x: T)`),
* monomorphized per call site like top-level generic functions: instance
* `n` is the module function `%C.m%n` taking `this` as param 0. They
@@ -795,7 +808,7 @@ function symbolSlotReturnType(
): IrType | null {
if (symbolFields.size === 0) return null;
if (fnLike.type !== undefined || !fnLike.body) return null;
- if (!isJsSourceFile(fnLike.getSourceFile())) return null;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(fnLike))) return null;
if (fnLike.asteriskToken !== undefined) return null;
if (fnLike.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)) return null;
const stmts = fnLike.body.statements.nodes;
@@ -805,9 +818,9 @@ function symbolSlotReturnType(
const visit = (n: ts.Node): void => {
if (ts.isFunctionLike(n)) return;
if (ts.isReturnStatement(n)) returns.push(n);
- n.forEachChild(visit);
+ ts.forEachChild(n, visit);
};
- fnLike.body.forEachChild(visit);
+ ts.forEachChild(fnLike.body, visit);
let out: IrType | null = null;
for (const r of returns) {
let e = r.expression;
@@ -1042,7 +1055,7 @@ export function builtinStreamInfoOf(
}
let irName: string | null = null;
for (const [name, rec] of RUNTIME_STREAM_CLASSES) {
- if (rec.lib === symbol.name) irName = name;
+ if (rec.lib === ts.symbolName(symbol)) irName = name;
}
if (!irName) return null;
// Both the bundled declarations and @types/node declare the classes
@@ -1083,7 +1096,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E
// JavaScript documentation cannot guarantee that a call or an indexed
// read returns a present number. Numeric fields receiving those values
// must retain undefined, so later arithmetic performs JS coercion (NaN).
- if (type.kind === "f64" && isJsSourceFile(expression.getSourceFile())) {
+ if (type.kind === "f64" && isJsSourceFile(ts.getSourceFileOfNode(expression))) {
const assignment = expression.parent;
const left = ts.isBinaryExpression(assignment) ? assignment.left : null;
const field =
@@ -1115,7 +1128,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E
if (ts.isCallExpression(rhs) || ts.isElementAccessExpression(rhs))
acceptsUnchecked = true;
}
- node.forEachChild(scan);
+ ts.forEachChild(node, scan);
};
scan(owner);
if (acceptsUnchecked) return DYN;
@@ -1152,7 +1165,7 @@ function jsFieldInitializerType(lowerer: Lowerer, type: IrType, expression: ts.E
owner = owner.parent;
if (owner && (ts.isClassDeclaration(owner) || ts.isClassExpression(owner)) && owner.name) {
const className = owner.name.text;
- for (const statement of owner.getSourceFile().statements.nodes) {
+ for (const statement of ts.getSourceFileOfNode(owner).statements.nodes) {
if (
!ts.isExpressionStatement(statement) ||
!ts.isBinaryExpression(statement.expression)
@@ -1255,7 +1268,7 @@ function returnsWidenedJsField(
ret: IrType,
): boolean {
if (
- !isJsSourceFile(member.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(member)) ||
member.type ||
member.asteriskToken ||
hasExplicitJsDocReturn(member)
@@ -1318,7 +1331,7 @@ function returnsWidenedJsField(
const scan = (node: ts.Node): void => {
if (result || ts.isFunctionLike(node)) return;
if (ts.isReturnStatement(node) && node.expression && widened(node.expression)) result = true;
- node.forEachChild(scan);
+ ts.forEachChild(node, scan);
};
if (member.body) scan(member.body);
return result;
@@ -1397,7 +1410,7 @@ export function builtinEmitterInfoOf(
}
return null;
}
- if (symbol.name !== "EventEmitter") return null;
+ if (ts.symbolName(symbol) !== "EventEmitter") return null;
const declared = symbol.declarations.some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
let node: ts.Node | undefined = d.parent;
@@ -1427,7 +1440,7 @@ export function builtinErrorInfoOf(
if (alias !== null) return alias.kind === "error" ? alias.info : null;
if (!lowerer.isStdlibSymbol(symbol)) return null;
for (const [irName, rec] of RUNTIME_ERROR_CLASSES) {
- if (rec.lib === symbol.name) return lowerer.classes.get(irName) ?? null;
+ if (rec.lib === ts.symbolName(symbol)) return lowerer.classes.get(irName) ?? null;
}
return null;
}
@@ -1572,9 +1585,9 @@ function collectClassShapeDeferring(lowerer: Lowerer, decl: ts.ClassDeclaration)
if (
baseDeclaration &&
!lowerer.isStdlibSymbol(baseSymbol ?? undefined) &&
- !baseDeclaration.getSourceFile().isDeclarationFile &&
- (baseDeclaration.getSourceFile() !== decl.getSourceFile() ||
- baseDeclaration.getStart() < decl.getStart())
+ !ts.getSourceFileOfNode(baseDeclaration).isDeclarationFile &&
+ (ts.getSourceFileOfNode(baseDeclaration) !== ts.getSourceFileOfNode(decl) ||
+ ts.tokenStartOfNode(baseDeclaration) < ts.tokenStartOfNode(decl))
)
lowerer.collectClassShape(baseDeclaration);
lowerer.collectClassShapeInner(decl);
@@ -1779,7 +1792,7 @@ export function collectClassShapeInner(
// source (V8 has not shipped the proposal; the type-stripping
// loaders leave `@dec` in place) — there is no runtime behavior
// to be exact against.
- if (isJsSourceFile(decl.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(decl))) {
lowerer.unsupported(
"SC1090",
(classDecoratorNodes[0] ?? decoratorNodesOf(decoratedMembers[0]!)[0])!,
@@ -1917,7 +1930,7 @@ export function collectClassShapeInner(
const source = signatures.length === 1 ? signatures[0]!.declaration : undefined;
if (
!source ||
- !isJsSourceFile(source.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(source)) ||
!(ts.isFunctionDeclaration(source) || ts.isFunctionExpression(source))
)
return false;
@@ -2033,13 +2046,13 @@ export function collectClassShapeInner(
const baseDecl = propBase.decl;
if (
baseDecl != null &&
- baseDecl.getSourceFile() === decl.getSourceFile() &&
- baseDecl.getStart() > decl.getStart()
+ ts.getSourceFileOfNode(baseDecl) === ts.getSourceFileOfNode(decl) &&
+ ts.tokenStartOfNode(baseDecl) > ts.tokenStartOfNode(decl)
) {
lowerer.unsupported(
"SC1090",
t,
- `extending '${t.expression.getText()}' above the statement that assigns it (the property is still undefined when this class evaluates — Node throws here; assign the base first)`,
+ `extending '${ts.getTextOfNode(t.expression)}' above the statement that assigns it (the property is still undefined when this class evaluates — Node throws here; assign the base first)`,
);
}
base = propBase;
@@ -2057,7 +2070,7 @@ export function collectClassShapeInner(
lowerer.unsupported(
"SC1090",
t,
- `extending the reassigned property '${t.expression.getText()}' (the runtime base is whichever assignment ran last — bind the class exactly once)`,
+ `extending the reassigned property '${ts.getTextOfNode(t.expression)}' (the runtime base is whichever assignment ran last — bind the class exactly once)`,
);
}
}
@@ -2213,11 +2226,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,
@@ -2249,6 +2258,7 @@ export function collectClassShapeInner(
}
>();
const methodEntryFences = new Map();
+ const returnAdapters = new Map();
// Own accessor declarations ("get:x"/"set:x" → node), for the
// partial-override analysis below (diagnostics need the node).
const accessorNodes = new Map();
@@ -2279,7 +2289,7 @@ export function collectClassShapeInner(
// Public JS accessors need live constructor descriptors, including
// receiver-sensitive getters and setters inherited by subclasses.
const checkedStatics =
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
((base && hasRuntimeStatics(base)) ||
decl.members.nodes.some(
(member) =>
@@ -2353,7 +2363,7 @@ export function collectClassShapeInner(
for (const member of decl.members.nodes) {
if (
(functionLocalClass(decl) || checkedStatics) &&
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
(ts.isClassStaticBlockDeclaration(member) ||
ts
.getModifiers(member)
@@ -2364,7 +2374,7 @@ export function collectClassShapeInner(
}
if (
functionLocalClass(decl) &&
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
(ts.isMethodDeclaration(member) || ts.isAccessor(member)) &&
member.body &&
!ts.isPrivateIdentifier(member.name)
@@ -2436,7 +2446,7 @@ export function collectClassShapeInner(
if (type.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name));
if (
type.kind === "dyn" &&
- !isJsSourceFile(member.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(member)) &&
!lowerer.classFieldStoresDyn(lowerer.typeOf(member.name)) &&
(member.type !== undefined ||
(lowerer.typeOf(member.name).flags & ts.TypeFlags.Any) === 0)
@@ -2470,7 +2480,7 @@ export function collectClassShapeInner(
}
staticMethods.set(member.name.text, { params: shapes, ret: ft.ret, member });
if (
- implicitMonoFile(decl.getSourceFile()) &&
+ implicitMonoFile(ts.getSourceFileOfNode(decl)) &&
!functionLocalClass(decl) &&
inst === undefined &&
decl.typeParameters === undefined
@@ -2527,7 +2537,7 @@ export function collectClassShapeInner(
// read of an Error-rooted class is the checked-dynamic property
// (fieldTarget's errorCause container), so only types that read
// back unchanged from it are accepted.
- if (!isJsSourceFile(member.getSourceFile())) {
+ if (!isJsSourceFile(ts.getSourceFileOfNode(member))) {
if (
modifiers?.some(
(m) =>
@@ -2690,6 +2700,24 @@ export function collectClassShapeInner(
);
}
}
+ // A completion callback declared with the broad `Function` type
+ // (`cb: Function`) takes the checked-dynamic callable the runtime
+ // passes implicitly typed callbacks; calls through it are
+ // dynamic calls of that callable.
+ const callbackIndex = STREAM_COMPLETION_CALLBACK_INDEX.get(name);
+ const callbackParam =
+ ts.isMethodDeclaration(member) && callbackIndex !== undefined
+ ? member.parameters.nodes[callbackIndex]
+ : undefined;
+ if (
+ callbackParam !== undefined &&
+ ts.isIdentifier(callbackParam.name) &&
+ !callbackParam.dotDotDotToken &&
+ !callbackParam.questionToken &&
+ !callbackParam.initializer &&
+ lowerer.checker.typeToString(lowerer.typeOf(callbackParam.name)) === "Function"
+ )
+ lowerer.checkedCallbackParams.add(callbackParam);
}
}
if (ts.isPropertyDeclaration(member)) {
@@ -2728,10 +2756,12 @@ export function collectClassShapeInner(
// unconditional `this[key] = ...` at the constructor's top
// level), checked with the unguarded fields below.
const declaredOnly =
- !member.initializer && !isJsSourceFile(member.getSourceFile()) && !!member.type;
+ !member.initializer && !isJsSourceFile(ts.getSourceFileOfNode(member)) && !!member.type;
if (
fieldName === null ||
- (!member.initializer && !isJsSourceFile(member.getSourceFile()) && !declaredOnly)
+ (!member.initializer &&
+ !isJsSourceFile(ts.getSourceFileOfNode(member)) &&
+ !declaredOnly)
) {
lowerer.unsupported(
"SC1090",
@@ -2749,7 +2779,9 @@ export function collectClassShapeInner(
? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred))
: DYN;
let type =
- mapped?.kind === "record" && isJsSourceFile(member.getSourceFile()) && !member.type
+ mapped?.kind === "record" &&
+ isJsSourceFile(ts.getSourceFileOfNode(member)) &&
+ !member.type
? DYN
: mapped
? jsOpenObjectType(member, mapped, lowerer.shapes, lowerer.unions)
@@ -2758,11 +2790,11 @@ export function collectClassShapeInner(
!type ||
type.kind === "void" ||
(type.kind === "dyn" &&
- !isJsSourceFile(member.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(member)) &&
!lowerer.classFieldStoresDyn(inferred))
)
lowerer.badType(member, inferred);
- if (isJsSourceFile(member.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(member))) {
type = inheritedJsFieldType(lowerer, decl, fieldName, type);
if (base?.fields.get(fieldName)?.kind === "dyn") type = DYN;
if (base && !fields.has(fieldName) && lowerer.dynConvertible(type)) type = DYN;
@@ -2876,7 +2908,7 @@ export function collectClassShapeInner(
// as undefined on construction, verified), and reads/writes ride
// the ordinary undefined-armed union machinery.
let type = lowerer.irTypeOf(member.name);
- if (isJsSourceFile(member.getSourceFile()) && !member.initializer) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(member)) && !member.initializer) {
const constructor = decl.members.nodes.find(
(part): part is ts.ConstructorDeclaration =>
ts.isConstructorDeclaration(part) && !!part.body,
@@ -2904,19 +2936,25 @@ export function collectClassShapeInner(
// Unannotated JavaScript callback slots retain the original
// callable and its receiver instead of adapting its signature.
if (
- isJsSourceFile(member.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(member)) &&
!member.type &&
- !/@type\b/.test(member.getSourceFile().text.slice(member.pos, member.getStart())) &&
+ !/@type\b/.test(
+ ts.getSourceFileOfNode(member).text.slice(member.pos, ts.tokenStartOfNode(member)),
+ ) &&
(type.kind === "func" || type.kind === "record") &&
lowerer.dynConvertible(type)
)
type = DYN;
// JS permits reads before a constructor's first assignment even
// when its inferred field type omits undefined.
- if (isJsSourceFile(member.getSourceFile()) && !member.initializer && type.kind !== "dyn") {
+ if (
+ isJsSourceFile(ts.getSourceFileOfNode(member)) &&
+ !member.initializer &&
+ type.kind !== "dyn"
+ ) {
type = lowerer.withUndefinedArmOf(type) ?? DYN;
}
- if (isJsSourceFile(member.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(member))) {
type = inheritedJsFieldType(lowerer, decl, member.name.text, type);
if (base?.fields.get(member.name.text)?.kind === "dyn") type = DYN;
if (base && !fields.has(member.name.text) && lowerer.dynConvertible(type)) type = DYN;
@@ -2927,7 +2965,7 @@ export function collectClassShapeInner(
// storage as implicit constructor-assigned JS fields.
if (
type.kind === "dyn" &&
- !isJsSourceFile(member.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(member)) &&
!lowerer.classFieldStoresDyn(lowerer.typeOf(member.name))
) {
lowerer.unsupported(
@@ -3199,7 +3237,7 @@ export function collectClassShapeInner(
// name (the override stays on the vtable), a shadowed field, or
// a generic-class instantiation's member.
if (
- implicitMonoFile(decl.getSourceFile()) &&
+ implicitMonoFile(ts.getSourceFileOfNode(decl)) &&
!functionLocalClass(decl) &&
ts.isIdentifier(member.name) &&
!lowerer.virtualJsMethods.has(member) &&
@@ -3240,14 +3278,14 @@ export function collectClassShapeInner(
// JS inference may retain only a symbol sentinel while a method
// also returns values from an untyped mutable field or local.
if (
- isJsSourceFile(member.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(member)) &&
!member.type &&
!hasExplicitJsDocReturn(member) &&
ft.ret.kind === "symbol"
)
ft.ret = DYN;
if (
- isJsSourceFile(member.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(member)) &&
lowerer.prototypeMethodAccesses.has(mName) &&
!lowerer.findMethodOn(base, mName)?.declarer.def.runtime &&
shapes.every((p) => p.mode === "required" || p.mode === "omittable")
@@ -3301,7 +3339,7 @@ export function collectClassShapeInner(
// against the STATIC receiver's shape, so `m(x?: number)` and
// `m(x: number | undefined)` interchange soundly in overrides.
const overridden = lowerer.findMethodOn(base, mName);
- if (overridden && isJsSourceFile(member.getSourceFile())) {
+ if (overridden && isJsSourceFile(ts.getSourceFileOfNode(member))) {
// An unannotated JS override can return a checked value into
// the inherited native ABI, just as its untyped parameters do.
// Body lowering inserts the conversion at each return.
@@ -3349,7 +3387,7 @@ export function collectClassShapeInner(
// readers keep the exact-signature fence.
if (
overridden &&
- isJsSourceFile(member.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(member)) &&
shapes.length < overridden.sig.params.length &&
!bodyReadsArguments(member) &&
shapes.every(
@@ -3377,9 +3415,9 @@ export function collectClassShapeInner(
// a statically typed receiver see the same ABI.
if (
overridden &&
- !isJsSourceFile(member.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(member)) &&
overridden.declarer.decl !== null &&
- isJsSourceFile(overridden.declarer.decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(overridden.declarer.decl)) &&
member.asteriskToken === undefined &&
!overridden.sig.async &&
!overridden.sig.gen &&
@@ -3430,11 +3468,71 @@ export function collectClassShapeInner(
))
) {
const fencedJsOverride =
- isJsSourceFile(member.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(member)) &&
overridden.declarer.decl !== null &&
- isJsSourceFile(overridden.declarer.decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(overridden.declarer.decl)) &&
overridden.sig.gen === undefined;
- if (fencedJsOverride) {
+ // A TypeScript override that only changes the result, into a
+ // type that converts into the inherited one (`Promise` over
+ // an `any` base, `string | undefined` over `string`): the slot
+ // keeps the inherited ABI and converts the body's result at the
+ // boundary (lowerClassMethodMemberInner).
+ const ownAsync =
+ member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true;
+ if (
+ !fencedJsOverride &&
+ !functionLocalClass(decl) &&
+ overridden.sig.gen === undefined &&
+ // An async base's slot answers its promise: a Promise type, or
+ // the checked-dynamic value of an untyped async method.
+ (overridden.sig.async !== true ||
+ overridden.sig.ret.kind === "promise" ||
+ overridden.sig.ret.kind === "dyn") &&
+ !overridden.sig.abstract &&
+ overridden.sig.params.length === shapes.length &&
+ overridden.sig.params.every(
+ (p, i) =>
+ p.mode === shapes[i]!.mode &&
+ (p.mode === "required" || p.mode === "omittable") &&
+ // Each inherited argument converts into the override's
+ // parameter: equal, a checked extraction from an `any`
+ // slot, or a widening (a contravariant parameter).
+ (typeEquals(p.type, shapes[i]!.type) ||
+ (p.type.kind === "dyn" &&
+ canDynCheckTo(
+ shapes[i]!.type,
+ (id) => lowerer.shapes.get(id),
+ (id) => lowerer.unions.get(id),
+ )) ||
+ lowerer.coercibleValue(p.type, shapes[i]!.type)),
+ ) &&
+ (ownAsync ? ft.ret.kind === "promise" : true) &&
+ (typeEquals(ft.ret, overridden.sig.ret) ||
+ (overridden.sig.ret.kind === "dyn"
+ ? ft.ret.kind !== "void" && lowerer.dynConvertible(ft.ret)
+ : lowerer.coercibleValue(ft.ret, overridden.sig.ret) ||
+ // A promise whose payload widens into the inherited one
+ // (`Promise` over an untyped async `Promise`).
+ (ft.ret.kind === "promise" &&
+ overridden.sig.ret.kind === "promise" &&
+ lowerer.promisePayloadWidens(ft.ret, overridden.sig.ret)) ||
+ // A JavaScript override's inferred `any` result checks
+ // into the inherited type (`toString() { return
+ // this.message }` over Error's).
+ (ft.ret.kind === "dyn" &&
+ isJsSourceFile(ts.getSourceFileOfNode(member)) &&
+ overridden.sig.ret.kind !== "void" &&
+ canDynCheckTo(
+ overridden.sig.ret,
+ (id) => lowerer.shapes.get(id),
+ (id) => lowerer.unions.get(id),
+ ))))
+ ) {
+ returnAdapters.set(mName, { params: [...shapes], ret: ft.ret, async: ownAsync });
+ shapes.splice(0, shapes.length, ...overridden.sig.params);
+ ft.ret = overridden.sig.ret;
+ if (overridden.sig.async === true) thunkAsyncSlot(overridden, mName);
+ } else if (fencedJsOverride) {
// A dormant JS method must not poison the whole class. Keep
// the inherited call/dispatch ABI and refuse at method entry;
// lowering this body with the base signature would change JS.
@@ -3489,9 +3587,36 @@ export function collectClassShapeInner(
// fiber spawn wrapper; vtable slots hold raw implementations) —
// an override chain touching an async method on either end would
// put a spawn wrapper behind a virtual slot, so it fences.
- if (overridden && (asyncMember || overridden.sig.async === true)) {
+ // An override chain touching async methods dispatches through
+ // plain slot functions that return the body's promise: the async
+ // bodies lower as `%C.m$own` behind them (thunkAsyncSlot).
+ if (
+ overridden &&
+ !returnAdapters.has(mName) &&
+ (asyncMember || overridden.sig.async === true) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(member)) &&
+ !functionLocalClass(decl) &&
+ member.asteriskToken === undefined &&
+ overridden.sig.gen === undefined &&
+ !overridden.sig.abstract &&
+ ft.ret.kind === "promise" &&
+ typeEquals(ft.ret, overridden.sig.ret) &&
+ overridden.sig.params.length === shapes.length &&
+ overridden.sig.params.every(
+ (p, i) => typeEquals(p.type, shapes[i]!.type) && p.mode === shapes[i]!.mode,
+ )
+ ) {
+ if (overridden.sig.async === true) thunkAsyncSlot(overridden, mName);
+ if (asyncMember)
+ returnAdapters.set(mName, { params: [...shapes], ret: ft.ret, async: true });
+ }
+ if (
+ overridden &&
+ !returnAdapters.has(mName) &&
+ (asyncMember || overridden.sig.async === true)
+ ) {
const message = `overriding ${overridden.sig.async === true ? "the async method" : "a method with an async method"} '${mName}' (async methods dispatch statically — the vtable slot machinery has no fiber-spawn story)`;
- if (isJsSourceFile(member.getSourceFile()))
+ if (isJsSourceFile(ts.getSourceFileOfNode(member)))
methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message));
else lowerer.unsupported("SC1090", member.name, message);
}
@@ -3500,7 +3625,7 @@ export function collectClassShapeInner(
(member.asteriskToken !== undefined || overridden.sig.gen !== undefined)
) {
const message = `overriding the generator method '${mName}' (generator methods dispatch statically — a virtual slot cannot enter their spawn wrapper)`;
- if (isJsSourceFile(member.getSourceFile()))
+ if (isJsSourceFile(ts.getSourceFileOfNode(member)))
methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message));
else lowerer.unsupported("SC1090", member.name, message);
}
@@ -3520,7 +3645,7 @@ export function collectClassShapeInner(
} else {
methods.set(
mName,
- asyncMember
+ asyncMember && !returnAdapters.has(mName)
? { params: shapes, ret: ft.ret, async: true as const }
: { params: shapes, ret: ft.ret },
);
@@ -3572,7 +3697,7 @@ export function collectClassShapeInner(
const mName = `${isGet ? "get" : "set"}:${prop}`;
if (
fields.has(prop) &&
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
base?.fields.has(prop) &&
!fieldOrder.some((field) => field.name === prop)
) {
@@ -3887,7 +4012,7 @@ export function collectClassShapeInner(
// assignment ran — the zeroed-memory trap a TS declaration order
// forbids via strictPropertyInitialization — so they keep a named
// fence instead of reading undefined without a diagnostic.
- if (isJsSourceFile(decl.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(decl))) {
const hasAccessor = (name: string): boolean =>
methods.has(`get:${name}`) ||
methods.has(`set:${name}`) ||
@@ -3911,7 +4036,7 @@ export function collectClassShapeInner(
// goes through the key.
const lateBoundByKey = new Map();
for (const p of instType ? lowerer.checker.getPropertiesOfType(instType) : []) {
- if (!p.name.startsWith("__@")) continue;
+ if (!ts.symbolName(p).startsWith("__@")) continue;
const keySym = lateBoundKeySymOf(lowerer, p);
if (keySym) lateBoundByKey.set(keySym, p);
}
@@ -3967,7 +4092,7 @@ export function collectClassShapeInner(
if (
t &&
(t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0 &&
- isJsSourceFile(decl.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(decl))
)
type = DYN;
if (type) type = jsOpenObjectType(assign, type, lowerer.shapes, lowerer.unions);
@@ -4085,10 +4210,14 @@ export function collectClassShapeInner(
// static layouts cannot represent. Named fence, at the first
// assignment site.
for (const p of instType ? lowerer.checker.getPropertiesOfType(instType) : []) {
- if (errorRooted && p.name === "cause") continue;
- if (fields.has(p.name) || methods.has(p.name)) continue;
- if (hasAccessor(p.name)) continue;
- if (base && (base.fields.has(p.name) || lowerer.findMethodOn(base, p.name))) continue;
+ if (errorRooted && ts.symbolName(p) === "cause") continue;
+ if (fields.has(ts.symbolName(p)) || methods.has(ts.symbolName(p))) continue;
+ if (hasAccessor(ts.symbolName(p))) continue;
+ if (
+ base &&
+ (base.fields.has(ts.symbolName(p)) || lowerer.findMethodOn(base, ts.symbolName(p)))
+ )
+ continue;
const site = p.declarations.find(
(d) =>
ts.isPropertyAccessExpression(d) ||
@@ -4102,7 +4231,7 @@ export function collectClassShapeInner(
// identities (symbol parameters, mutable bindings, computed
// descriptions) or assignments outside the constructor's top
// level.
- if (p.name.startsWith("__@")) {
+ if (ts.symbolName(p).startsWith("__@")) {
const keySym = lateBoundKeySymOf(lowerer, p);
const key = keySym ? classSymbolKeyOfSymbol(lowerer, keySym) : null;
if (key && symbolFields.has(key.identity)) continue;
@@ -4122,24 +4251,26 @@ export function collectClassShapeInner(
// Unmappable inferences and arm-less kinds keep the fence.
// TypeScript classes keep the compile-time fence too: an annotated
// program can spell `T | undefined` itself.
- if (isJsSourceFile(decl.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(decl))) {
const original = undefArmedFieldType(lowerer, p);
const armed =
- original === null ? null : inheritedJsFieldType(lowerer, decl, p.name, original);
+ original === null
+ ? null
+ : inheritedJsFieldType(lowerer, decl, ts.symbolName(p), original);
if (armed !== null) {
// Unknown JS properties use the instance bag: they do not
// exist until assigned, and a subclass may declare the same
// name as an ordinary layout field.
if (armed.kind === "dyn") continue;
- fields.set(p.name, armed);
- fieldOrder.push({ name: p.name, type: armed, initializer: undefined });
+ fields.set(ts.symbolName(p), armed);
+ fieldOrder.push({ name: ts.symbolName(p), type: armed, initializer: undefined });
continue;
}
}
lowerer.unsupported(
"SC1090",
site,
- `fields assigned outside the constructor's top level ('this.${p.name}' would be undefined until the first assignment runs — assign it unconditionally at the top of the constructor)`,
+ `fields assigned outside the constructor's top level ('this.${ts.symbolName(p)}' would be undefined until the first assignment runs — assign it unconditionally at the top of the constructor)`,
);
}
}
@@ -4182,7 +4313,7 @@ export function collectClassShapeInner(
if (
ctor &&
!mixinForwarding &&
- isJsSourceFile(ctor.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(ctor)) &&
!ctorParams.some((shape) => shape.mode === "rest" || shape.mode === "dynRest") &&
bodyReadsArguments(ctor)
) {
@@ -4238,6 +4369,7 @@ export function collectClassShapeInner(
fieldOrder,
methods,
...(methodEntryFences.size > 0 ? { methodEntryFences } : {}),
+ ...(returnAdapters.size > 0 ? { returnAdapters } : {}),
decl,
...(emitOverride !== undefined ? { emitOverride } : {}),
ctor,
@@ -4260,6 +4392,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).
@@ -4570,7 +4708,7 @@ export function lowerStaticFieldInits(lowerer: Lowerer, info: ClassInfo): IrStmt
// Keep their refusal attached to the class declaration instead of
// letting a poisoned initializer escape the module lowering pass.
if (!(error instanceof PoisonError)) throw error;
- if (info.decl && isJsSourceFile(info.decl.getSourceFile())) {
+ if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) {
const fence =
lowerer.diagSink === null
? lowerer.deferToRuntimeFence(diagsBefore, info.decl, { kind: "statement" })
@@ -4697,8 +4835,16 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[]
| { pos: number; kind: "field"; f: ClassInfo["staticFields"][number] }
| { pos: number; kind: "block"; b: ts.ClassStaticBlockDeclaration };
const items: Item[] = [
- ...info.staticFields.map((f): Item => ({ pos: f.initializer.getStart(), kind: "field", f })),
- ...(info.staticBlocks ?? []).map((b): Item => ({ pos: b.getStart(), kind: "block", b })),
+ ...info.staticFields.map((f): Item => ({
+ pos: ts.tokenStartOfNode(f.initializer),
+ kind: "field",
+ f,
+ })),
+ ...(info.staticBlocks ?? []).map((b): Item => ({
+ pos: ts.tokenStartOfNode(b),
+ kind: "block",
+ b,
+ })),
].sort((a, b) => a.pos - b.pos);
for (const item of items) {
if (item.kind === "block") {
@@ -4745,7 +4891,7 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[]
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file);
- if (isJsSourceFile(f.initializer.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(f.initializer))) {
const fence =
lowerer.diagSink === null
? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" })
@@ -4804,7 +4950,7 @@ export function analyzeClassDecoration(lowerer: Lowerer, info: ClassInfo): void
ts.isVariableDeclaration(vdecl) &&
vdecl.initializer === undefined &&
(ts.getCombinedModifierFlags(vdecl) & ts.ModifierFlags.Ambient) !== 0 &&
- !vdecl.getSourceFile().isDeclarationFile;
+ !ts.getSourceFileOfNode(vdecl).isDeclarationFile;
if (ambientVar || ambientUndefinedFnSymbolOf(lowerer, ambientCallee) !== null) {
shapes.push({ kind: "ambientThrow", name: ambientCallee.text });
continue;
@@ -5183,16 +5329,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,
@@ -5432,7 +5573,7 @@ export function lowerStaticFieldRead(
// JavaScript often assigns public statics after the class declaration.
// Their storage stays mutable and checked, while reads retain the
// checker's concrete type instead of losing the method receiver.
- const type = isJsSourceFile(expr.getSourceFile())
+ const type = isJsSourceFile(ts.getSourceFileOfNode(expr))
? null
: lowerer.mapTypeOf(lowerer.typeOf(expr));
return type ? lowerer.coerceToExpected(value, type) : value;
@@ -5473,7 +5614,7 @@ function namedEvaluationName(expr: ts.ClassLikeDeclaration): string {
* (private-brands.ts), so the class must be a named declaration (undici's
* webidl FastIterableIterator). The checker rejects writes to that binding. */
function localPrivateFieldsAgree(expr: ts.ClassLikeDeclaration): boolean {
- if (!isJsSourceFile(expr.getSourceFile()) || !ts.isClassDeclaration(expr) || !expr.name)
+ if (!isJsSourceFile(ts.getSourceFileOfNode(expr)) || !ts.isClassDeclaration(expr) || !expr.name)
return false;
if (
expr.members.nodes.some(
@@ -5581,7 +5722,7 @@ export function lowerClassExpressionInfo(
expr.members.nodes.some(
(member) =>
decoratorNodesOf(member).length ||
- (!isJsSourceFile(expr.getSourceFile()) &&
+ (!isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(ts.isClassStaticBlockDeclaration(member) ||
ts
.getModifiers(member)
@@ -5705,7 +5846,7 @@ export function lowerClassExpression(lowerer: Lowerer, expr: ts.ClassLikeDeclara
decoratorNodesOf(expr).length > 0 ||
expr.members.nodes.some((m) => decoratorNodesOf(m).length > 0)
) {
- if (!isJsSourceFile(expr.getSourceFile()) && expr.typeParameters === undefined) {
+ if (!isJsSourceFile(ts.getSourceFileOfNode(expr)) && expr.typeParameters === undefined) {
const thrown = guaranteedDecorationThrow(lowerer, expr);
if (thrown) {
// The expression's static type never materializes — the read
@@ -5991,12 +6132,12 @@ function enclosingTopLevelClassAssignment(expr: ts.ClassExpression): ts.BinaryEx
* `Common.I`): a second assignment makes the binding dynamic, so
* callers refuse to pin the first one's class. */
function countAssignmentsTo(assign: ts.BinaryExpression): number {
- const sf = assign.getSourceFile();
+ const sf = ts.getSourceFileOfNode(assign);
// `exports.I` and `module.exports.I` are the SAME binding in Node
// (exports aliases module.exports until a table replaces it) — fold
// the member spellings together before comparing.
const canon = (lhs: ts.Expression): string => {
- const text = lhs.getText().replace(/\s+/g, "");
+ const text = ts.getTextOfNode(lhs).replace(/\s+/g, "");
return text.startsWith("module.exports.") ? text.slice("module.".length) : text;
};
const target = canon(assign.left);
@@ -6054,10 +6195,10 @@ export function lowerStaticMethodCall(
if (!ts.isIdentifier(access.expression)) {
if (
!isModuleExportsAccess(access.expression) ||
- !isCjsJsFile(access.getSourceFile(), lowerer.program)
+ !isCjsJsFile(ts.getSourceFileOfNode(access), lowerer.program)
) {
let receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
- if (receiverType?.kind !== "classval" && isJsSourceFile(call.getSourceFile())) {
+ if (receiverType?.kind !== "classval" && isJsSourceFile(ts.getSourceFileOfNode(call))) {
const receiver = tryLowerExpression(lowerer, access.expression);
if (receiver?.type.kind === "classval") receiverType = receiver.type;
}
@@ -6078,7 +6219,7 @@ export function lowerStaticMethodCall(
loc: locOf(call),
};
}
- const whole = cjsClassExprWholeExportOf(access.getSourceFile());
+ const whole = cjsClassExprWholeExportOf(ts.getSourceFileOfNode(access));
if (!whole) return null;
return staticCallOn(
lowerer,
@@ -6375,9 +6516,9 @@ function hasLexicalSuper(root: ts.Node): boolean {
ts.isClassExpression(node)
)
return;
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- root.forEachChild(visit);
+ ts.forEachChild(root, visit);
return found;
}
@@ -6406,9 +6547,9 @@ function capturesEnclosingScope(
if (decl && decl !== scope && inside(decl, scope) && !inside(decl, member)) captured = true;
return;
}
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- member.forEachChild(visit);
+ ts.forEachChild(member, visit);
return captured;
}
@@ -6531,7 +6672,10 @@ export function lowerClassValueProperty(
// `module.exports.label` in a class-replaced CJS module: the
// receiver is the exact exported class, and the read is
// side-effect-free — as bindable as an identifier.
- !(isModuleExportsAccess(expr.expression) && isCjsJsFile(expr.getSourceFile(), lowerer.program))
+ !(
+ isModuleExportsAccess(expr.expression) &&
+ isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program)
+ )
) {
// Devirtualized reads DISCARD the receiver value, so only
// side-effect-free receivers are claimed (the instanceOf fold
@@ -6615,6 +6759,28 @@ function abstractMemberSignature(
/** A nominal return subtype uses the same owned pointer as the inherited
* return slot. The current class is still being collected, so consult its
* already collected base chain before the complete class graph is available. */
+/** An inherited ASYNC method that an override now dispatches against: its
+ * slot becomes a plain function returning the body's promise, and the
+ * async body lowers behind it as `%C.m$own` (the adapter path with an
+ * identity conversion), so a virtual call never enters an async body
+ * without its spawn wrapper. */
+function thunkAsyncSlot(
+ overridden: { declarer: ClassInfo; sig: ClassMethodSignature },
+ mName: string,
+): void {
+ if (overridden.sig.async !== true) return;
+ const declarer = overridden.declarer;
+ declarer.returnAdapters ??= new Map();
+ if (!declarer.returnAdapters.has(mName)) {
+ declarer.returnAdapters.set(mName, {
+ params: [...overridden.sig.params],
+ ret: overridden.sig.ret,
+ async: true,
+ });
+ }
+ delete overridden.sig.async;
+}
+
function overrideReturnAbi(
lowerer: Lowerer,
declared: IrType,
@@ -6773,7 +6939,7 @@ export function collectInheritedJsFieldWrites(
if (name !== null) addWrite(name, part.right);
}
}
- part.forEachChild(scan);
+ ts.forEachChild(part, scan);
};
for (const member of node.members.nodes) {
if (
@@ -6814,7 +6980,7 @@ export function collectInheritedJsFieldWrites(
if (
!owner ||
(!ts.isClassDeclaration(owner) && !ts.isClassExpression(owner)) ||
- !isJsSourceFile(owner.getSourceFile())
+ !isJsSourceFile(ts.getSourceFileOfNode(owner))
)
continue;
let fields = lowerer.inheritedJsFieldWrites.get(owner);
@@ -6852,7 +7018,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou
if (
node.heritageClauses?.nodes.length &&
accessors.length &&
- isJsSourceFile(node.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(node))
) {
const symbol = lowerer.typeOf(node).getSymbol();
const instance = symbol ? lowerer.checker.getDeclaredTypeOfSymbol(symbol) : null;
@@ -6869,7 +7035,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou
if (
(ts.isGetAccessorDeclaration(declaration) ||
ts.isSetAccessorDeclaration(declaration)) &&
- isJsSourceFile(declaration.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(declaration))
) {
lowerer.checkedVirtualJsAccessors.add(declaration);
lowerer.checkedVirtualJsAccessors.add(member);
@@ -6911,12 +7077,12 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou
for (const declaration of property ? property.declarations : []) {
if (
ts.isMethodDeclaration(declaration) &&
- isJsSourceFile(declaration.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(declaration)) &&
!checkedJsOverride(lowerer, declaration, member)
) {
lowerer.virtualJsMethods.add(declaration);
if (
- isJsSourceFile(member.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(member)) &&
!member.typeParameters?.nodes &&
!member.asteriskToken &&
!member.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) &&
@@ -6962,7 +7128,7 @@ export function collectVirtualJsMethods(lowerer: Lowerer, files: readonly ts.Sou
if (node !== member && ts.isFunctionLike(node)) return;
if (ts.isReturnStatement(node) && node.expression)
lowerer.checkedVirtualJsValueReturns.add(member);
- node.forEachChild(scan);
+ ts.forEachChild(node, scan);
};
scan(member);
}
@@ -6995,7 +7161,7 @@ function checkedJsOverride(
): boolean {
return [base, member].every(
(method) =>
- lowerer.checkedJsFile(method.getSourceFile()) &&
+ lowerer.checkedJsFile(ts.getSourceFileOfNode(method)) &&
method.parameters.nodes.every(documentedParameter),
);
}
@@ -7143,7 +7309,7 @@ export function lowerClassGenericMethodCall(
result.type.kind === "object" &&
!ts.isExpressionStatement(call.parent) &&
ts.isMethodDeclaration(info.decl) &&
- isJsSourceFile(info.decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
returnsOnlyThisValue(info.decl)
) {
return lowerer.maybeNarrow(result, call);
@@ -7248,7 +7414,7 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
out.push(lowerer.lowerClassCtor(info));
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
- if (info.decl && isJsSourceFile(info.decl.getSourceFile())) {
+ if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) {
const types: IrType[] = [
{ kind: "object", className },
...info.ctorParams.map((param) => param.type),
@@ -7837,6 +8003,82 @@ function lowerClassMethodMemberInner(
allowDiagSink: true,
});
}
+ // An override converting its declared result into the inherited ABI:
+ // the body lowers as `%C.m$own` under its declared result, and the slot
+ // function forwards to it and converts what it returns.
+ const adapter = info.returnAdapters?.get(mName);
+ if (adapter) {
+ const ownSig: ClassMethodSignature = { ...sig, params: adapter.params, ret: adapter.ret };
+ if (adapter.async) ownSig.async = true;
+ const own = lowerClassMethodBody(lowerer, info, fnLike, `%${className}.${mName}$own`, ownSig);
+ if (!own) return null;
+ lowerer.liftedFns.push(own);
+ const loc = locOf(fnLike);
+ const params: IrParam[] = [
+ { localId: "this.0", name: "this", type: thisType },
+ ...sig.params.map((p, i) => ({ localId: `p.${i}`, name: `p${i}`, type: p.type })),
+ ];
+ const fnCtx = newFnCtx(false, null, null, sig.ret);
+ lowerer.fnStack.push(fnCtx);
+ try {
+ const call: IrExpr = {
+ kind: "call",
+ callee: own.name,
+ args: params.map((p, i): IrExpr => {
+ const ref: IrExpr = { kind: "varRef", localId: p.localId, type: p.type, loc };
+ // `this` passes through; each inherited argument converts into
+ // the override's declared parameter (checked where it narrows).
+ return i === 0 ? ref : lowerer.coerceInto(fnLike.name, ref, adapter.params[i - 1]!.type);
+ }),
+ type: adapter.ret,
+ loc,
+ };
+ // A void slot discards the override's result after running it. A
+ // toString slot hands on what every ToString consumer of the result
+ // makes of it (`String(err)`, templates): undefined reads
+ // "undefined", as in Node. Other slots check the result into the
+ // inherited type.
+ const result = (): IrExpr =>
+ mName === "toString" && sig.ret.kind === "string"
+ ? call.type.kind === "dyn"
+ ? { kind: "libCall", fn: "dyn.stringConstructor", args: [call], type: STRING, loc }
+ : lowerer.ensureString(call, fnLike.name)
+ : lowerer.coerceInto(fnLike.name, call, sig.ret);
+ const body: IrStmt[] =
+ sig.ret.kind === "void"
+ ? [
+ { kind: "exprStmt", expr: call, loc },
+ { kind: "return", value: null, loc },
+ ]
+ : [{ kind: "return", value: result(), loc }];
+ return {
+ name: `%${className}.${mName}`,
+ params,
+ returnType: sig.ret,
+ locals: [
+ ...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
+ ...lowerer.ctx.locals,
+ ],
+ body,
+ loc,
+ };
+ } finally {
+ lowerer.fnStack.pop();
+ }
+ }
+ return lowerClassMethodBody(lowerer, info, fnLike, `%${className}.${mName}`, sig);
+}
+
+function lowerClassMethodBody(
+ lowerer: Lowerer,
+ info: ClassInfo,
+ fnLike: ts.MethodDeclaration | ts.AccessorDeclaration,
+ fnName: string,
+ sig: ClassMethodSignature,
+): IrFunction | null {
+ const className = info.def.name;
+ const thisType: IrType = { kind: "object", className };
+ if (!fnLike.body) return null;
const prevClass = lowerer.currentClass;
lowerer.currentClass = info;
// ASYNC methods: the module function is an async IrFunction — its
@@ -7878,7 +8120,7 @@ function lowerClassMethodMemberInner(
const body = [...declared.prologue, ...lowerer.lowerStmts(fnLike.body.statements.nodes)];
appendImplicitUndefinedReturn(lowerer, body, bodyReturn, locOf(fnLike));
const fn: IrFunction = {
- name: `%${className}.${mName}`,
+ name: fnName,
params,
returnType: bodyReturn,
locals: lowerer.ctx.locals,
@@ -8085,7 +8327,7 @@ export function fieldInitStmts(lowerer: Lowerer, info: ClassInfo, thisLocal: IrL
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
lowerer.countStatementFailed(f.initializer, locOf(f.initializer).file);
- if (isJsSourceFile(f.initializer.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(f.initializer))) {
const fence =
lowerer.diagSink === null
? lowerer.deferToRuntimeFence(diagsBefore, f.initializer, { kind: "statement" })
@@ -8584,7 +8826,7 @@ export function lowerSuperMethodCall(
kind: "dynCall",
callee,
receiver: lowerer.coerceToExpected(thisRef, DYN),
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
@@ -8737,6 +8979,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(ts.getSourceFileOfNode(info.decl), 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 +9157,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 +9204,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 .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) {
@@ -9156,7 +9449,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// lowering ahead of the assignment statement).
if (
!expr.expression.questionDotToken &&
- ((isCjsJsFile(expr.getSourceFile(), lowerer.program) &&
+ ((isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program) &&
(isModuleExportsAccess(expr.expression.expression) ||
(ts.isIdentifier(expr.expression.expression) &&
expr.expression.expression.text === "exports" &&
@@ -9172,7 +9465,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
memberSym,
resolved,
lowerer.resolveValueSymbol(expr.expression.name) ?? undefined,
- lowerer.cjsModuleExportSymbol(expr.getSourceFile(), expr.expression.name.text),
+ lowerer.cjsModuleExportSymbol(ts.getSourceFileOfNode(expr), expr.expression.name.text),
];
const exportSym = candidates.find((s) => s !== undefined);
const g = candidates
@@ -9387,7 +9680,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// libregexp engine the literals ride. The pattern compiles EAGERLY,
// so bad input throws Node's catchable SyntaxError at construction.
// Checked arguments retain native regexes and perform runtime coercion.
- if (symbol && symbol.name === "RegExp" && lowerer.isStdlibSymbol(symbol)) {
+ if (symbol && ts.symbolName(symbol) === "RegExp" && lowerer.isStdlibSymbol(symbol)) {
const args = expr.arguments?.nodes ?? [];
if (args.length > 2) {
lowerer.noLowering(`new RegExp with ${args.length} arguments`, expr);
@@ -9455,32 +9748,71 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
loc,
};
}
- if (symbol && symbol.name === "URL" && lowerer.isStdlibSymbol(symbol)) {
+ if (symbol && ts.symbolName(symbol) === "URL" && lowerer.isStdlibSymbol(symbol)) {
return lowerUrlNew(lowerer, expr);
}
// `new URLSearchParams(init?)`: the WHATWG list (stdlib provenance —
// see lowerSearchParamsNew for the lowered init shapes).
- if (symbol && symbol.name === "URLSearchParams" && lowerer.isStdlibSymbol(symbol)) {
+ if (symbol && ts.symbolName(symbol) === "URLSearchParams" && lowerer.isStdlibSymbol(symbol)) {
return lowerSearchParamsNew(lowerer, expr, loc);
}
// Date's read-only value slice: store the constructor's TimeClip'd
// epoch milliseconds as the scalar date kind. That is sufficient
// for every getter and toISOString; identity and setters stay
// fenced, so copying the scalar cannot create an observable lie.
- if (symbol && symbol.name === "Date" && lowerer.isStdlibSymbol(symbol)) {
+ if (symbol && ts.symbolName(symbol) === "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,
);
}
- if (isJsSourceFile(expr.getSourceFile())) {
+ // 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(ts.getSourceFileOfNode(expr))) {
+ 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(ts.getSourceFileOfNode(expr))) {
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",
@@ -9521,7 +9853,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// normalized at run time (Node's normalizeEncoding, which throws
// ERR_UNKNOWN_ENCODING for an unknown name). Omitted means utf8,
// Node's default.
- if (symbol && symbol.name === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) {
+ if (symbol && ts.symbolName(symbol) === "StringDecoder" && lowerer.isStdlibSymbol(symbol)) {
const args = expr.arguments?.nodes ?? [];
if (args.length > 1) {
lowerer.noLowering("new StringDecoder with 2 arguments", expr, undefined, symbol);
@@ -9555,12 +9887,13 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
loc,
};
}
+ const codec = symbol ? ts.symbolName(symbol) : undefined;
if (
symbol &&
- (symbol.name === "TextDecoder" || symbol.name === "TextEncoder") &&
+ (codec === "TextDecoder" || codec === "TextEncoder") &&
lowerer.isStdlibSymbol(symbol)
) {
- return lowerTextCodecNew(lowerer, expr, symbol.name);
+ return lowerTextCodecNew(lowerer, expr, codec);
}
// `new Uint8Array(...)` / `new Uint32Array(...)` / `new
// Float32Array(...)` / `new Float64Array(...)`: the typed-array
@@ -9594,11 +9927,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// types get their half named specifically instead of the component
// fence (SC2009, which names Map slots at value positions elsewhere).
// Both Array() and new Array() share the elements/count lowering.
- if (symbol?.name === "Object" && lowerer.isStdlibSymbol(symbol)) {
+ if (symbol !== null && ts.symbolName(symbol) === "Object" && lowerer.isStdlibSymbol(symbol)) {
const object = lowerObjectConstructor(lowerer, expr.arguments?.nodes ?? [], loc);
if (object) return object;
}
- if (symbol?.name === "Array" && lowerer.isStdlibSymbol(symbol)) {
+ if (symbol !== null && ts.symbolName(symbol) === "Array" && lowerer.isStdlibSymbol(symbol)) {
return lowerArrayConstructor(lowerer, expr, expr.arguments?.nodes ?? []);
}
// A const alias (`const M = Map`) constructs the same Map.
@@ -9614,7 +9947,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
: null;
let tsType = lowerer.typeOf(expr);
let mapped = lowerer.mapTypeOf(tsType);
- if (mapped === null && isJsSourceFile(expr.getSourceFile())) mapped = lowerer.irTypeOf(expr);
+ if (mapped === null && isJsSourceFile(ts.getSourceFileOfNode(expr)))
+ mapped = lowerer.irTypeOf(expr);
const fieldType = assignedThisFieldType(lowerer, expr);
if (mapped?.kind !== "map" && fieldType?.kind === "map") mapped = fieldType;
// A fresh map built from pair literals can use its destination's
@@ -9676,7 +10010,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// unconditionally; the format path never touches them. TypeScript
// keeps the compile fence.
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(expr.arguments?.nodes.length ?? 0) === 0 &&
targs.length > 0 &&
targs.every((t) => (t.flags & ts.TypeFlags.Any) !== 0)
@@ -9712,7 +10046,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
}
// Weak containers share checked native key identities and lifetime hooks.
if (
- (symbol?.name === "WeakMap" || symbol?.name === "WeakSet") &&
+ ((symbol !== null && ts.symbolName(symbol) === "WeakMap") ||
+ (symbol !== null && ts.symbolName(symbol) === "WeakSet")) &&
lowerer.isStdlibSymbol(symbol)
) {
const args = expr.arguments?.nodes ?? [];
@@ -9720,13 +10055,13 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
lowerer.unsupported(
"SC1090",
expr,
- `${symbol.name} construction with surplus or spread arguments`,
+ `${ts.symbolName(symbol)} construction with surplus or spread arguments`,
);
}
return {
kind: "libCall",
- fn: symbol.name === "WeakMap" ? "weakMap.new" : "weakSet.new",
+ fn: ts.symbolName(symbol) === "WeakMap" ? "weakMap.new" : "weakSet.new",
args: [args[0] ? lowerer.lowerExprExpecting(args[0], DYN) : dynUndefinedExpr(loc)],
type: DYN,
loc,
@@ -9745,7 +10080,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
}
if (
!mapped &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(expr.arguments?.nodes.length ?? 0) <= 1 &&
!expr.arguments?.nodes.some(ts.isSpreadElement)
) {
@@ -9768,7 +10103,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// the honest construction is a Set of those scalars.
if (
!mapped &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
(expr.arguments?.nodes.length ?? 0) === 1 &&
ts.isArrayLiteralExpression(expr.arguments!.nodes[0]!) &&
!(expr.arguments!.nodes[0] as ts.ArrayLiteralExpression).elements.nodes.some(
@@ -9811,7 +10146,11 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// `new AsyncLocalStorage()` (node:async_hooks): a fresh store id —
// an f64 handle (type-mapper.ts), the Channel story. Construction options
// ({ defaultValue, name }) have no lowering yet.
- if (symbol?.name === "AsyncLocalStorage" && lowerer.isStdlibSymbol(symbol)) {
+ if (
+ symbol !== null &&
+ ts.symbolName(symbol) === "AsyncLocalStorage" &&
+ lowerer.isStdlibSymbol(symbol)
+ ) {
if ((expr.arguments?.nodes.length ?? 0) > 0) {
lowerer.noLowering(
"new AsyncLocalStorage(options)",
@@ -9822,7 +10161,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
return { kind: "libCall", fn: "als.new", args: [], type: F64, loc };
}
// `new Promise((resolve) => ...)`: the ambient Promise constructor.
- if (symbol?.name === "Promise" && lowerer.isStdlibSymbol(symbol)) {
+ if (symbol !== null && ts.symbolName(symbol) === "Promise" && lowerer.isStdlibSymbol(symbol)) {
const type = lowerer.irTypeOf(expr);
if (type.kind !== "promise") lowerer.badType(expr, lowerer.typeOf(expr));
const args = expr.arguments?.nodes ?? [];
@@ -9838,7 +10177,10 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
if (ts.isIdentifier(a0) && a0.text === "setImmediate") {
const sym = lowerer.checker.getSymbolAtLocation(a0);
const decls = sym ? sym.declarations : [];
- if (decls.length > 0 && decls.every((d) => lowerer.isStdlibFile(d.getSourceFile()))) {
+ if (
+ decls.length > 0 &&
+ decls.every((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))
+ ) {
// The settled value is the undefined dyn value — the result
// is promise whatever T the checker inferred for the
// unusual executor (Promise in the suite's shape).
@@ -9859,7 +10201,9 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// catch-side instanceof and the uncaught printer working). First
// settle wins, exactly JS: reject-after-resolve and double-reject
// are no-ops, and an executor throw after any settle is swallowed.
- const executor = lowerer.lowerExpr(args[0]!);
+ let executor = lowerer.lowerExpr(args[0]!);
+ if (executor.type.kind !== "func") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!));
+ executor = adaptPromiseExecutor(lowerer, executor, type.inner, loc) ?? executor;
if (executor.type.kind !== "func") lowerer.badType(args[0]!, lowerer.typeOf(args[0]!));
if (executor.type.params.length > 1) {
const rj = executor.type.params[1]!;
@@ -10126,6 +10470,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 ?? [],
@@ -10164,7 +10511,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
if (
def.arms.every(
(arm) =>
- arm.kind === "classval" || (isJsSourceFile(expr.getSourceFile()) && isUnitType(arm)),
+ arm.kind === "classval" ||
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) && isUnitType(arm)),
) &&
lowerer.dynConvertible(calleeT)
) {
@@ -10197,7 +10545,7 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const sym =
raw && raw.flags & ts.SymbolFlags.Alias ? lowerer.checker.getAliasedSymbol(raw) : raw;
const hint =
- sym && lowerer.isStdlibSymbol(sym) ? own(STDLIB_CTOR_HINTS, sym.name) : undefined;
+ sym && lowerer.isStdlibSymbol(sym) ? own(STDLIB_CTOR_HINTS, ts.symbolName(sym)) : undefined;
if (hint !== undefined) {
lowerer.unsupported("SC1090", expr, hint);
}
diff --git a/packages/compiler/src/frontend/lowering/lower-comptime.ts b/packages/compiler/src/frontend/lowering/lower-comptime.ts
index 41c6704c5..e442b34a5 100644
--- a/packages/compiler/src/frontend/lowering/lower-comptime.ts
+++ b/packages/compiler/src/frontend/lowering/lower-comptime.ts
@@ -4,7 +4,7 @@ import { InternalCompilerError } from "../../errors.js";
* literals — records/arrays/unions included, subject to comptimeBakeable. */
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
-import { type IrExpr, type IrType } from "../../ir/ir.js";
+import { isUnitType, type IrExpr, type IrType } from "../../ir/ir.js";
import { comptimeFailedDiag } from "../../diagnostics/diagnostic.js";
import { locOf } from "../program.js";
import { PoisonError } from "./lowerer.js";
@@ -76,7 +76,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr
"SC1090",
expr,
`baking comptime results of type '${lowerer.fmt(target)}' into the binary ` +
- `(only number, string, boolean, arrays, and records lower to literals)`,
+ `(number, string, boolean, and arrays, records and unions of those lower to literals; a record or union type reachable from itself does not)`,
);
}
// 5. The host executes the extracted callback under a finite budget.
@@ -86,7 +86,7 @@ export function lowerComptime(lowerer: Lowerer, expr: ts.CallExpression): IrExpr
try {
if (lowerer.frontendServices === undefined)
throw new Error("compile-time evaluation requires frontend services");
- result = lowerer.frontendServices.evaluateComptime(cb.getText(), COMPTIME_TIMEOUT_MS);
+ result = lowerer.frontendServices.evaluateComptime(ts.getTextOfNode(cb), COMPTIME_TIMEOUT_MS);
} catch (e) {
const detail = comptimeFailureDetail(e);
lowerer.pushDiag(comptimeFailedDiag(detail, locOf(cb)));
@@ -110,20 +110,104 @@ function comptimeFailureDetail(error: unknown): string {
/** True when a comptime result of this type can be written as a literal
* expression: number/string/boolean, arrays and record shapes of those,
- * nested. Closures, class instances, unions, promises, dyn, and void
- * cannot be baked. */
+ * and unions of those with null or undefined (the mixed rows of a JSON
+ * table: `(string | number)[][]`), nested. Closures, class instances,
+ * promises, dyn, void, and recursive types (a record or union reachable
+ * from itself, directly or through arrays) cannot be baked. */
export function comptimeBakeable(lowerer: Lowerer, t: IrType): boolean {
+ return bakeableWithin(lowerer, t, new Set());
+}
+
+/** comptimeBakeable below the records and unions in `open` (keyed
+ * `record:` and `union:`), whose members are being
+ * checked: reaching one of them again means the type is recursive. */
+function bakeableWithin(lowerer: Lowerer, t: IrType, open: Set): boolean {
switch (t.kind) {
case "f64":
case "string":
case "bool":
return true;
case "array":
- return lowerer.comptimeBakeable(t.elem);
+ return bakeableWithin(lowerer, t.elem, open);
case "record": {
const shape = lowerer.shapes.get(t.shapeId);
- return !!shape && shape.fields.every((f) => lowerer.comptimeBakeable(f.type));
+ const key = `record:${t.shapeId}`;
+ if (!shape || open.has(key)) return false;
+ open.add(key);
+ const bakeable = shape.fields.every((f) => bakeableWithin(lowerer, f.type, open));
+ open.delete(key);
+ return bakeable;
}
+ case "union": {
+ const arms = lowerer.unions.get(t.unionId)?.arms;
+ const key = `union:${t.unionId}`;
+ if (arms === undefined || open.has(key)) return false;
+ open.add(key);
+ const bakeable = arms.every(
+ (arm) => isUnitType(arm) || (arm.kind !== "union" && bakeableWithin(lowerer, arm, open)),
+ );
+ open.delete(key);
+ return bakeable;
+ }
+ default:
+ return false;
+ }
+}
+
+/** The tag of the arm of union `unionId` that `value` bakes into, or -1.
+ * A record value whose discriminant field holds one of the union's
+ * discriminant values takes that case's arm, since arms with overlapping
+ * shapes can both fit it. Any other value takes the first arm it fits, in
+ * the union's arm order: a JSON value fits exactly one scalar arm, and
+ * array or record arms are told apart by their contents. */
+function comptimeUnionArm(lowerer: Lowerer, unionId: string, value: unknown): number {
+ const def = lowerer.unions.get(unionId);
+ if (!def) return -1;
+ const discriminant = def.discriminant;
+ if (
+ discriminant !== undefined &&
+ typeof value === "object" &&
+ value !== null &&
+ !Array.isArray(value)
+ ) {
+ const key = (value as Record)[discriminant.field];
+ const named = discriminant.cases.find((c) => c.values.some((v) => v === key));
+ if (named !== undefined)
+ return comptimeFits(lowerer, value, def.arms[named.tag]!) ? named.tag : -1;
+ }
+ return def.arms.findIndex((arm) => comptimeFits(lowerer, value, arm));
+}
+
+/** Whether `value` bakes into the bakeable type `t` without a mismatch:
+ * the arm choice of a union literal. Records are exact, as in
+ * comptimeValueToIr; an absent field reads as undefined. */
+function comptimeFits(lowerer: Lowerer, value: unknown, t: IrType): boolean {
+ switch (t.kind) {
+ case "f64":
+ return typeof value === "number" && Number.isFinite(value);
+ case "string":
+ return typeof value === "string";
+ case "bool":
+ return typeof value === "boolean";
+ case "nullT":
+ return value === null;
+ case "undefinedT":
+ return value === undefined;
+ case "array":
+ return Array.isArray(value) && value.every((el) => comptimeFits(lowerer, el, t.elem));
+ case "record": {
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
+ const shape = lowerer.shapes.get(t.shapeId);
+ if (!shape) return false;
+ const obj = value as Record;
+ const declared = new Set(shape.fields.map((f) => f.name));
+ return (
+ Object.keys(obj).every((key) => declared.has(key)) &&
+ shape.fields.every((f) => comptimeFits(lowerer, obj[f.name], f.type))
+ );
+ }
+ case "union":
+ return comptimeUnionArm(lowerer, t.unionId, value) >= 0;
default:
return false;
}
@@ -142,7 +226,7 @@ export function rejectComptimeCaptures(
lowerer: Lowerer,
cb: ts.ArrowFunction | ts.FunctionExpression,
): void {
- const sf = cb.getSourceFile();
+ const sf = ts.getSourceFileOfNode(cb);
const inTypePosition = (node: ts.Node): boolean => {
for (let p: ts.Node | undefined = node.parent; p && p !== cb; p = p.parent) {
if (ts.isTypeNode(p)) return true;
@@ -165,7 +249,7 @@ export function rejectComptimeCaptures(
const decls = symbol ? symbol.declarations : undefined;
if (symbol && decls && decls.length > 0) {
const ownLocal = decls.every(
- (d) => d.getSourceFile() === sf && d.pos >= cb.pos && d.end <= cb.end,
+ (d) => ts.getSourceFileOfNode(d) === sf && d.pos >= cb.pos && d.end <= cb.end,
);
if (!ownLocal) {
if (node.text === "comptime" && lowerer.isStdlibSymbol(symbol)) {
@@ -265,6 +349,23 @@ export function comptimeValueToIr(
}));
return { kind: "recordLit", fields, type: expected, loc };
}
+ case "union": {
+ const arms = lowerer.unions.get(expected.unionId)!.arms; // bakeable-checked
+ const tag = comptimeUnionArm(lowerer, expected.unionId, value);
+ if (tag < 0) fail(describeComptimeValue(value));
+ const arm = arms[tag]!;
+ const payload: IrExpr = isUnitType(arm)
+ ? { kind: "unitLit", unit: arm.kind === "nullT" ? "null" : "undefined", type: arm, loc }
+ : lowerer.comptimeValueToIr(value, arm, path, blame);
+ return {
+ kind: "unionWrap",
+ unionId: expected.unionId,
+ tag,
+ value: payload,
+ type: expected,
+ loc,
+ };
+ }
default:
// comptimeBakeable already fenced these off.
throw new InternalCompilerError(`lowerer bug: unbakeable comptime target '${expected.kind}'`);
diff --git a/packages/compiler/src/frontend/lowering/lower-console.ts b/packages/compiler/src/frontend/lowering/lower-console.ts
index a7fac4f51..eec308342 100644
--- a/packages/compiler/src/frontend/lowering/lower-console.ts
+++ b/packages/compiler/src/frontend/lowering/lower-console.ts
@@ -209,7 +209,7 @@ export function lowerConsoleInstanceCall(
if (access.questionDotToken) return null;
const recvType = lowerer.typeOf(access.expression);
const sym = recvType.getAliasSymbol() ?? recvType.getSymbol();
- if (!sym || sym.name !== "Console" || !lowerer.isStdlibSymbol(sym)) return null;
+ if (!sym || ts.symbolName(sym) !== "Console" || !lowerer.isStdlibSymbol(sym)) return null;
const toStderr = OUTPUT_MEMBERS.get(access.name.text);
if (toStderr === undefined) {
lowerer.noLowering(
diff --git a/packages/compiler/src/frontend/lowering/lower-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts
index 54f9d158b..4cb238352 100644
--- a/packages/compiler/src/frontend/lowering/lower-dgram.ts
+++ b/packages/compiler/src/frontend/lowering/lower-dgram.ts
@@ -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") {
@@ -193,7 +195,7 @@ export function lowerDgramDnsModuleCall(
sawType = true;
} else if (name === "reuseAddr") {
reuseAddr = lowerer.lowerExprExpecting(prop.initializer, BOOL);
- } else if (name === "signal" && isJsSourceFile(expr.getSourceFile())) {
+ } else if (name === "signal" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
// A provably-non-AbortSignal signal (the invalid-input probes:
// strings, numbers, plain records) throws Node's
// validateAbortSignal ladder; plausible signal values keep the
@@ -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) 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 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 => {
@@ -449,7 +578,7 @@ export function lowerDgramMethodCall(
return { kind: "libCall", fn, args: [receiver, data, port, host], type: VOID, loc };
}
if (
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
args.length <= 5 &&
!args.some(ts.isSpreadElement)
) {
diff --git a/packages/compiler/src/frontend/lowering/lower-enums.ts b/packages/compiler/src/frontend/lowering/lower-enums.ts
index 159d72f32..e72dd2780 100644
--- a/packages/compiler/src/frontend/lowering/lower-enums.ts
+++ b/packages/compiler/src/frontend/lowering/lower-enums.ts
@@ -29,20 +29,21 @@
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). */
function memberNameText(member: ts.EnumMember): string {
const n = member.name;
- return ts.isIdentifier(n) || ts.isStringLiteralLike(n) ? n.text : n.getText();
+ return ts.isIdentifier(n) || ts.isStringLiteralLike(n) ? n.text : ts.getTextOfNode(n);
}
/** Ambient: a `declare`d declaration, or one living in a .d.ts. */
function isAmbientEnumDecl(decl: ts.EnumDeclaration): boolean {
return (
(ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Ambient) !== 0 ||
- decl.getSourceFile().isDeclarationFile
+ ts.getSourceFileOfNode(decl).isDeclarationFile
);
}
@@ -125,7 +126,7 @@ function lowerEnumMemberRead(
// runtime values, so SC2020-family member fences own those. A user's
// `declare enum` follows the
// `declare const __VERSION__` stance: ReferenceError at the access.
- const sf = enumDecl.getSourceFile();
+ const sf = ts.getSourceFileOfNode(enumDecl);
if (lowerer.isStdlibFile(sf)) return null;
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (!declared || declared.kind === "void") return null;
@@ -164,10 +165,10 @@ function lowerEnumReverseRead(
key: ts.Expression,
): IrExpr | null {
const loc = locOf(expr);
- const enumName = recvSym.name;
+ const enumName = ts.symbolName(recvSym);
// Ambient: no object exists — the member-read stance.
if (decls.every((d) => isAmbientEnumDecl(d))) {
- if (decls.some((d) => lowerer.isStdlibFile(d.getSourceFile()))) {
+ if (decls.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))) {
return null;
}
return {
@@ -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();
+ const reverse = new Set();
+ 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 {
diff --git a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
index 06e9c4ab1..f8588fe48 100644
--- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
+++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
@@ -72,6 +72,8 @@ import { STREAM_FORCED_EVENT_NAMES, streamForcedTuple, streamSidesOf } from "./l
import { boolLit, strLit, varRef } from "../../ir/build.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { lowerAddAbortListener } from "./builtin-calls.js";
+import { requestGeneralStreamSurface } from "./general-streams.js";
+import { nodeStreamInterfaceSide } from "./dyn-streams.js";
/** True when the class descends from (or is) the runtime emitter. */
export function emitterRooted(lowerer: Lowerer, info: ClassInfo | undefined | null): boolean {
@@ -412,7 +414,11 @@ function isErrorMonitorRef(lowerer: Lowerer, arg: ts.Expression): boolean {
const isMonitorType = (t: ts.Type): boolean => {
if (!(t.flags & ts.TypeFlags.UniqueESSymbol)) return false;
const symbol = t.getSymbol();
- return symbol?.name === "errorMonitor" && lowerer.isStdlibSymbol(symbol);
+ return (
+ symbol !== undefined &&
+ ts.symbolName(symbol) === "errorMonitor" &&
+ lowerer.isStdlibSymbol(symbol)
+ );
};
// The unique symbol TYPE names its declaration through any alias.
if (isMonitorType(lowerer.typeOf(arg))) return true;
@@ -1245,7 +1251,19 @@ export function lowerEmitterMethodCall(
);
}
const p = cbT.params[0];
- if (p !== undefined && !(p.kind === "bytes" && p.elem === "u8") && p.kind !== "string") {
+ // A `Buffer | string` chunk (NodeJS.ReadableStream's listener
+ // shape) takes whichever side the stream delivers.
+ const chunkUnion =
+ p?.kind === "union" &&
+ (lowerer.unions.get(p.unionId)?.arms ?? []).every(
+ (arm) => (arm.kind === "bytes" && arm.elem === "u8") || arm.kind === "string",
+ );
+ if (
+ p !== undefined &&
+ !(p.kind === "bytes" && p.elem === "u8") &&
+ p.kind !== "string" &&
+ !chunkUnion
+ ) {
lowerer.noLowering(
`a 'data' listener whose chunk parameter is '${lowerer.fmt(p)}'`,
args[1]!,
@@ -1683,7 +1701,7 @@ export function emitOverrideShapeReason(
const p0T = lowerer.mapTypeOf(lowerer.typeOf(p0.name));
const jsUnannotated =
p0.type === undefined &&
- isJsSourceFile(member.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(member)) &&
(p0T === null || p0T.kind === "dyn");
if (p0T?.kind !== "string" && !jsUnannotated) {
return "the event parameter must be typed 'string' (symbol event names have no lowering)";
@@ -1717,9 +1735,9 @@ export function emitOverrideShapeReason(
"the event parameter cannot be reassigned (the forward's event name must stay pinned)";
}
}
- n.forEachChild(visit);
+ ts.forEachChild(n, visit);
};
- member.body.forEachChild(visit);
+ ts.forEachChild(member.body, visit);
return reason;
}
@@ -2192,36 +2210,90 @@ function eventsOnceBoxer(
return { kind: "closure", fnName: name, captures: [], type: fnType, loc };
}
-/** events.once(emitter, name): a promise of the next `name` event's
- * arguments as an array, rejected by an earlier 'error' event (unless the
- * awaited event is 'error'). The emitter's listeners are typed by the
- * program-wide event table, so the runtime registers one-shot typed
- * listeners whose compiler-built boxers turn the event's tuple into the
- * array. EventTarget and stream receivers, computed names and options are
- * refused. */
+/** The `signal` of an events.once options object literal (a checked-
+ * dynamic AbortSignal or undefined), or null when the literal names none.
+ * Other options forms are refused. */
+function eventsOnceSignal(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
+ if (!ts.isObjectLiteralExpression(node)) {
+ lowerer.noLowering(
+ "events.once options that are not an object literal",
+ node,
+ "pass the options as an object literal: { signal }",
+ );
+ return null;
+ }
+ let signal: IrExpr | null = null;
+ for (const prop of node.properties.nodes) {
+ const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null;
+ if (
+ name === "signal" &&
+ signal === null &&
+ (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop))
+ ) {
+ signal = lowerer.lowerExprExpecting(
+ ts.isPropertyAssignment(prop) ? prop.initializer : (prop.name as ts.Identifier),
+ DYN,
+ );
+ } else {
+ lowerer.noLowering(
+ "events.once options other than one signal",
+ prop,
+ "the lowered options object takes signal: AbortSignal",
+ );
+ }
+ }
+ return signal;
+}
+
+/** events.once(emitter, name[, { signal }]): a promise of the next `name`
+ * event's arguments as an array, rejected by an earlier 'error' event
+ * (unless the awaited event is 'error') or by the signal's abort with an
+ * AbortError. The emitter's listeners are typed by the program-wide event
+ * table (a stream's runtime-emitted events by their fixed tuples), so the
+ * runtime registers one-shot typed listeners whose compiler-built boxers
+ * turn the event's tuple into the array. EventTarget receivers, a stream's
+ * 'data' event, computed names and options other than a literal { signal }
+ * are refused. */
export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr {
const args = call.arguments.nodes;
- if (args.length < 2 || args.some(ts.isSpreadElement)) {
- lowerer.noLowering("events.once without an emitter and an event name", call);
- }
- if (args.length > 2) {
- lowerer.noLowering(
- "events.once with options",
- args[2]!,
- "the lowered form is events.once(emitter, name); AbortSignal options are not supported yet",
- );
+ if (args.length < 2 || args.length > 3 || args.some(ts.isSpreadElement)) {
+ lowerer.noLowering("events.once without an emitter, an event name and options", call);
}
// Node's Promise: the arguments array of checked-dynamic values.
const arrayT = arrayOf(DYN);
const resultT: IrType = { kind: "promise", inner: arrayT };
- const receiver = lowerer.lowerExpr(args[0]!);
+ let receiver = lowerer.lowerExpr(args[0]!);
+ // A checked-dynamic NodeJS stream slot (dyn-streams.ts): a native stream
+ // unboxes by identity; any other value throws the not-supported Error.
+ if (
+ receiver.type.kind === "dyn" &&
+ lowerer.dynNodeStreams &&
+ nodeStreamInterfaceSide(lowerer, lowerer.typeOf(args[0]!)) !== null
+ ) {
+ receiver = {
+ kind: "libCall",
+ fn: "stream.unboxDyn",
+ args: [
+ receiver,
+ strLit(
+ "events.once over stream objects other than native streams is not supported yet",
+ loc,
+ ),
+ ],
+ type: { kind: "object", className: "%Duplex" },
+ loc,
+ };
+ }
const info =
receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined;
- if (!emitterRooted(lowerer, info) || streamSidesOf(lowerer, info) !== null) {
+ if (!emitterRooted(lowerer, info)) {
+ // A NodeJS stream interface slot that holds a stdio handle here: the
+ // program lowers again with the runtime stream classes.
+ requestGeneralStreamSurface(lowerer, args[0]!);
lowerer.noLowering(
"events.once over this value",
args[0]!,
- "the emitter must be an EventEmitter instance; EventTarget and stream receivers are not supported yet",
+ "the emitter must be an EventEmitter or stream instance; EventTarget receivers are not supported yet",
);
}
const name = literalEventName(lowerer, args[1]!);
@@ -2233,6 +2305,14 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc:
);
}
const event = name ?? "";
+ const sides = streamSidesOf(lowerer, info);
+ if ((sides === "r" || sides === "rw") && (event === "data" || event === "keypress")) {
+ lowerer.noLowering(
+ `events.once of a stream's '${event}' event`,
+ args[1]!,
+ `the chunk has no checked-dynamic boxing here; register stream.once('${event}', listener) instead`,
+ );
+ }
const table = emitterEvents(lowerer);
if (event !== "error" && flexibleJsEvent(event, table.get(event))) {
lowerer.noLowering(
@@ -2240,14 +2320,22 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc:
args[1]!,
);
}
- const tuple = tupleOf(lowerer, table, event, call);
+ const tuple = streamForcedTuple(lowerer, info, event) ?? tupleOf(lowerer, table, event, call);
const errorTuple: IrType[] = [{ kind: "object", className: "%Error" }];
const boxer = eventsOnceBoxer(lowerer, call, tuple, arrayT, loc);
const errorBoxer = eventsOnceBoxer(lowerer, call, errorTuple, arrayT, loc);
+ const signal = args.length > 2 ? eventsOnceSignal(lowerer, args[2]!) : null;
+ const common = [
+ receiver,
+ eventNameExpr(event, loc),
+ boxer,
+ errorBoxer,
+ boolLit(event !== "error", loc),
+ ];
return {
kind: "libCall",
- fn: "events.once",
- args: [receiver, eventNameExpr(event, loc), boxer, errorBoxer, boolLit(event !== "error", loc)],
+ fn: signal === null ? "events.once" : "events.onceSignal",
+ args: signal === null ? common : [...common, signal],
type: resultT,
loc,
};
diff --git a/packages/compiler/src/frontend/lowering/lower-expando.ts b/packages/compiler/src/frontend/lowering/lower-expando.ts
index 5ed97d4d7..6adce8f67 100644
--- a/packages/compiler/src/frontend/lowering/lower-expando.ts
+++ b/packages/compiler/src/frontend/lowering/lower-expando.ts
@@ -98,8 +98,8 @@ function expandoFnSymbolOf(lowerer: Lowerer, recv: ts.Expression): ts.Symbol | n
const sym = lowerer.resolveValueSymbol(recv);
if (!sym) return null;
const decl = sym.valueDeclaration;
- if (!decl || decl.getSourceFile().isDeclarationFile) return null;
- if (isJsSourceFile(decl.getSourceFile())) return null;
+ if (!decl || ts.getSourceFileOfNode(decl).isDeclarationFile) return null;
+ if (isJsSourceFile(ts.getSourceFileOfNode(decl))) return null;
if (ts.isFunctionDeclaration(decl)) {
return ts.isSourceFile(decl.parent) ? sym : null;
}
@@ -177,7 +177,7 @@ export function collectExpandoMembers(lowerer: Lowerer, sf: ts.SourceFile): void
}
const existing = memberOf(members, w.key);
if (existing) {
- existing.firstWriteStart = Math.min(existing.firstWriteStart, node.getStart());
+ existing.firstWriteStart = Math.min(existing.firstWriteStart, ts.tokenStartOfNode(node));
} else if (
!(typeof w.key === "string" && READONLY_FN_MEMBERS.has(w.key)) &&
!nsOwnedMember(lowerer, w.access)
@@ -198,18 +198,22 @@ export function collectExpandoMembers(lowerer: Lowerer, sf: ts.SourceFile): void
}
if (type?.kind === "void" && isUnitOnlyTsType(tsType)) type = unitOnlyUnion(lowerer.unions);
if (type && type.kind !== "void") {
- const fnName = w.fnSym.name;
+ const fnName = ts.symbolName(w.fnSym);
const memberName =
typeof w.key === "string"
? w.key
- : `sym%${w.key.name}%${w.key.declarations[0]?.getStart() ?? 0}`;
+ : `sym%${ts.symbolName(w.key)}%${(w.key.declarations.length === 0 ? undefined : ts.tokenStartOfNode(w.key.declarations[0]!)) ?? 0}`;
const g: IrGlobal = {
id: `%g.${tag}${fnName}%.${memberName}`,
name: `${fnName}.${memberName}`,
type,
mutable: true,
};
- const entry: ExpandoMember = { global: g, firstWriteStart: node.getStart(), file: sf };
+ const entry: ExpandoMember = {
+ global: g,
+ firstWriteStart: ts.tokenStartOfNode(node),
+ file: sf,
+ };
if (typeof w.key === "string") members.names.set(w.key, entry);
else members.symbols.set(w.key, entry);
lowerer.globalsList.push(g);
@@ -248,7 +252,7 @@ function nsOwnedMember(
if (ts.isModuleDeclaration(p)) {
return !(
p.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.DeclareKeyword) ||
- p.getSourceFile().isDeclarationFile
+ ts.getSourceFileOfNode(p).isDeclarationFile
);
}
}
@@ -308,11 +312,11 @@ export function expandoMemberRead(
const member = memberOf(lowerer.expandoMembers.get(fnSym), key);
if (!member) return null;
if (
- access.getSourceFile() === member.file &&
+ ts.getSourceFileOfNode(access) === member.file &&
initExecutingPosition(access) &&
- access.getStart() < member.firstWriteStart
+ ts.tokenStartOfNode(access) < member.firstWriteStart
) {
- const name = typeof key === "string" ? key : key.name;
+ const name = typeof key === "string" ? key : ts.symbolName(key);
lowerer.unsupported(
"SC1090",
access,
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 6cbe5d7a4..7789f8a86 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -1,4 +1,4 @@
-import { refuseWebAssemblyUse } from "./builtins/webassembly.js";
+import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js";
import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
/* Expression lowering: the expression dispatch (lowerExpr), literals
@@ -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,
@@ -141,6 +151,21 @@ export type FieldTarget =
// (the write-slot type; reads arm it with undefined under
// noUncheckedIndexedAccess in fieldGetExpr).
| { container: "recordOvf"; obj: IrExpr; shapeId: string; field: string; fieldType: IrType }
+ // A computed key of a record in a read-modify-write (`r[k] += v`,
+ // `r[k]++`, `r[k] ??= v`; keyed-field-targets.ts): `keys` lists the
+ // declared fields a literal-union key can name, which share fieldType;
+ // null keys mean a string key into a pure index-signature record, whose
+ // entries fieldType types. `key` is null when the one literal is static.
+ | {
+ container: "recordKeyed";
+ obj: IrExpr;
+ shapeId: string;
+ field: string;
+ keys: readonly string[] | null;
+ key: IrExpr | null;
+ fieldType: IrType;
+ missNaN?: true;
+ }
| { container: "accessor"; obj: IrExpr; className: string; field: string; fieldType: IrType }
// A RECORD accessor property (an object-literal `get x()`/`set x(v)` —
// the shape carries %get:/%set: closure slots): reads call the getter
@@ -168,9 +193,9 @@ export function templateRawTextOf(
): string {
const own = (node as { rawText?: string }).rawText;
if (own !== undefined) return own;
- const sf = node.getSourceFile();
- const start = node.getStart(sf);
- const end = node.getEnd();
+ const sf = ts.getSourceFileOfNode(node);
+ const start = ts.tokenStartOfNode(node);
+ const end = node.end;
const tailTrim =
node.kind === ts.SyntaxKind.TemplateHead || node.kind === ts.SyntaxKind.TemplateMiddle ? 2 : 1;
return sf.text.slice(start + 1, end - tailTrim);
@@ -253,7 +278,10 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
if (externalTypeSpecifier !== null) {
lowerer.externalHostFence(externalTypeSpecifier, expr);
}
- // Every reached use of the WebAssembly API fences at its expression.
+ // WebAssembly API values lower; every other reached use of the API
+ // fences at its expression.
+ const webAssemblyValue = lowerWebAssemblyValue(lowerer, expr);
+ if (webAssemblyValue !== null) return webAssemblyValue;
refuseWebAssemblyUse(lowerer, expr);
if (ts.isNumericLiteral(expr)) {
@@ -268,14 +296,14 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// integer within ±(2^53−1)) and non-integer spellings carry
// nothing, so the IR is unchanged for programs that held their
// numbers.
- const spelled = expr.getText().replace(/_/g, "");
+ const spelled = ts.getTextOfNode(expr).replace(/_/g, "");
if (/^\d+$/.test(spelled) && String(Number(spelled)) !== spelled) {
return { kind: "numLit", value: Number(spelled), spelling: spelled, type: F64, loc };
}
return { kind: "numLit", value, type: F64, loc };
}
if (ts.isBigIntLiteral(expr)) {
- const spelling = expr.getText().replace(/_/g, "").replace(/n$/i, "");
+ const spelling = ts.getTextOfNode(expr).replace(/_/g, "").replace(/n$/i, "");
return {
kind: "libCall",
fn: "bigint.parse",
@@ -456,7 +484,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// Only arrows can see an enclosing method's `this` (JS lexical-this);
// `this` in plain nested functions is already a tsc error under
// noImplicitThis, so the generic scope walk here is preflight-safe.
- const local = lowerer.resolveThis();
+ const local = lowerer.resolveThis(expr);
if (local) {
const value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc };
// `this.#x` in a function-local class's prototype method, whose `this`
@@ -521,7 +549,7 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// the read answers the strict-mode plain-call undefined, the old
// constant. TypeScript keeps the fence (noImplicitThis makes it a
// compile-time story there).
- if (isJsSourceFile(expr.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
// Module scope of a CommonJS file: `this` is module.exports.
const exportsThis = lowerCjsTopLevelThis(lowerer, expr);
if (exportsThis) return exportsThis;
@@ -1029,6 +1057,15 @@ export function nullishBranches(
export function lowerCondition(lowerer: Lowerer, expr: ts.Expression): IrExpr {
let e: ts.Expression = expr;
while (ts.isParenthesizedExpression(e)) e = e.expression;
+ // `if (Object.setPrototypeOf)`: the feature test of transpiled Error
+ // subclasses' prototype reset. Node always has it.
+ if (
+ ts.isPropertyAccessExpression(e) &&
+ !e.questionDotToken &&
+ e.name.text === "setPrototypeOf" &&
+ lowerer.isStdlibGlobal(e.expression, "Object")
+ )
+ return { kind: "boolLit", value: true, type: BOOL, loc: locOf(e) };
if (ts.isPropertyAccessExpression(e)) {
const presence = lowerPromiseThenPresence(lowerer, e, {
kind: "boolLit",
@@ -1511,7 +1548,7 @@ export function lowerAbsenceProbe(lowerer: Lowerer, node: ts.Expression): IrExpr
// statement ruled non-null without narrowing it (jose's secs()).
if (
receiverT?.kind === "union" &&
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
!expr.questionDotToken &&
!hasOptionalChainGuard(expr.expression)
) {
@@ -1701,7 +1738,8 @@ export function stringWrapperToString(lowerer: Lowerer, node: ts.Expression): Ir
while (ts.isParenthesizedExpression(e)) e = e.expression;
if (!ts.isNewExpression(e) || !ts.isIdentifier(e.expression)) return null;
const symbol = lowerer.resolveValueSymbol(e.expression);
- if (symbol?.name !== "String" || !lowerer.isStdlibSymbol(symbol)) return null;
+ if (symbol === null || ts.symbolName(symbol) !== "String" || !lowerer.isStdlibSymbol(symbol))
+ return null;
const args = e.arguments?.nodes ?? [];
if (args.length > 1 || args.some(ts.isSpreadElement)) return null;
if (args.length === 0) return { kind: "strLit", value: "", type: STRING, loc: locOf(e) };
@@ -1990,7 +2028,7 @@ export function aliasTypeofNarrows(
const leaf = aliasTypeofArmOf(lowerer, e, strip);
if (leaf) out.push(leaf);
};
- if (implicitMonoFile(cond.getSourceFile())) walk(cond, polarity);
+ if (implicitMonoFile(ts.getSourceFileOfNode(cond))) walk(cond, polarity);
return out;
}
@@ -2057,6 +2095,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();
+ 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(ts.getSourceFileOfNode(getter)).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,
@@ -2133,7 +2267,25 @@ export function caughtRead(
// Unnarrowed reads cross into unknown. The caught-value dispatch pass
// retains known native class payloads through live identity capsules;
// other payload kinds use the runtime's ordinary conversion.
- if (narrowed?.kind === "dyn") {
+ // `catch (e: any)` (or useUnknownInCatchVariables off) reads the same way.
+ if (
+ narrowed?.kind === "dyn" ||
+ (narrowed === null && (lowerer.typeOf(node).flags & ts.TypeFlags.Any) !== 0)
+ ) {
+ return { kind: "caughtToDyn", value: ref, type: DYN, loc };
+ }
+
+ // A binding a user type predicate (or an assertion) narrowed to a
+ // structural type (`isErrorLike(e)` → `{ message: string; … }`, an
+ // intersection like `TypeError & { code: unknown }`): the thrown value is
+ // still whatever was thrown, so it crosses as a checked-dynamic value and
+ // each member read validates its own result, as JavaScript reads it.
+ const checked = lowerer.typeOf(node);
+ if (
+ narrowed?.kind === "record" ||
+ narrowed?.kind === "union" ||
+ (narrowed === null && (checked.flags & (ts.TypeFlags.Object | ts.TypeFlags.Intersection)) !== 0)
+ ) {
return { kind: "caughtToDyn", value: ref, type: DYN, loc };
}
lowerer.unsupported("SC1063", node);
@@ -2425,7 +2577,7 @@ export function lowerOptionalArrayIndexRead(
key: IrExpr,
): IrExpr | null {
if (
- !isJsSourceFile(expr.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(expr)) ||
expr.questionDotToken ||
hasOptionalChainGuard(expr.expression)
)
@@ -2588,6 +2740,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 +2789,9 @@ export function lowerPromiseRejectCall(
// A maybe-promise context (`return Promise.reject(e)` from a function
// returning `T | Promise`) 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
diff --git a/packages/compiler/src/frontend/lowering/lower-functions.ts b/packages/compiler/src/frontend/lowering/lower-functions.ts
index 0ef601672..bec8528c4 100644
--- a/packages/compiler/src/frontend/lowering/lower-functions.ts
+++ b/packages/compiler/src/frontend/lowering/lower-functions.ts
@@ -1,3 +1,4 @@
+import { declareTypedArguments } from "./typed-arguments.js";
import { varRef } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -129,7 +130,7 @@ function declareFunctionArguments(
lowerer.ctx.argumentsLocal = local;
}
if (
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
!ts.isArrowFunction(node) &&
shapes.length === 1 &&
shapes[0]?.mode === "dynRest" &&
@@ -203,7 +204,7 @@ export function lowerLambda(
// Fluent methods must return that receiver without a class cast.
if (
ts.isMethodDeclaration(node) &&
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
!node.type &&
!node.asteriskToken &&
!hasExplicitJsDocReturn(node) &&
@@ -214,6 +215,19 @@ export function lowerLambda(
ret: funcType.ret.kind === "promise" ? { ...funcType.ret, inner: DYN } : DYN,
};
}
+ // An ASYNC lambda whose signature answered a bare checked-dynamic result
+ // (`retry(async () => loose())` against a library slot typed
+ // `(...) => A | Promise` with A = any — the union collapses to `any`):
+ // calling it still produces a promise, so the value's result is
+ // Promise, the payload the dyn the body returns.
+ if (
+ funcType.ret.kind === "dyn" &&
+ !ts.isAccessor(node) &&
+ node.asteriskToken === undefined &&
+ node.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) === true
+ ) {
+ funcType = { ...funcType, ret: { kind: "promise", inner: DYN } };
+ }
// A lambda IS a value: the completed-ABI rule applies at birth. The
// contextual (target) type decides — `(x?: number) => void` may flow
// into a slot annotated `(x: number | undefined) => void` (same ABI
@@ -222,7 +236,7 @@ export function lowerLambda(
if (
!(
ts.isMethodDeclaration(node) &&
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
canBoxFuncIntoDyn(
funcType,
(id) => lowerer.shapes.get(id),
@@ -273,14 +287,14 @@ export function lowerLambda(
if (lowerer.methodLambdasInProgress.has(node))
lowerer.methodLambdasInProgress.set(node, { fnName, type: funcType, selfReads: 0 });
const inferredSignature =
- isJsSourceFile(node.getSourceFile()) && !node.type
+ isJsSourceFile(ts.getSourceFileOfNode(node)) && !node.type
? lowerer.checker.getSignatureFromDeclaration(node)
: undefined;
const returnsConstructor =
inferredSignature &&
producesConstructor(lowerer, lowerer.checker.getReturnTypeOfSignature(inferredSignature));
if (
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
!node.type &&
!isAsync &&
!isGenerator &&
@@ -302,13 +316,15 @@ export function lowerLambda(
try {
const receiver =
(checkedReceiver ||
- isJsSourceFile(node.getSourceFile()) ||
+ isJsSourceFile(ts.getSourceFileOfNode(node)) ||
node.parameters.nodes.some(isThisParameter)) &&
!ts.isArrowFunction(node)
? declareCallReceiver(lowerer, node, isGenerator)
: null;
const { params, prologue } = lowerer.declareParams(node.parameters.nodes, shapes);
if (receiver) prologue.unshift(receiver);
+ if (ts.isFunctionExpression(node) && lowerer.typedArgumentsLambdas.has(node))
+ declareTypedArguments(lowerer, node, shapes, params, prologue);
declareFunctionArguments(
lowerer,
node,
@@ -445,7 +461,7 @@ export function lowerLambda(
// position. ICEs (SC9001) stay compile errors, exactly like
// lowerStmts; probe mode (diagSink) keeps the poison.
if (!(e instanceof PoisonError)) throw e;
- if (!isJsSourceFile(node.getSourceFile())) throw e;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(node))) throw e;
const params: IrParam[] = funcType.params.map((t, i) => ({
localId: `%pf${i}`,
name: `%pf${i}`,
@@ -494,7 +510,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
lowerer.fnStack.push(ctx);
try {
const receiver =
- decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(decl.getSourceFile())
+ decl.parameters.nodes.some(isThisParameter) || isJsSourceFile(ts.getSourceFileOfNode(decl))
? declareCallReceiver(lowerer, decl, sig.generator !== undefined)
: null;
const { params, prologue } = lowerer.declareParams(decl.parameters.nodes, sig.params);
@@ -547,7 +563,7 @@ export function lowerFunction(lowerer: Lowerer, decl: ts.FunctionDeclaration): I
// at the declaration's position — so a reachable-but-broken
// signature stops the RUN at its own site instead of the build.
// ICEs (SC9001) stay compile errors, exactly like lowerStmts.
- if (isJsSourceFile(decl.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(decl))) {
// An ABI type naming a class that never REGISTERED (the sentence-
// walker idiom's path type — the #private fence) is fine to emit:
// callers CAN lower calls to this symbol (a same-typed param
diff --git a/packages/compiler/src/frontend/lowering/lower-generators.ts b/packages/compiler/src/frontend/lowering/lower-generators.ts
index b1d0a1aa5..19a75a0b4 100644
--- a/packages/compiler/src/frontend/lowering/lower-generators.ts
+++ b/packages/compiler/src/frontend/lowering/lower-generators.ts
@@ -207,7 +207,7 @@ function isGeneratorMember(lowerer: Lowerer, access: ts.PropertyAccessExpression
(d) =>
ts.isInterfaceDeclaration(d.parent) &&
(d.parent.name.text === "Generator" || d.parent.name.text === "AsyncGenerator") &&
- lowerer.isStdlibFile(d.getSourceFile()),
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
)
);
}
@@ -272,7 +272,7 @@ export function isSettledIteratorValueRead(
ts.isInterfaceDeclaration(d.parent) &&
(d.parent.name.text === "IteratorYieldResult" ||
d.parent.name.text === "IteratorReturnResult") &&
- lowerer.isStdlibFile(d.getSourceFile()),
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
)
)
return false;
@@ -282,13 +282,13 @@ export function isSettledIteratorValueRead(
sym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d)) &&
- lowerer.isStdlibFile(d.getSourceFile()),
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
);
const alias = receiver.getAliasSymbol();
const symbol = receiver.getSymbol();
if (
- !(alias?.name === "IteratorResult" && stdlib(alias)) &&
- !(symbol?.name === "IteratorReturnResult" && stdlib(symbol))
+ !(alias !== undefined && ts.symbolName(alias) === "IteratorResult" && stdlib(alias)) &&
+ !(symbol !== undefined && ts.symbolName(symbol) === "IteratorReturnResult" && stdlib(symbol))
)
return false;
const checked = lowerer.mapTypeOf(lowerer.typeOf(node));
diff --git a/packages/compiler/src/frontend/lowering/lower-global-value.ts b/packages/compiler/src/frontend/lowering/lower-global-value.ts
index a5f516e86..4b8e7b10a 100644
--- a/packages/compiler/src/frontend/lowering/lower-global-value.ts
+++ b/packages/compiler/src/frontend/lowering/lower-global-value.ts
@@ -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) => ts.symbolName(property))
+ .filter((name) => name !== TEMPORAL_GLOBAL);
return {
kind: "libCall",
fn: "global.native",
diff --git a/packages/compiler/src/frontend/lowering/lower-inspect.ts b/packages/compiler/src/frontend/lowering/lower-inspect.ts
index 3be155402..a8657f978 100644
--- a/packages/compiler/src/frontend/lowering/lower-inspect.ts
+++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js";
+import { builtinClassDynValueOf, builtinClassKindOf } from "./builtins/builtin-class-dyn.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -492,7 +493,7 @@ function classInspectsThroughView(lowerer: Lowerer, info: ClassInfo | undefined)
if (info.subclasses.length > 0) return true;
return (
info.decl != null &&
- isJsSourceFile(info.decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
classDynViewSupported(
info.def.fields,
(id) => lowerer.shapes.get(id),
@@ -2609,8 +2610,15 @@ function lowerUtilCallableCall(
const arity = member === "deprecate" ? 4 : member === "callbackify" ? 1 : 2;
// Program classes keep static prototypes: there is no function object
// whose prototype chain the runtime could relink or wrap.
+ // EventEmitter and the stream classes are the exception as superCtor:
+ // the runtime has untyped constructors for them (builtin-class-dyn.ts).
const classArgs = member === "inherits" ? 2 : member === "deprecate" ? 1 : 0;
- for (const arg of expr.arguments.nodes.slice(0, classArgs)) {
+ const builtinBase =
+ member === "inherits" && expr.arguments.nodes[1] !== undefined
+ ? builtinClassKindOf(lowerer, expr.arguments.nodes[1])
+ : null;
+ for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) {
+ if (index === 1 && builtinBase !== null) continue;
if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval")
lowerer.noLowering(
`util.${member} over a class constructor`,
@@ -2629,9 +2637,12 @@ function lowerUtilCallableCall(
const stmts: IrStmt[] = [];
const args: IrExpr[] = [];
for (let i = 0; i < arity; i++) {
- const input: IrExpr = expr.arguments.nodes[i]
- ? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN)
- : dynUndefinedExpr(loc);
+ const node = expr.arguments.nodes[i];
+ const input: IrExpr = !node
+ ? dynUndefinedExpr(loc)
+ : i === 1 && builtinBase !== null
+ ? (builtinClassDynValueOf(lowerer, node) ?? lowerer.lowerExprExpecting(node, DYN))
+ : lowerer.lowerExprExpecting(node, DYN);
const saved = lowerer.declareHiddenLocal("%utilInput", DYN);
stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc });
args.push(varRef(saved.id, DYN, loc));
@@ -2649,7 +2660,7 @@ function lowerUtilCallableCall(
* a statically typed callback parameter cannot receive a value its type
* excludes. Untyped (JavaScript or unknown) slots keep the wrapper dyn. */
function callbackifyErrorExcludesNull(lowerer: Lowerer, expr: ts.CallExpression): boolean {
- if (isJsSourceFile(expr.getSourceFile())) return false;
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) return false;
const checker = lowerer.checker;
const slot = checker.getContextualType(expr) ?? lowerer.typeOf(expr);
const wrapper = checker.getCallSignatures(slot)[0];
@@ -2755,7 +2766,7 @@ function lowerGetCallSitesCall(
loc: SrcLoc,
): IrExpr | null {
if (expr.arguments.nodes.length !== 0) return null;
- if (!isJsSourceFile(expr.getSourceFile())) return null;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(expr))) return null;
const frame = (): IrExpr => {
const field = (name: string, value: IrExpr): { key: IrExpr; value: IrExpr } => ({
key: { kind: "strLit", value: name, type: STRING, loc },
@@ -2767,7 +2778,7 @@ function lowerGetCallSitesCall(
field("functionName", { kind: "strLit", value: "", type: STRING, loc }),
field("scriptName", {
kind: "strLit",
- value: expr.getSourceFile().fileName,
+ value: ts.getSourceFileOfNode(expr).fileName,
type: STRING,
loc,
}),
diff --git a/packages/compiler/src/frontend/lowering/lower-mixins.ts b/packages/compiler/src/frontend/lowering/lower-mixins.ts
index 90789b4a0..766059dfb 100644
--- a/packages/compiler/src/frontend/lowering/lower-mixins.ts
+++ b/packages/compiler/src/frontend/lowering/lower-mixins.ts
@@ -174,7 +174,7 @@ function mixinFnShapeInner(
if (n === t.expression) return undefined;
if (
ts.isIdentifier(n) &&
- n.text === p.name.getText() &&
+ n.text === ts.getTextOfNode(p.name) &&
lowerer.checker.getSymbolAtLocation(n) === paramSym
) {
extraRef = true;
@@ -451,8 +451,8 @@ function instantiateMixinCall(
const base = mixinBaseClassOf(lowerer, call.arguments.nodes[0]!);
const instName = lowerer.qualify(
- call.getSourceFile(),
- `%mx${call.getStart()}.${shape.classNode.name?.text ?? ""}`,
+ ts.getSourceFileOfNode(call),
+ `%mx${ts.tokenStartOfNode(call)}.${shape.classNode.name?.text ?? ""}`,
);
const ordinal = lowerer.mixinOrdinals.get(shape.classNode) ?? 0;
lowerer.mixinOrdinals.set(shape.classNode, ordinal + 1);
@@ -513,7 +513,10 @@ function instantiateMixinCall(
);
}
if (info.mixinInstance) {
- info.mixinInstance.statics = { sf: call.getSourceFile(), pos: holder.getStart() };
+ info.mixinInstance.statics = {
+ sf: ts.getSourceFileOfNode(call),
+ pos: ts.tokenStartOfNode(holder),
+ };
}
}
return info;
diff --git a/packages/compiler/src/frontend/lowering/lower-mock.ts b/packages/compiler/src/frontend/lowering/lower-mock.ts
index be68abaca..fcc68c787 100644
--- a/packages/compiler/src/frontend/lowering/lower-mock.ts
+++ b/packages/compiler/src/frontend/lowering/lower-mock.ts
@@ -797,7 +797,7 @@ export function lowerMockMethod(
const objShape = objType?.kind === "record" ? lowerer.shapes.get(objType.shapeId) : undefined;
const fn = objShape?.fields.find((f) => f.name === field)?.type;
if (!objType || !objShape || fn?.kind !== "func") {
- const typeName = lowerer.typeOf(args[0]!).getSymbol()?.name;
+ const typeName = ts.symbolNameOf(lowerer.typeOf(args[0]!).getSymbol());
lowerer.noLowering(
typeName && !typeName.startsWith("__")
? `${what} on '${typeName}'`
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index fa4c98372..f0199d6a4 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -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. */
@@ -692,7 +692,10 @@ export function collectJsonImports(lowerer: Lowerer, parts: FileParts[]): void {
const nameSym = lowerer.checker.getSymbolAtLocation(clause.name);
if (!nameSym || !(nameSym.flags & ts.SymbolFlags.Alias)) continue;
const target = lowerer.checker.getAliasedSymbol(nameSym);
- const jsonSf = target.declarations[0]?.getSourceFile();
+ const jsonSf =
+ target.declarations.length === 0
+ ? undefined
+ : ts.getSourceFileOfNode(target.declarations[0]!);
if (!jsonSf || !jsonSf.fileName.endsWith(".json")) continue;
try {
const tsType = lowerer.typeOf(clause.name);
@@ -872,6 +875,14 @@ export function moduleArtifacts(
valueInstanceOfRoots.add(root.def.name);
}
break;
+ case "libCall":
+ // The general stream representation's child stdio read bridges
+ // through the runtime install that boxes both stream classes.
+ if (node.fn === "child.stdioStream") {
+ visitClass("%Readable");
+ visitClass("%Writable");
+ }
+ break;
case "dynFrom":
if (boxedChild(node.value.type)) {
// A generic child can expose native stdio without importing stream.
@@ -1167,7 +1178,7 @@ function jsDynHoldableInitializer(lowerer: Lowerer, init: ts.Expression | undefi
if (
(ts.isFunctionExpression(e) || ts.isArrowFunction(e)) &&
!e.typeParameters?.nodes.length &&
- isJsSourceFile(e.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(e))
)
return true;
if (ts.isIdentifier(e) || ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
@@ -1266,7 +1277,7 @@ export function collectGlobals(
const mutableVar =
vd !== undefined &&
ts.isVariableDeclaration(vd) &&
- !vd.getSourceFile().isDeclarationFile &&
+ !ts.getSourceFileOfNode(vd).isDeclarationFile &&
(ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const) === 0;
// Immutable targets: alias plumbing — importers resolve to the
// target's storage (the alias-resolved read IS the snapshot when
@@ -1538,7 +1549,7 @@ export function collectGlobals(
const mutableVar =
vd !== undefined &&
ts.isVariableDeclaration(vd) &&
- !vd.getSourceFile().isDeclarationFile &&
+ !ts.getSourceFileOfNode(vd).isDeclarationFile &&
(ts.getCombinedNodeFlags(vd) & ts.NodeFlags.Const) === 0;
if (!mutableVar) continue;
const diagsBefore = lowerer.diags.length;
@@ -1549,7 +1560,7 @@ export function collectGlobals(
}
if (type.kind === "void") lowerer.badType(stmt.name, lowerer.typeOf(stmt.name));
const g: IrGlobal = {
- id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${stmt.getStart()}`,
+ id: `%g.${tag}${nsPathPrefix(stmt)}%alias.${stmt.name.text}%${ts.tokenStartOfNode(stmt)}`,
name: stmt.name.text,
type,
mutable: false,
@@ -2274,12 +2285,12 @@ export function collectGlobals(
: undefined;
const callDeclaration = callSignature && callSignature.declaration;
const callType =
- callDeclaration && isJsSourceFile(callDeclaration.getSourceFile())
+ callDeclaration && isJsSourceFile(ts.getSourceFileOfNode(callDeclaration))
? lowerer.mapTypeOf(lowerer.typeOf(nameNode))
: null;
const inferredJsCall =
callDeclaration &&
- isJsSourceFile(callDeclaration.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(callDeclaration)) &&
(!callType ||
(unannotatedCall &&
callType.kind === "union" &&
@@ -2702,7 +2713,7 @@ export function declSymbolOf(
ts.canHaveModifiers(decl) &&
ts.getModifiers(decl)?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) === true;
if (!isDefault) return undefined;
- const sym = defaultExportSymbolOf(lowerer, decl.getSourceFile());
+ const sym = defaultExportSymbolOf(lowerer, ts.getSourceFileOfNode(decl));
return sym ?? undefined;
}
@@ -2720,7 +2731,7 @@ export function declSymbolOf(
* expression so the expression's OWN diagnostic wins when it has one,
* then poisons. */
export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment): IrStmt | null {
- const symbol = defaultExportSymbolOf(lowerer, stmt.getSourceFile());
+ const symbol = defaultExportSymbolOf(lowerer, ts.getSourceFileOfNode(stmt));
if (symbol && symbol.flags & ts.SymbolFlags.Alias && !lowerer.globalsBySymbol.has(symbol))
return null;
const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined;
@@ -2733,7 +2744,7 @@ export function lowerDefaultExport(lowerer: Lowerer, stmt: ts.ExportAssignment):
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
if (
ts.isClassExpression(inner) &&
- !isJsSourceFile(stmt.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(stmt)) &&
inner.typeParameters === undefined &&
(decoratorNodesOf(inner).length > 0 ||
inner.members.nodes.some((m) => decoratorNodesOf(m).length > 0)) &&
@@ -2851,6 +2862,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));
}
}
}
@@ -2945,7 +2964,7 @@ export function lowerFileInit(
c.decl &&
ts.isClassDeclaration(c.decl) &&
!c.mixinInstance &&
- c.decl.getSourceFile() === sf &&
+ ts.getSourceFileOfNode(c.decl) === sf &&
(lowerer.classInitFlags.has(c.def.name) ||
c.callableBase !== undefined ||
c.def.baseValueGlobal !== undefined ||
@@ -2954,7 +2973,7 @@ export function lowerFileInit(
(c.staticBlocks?.length ?? 0) > 0 ||
c.classDecorators !== undefined),
)
- .map((c) => ({ pos: c.decl!.getStart(), info: c }));
+ .map((c) => ({ pos: ts.tokenStartOfNode(c.decl!), info: c }));
// Statics-bearing MIXIN instantiations whose call evaluates in THIS
// file: their declaration-time code runs when the call does — the
// top-level statement lower-mixins recorded. Insertion order is
@@ -2974,7 +2993,7 @@ export function lowerFileInit(
// anything the statement's lowering emits. Hoisted class
// declarations never share a kept statement's start, so `<=`
// changes nothing for them.
- while (at < statics.length && statics[at]!.pos <= stmt.getStart()) {
+ while (at < statics.length && statics[at]!.pos <= ts.tokenStartOfNode(stmt)) {
body.push(
...withClassInitFlag(
lowerer,
diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts
index 23b7d0f57..9997eb7d9 100644
--- a/packages/compiler/src/frontend/lowering/lower-namespaces.ts
+++ b/packages/compiler/src/frontend/lowering/lower-namespaces.ts
@@ -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
@@ -177,10 +178,10 @@ export function nsPathPrefix(node: ts.Node, modifierDecl?: ts.Node): string {
const nested = p.parent !== undefined && ts.isModuleBlock(p.parent);
const nonExported =
nested && (ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Export) === 0;
- segs.unshift(p.name.text + (nonExported ? `%${p.getStart()}` : ""));
+ segs.unshift(p.name.text + (nonExported ? `%${ts.tokenStartOfNode(p)}` : ""));
}
}
- return `${segs.join("%")}${exported ? "" : `%${parent.getStart()}`}.`;
+ return `${segs.join("%")}${exported ? "" : `%${ts.tokenStartOfNode(parent)}`}.`;
}
/** The registered namespace-block kind a symbol's declaration sits in
@@ -274,7 +275,11 @@ function moduleNsSourceFileOf(lowerer: Lowerer, e: ts.Expression): ts.SourceFile
stmt.moduleSpecifier !== undefined &&
ts.isStringLiteral(stmt.moduleSpecifier)
) {
- const dep = resolveImport(lowerer.program, d.getSourceFile(), stmt.moduleSpecifier.text);
+ const dep = resolveImport(
+ lowerer.program,
+ ts.getSourceFileOfNode(d),
+ stmt.moduleSpecifier.text,
+ );
if (dep && !dep.isDeclarationFile && lowerer.fileTag.has(dep)) return dep;
}
}
@@ -333,7 +338,7 @@ export function ambientNsRootOf(lowerer: Lowerer, e: ts.Expression): ts.Identifi
return null;
}
if (ts.isModuleDeclaration(d) && !ts.isIdentifier(d.name)) return null;
- if (d.getSourceFile().isDeclarationFile) return null; // lib/@types keep their own fences
+ if (ts.getSourceFileOfNode(d).isDeclarationFile) return null; // lib/@types keep their own fences
if (
ts.isModuleDeclaration(d)
? !isAmbientModuleDecl(d)
@@ -425,13 +430,13 @@ 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;
for (const d of decls) {
if (!ts.isVariableDeclaration(d) || d.initializer !== undefined) return null;
- if (d.getSourceFile().isDeclarationFile) return null; // lib/@types keep their own fences
+ if (ts.getSourceFileOfNode(d).isDeclarationFile) return null; // lib/@types keep their own fences
if (!(ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient)) return null;
}
return root;
@@ -440,6 +445,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 " 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
@@ -472,7 +495,7 @@ export function trapDeclRootOf(
if (root === null) return null;
for (const nameNode of boundIdentifiersOf(decl.name)) {
const sym = lowerer.checker.getSymbolAtLocation(nameNode);
- if (sym && bindingEverWritten(lowerer, sym, decl.getSourceFile())) {
+ if (sym && bindingEverWritten(lowerer, sym, ts.getSourceFileOfNode(decl))) {
const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(sym));
if (mapped !== null && mapped.kind !== "void") return null;
}
@@ -497,7 +520,7 @@ export function trapDeclRootOf(
* initializer into an ordinary lowering that fences). ambientUndefVarRootOf
* carries its own collect-guard, so the extra question stays a probe. */
function bindingEverWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): boolean {
- const symText = sym.name;
+ const symText = ts.symbolName(sym);
const namesSym = (e: ts.Node): boolean =>
ts.isIdentifier(e) && e.text === symText && lowerer.checker.getSymbolAtLocation(e) === sym;
const patternWrites = (target: ts.Node): boolean => {
@@ -506,9 +529,9 @@ function bindingEverWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile)
const walk = (m: ts.Node): void => {
if (hit) return;
if (namesSym(m)) hit = true;
- else m.forEachChild(walk);
+ else ts.forEachChild(m, walk);
};
- target.forEachChild(walk);
+ ts.forEachChild(target, walk);
return hit;
};
let written = false;
@@ -553,9 +576,9 @@ function bindingEverWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile)
written = true;
}
}
- n.forEachChild(visit);
+ ts.forEachChild(n, visit);
};
- sf.forEachChild(visit);
+ ts.forEachChild(sf, visit);
return written;
}
@@ -616,7 +639,7 @@ export function ambientUndefinedFnSymbolOf(
} else if (!ts.isModuleDeclaration(d) && !ts.isClassDeclaration(d)) {
return null;
}
- if (d.getSourceFile().isDeclarationFile) return null;
+ if (ts.getSourceFileOfNode(d).isDeclarationFile) return null;
if (!(ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient)) return null;
}
return sawFn ? sym : null;
@@ -657,7 +680,7 @@ function initExecutingPosition(node: ts.Node): boolean {
function earliestSameFileDeclStart(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): number {
let earliest = Infinity;
for (const d of sym.declarations) {
- if (d.getSourceFile() === sf) earliest = Math.min(earliest, d.getStart());
+ if (ts.getSourceFileOfNode(d) === sf) earliest = Math.min(earliest, ts.tokenStartOfNode(d));
}
return earliest;
}
@@ -672,10 +695,10 @@ export function fenceEarlyAliasUse(lowerer: Lowerer, ident: ts.Identifier, refNo
const aliasDecl = sym.declarations.find(ts.isImportEqualsDeclaration);
if (!aliasDecl || ts.isExternalModuleReference(aliasDecl.moduleReference)) return;
if (!initExecutingPosition(refNode)) return;
- const sf = refNode.getSourceFile();
- if (aliasDecl.getSourceFile() !== sf) return; // cross-file: module init order applies
- const aliasStart = aliasDecl.getStart();
- if (refNode.getStart() < aliasStart) {
+ const sf = ts.getSourceFileOfNode(refNode);
+ if (ts.getSourceFileOfNode(aliasDecl) !== sf) return; // cross-file: module init order applies
+ const aliasStart = ts.tokenStartOfNode(aliasDecl);
+ if (ts.tokenStartOfNode(refNode) < aliasStart) {
lowerer.unsupported(
"SC1090",
refNode,
@@ -711,8 +734,8 @@ export function fenceEarlyNsMemberRef(
memberSym: ts.Symbol,
): void {
if (!initExecutingPosition(access)) return;
- const sf = access.getSourceFile();
- const refStart = access.getStart();
+ const sf = ts.getSourceFileOfNode(access);
+ const refStart = ts.tokenStartOfNode(access);
const memberStart = earliestSameFileDeclStart(lowerer, memberSym, sf);
if (memberStart !== Infinity && refStart < memberStart) {
lowerer.unsupported(
diff --git a/packages/compiler/src/frontend/lowering/lower-node-module.ts b/packages/compiler/src/frontend/lowering/lower-node-module.ts
index fee25b10f..37675777c 100644
--- a/packages/compiler/src/frontend/lowering/lower-node-module.ts
+++ b/packages/compiler/src/frontend/lowering/lower-node-module.ts
@@ -45,7 +45,7 @@ function cjsGlobal(lowerer: Lowerer, node: ts.Expression, name: "module" | "requ
const decls = symbol ? symbol.declarations : [];
const sourceShadow = decls.some(
(decl) =>
- !decl.getSourceFile().isDeclarationFile &&
+ !ts.getSourceFileOfNode(decl).isDeclarationFile &&
(ts.isVariableDeclaration(decl) ||
ts.isParameter(decl) ||
ts.isBindingElement(decl) ||
@@ -55,7 +55,7 @@ function cjsGlobal(lowerer: Lowerer, node: ts.Expression, name: "module" | "requ
ts.isNamespaceImport(decl) ||
ts.isImportEqualsDeclaration(decl)),
);
- return !sourceShadow && !isNodeEsmFile(node.getSourceFile(), lowerer.program);
+ return !sourceShadow && !isNodeEsmFile(ts.getSourceFileOfNode(node), lowerer.program);
}
function graphSyntax(lowerer: Lowerer, sf: ts.SourceFile): boolean {
@@ -174,7 +174,7 @@ export function cjsModuleRegistryPrelude(lowerer: Lowerer, loc: IrExpr["loc"]):
}
export function cjsModuleRef(lowerer: Lowerer, node: ts.Node): IrExpr | null {
- const id = lowerer.cjsModuleIdOf.get(node.getSourceFile());
+ const id = lowerer.cjsModuleIdOf.get(ts.getSourceFileOfNode(node));
return id === undefined ? null : numLit(id, locOf(node));
}
@@ -203,8 +203,8 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean {
const symbol = type.getSymbol();
if (
symbol &&
- (symbol.name === "Module" || symbol.name === "ScriptcModule") &&
- symbol.declarations.some((decl) => lowerer.isStdlibFile(decl.getSourceFile()))
+ (ts.symbolName(symbol) === "Module" || ts.symbolName(symbol) === "ScriptcModule") &&
+ symbol.declarations.some((decl) => lowerer.isStdlibFile(ts.getSourceFileOfNode(decl)))
) {
return true;
}
@@ -217,7 +217,7 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean {
(ts.isInterfaceDeclaration(parent) || ts.isClassDeclaration(parent)) &&
parent.name &&
(parent.name.text === "ScriptcModule" || parent.name.text === "Module") &&
- lowerer.isStdlibFile(parent.getSourceFile())
+ lowerer.isStdlibFile(ts.getSourceFileOfNode(parent))
) {
return true;
}
@@ -237,7 +237,7 @@ function nodeModuleType(lowerer: Lowerer, node: ts.Expression): boolean {
* file assigns an object or function. */
function moduleExportsAlwaysTruthy(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
if (!truthinessOnly(expr)) return false;
- const sf = expr.getSourceFile();
+ const sf = ts.getSourceFileOfNode(expr);
if (isNodeEsmFile(sf, lowerer.program)) return false;
let truthy = true;
ts.walkPreorder(sf, (node) => {
@@ -307,7 +307,7 @@ export function isCjsTopLevelThis(lowerer: Lowerer, node: ts.Node): boolean {
)
return false;
}
- return isCjsJsFile(node.getSourceFile(), lowerer.program);
+ return isCjsJsFile(ts.getSourceFileOfNode(node), lowerer.program);
}
export function usesCjsTopLevelThis(lowerer: Lowerer, sf: ts.SourceFile): boolean {
@@ -341,7 +341,7 @@ export function usesCjsTopLevelThis(lowerer: Lowerer, sf: ts.SourceFile): boolea
* behave as in Node. */
export function lowerCjsTopLevelThis(lowerer: Lowerer, expr: ts.Expression): IrExpr | null {
if (!isCjsTopLevelThis(lowerer, expr)) return null;
- const global = lowerer.cjsExportsThis.get(expr.getSourceFile());
+ const global = lowerer.cjsExportsThis.get(ts.getSourceFileOfNode(expr));
if (!global) return null;
let receiver: ts.Node = expr;
while (ts.isParenthesizedExpression(receiver.parent)) receiver = receiver.parent;
@@ -433,7 +433,7 @@ export function lowerNodeModuleIdentifier(lowerer: Lowerer, expr: ts.Identifier)
if (!isCjsModuleGlobal(lowerer, expr)) return null;
const ref = cjsModuleRef(lowerer, expr);
if (ref) return ref;
- if (isCjsJsFile(expr.getSourceFile(), lowerer.program)) {
+ if (isCjsJsFile(ts.getSourceFileOfNode(expr), lowerer.program)) {
lowerer.unsupported(
"SC1090",
expr,
diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts
index 7cdb9ef5d..afae7dd7b 100644
--- a/packages/compiler/src/frontend/lowering/lower-server.ts
+++ b/packages/compiler/src/frontend/lowering/lower-server.ts
@@ -53,7 +53,7 @@ import {
sideEffectFreeOptionValue,
TLS_SERVER_DOCUMENTED_OPTIONS,
} from "./surfaces.js";
-import { conditionalSpreadOf } from "./expressions/object-literals.js";
+import { conditionalSpreadOf, lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { resultIsDiscarded } from "./call-position.js";
import { lowerCallbackArg as lowerCallbackArgShared } from "./callback-arg.js";
import { pairsSnapshotHelper } from "./pairs-snapshot.js";
@@ -719,7 +719,7 @@ export function lowerNetModuleCall(
// ERR_INVALID_ARG_TYPE on 'options' before any server exists. DYN
// result — the checked-dynamic lane's concise arrows box it, and
// the throw means it never materializes.
- if (isJsSourceFile(expr.getSourceFile())) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const t = lowerer.mapTypeOf(lowerer.typeOf(args[0]!));
if (t !== null && (t.kind === "string" || t.kind === "f64" || t.kind === "bool")) {
const raw = lowerer.lowerExpr(args[0]!);
@@ -770,7 +770,7 @@ export function lowerNetModuleCall(
// invalid-input probes build theirs with computed keys): the
// checked-dynamic walk validates Node-order and the compiler-rendered
// fence is the post-validation tail.
- if (args.length === 1 && isJsSourceFile(expr.getSourceFile())) {
+ if (args.length === 1 && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const t = lowerer.mapTypeOf(lowerer.typeOf(args[0]!));
if (t !== null && (t.kind === "dyn" || t.kind === "record")) {
const raw = lowerer.lowerExpr(args[0]!);
@@ -885,7 +885,7 @@ function lowerNetConnectOptions(
): IrExpr {
const args = expr.arguments.nodes;
const optsNode = args[0] as ts.ObjectLiteralExpression;
- const isJs = isJsSourceFile(expr.getSourceFile());
+ const isJs = isJsSourceFile(ts.getSourceFileOfNode(expr));
// The checked-dynamic option-bag route (JS sources): a literal with
// computed keys (the invalid-input probes' spelling) or an objectMode-
// trio member rides WHOLE to the runtime walk — Node's Socket-ctor
@@ -1524,7 +1524,7 @@ function lowerNetServerMethodCall(
} else if (
key === "signal" &&
ts.isPropertyAssignment(prop) &&
- isJsSourceFile(call.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(call))
) {
// A provably-non-AbortSignal signal (the invalid-input probes:
// strings, numbers, plain records) throws Node's
@@ -1627,6 +1627,29 @@ function lowerNetServerMethodCall(
: [receiver, port, host, v6only, reusePort, cbV],
);
}
+ // listen(path[, cb]): a string first argument names a Unix socket
+ // (the runtime takes a numeric string as a port, Node's isPipeName).
+ if (lowerer.mapTypeOf(lowerer.typeOf(args[0]!))?.kind === "string") {
+ if (args.length > 2) {
+ lowerer.noLowering(
+ `listen(path) with ${args.length} arguments`,
+ call,
+ "the supported form is listen(path[, callback])",
+ );
+ }
+ const path = lowerer.lowerExprExpecting(args[0]!, STRING);
+ if (args.length === 1) return listenResult("net.listenPath", [receiver, path]);
+ const { cb } = lowerCallbackArg(
+ lowerer,
+ args[1]!,
+ "listen callbacks",
+ 0,
+ () => false,
+ "use ()",
+ [],
+ );
+ return listenResult("net.listenPathCb", [receiver, path, cb]);
+ }
const port = lowerer.lowerExprExpecting(args[0]!, F64);
// The optional middle host — listen(port, '127.0.0.1'[, cb]): a
// string-typed second argument is the bind address (the net.connect
@@ -1682,6 +1705,21 @@ function lowerNetServerMethodCall(
if (args.length === 0) {
return { kind: "libCall", fn: "net.serverClose", args: [receiver], type: VOID, loc };
}
+ if (
+ lowerer.checker
+ .getCallSignatures(lowerer.typeOf(args[0]!))
+ .some((sig) => sig.getParameters().length > 0)
+ ) {
+ // close((err) => ...): undefined once the server closes, or Node's
+ // ERR_SERVER_NOT_RUNNING when it was not listening.
+ return {
+ kind: "libCall",
+ fn: "net.serverCloseDyn",
+ args: [receiver, lowerSocketDynListener(lowerer, args[0]!, "close callbacks")],
+ type: VOID,
+ loc,
+ };
+ }
const { cb } = lowerCallbackArg(
lowerer,
args[0]!,
@@ -2055,6 +2093,32 @@ function lowerNetServerMethodCall(
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
if (result && isAddressInfoRecord(lowerer, result)) {
const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T);
+ if (unixListenReachable(lowerer)) {
+ // A Unix-socket server's address() is its path, which this record
+ // cannot hold: the check throws for one before the record reads.
+ const local = lowerer.declareHiddenLocal("%addressServer", NETSERVER_T);
+ const server = (): IrExpr => varRef(local.id, NETSERVER_T, loc);
+ return {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: local.id, init: receiver, loc },
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "net.serverAddressCheck",
+ args: [server()],
+ type: VOID,
+ loc,
+ },
+ loc,
+ },
+ ],
+ result: { kind: "libCall", fn: "net.serverAddress", args: [server()], type: result, loc },
+ type: result,
+ loc,
+ };
+ }
return { kind: "libCall", fn: "net.serverAddress", args: [receiver], type: result, loc };
}
// @types/node's `string | AddressInfo | null`: the record while the
@@ -2074,20 +2138,50 @@ function lowerNetServerMethodCall(
const receiver = coerceToHandle(lowerer, access.expression, NETSERVER_T);
const local = lowerer.declareHiddenLocal("%addressServer", NETSERVER_T);
const server = varRef(local.id, NETSERVER_T, loc);
+ const recordArm: IrExpr = {
+ kind: "unionWrap",
+ unionId,
+ tag: lowerer.armTag(unionId, record),
+ value: { kind: "libCall", fn: "net.serverAddress", args: [server], type: record, loc },
+ type: result,
+ loc,
+ };
+ const stringTag = lowerer.armTag(unionId, STRING);
+ let listeningArm: IrExpr = recordArm;
+ const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
+ if (stringTag >= 0 && unixListenReachable(lowerer)) {
+ // Node answers a Unix-socket server's address() with its path.
+ const pathLocal = lowerer.declareHiddenLocal("%addressPath", STRING);
+ const path = (): IrExpr => varRef(pathLocal.id, STRING, loc);
+ stmts.push({
+ kind: "varDecl",
+ localId: pathLocal.id,
+ init: { kind: "libCall", fn: "net.serverUnixPath", args: [server], type: STRING, loc },
+ loc,
+ });
+ listeningArm = {
+ kind: "ternary",
+ cond: {
+ kind: "strEq",
+ negated: true,
+ left: path(),
+ right: strLit("", loc),
+ type: BOOL,
+ loc,
+ },
+ then: { kind: "unionWrap", unionId, tag: stringTag, value: path(), type: result, loc },
+ else_: recordArm,
+ type: result,
+ loc,
+ };
+ }
return {
kind: "seqExpr",
- stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }],
+ stmts,
result: {
kind: "ternary",
cond: { kind: "libCall", fn: "net.serverListening", args: [server], type: BOOL, loc },
- then: {
- kind: "unionWrap",
- unionId,
- tag: lowerer.armTag(unionId, record),
- value: { kind: "libCall", fn: "net.serverAddress", args: [server], type: record, loc },
- type: result,
- loc,
- },
+ then: listeningArm,
else_: {
kind: "unionWrap",
unionId,
@@ -2143,6 +2237,34 @@ function socketEncodingReachable(lowerer: Lowerer): boolean {
return reachable;
}
+/** Whether some server in the program may listen on a Unix socket: a
+ * `.listen(x, ...)` call on a server (or an untyped value) whose first
+ * argument is string-typed. Only then does address() check for the socket
+ * path, so other programs keep the record-only lowering. */
+function unixListenReachable(lowerer: Lowerer): boolean {
+ if (lowerer.unixListenReachable !== null) return lowerer.unixListenReachable;
+ let reachable = false;
+ for (const file of lowerer.program.getSourceFiles()) {
+ if (file.isDeclarationFile || !file.text.includes("listen")) continue;
+ ts.walkPreorder(file, (node) => {
+ if (reachable) return "skip";
+ if (!ts.isCallExpression(node) || node.arguments.nodes.length === 0) return undefined;
+ const callee = node.expression;
+ if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "listen") return undefined;
+ const kind = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(callee.expression))?.kind;
+ if (kind !== undefined && kind !== "netServer" && kind !== "dyn") return undefined;
+ const arg = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(node.arguments.nodes[0]!));
+ // Only a string-typed argument lowers to listen(path) (an untyped
+ // one checks into a port).
+ if (arg?.kind === "string") reachable = true;
+ return undefined;
+ });
+ if (reachable) break;
+ }
+ lowerer.unixListenReachable = reachable;
+ return reachable;
+}
+
/** An inline 'data' listener whose chunk parameter is typed Buffer, for a
* socket that may have an encoding set: the chunk binds as a checked
* dynamic value, so the body sees what Node passes (a string after
@@ -2212,7 +2334,7 @@ function lowerNetSocketMethodCall(
if (
encT.value === "buffer" &&
chunkT.kind === "string" &&
- isJsSourceFile(call.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(call))
) {
lowerer.lowerExpr(args[0]!); // evaluation order (effect-free in practice)
return nodeThrowExpr(1, "ERR_UNKNOWN_ENCODING", "Unknown encoding: buffer", VOID, loc);
@@ -2736,6 +2858,17 @@ function lowerNetSocketMethodCall(
: "net.sockOnConnect";
return { kind: "libCall", fn, args: [receiver, cb, once], type: VOID, loc };
}
+ if (event === "lookup") {
+ // (err, address, family, host) after the host name resolves, before
+ // the connect: once per resolved address, or once with the error.
+ return {
+ kind: "libCall",
+ fn: "net.sockOnLookup",
+ args: [receiver, lowerSocketDynListener(lowerer, args[1]!, "lookup listeners"), once],
+ type: VOID,
+ loc,
+ };
+ }
if (event === "finish") {
// Fires once when the FIN goes out — once either way (the event
// happens at most once per socket).
@@ -2795,7 +2928,7 @@ function lowerNetSocketMethodCall(
lowerer.noLowering(
`socket.${name}(${event === null ? "non-literal event" : `"${event}"`}, ...)`,
args[0]!,
- '"data", "end", "close", "error", "connect", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)',
+ '"data", "end", "close", "error", "connect", "lookup", "ready", "timeout", "readable", "secureConnect", and "session" are the supported socket events (as literals)',
);
}
lowerer.noLowering(
@@ -3971,6 +4104,30 @@ export function lowerServerProperty(
if (lowerer.mapTypeOf(lowerer.typeOf(inner.expression))?.kind !== "netServer") return null;
if (!lowerer.isStdlibMember(inner)) return null;
const receiver = coerceToHandle(lowerer, inner.expression, NETSERVER_T);
+ if (unixListenReachable(lowerer)) {
+ const local = lowerer.declareHiddenLocal("%addressServer", NETSERVER_T);
+ const server = (): IrExpr => varRef(local.id, NETSERVER_T, loc);
+ return {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: local.id, init: receiver, loc },
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "net.serverAddressCheck",
+ args: [server()],
+ type: VOID,
+ loc,
+ },
+ loc,
+ },
+ ],
+ result: { kind: "libCall", fn: "net.serverPort", args: [server()], type: F64, loc },
+ type: F64,
+ loc,
+ };
+ }
return { kind: "libCall", fn: "net.serverPort", args: [receiver], type: F64, loc };
}
@@ -4694,7 +4851,7 @@ function tlsLiteralNeedsRuntimeWalk(
node: ts.ObjectLiteralExpression,
includeHttpTimeoutOptions = false,
): boolean {
- const jsSource = isJsSourceFile(node.getSourceFile());
+ const jsSource = isJsSourceFile(ts.getSourceFileOfNode(node));
return node.properties.nodes.some(
(p) =>
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
@@ -4914,7 +5071,7 @@ function lowerTlsModuleCall(
bi.member === "getCACertificates" &&
args.length === 1 &&
!args.some(ts.isSpreadElement) &&
- isJsSourceFile(expr.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(expr))
) {
// The type-argument ladder (validateString + the documented name
// set); the real CA list has no lowering, so a valid name meets the
@@ -5580,7 +5737,7 @@ function lowerHttp2ModuleCall(
if (ts.isConditionalExpression(inner)) {
const whenTrue = stripParensAndCasts(inner.whenTrue);
const whenFalse = stripParensAndCasts(inner.whenFalse);
- const condText = inner.condition.getText();
+ const condText = ts.getTextOfNode(inner.condition);
const ok =
ts.isObjectLiteralExpression(whenTrue) &&
ts.isObjectLiteralExpression(whenFalse) &&
@@ -5591,7 +5748,7 @@ function lowerHttp2ModuleCall(
ts.isStringLiteral(whenTrue.properties.nodes[0]!.name)) &&
(whenTrue.properties.nodes[0]!.name as ts.Identifier | ts.StringLiteral).text ===
"SNICallback" &&
- whenTrue.properties.nodes[0]!.initializer.getText() === condText;
+ ts.getTextOfNode(whenTrue.properties.nodes[0]!.initializer) === condText;
if (!ok) {
lowerer.noLowering(
"createSecureServer options with a conditional spread",
@@ -5827,6 +5984,44 @@ function lowerHttp2ModuleCall(
);
}
+/** The checked-dynamic options value of a request whose options the
+ * runtime reads: a non-literal options value (a RequestOptions variable —
+ * checked-dynamic storage — or a record that converts to one), or a
+ * literal the static walk cannot lower because it spreads another options
+ * value or sets socketPath. Null keeps the static literal walk (and its
+ * fences), which every other literal takes unchanged. */
+function runtimeRequestOptions(
+ lowerer: Lowerer,
+ optsNode: ts.Expression,
+ secureish: boolean,
+): IrExpr | null {
+ if (ts.isObjectLiteralExpression(optsNode)) {
+ const needsRuntime = optsNode.properties.nodes.some((prop) => {
+ if (ts.isSpreadAssignment(prop)) {
+ // the static walk's one spread: `...(c ? { rejectUnauthorized } : {})` on https
+ const cs = conditionalSpreadOf(prop.expression);
+ return !(
+ secureish &&
+ cs !== null &&
+ cs !== "unsupported" &&
+ cs.props.length === 1 &&
+ cs.props[0]!.name.text === "rejectUnauthorized"
+ );
+ }
+ return (
+ (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) &&
+ (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) &&
+ prop.name.text === "socketPath"
+ );
+ });
+ if (!needsRuntime) return null;
+ return lowerDynObjectLiteral(lowerer, optsNode);
+ }
+ const t = lowerer.mapTypeOf(lowerer.typeOf(optsNode));
+ if (!t || !(t.kind === "dyn" || (t.kind === "record" && lowerer.dynConvertible(t)))) return null;
+ return lowerer.coerceToExpected(lowerer.lowerExpr(optsNode), DYN);
+}
+
/** http.request(options[, cb]) / http.get(options[, cb]) — the client.
* The options argument must be an OBJECT LITERAL; the lowered keys are
* hostname/host, port, path, method, timeout, and headers (a nested
@@ -5869,6 +6064,40 @@ function lowerHttpClientCall(
);
}
const optsNode = args[0]!;
+ const runtimeOptions =
+ binding === null ? runtimeRequestOptions(lowerer, optsNode, secureish) : null;
+ if (runtimeOptions !== null) {
+ // The options record is read at run time (the ClientRequest
+ // constructor's walk in the runtime): a RequestOptions variable, or a
+ // literal that spreads one or sets socketPath.
+ const autoEnd = boolLit(member === "get", loc);
+ const secureDyn = secure === true;
+ if (args.length === 1) {
+ return {
+ kind: "libCall",
+ fn: secureDyn ? "https.requestDyn" : "http.requestDyn",
+ args: [runtimeOptions, autoEnd],
+ type: HTTPCLIENTREQ_T,
+ loc,
+ };
+ }
+ const { cb } = lowerCallbackArg(
+ lowerer,
+ args[1]!,
+ "response callbacks",
+ 1,
+ (p) => p.kind === "httpReq",
+ "use (res) or ()",
+ [HTTPREQ_T],
+ );
+ return {
+ kind: "libCall",
+ fn: secureDyn ? "https.requestDynCb" : "http.requestDynCb",
+ args: [runtimeOptions, autoEnd, cb],
+ type: HTTPCLIENTREQ_T,
+ loc,
+ };
+ }
if (!ts.isObjectLiteralExpression(optsNode)) {
const t = lowerer.mapTypeOf(lowerer.typeOf(optsNode));
if ((t?.kind === "string" || t?.kind === "url") && binding === null) {
@@ -7149,6 +7378,27 @@ function lowerHttpReqMethodCall(
const event = evT.isStringLiteralType() ? evT.value : null;
const receiver = coerceToHandle(lowerer, access.expression, HTTPREQ_T);
if (event === "data") {
+ const sigs = lowerer.checker.getCallSignatures(lowerer.typeOf(args[1]!));
+ const firstParam = sigs.length === 1 ? sigs[0]!.getParameters()[0] : undefined;
+ const param =
+ firstParam !== undefined
+ ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(firstParam))
+ : null;
+ if (param !== null && param.kind === "string") {
+ // A chunk typed string (the setEncoding('utf8') shape), as on a
+ // socket: the listener takes the chunk through a checked
+ // boundary, so a string arrives after setEncoding and a Buffer
+ // without it fails the check instead of being decoded.
+ const boxed = lowerSocketDynListener(lowerer, args[1]!, "data listeners");
+ const cb: IrExpr = { kind: "dynCheck", value: boxed, type: funcOf([DYN], VOID), loc };
+ return {
+ kind: "libCall",
+ fn: "http.reqOnData",
+ args: [receiver, cb, once],
+ type: VOID,
+ loc,
+ };
+ }
const { cb } = lowerCallbackArg(
lowerer,
args[1]!,
@@ -7283,7 +7533,7 @@ function lowerHttpResMethodCall(
}
if (name === "writeContinue" || name === "writeProcessing" || name === "writeEarlyHints") {
requireStatementPosition(lowerer, call, `res.${name}(...)`);
- if (lowerer.typeOf(access.expression).getSymbol()?.name === "Http2ServerResponse") {
+ if (ts.symbolNameOf(lowerer.typeOf(access.expression).getSymbol()) === "Http2ServerResponse") {
lowerer.noLowering(
`Http2ServerResponse.${name}`,
call,
@@ -7392,7 +7642,7 @@ function lowerHttpResMethodCall(
}
if (name === "addTrailers") {
requireStatementPosition(lowerer, call, "res.addTrailers(...)");
- if (lowerer.typeOf(access.expression).getSymbol()?.name === "Http2ServerResponse") {
+ if (ts.symbolNameOf(lowerer.typeOf(access.expression).getSymbol()) === "Http2ServerResponse") {
lowerer.noLowering(
"Http2ServerResponse.addTrailers",
call,
@@ -7711,6 +7961,30 @@ function lowerHttpResMethodCall(
if (cbArg !== null) return endWithCb(fn, [receiver, data], cbArg);
return { kind: "libCall", fn, args: [receiver, data], type: VOID, loc };
}
+ // end(payload ? text : undefined): an absent chunk ends without data,
+ // as end() does. The checked-dynamic end takes each arm at run time.
+ if (name === "end" && data.type.kind === "union" && lowerer.dynConvertible(data.type)) {
+ const arms = lowerer.unions.get(data.type.unionId)?.arms ?? [];
+ if (
+ arms.every(
+ (arm) =>
+ arm.kind === "string" ||
+ (arm.kind === "bytes" && arm.elem === "u8") ||
+ arm.kind === "undefinedT" ||
+ arm.kind === "nullT",
+ )
+ ) {
+ const dynData: IrExpr = { kind: "dynFrom", value: data, type: DYN, loc: locOf(dataNode) };
+ if (cbArg !== null) return endWithCb("http.resEndDyn", [receiver, dynData], cbArg);
+ return {
+ kind: "libCall",
+ fn: "http.resEndDyn",
+ args: [receiver, dynData],
+ type: VOID,
+ loc,
+ };
+ }
+ }
lowerer.noLowering(
`${name} of '${lowerer.fmt(data.type)}' data`,
dataNode ?? call,
diff --git a/packages/compiler/src/frontend/lowering/lower-stdlib.ts b/packages/compiler/src/frontend/lowering/lower-stdlib.ts
index 2f0e1c66f..bc8955a81 100644
--- a/packages/compiler/src/frontend/lowering/lower-stdlib.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stdlib.ts
@@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
import { lowerToNumberArgument } from "./lower-exprs.js";
import type { Lowerer } from "./lowerer.js";
+import { isDeclaredInAmbientNamespace } from "../type-mapper.js";
import {
arrayOf,
BOOL,
@@ -42,6 +43,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,
@@ -664,7 +667,7 @@ export function lowerStaticResponseCall(lowerer: Lowerer, call: ts.CallExpressio
}
const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression));
const sym = recvType.getAliasSymbol() ?? recvType.getSymbol();
- if (!sym || sym.name !== "Response" || !lowerer.isStdlibSymbol(sym)) return null;
+ if (!sym || ts.symbolName(sym) !== "Response" || !lowerer.isStdlibSymbol(sym)) return null;
const recv = lowerer.lowerExpr(access.expression);
if (recv.type.kind !== "dyn") return null;
return lowerStaticFixedFetchMethodCall(lowerer, call, access, member, recv, [], (receiver) =>
@@ -750,7 +753,7 @@ function lowerStaticFixedFetchMethodCall(
loc,
},
then: answer,
- else_: nodeThrowExpr(1, "", `${access.getText()} is not a function`, answer.type, loc),
+ else_: nodeThrowExpr(1, "", `${ts.getTextOfNode(access)} is not a function`, answer.type, loc),
type: answer.type,
loc,
};
@@ -924,7 +927,7 @@ export function fenceStaticResponseMember(
): IrExpr | null {
const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression));
const sym = recvType.getAliasSymbol() ?? recvType.getSymbol();
- if (!sym || sym.name !== "Response" || !lowerer.isStdlibSymbol(sym)) return null;
+ if (!sym || ts.symbolName(sym) !== "Response" || !lowerer.isStdlibSymbol(sym)) return null;
const members = fetchAccessMemberNames(lowerer, access);
if (members === null) return null;
const status = (member: string) =>
@@ -966,7 +969,7 @@ export function fenceStaticHeadersMember(
): IrExpr | null {
const recvType = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression));
const sym = recvType.getAliasSymbol() ?? recvType.getSymbol();
- if (!sym || sym.name !== "Headers" || !lowerer.isStdlibSymbol(sym)) return null;
+ if (!sym || ts.symbolName(sym) !== "Headers" || !lowerer.isStdlibSymbol(sym)) return null;
const members = fetchAccessMemberNames(lowerer, access);
if (members === null) return null;
const status = (member: string) =>
@@ -1021,7 +1024,16 @@ export function fenceStaticAbortControllerMemberRead(
function isStdlibFetchInterface(lowerer: Lowerer, node: ts.Node, owner: string): ts.Symbol | null {
const type = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(node));
for (const symbol of [type.getSymbol(), type.getAliasSymbol()]) {
- if (symbol?.name === owner && lowerer.isStdlibSymbol(symbol)) return symbol;
+ // NodeJS.ReadableStream shares the WHATWG interface's name.
+ if (
+ symbol !== undefined &&
+ ts.symbolName(symbol) === owner &&
+ lowerer.isStdlibSymbol(symbol) &&
+ !symbol.declarations.some(
+ (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"),
+ )
+ )
+ return symbol;
}
return null;
}
@@ -1082,10 +1094,10 @@ function fetchInterfaceInventoryOwner(
const symbol = [type.getSymbol(), type.getAliasSymbol()].find(
(candidate) =>
candidate !== undefined &&
- NODE24_FETCH_COMPAT_PROFILE.inventory.interfaces.includes(candidate.name) &&
+ NODE24_FETCH_COMPAT_PROFILE.inventory.interfaces.includes(ts.symbolName(candidate)) &&
lowerer.isStdlibSymbol(candidate),
);
- return symbol ? { owner: symbol.name, symbol } : null;
+ return symbol ? { owner: ts.symbolName(symbol), symbol } : null;
}
function fenceFetchObjectMembers(
@@ -1297,7 +1309,7 @@ export function lowerFetchElementMethodCall(
kind: "dynInvoke",
recv: receiverRef,
method: member,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: argumentRefs,
type: DYN,
loc,
@@ -1310,7 +1322,7 @@ export function lowerFetchElementMethodCall(
let result: IrExpr = nodeThrowExpr(
1,
"",
- `${access.getText()} is not a function`,
+ `${ts.getTextOfNode(access)} is not a function`,
invocationType,
loc,
);
@@ -1464,7 +1476,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);
@@ -1615,9 +1628,10 @@ export function lowerStaticAbortControllerCall(
const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression));
const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol();
if (
- symbol?.name !== "AbortController" ||
+ symbol === undefined ||
+ ts.symbolName(symbol) !== "AbortController" ||
!symbol.declarations.some(
- (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
+ (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
)
) {
return null;
@@ -1640,7 +1654,7 @@ export function lowerStaticAbortControllerCall(
kind: "dynInvoke",
recv: receiverRef,
method: "abort",
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: argumentRefs,
type: DYN,
loc,
@@ -1669,9 +1683,10 @@ export function lowerStaticReadableStreamControllerCall(
const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression));
const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol();
if (
- symbol?.name !== "ReadableStreamDefaultController" ||
+ symbol === undefined ||
+ ts.symbolName(symbol) !== "ReadableStreamDefaultController" ||
!symbol.declarations.some(
- (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
+ (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
)
) {
return null;
@@ -1695,7 +1710,7 @@ export function lowerStaticReadableStreamControllerCall(
kind: "dynInvoke",
recv: receiver,
method: member,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: argumentRefs,
type: DYN,
loc,
@@ -1744,7 +1759,7 @@ export function lowerStaticReadableStreamCancelCall(
kind: "dynInvoke",
recv: receiverRef,
method: "cancel",
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: argumentRefs,
type: DYN,
loc,
@@ -1777,9 +1792,10 @@ export function lowerStaticAbortSignalListenerCall(
const receiverTs = lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(access.expression));
const symbol = receiverTs.getAliasSymbol() ?? receiverTs.getSymbol();
if (
- (symbol?.name !== "AbortSignal" && symbol?.name !== "EventTarget") ||
+ ((symbol === undefined || ts.symbolName(symbol) !== "AbortSignal") &&
+ (symbol === undefined || ts.symbolName(symbol) !== "EventTarget")) ||
!symbol.declarations.some(
- (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
+ (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
)
) {
return null;
@@ -1802,7 +1818,7 @@ export function lowerStaticAbortSignalListenerCall(
kind: "dynInvoke",
recv: receiver,
method: member,
- calleeName: access.getText(),
+ calleeName: ts.getTextOfNode(access),
args: argumentRefs,
type: DYN,
loc,
@@ -1877,6 +1893,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 +1907,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);
@@ -2124,9 +2150,10 @@ export function lowerStaticReadableStreamReaderCall(
const receiverTs = lowerer.typeOf(access.expression);
const symbol = receiverTs.getSymbol();
if (
- symbol?.name !== "ReadableStreamDefaultReader" ||
+ symbol === undefined ||
+ ts.symbolName(symbol) !== "ReadableStreamDefaultReader" ||
!symbol.declarations.some(
- (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(d.getSourceFile()),
+ (d) => ts.isInterfaceDeclaration(d) && lowerer.isStdlibFile(ts.getSourceFileOfNode(d)),
)
) {
return null;
@@ -2167,7 +2194,7 @@ export function lowerDynamicImportCall(lowerer: Lowerer, call: ts.CallExpression
if (call.arguments.nodes.length !== 1) {
lowerer.unsupported("SC1090", call, "dynamic import() with import attributes");
}
- const res = lowerer.dynImports.get(`${call.getSourceFile().fileName}\u0000${arg.text}`);
+ const res = lowerer.dynImports.get(`${ts.getSourceFileOfNode(call).fileName}\u0000${arg.text}`);
if (res === undefined) lowerer.refuseUnsupportedApi("'import()'", call);
if (res.kind === "program-module") {
return lowerOwnStaticModuleImport(lowerer, call, arg);
@@ -2436,6 +2463,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",
@@ -2543,6 +2574,6 @@ export function npmMemberFence(lowerer: Lowerer, access: ts.PropertyAccessExpres
export function npmPackageOfSymbol(lowerer: Lowerer, sym: ts.Symbol | undefined): string | null {
const decls = sym ? sym.declarations : undefined;
if (!decls || decls.length === 0) return null;
- if (!decls.every((d) => lowerer.isNpmFile(d.getSourceFile()))) return null;
- return npmPackageNameOf(decls[0]!.getSourceFile().fileName);
+ if (!decls.every((d) => lowerer.isNpmFile(ts.getSourceFileOfNode(d)))) return null;
+ return npmPackageNameOf(ts.getSourceFileOfNode(decls[0]!).fileName);
}
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 064cd7a7b..69f6cc99b 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -1,4 +1,8 @@
+import { lowerTupleAliasDecl, stripTypeWrappers } from "./typed-arguments.js";
+import { declaredMemberOf } from "./expressions/property-access.js";
import { hostsAll } from "./expressions/hosted-narrowing.js";
+import { requestGeneralStreamSurface } from "./general-streams.js";
+import { lowerForAwaitDynStream } from "./dyn-streams.js";
import {
matchAllRowsReadByElements,
storedMatchAllRowsReadByElements,
@@ -32,6 +36,7 @@ import {
import {
cjsExportMemberWriteGlobalOf,
isCjsExportsIdentifier,
+ isCjsModuleExports,
isFreshExportsReset,
} from "./cjs-exports-object.js";
import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js";
@@ -195,6 +200,7 @@ import {
import { globalSymbolKey } from "./expressions/global-symbols.js";
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
import {
+ builtinGlobalValue,
jsHoldsDynObject,
lowerEnvironmentKey,
lowerNativeFunctionAssignment,
@@ -223,6 +229,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[];
@@ -249,8 +256,8 @@ function npmStaticReturnedEmptyArrayType(
decl.initializer === undefined ||
!ts.isArrayLiteralExpression(decl.initializer) ||
decl.initializer.elements.nodes.length !== 0 ||
- !isJsSourceFile(decl.getSourceFile()) ||
- npmStaticPackageOfPath(decl.getSourceFile().fileName) === null ||
+ !isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
+ npmStaticPackageOfPath(ts.getSourceFileOfNode(decl).fileName) === null ||
lowerer.ctx.returnType.kind !== "array"
) {
return null;
@@ -289,8 +296,8 @@ function npmStaticOptionalSelfWriteType(
): (IrType & { kind: "union" }) | null {
if (
!ts.isIdentifier(decl.name) ||
- !isJsSourceFile(decl.getSourceFile()) ||
- npmStaticPackageOfPath(decl.getSourceFile().fileName) === null ||
+ !isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
+ npmStaticPackageOfPath(ts.getSourceFileOfNode(decl).fileName) === null ||
initial.kind === "union"
) {
return null;
@@ -352,7 +359,9 @@ function stdlibTypeNamed(lowerer: Lowerer, node: ts.Node, name: string): boolean
if (t.flags & ts.TypeFlags.Union) return constituentTypes(t).some(visit);
const sym = t.getAliasSymbol() ?? t.getSymbol();
return (
- sym?.name === name && sym.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile()))
+ sym !== undefined &&
+ ts.symbolName(sym) === name &&
+ sym.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))
);
};
return visit(lowerer.typeOf(node));
@@ -606,7 +615,7 @@ function pureAnnotatedDeadConst(stmt: ts.Statement, sf: ts.SourceFile): boolean
return decls.every((d) => {
let init = d.initializer;
if (init === undefined) return false;
- const trivia = sf.text.slice(init.pos, init.getStart(sf));
+ const trivia = sf.text.slice(init.pos, ts.tokenStartOfNode(init));
if (!trivia.includes("@__PURE__") && !trivia.includes("#__PURE__")) return false;
// `as` casts are type-world wrappers (`(() => {...})() as unknown as
// { new (): any }` — cookie's NullObject shape); the runtime
@@ -633,7 +642,7 @@ function pureAnnotatedDeadConst(stmt: ts.Statement, sf: ts.SourceFile): boolean
* probes that would otherwise claim (and diagnose) the call-initializer
* shape. */
export function provenanceElidedConstDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
- const sf = decl.getSourceFile();
+ const sf = ts.getSourceFileOfNode(decl);
if (!isProvenanceSourceFile(sf.fileName)) return false;
const stmt = decl.parent?.parent;
if (!ts.isVariableStatement(stmt) || !pureAnnotatedDeadConst(stmt, sf)) return false;
@@ -676,7 +685,7 @@ export function lowerStmts(lowerer: Lowerer, stmts: readonly ts.Statement[]): Ir
// runtimeFence — a catchable, SC-coded throw at the statement's
// position — instead of failing the whole build). TypeScript keeps
// compile fences. ICEs (SC9001) always stay compile errors.
- const sf = stmts[0]?.getSourceFile();
+ const sf = stmts.length === 0 ? undefined : ts.getSourceFileOfNode(stmts[0]!);
const deferFences = sf !== undefined && isJsSourceFile(sf);
// Register this list as open for the forward-capture machinery: a
// nested function lowering mid-list may pre-declare a LATER const of
@@ -952,7 +961,7 @@ export function predeclareForwardCapture(lowerer: Lowerer, symbol: ts.Symbol): b
return false;
let type = lowerer.irTypeOf(decl.name);
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!hasJsTypeAnnotation(decl) &&
(ts.isObjectLiteralExpression(decl.initializer) ||
@@ -1100,7 +1109,7 @@ function jsBindingKeepsCheckedObject(
nameNode: ts.Identifier,
type: IrType,
): boolean {
- if (!isJsSourceFile(nameNode.getSourceFile())) return false;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(nameNode))) return false;
const host = hostVariableDeclarationOf(nameNode);
if (!host || host.type || hasJsTypeAnnotation(host)) return false;
const arms = type.kind === "union" ? (lowerer.unions.get(type.unionId)?.arms ?? []) : [type];
@@ -1115,8 +1124,9 @@ function jsBindingKeepsCheckedObject(
function mentionsSymbol(lowerer: Lowerer, node: ts.Node, symbol: ts.Symbol): boolean {
if (ts.isIdentifier(node)) return lowerer.checker.getSymbolAtLocation(node) === symbol;
return (
- node.forEachChild((child) => (mentionsSymbol(lowerer, child, symbol) ? true : undefined)) ===
- true
+ ts.forEachChild(node, (child) =>
+ mentionsSymbol(lowerer, child, symbol) ? true : undefined,
+ ) === true
);
}
@@ -1162,7 +1172,8 @@ function lowerJsVarFromExisting(
symbol: ts.Symbol,
): IrStmt | null {
if (!ts.isIdentifier(decl.name) || !decl.initializer) return null;
- if (!isJsSourceFile(decl.getSourceFile()) || lowerer.ctx.hoistedVars.has(symbol)) return null;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(decl)) || lowerer.ctx.hoistedVars.has(symbol))
+ return null;
if (lowerer.bindingIn(lowerer.ctx, symbol)) return null;
let node: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(node) || ts.isSatisfiesExpression(node))
@@ -1205,13 +1216,13 @@ function varBindingType(lowerer: Lowerer, nameNode: ts.Identifier): IrType | nul
)
return DYN;
if (
- isJsSourceFile(nameNode.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(nameNode)) &&
ts.isVariableDeclaration(nameNode.parent) &&
isNativeBuiltinValueInitializer(lowerer, nameNode.parent.initializer)
)
return DYN;
let type = lowerer.mapTypeOf(lowerer.typeOf(nameNode));
- if (type?.kind === "record" && isJsSourceFile(nameNode.getSourceFile())) {
+ if (type?.kind === "record" && isJsSourceFile(ts.getSourceFileOfNode(nameNode))) {
const shape = lowerer.shapes.get(type.shapeId);
if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) type = DYN;
}
@@ -1490,6 +1501,8 @@ const THROWN_AS_DYN: ReadonlySet = new Set([
]);
export function lowerStmt(lowerer: Lowerer, stmt: ts.Statement): IrStmt | IrStmt[] | null {
+ if (ownPrototypeResetStatement(lowerer, stmt))
+ return { kind: "block", body: [], loc: locOf(stmt) };
if (ts.isClassDeclaration(stmt)) {
if (!stmt.name) lowerer.unsupported("SC1090", stmt, "anonymous local class declarations");
const info = lowerer.lowerClassExpressionInfo(stmt);
@@ -1812,7 +1825,7 @@ function lowerVarDeclarator(
requireSpecOf(decl.initializer) !== null &&
!isNativeFfiRequire(lowerer, decl.initializer) &&
!isJsonRequireDecl(lowerer, decl) &&
- isJsSourceFile(decl.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(decl))
) {
const spec = requireSpecOf(decl.initializer)!;
if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, decl, false);
@@ -2061,7 +2074,7 @@ function lowerMatchAllDrainDecl(
export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): boolean {
const widened = lowerer.checker.getBaseTypeOfLiteralType(type);
const declarationBelongs = (d: ts.Node): boolean => {
- if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
+ if (!lowerer.isStdlibFile(ts.getSourceFileOfNode(d))) return false;
let inParseArgsType = false;
for (let node: ts.Node | undefined = d.parent; node; node = node.parent) {
if (
@@ -2077,7 +2090,11 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool
return false;
};
const sym = widened.getAliasSymbol() ?? widened.getSymbol();
- if (sym && isParseArgsDynTypeName(sym.name) && sym.declarations.some(declarationBelongs)) {
+ if (
+ sym &&
+ isParseArgsDynTypeName(ts.symbolName(sym)) &&
+ sym.declarations.some(declarationBelongs)
+ ) {
return true;
}
// A discriminant check erases the token alias and leaves one anonymous
@@ -2088,43 +2105,6 @@ export function isParseArgsDynCheckerType(lowerer: Lowerer, type: ts.Type): bool
.some((p) => p.declarations.some(declarationBelongs));
}
-/** True when an `any` value comes from a JavaScript function's result:
- * a call (possibly awaited) whose callee is declared in a JavaScript file,
- * or a const bound to one. A TypeScript program importing a JavaScript
- * package sees the package's inferred types (`jwtVerify` from jose
- * answers `any`), and its object destructuring reads the members of that
- * checked-dynamic value as JavaScript does — member reads on the same
- * value already compile. Other `any` sources keep the fence. */
-function javaScriptAnyOrigin(lowerer: Lowerer, node: ts.Expression, depth: number): boolean {
- let expr = node;
- while (ts.isParenthesizedExpression(expr) || ts.isAwaitExpression(expr)) expr = expr.expression;
- if (ts.isCallExpression(expr)) {
- const callee = ts.isPropertyAccessExpression(expr.expression)
- ? expr.expression.name
- : expr.expression;
- if (!ts.isIdentifier(callee)) return false;
- const symbol = lowerer.resolveValueSymbol(callee);
- const declarations = symbol ? symbol.declarations : [];
- return (
- declarations.length > 0 &&
- declarations.every((d) => isJsSourceFile(d.getSourceFile()) && ts.isFunctionLike(d))
- );
- }
- if (ts.isIdentifier(expr) && depth === 0) {
- const symbol = lowerer.resolveValueSymbol(expr);
- const declaration = symbol ? symbol.valueDeclaration : undefined;
- return (
- declaration !== undefined &&
- ts.isVariableDeclaration(declaration) &&
- ts.isIdentifier(declaration.name) &&
- declaration.initializer !== undefined &&
- (ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const) !== 0 &&
- javaScriptAnyOrigin(lowerer, declaration.initializer, depth + 1)
- );
- }
- return false;
-}
-
/** Destructuring declarations, desugared as sugar over indexed/field
* reads: the initializer evaluates ONCE into a hidden temp, then each
* bound name declares (or assigns its pre-registered module global) from
@@ -2163,18 +2143,18 @@ export function lowerDestructuringDecl(
const parseArgsDynObject =
init.type.kind === "dyn" &&
isParseArgsDynCheckerType(lowerer, lowerer.typeOf(decl.initializer));
- // An `any`-origin source needs a represented record or array shape for
- // destructuring. JS files retain their per-site runtime fences.
+ // An `any`-origin source destructures through a represented record or
+ // array shape, or through the checked-dynamic value it lowered to: the
+ // pattern reads members (or iterates) the dyn value exactly as a
+ // JavaScript file's pattern does, with the same per-site runtime fences
+ // (null/undefined sources throw Node's TypeError). Primitive and unit
+ // sources keep their own fences below; other carriers keep this one.
if (
lowerer.anyOrigin(decl.initializer) &&
- !isJsSourceFile(decl.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
init.type.kind !== "record" &&
init.type.kind !== "array" &&
- !(
- init.type.kind === "dyn" &&
- ts.isObjectBindingPattern(decl.name) &&
- javaScriptAnyOrigin(lowerer, decl.initializer, 0)
- )
+ init.type.kind !== "dyn"
) {
lowerer.anyOpFence("destructuring", decl);
}
@@ -2187,7 +2167,7 @@ export function lowerDestructuringDecl(
// the checker's own type instead of the carrier.
if (
init.type.kind === "string" &&
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!checkerStringSource(lowerer, decl.initializer)
) {
lowerer.unsupported(
@@ -2205,7 +2185,7 @@ export function lowerDestructuringDecl(
init.type.kind === "bool" ||
init.type.kind === "string" ||
isUnitType(init.type)) &&
- !isJsSourceFile(decl.getSourceFile()) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!(
init.type.kind === "string" &&
staticStringPattern(lowerer, decl.name as ts.ArrayBindingPattern | ts.ObjectBindingPattern)
@@ -2229,7 +2209,7 @@ export function lowerDestructuringDecl(
// (the message is the fence's — a unit value here is the failure
// path either way). TypeScript keeps the fence: narrowing first is
// the annotated fix.
- if (isJsSourceFile(decl.getSourceFile()) && init.type.kind === "union") {
+ if (isJsSourceFile(ts.getSourceFileOfNode(decl)) && init.type.kind === "union") {
const def = lowerer.unions.get(init.type.unionId);
const dataArms = (def?.arms ?? []).filter((a) => !isUnitType(a));
const arm = dataArms[0];
@@ -2279,6 +2259,50 @@ export function lowerDestructuringDecl(
if (groupsSource !== null) init = groupsSource;
const tmp = lowerer.declareHiddenLocal("%destr", init.type);
out.push({ kind: "varDecl", localId: tmp.id, init, loc });
+ // A TypeScript `any` source destructured through its checked-dynamic
+ // value: RequireObjectCoercible with V8's own TypeError ("Cannot
+ // destructure property 'a' of 'f(...)' as it is null.") for the source
+ // spellings the assignment form reproduces; other sources are refused,
+ // as in the assignment form. JavaScript files keep their existing
+ // per-read errors.
+ if (
+ init.type.kind === "dyn" &&
+ ts.isObjectBindingPattern(decl.name) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ lowerer.anyOrigin(decl.initializer)
+ ) {
+ const spelling = destrSpellingOf(decl.initializer);
+ if (spelling === null) {
+ lowerer.unsupported(
+ "SC1031",
+ decl.initializer,
+ "destructuring declaration from an `any` source whose expression spelling is unsupported (use a variable, ordinary property chain, or ordinary call)",
+ );
+ }
+ const first = decl.name.elements.nodes[0];
+ const firstKey =
+ first === undefined || first.dotDotDotToken !== undefined
+ ? undefined
+ : (first.propertyName ?? first.name);
+ // V8 names the first property when its key is an identifier, a string
+ // or a number; a computed first key takes the bare (source-only) form.
+ const firstProp =
+ firstKey === undefined || ts.isComputedPropertyName(firstKey)
+ ? undefined
+ : (patternKeyNameOf(lowerer, firstKey as ts.PropertyName) ?? undefined);
+ out.push({
+ kind: "exprStmt",
+ expr: {
+ kind: "dynDestrCheck",
+ value: { kind: "varRef", localId: tmp.id, type: init.type, loc },
+ spelling,
+ ...(firstProp !== undefined ? { firstProp } : {}),
+ type: init.type,
+ loc,
+ },
+ loc,
+ });
+ }
// V8's destructuring TypeError spells STATIC sources: an identifier
// source reads " is not iterable", an identifier-callee call
// " is not a function or its return value is not iterable";
@@ -2325,7 +2349,7 @@ export function matchGroupsPatternSource(
!ts.isObjectBindingPattern(pattern) ||
pattern.elements.nodes.length === 0 ||
!(row.type.kind === "dyn"
- ? isJsSourceFile(pattern.getSourceFile())
+ ? isJsSourceFile(ts.getSourceFileOfNode(pattern))
: isMatchSliceType(lowerer, row.type)) ||
!pattern.elements.nodes.every((el) => {
if (el.dotDotDotToken !== undefined || el.name === undefined) return false;
@@ -2368,7 +2392,9 @@ export function lowerBindingPattern(
const patternType = lowerer.typeOf(pattern);
const patternSymbol = patternType.getSymbol();
allowDynObject ||=
- patternSymbol?.name === "SegmentData" && lowerer.isStdlibSymbol(patternSymbol);
+ patternSymbol !== undefined &&
+ ts.symbolName(patternSymbol) === "SegmentData" &&
+ lowerer.isStdlibSymbol(patternSymbol);
allowDynObject ||=
(patternType.flags & ts.TypeFlags.Object) !== 0 &&
lowerer.mapTypeOf(patternType)?.kind === "dyn";
@@ -2398,20 +2424,30 @@ export function lowerBindingPattern(
lowerer.fenceStaticHeadersIteration(pattern);
if (
srcType.kind === "union" &&
- lowerer.unions.get(srcType.unionId)?.arms.every((arm) => arm.kind === "array")
+ lowerer.unions
+ .get(srcType.unionId)
+ ?.arms.every(
+ (arm) =>
+ arm.kind === "array" ||
+ arm.kind === "string" ||
+ (arm.kind === "record" && lowerer.shapes.get(arm.shapeId)?.tuple === true),
+ ) &&
+ lowerer.unions.get(srcType.unionId)!.arms.some((arm) => arm.kind !== "string")
) {
for (let i = 0; i < pattern.elements.nodes.length; i++) {
const el = pattern.elements.nodes[i]!;
if (ts.isOmittedExpression(el) || el.name === undefined) continue;
if (el.dotDotDotToken)
- lowerer.unsupported("SC1031", el, "rest elements over a union of array layouts");
+ lowerer.unsupported("SC1031", el, "rest elements over a union of array or tuple layouts");
const loc = locOf(el);
- let value = unionArrayValueRead(lowerer, srcRef(), numLit(i, loc), loc);
+ let value = unionArrayValueRead(lowerer, srcRef(), numLit(i, loc), loc, (text) =>
+ strCharsCall(lowerer, text, loc),
+ );
if (!value)
lowerer.unsupported(
"SC1031",
el,
- "array union elements without a common native representation",
+ "array or tuple union elements without a common native representation",
);
if (el.initializer) value = applyBindingDefault(lowerer, el, value);
lowerer.bindPatternTarget(el.name, value, isLet, out);
@@ -2816,7 +2852,7 @@ export function lowerBindingPattern(
// object patterns use the same exact keyed-read desugar.
if (
srcType.kind === "union" &&
- isJsSourceFile(pattern.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(pattern)) &&
lowerer.dynConvertible(srcType)
) {
lowerer.lowerBindingPattern(
@@ -3100,6 +3136,11 @@ export function lowerBindingPattern(
lowerArrayPositionObjectPattern(lowerer, pattern, srcRef, srcType, isLet, out);
return;
}
+ const nsSource = lowerer.sourceFileOfModuleNamespace(srcType);
+ if (nsSource !== null) {
+ lowerModuleNamespacePattern(lowerer, pattern, nsSource, isLet, out);
+ return;
+ }
if (srcType.kind !== "record") {
lowerer.unsupported(
"SC1031",
@@ -3219,7 +3260,7 @@ export function lowerBindingPattern(
lowerer.bindPatternTarget(el.name, value, isLet, out);
continue;
}
- if (!fieldType && isJsSourceFile(el.getSourceFile())) {
+ if (!fieldType && isJsSourceFile(ts.getSourceFileOfNode(el))) {
// Inferred JavaScript shapes can omit optional own properties and
// inherit values from their prototype. Read the live checked view.
const loc = locOf(el);
@@ -3353,10 +3394,73 @@ function patternBindingType(lowerer: Lowerer, name: ts.BindingName): IrType | nu
const type = lowerer.typeOf(name);
return (
lowerer.mapTypeOf(type) ??
- (isJsSourceFile(name.getSourceFile()) ? dynFallbackType(lowerer, name, type) : null)
+ (isJsSourceFile(ts.getSourceFileOfNode(name)) ? dynFallbackType(lowerer, name, type) : null)
);
}
+/** `const { a, b: c } = ns` / `const { f } = await import("./m")` over
+ * a program module's namespace: each element reads the export exactly
+ * like `ns.a` does — the member resolves to the exporter's own binding
+ * (nsMemberIdentOf's rule), read once at the pattern's position, so the
+ * bound name holds the export's value at that moment (destructuring
+ * copies; it does not alias the live binding). The namespace value
+ * itself was already evaluated into the pattern's temp, so its side
+ * effects (the dynamic import) keep their order. Rest elements would
+ * materialize the namespace object and keep their fence. The exports
+ * come from the source's module, not from the pattern's type: an
+ * annotation can name another module with a compatible shape. */
+function lowerModuleNamespacePattern(
+ lowerer: Lowerer,
+ pattern: ts.ObjectBindingPattern,
+ source: ts.SourceFile,
+ isLet: boolean,
+ out: IrStmt[],
+): void {
+ const moduleSym = lowerer.checker.getSymbolAtLocation(source);
+ if (moduleSym === undefined) {
+ lowerer.unsupported(
+ "SC1031",
+ pattern,
+ "destructuring a module namespace without a module symbol",
+ );
+ }
+ const nsType = lowerer.checker.getTypeOfSymbol(moduleSym);
+ for (const el of pattern.elements.nodes) {
+ if (el.name === undefined) continue;
+ if (el.dotDotDotToken) {
+ lowerer.unsupported(
+ "SC1031",
+ el,
+ "rest bindings over module namespaces (the namespace object is not materialized — bind the exports you need by name)",
+ );
+ }
+ const prop = el.propertyName ?? el.name;
+ const propName =
+ ts.isIdentifier(prop) ||
+ ts.isComputedPropertyName(prop) ||
+ ts.isStringLiteralLike(prop) ||
+ ts.isNumericLiteral(prop)
+ ? patternKeyNameOf(lowerer, prop as ts.PropertyName)
+ : null;
+ let exported =
+ propName === null ? undefined : lowerer.checker.getPropertyOfType(nsType, propName);
+ if (exported !== undefined && exported.flags & ts.SymbolFlags.Alias) {
+ exported = lowerer.checker.getAliasedSymbol(exported);
+ }
+ const declared = exported?.valueDeclaration?.name as ts.Node | undefined;
+ if (declared === undefined || !ts.isIdentifier(declared)) {
+ lowerer.unsupported(
+ "SC1031",
+ el,
+ "destructuring module namespace members that are not named value exports",
+ );
+ }
+ let value = lowerer.lowerExpr(declared);
+ if (el.initializer) value = applyBindingDefault(lowerer, el, value);
+ lowerer.bindPatternTarget(el.name, value, isLet, out);
+ }
+}
+
function patternKeyNameOf(lowerer: Lowerer, prop: ts.PropertyName): string | null {
if (ts.isIdentifier(prop) || ts.isPrivateIdentifier(prop)) return prop.text;
if (ts.isComputedPropertyName(prop)) return lowerer.foldedStringKeyOf(prop.expression);
@@ -3383,7 +3487,7 @@ function isRegexMatchResultPattern(lowerer: Lowerer, pattern: ts.ObjectBindingPa
const symbol = type.getAliasSymbol() ?? type.getSymbol();
return (
symbol !== undefined &&
- (symbol.name === "RegExpExecArray" || symbol.name === "RegExpMatchArray") &&
+ (ts.symbolName(symbol) === "RegExpExecArray" || ts.symbolName(symbol) === "RegExpMatchArray") &&
lowerer.isStdlibSymbol(symbol)
);
}
@@ -3694,7 +3798,7 @@ function stdlibGlobalTokenDestructure(
decl: ts.VariableDeclaration,
isLet: boolean,
): IrStmt[] | null {
- if (decl.initializer === undefined || !isJsSourceFile(decl.getSourceFile())) return null;
+ if (decl.initializer === undefined || !isJsSourceFile(ts.getSourceFileOfNode(decl))) return null;
const globalName = stdlibGlobalNameOf(lowerer, decl.initializer);
if (globalName === null) return null;
if (!ts.isObjectBindingPattern(decl.name)) {
@@ -3770,9 +3874,12 @@ function stdlibGlobalTokenDestructure(
`destructuring the non-builtin member '${propName}' of 'globalThis'`,
);
}
- const canonical = member.name === "global" ? "globalThis" : member.name;
+ const canonical = ts.symbolName(member) === "global" ? "globalThis" : ts.symbolName(member);
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 +3923,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({
@@ -4367,7 +4478,7 @@ function classInstanceRestValue(
const deferOrderCheck =
lowerer.instantiationContext !== null &&
lowerer.onEdge !== null &&
- !isJsSourceFile(blame.getSourceFile());
+ !isJsSourceFile(ts.getSourceFileOfNode(blame));
if (!deferOrderCheck) {
if (!orderMatches(emitOrder)) {
lowerer.unsupported(
@@ -4385,7 +4496,7 @@ function classInstanceRestValue(
// matters.
const context = lowerer.instantiationContext;
const countFailure = !lowerer.suppressStats;
- const sf = blame.getSourceFile();
+ const sf = ts.getSourceFileOfNode(blame);
lowerer.shapeOrderMetadataFinalizers.push(() => {
const current = lowerer.shapes.get(restT.shapeId) ?? restShape;
const currentOrder = current.declaredOrder ?? current.fields.map((f) => f.name);
@@ -4646,7 +4757,7 @@ function inferredEvolvingLetType(lowerer: Lowerer, decl: ts.VariableDeclaration)
if (
ts.isCallExpression(node.right) &&
(implicitMethodCallInfersReturn(lowerer, node.right) ||
- isJsSourceFile(node.right.getSourceFile()))
+ isJsSourceFile(ts.getSourceFileOfNode(node.right)))
) {
invalid = true;
return "stop";
@@ -4725,6 +4836,10 @@ export function lowerVarDecl(
}
}
if (!ts.isIdentifier(decl.name)) lowerer.unsupported("SC1031", decl.name);
+ {
+ const alias = lowerTupleAliasDecl(lowerer, decl);
+ if (alias) return alias;
+ }
// A NULLISH generic binding (`const i: I = null as any` — the
// declared type has no mapping, the value is provably null/undefined
@@ -4763,9 +4878,9 @@ export function lowerVarDecl(
(decl.name.text === "module" || decl.name.text === "exports") &&
ts.isVariableStatement(decl.parent?.parent) &&
ts.isSourceFile(decl.parent?.parent?.parent) &&
- !isJsSourceFile(decl.getSourceFile()) &&
- !decl
- .getSourceFile()
+ !isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ !ts
+ .getSourceFileOfNode(decl)
.statements.nodes.some(
(s) =>
ts.isImportDeclaration(s) ||
@@ -4970,7 +5085,7 @@ export function lowerVarDecl(
: undefined,
);
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!hasJsTypeAnnotation(decl) &&
raw.type.kind === "dyn" &&
@@ -5042,7 +5157,7 @@ export function lowerVarDecl(
// immediately (the type covers the unassigned state), so they
// initialize to the interned undefined arm like an omitted optional.
let type = lowerer.irTypeOf(decl.name);
- if (type.kind === "dyn" && isLet && isJsSourceFile(decl.getSourceFile())) {
+ if (type.kind === "dyn" && isLet && isJsSourceFile(ts.getSourceFileOfNode(decl))) {
type = inferredEvolvingLetType(lowerer, decl) ?? type;
}
// `var v: void;` / `let x: undefined;` — a unit-only binding rides
@@ -5069,7 +5184,7 @@ export function lowerVarDecl(
// function (with its hint), not the unmappable declared type. If it
// poisons, still declare the local when its type is representable, so
// later references to this name don't produce cascading errors.
- const inferredObjectType = isJsSourceFile(decl.getSourceFile())
+ const inferredObjectType = isJsSourceFile(ts.getSourceFileOfNode(decl))
? lowerer.mapTypeOf(lowerer.typeOf(decl.name))
: null;
const inferredObjectArms =
@@ -5086,7 +5201,7 @@ export function lowerVarDecl(
inferredObjectArms.some(
(arm) => arm.kind === "record" && jsObjectBindingExpands(lowerer, decl, arm),
) ||
- (isJsSourceFile(decl.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!hasJsTypeAnnotation(decl) &&
ts.isObjectLiteralExpression(decl.initializer) &&
@@ -5102,7 +5217,7 @@ export function lowerVarDecl(
try {
init =
expandsObject ||
- (isJsSourceFile(decl.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
isNativeBuiltinValueInitializer(lowerer, decl.initializer))
? lowerer.lowerExprExpecting(decl.initializer, DYN)
@@ -5139,12 +5254,22 @@ export function lowerVarDecl(
(init.type.kind === "moduleNs" ? init.type : null) ??
(init.type.kind === "dyn" ? DYN : null);
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!hasJsTypeAnnotation(decl) &&
init.type.kind === "func"
)
type = init.type;
+ // A typed-arguments function expression (typed-arguments.ts): the
+ // checker types the binding variadic, but its value has the fixed
+ // signature every use asserts.
+ if (
+ !decl.type &&
+ init.type.kind === "func" &&
+ (lowerer.typedArgumentsLambdas.has(stripTypeWrappers(decl.initializer)) ||
+ lowerer.nullishFuncResults.has(stripTypeWrappers(decl.initializer)))
+ )
+ type = init.type;
// A TypeScript lambda whose closure returns the runtime result (a missing
// read, a `??` default of another type) instead of the checker's bare
// return type: the binding keeps the checker's parameters and takes the
@@ -5160,8 +5285,8 @@ export function lowerVarDecl(
if (
lowerer.implicitParamTypes !== null &&
init.type.kind === "dyn" &&
- isJsSourceFile(decl.getSourceFile()) &&
- npmStaticPackageOfPath(decl.getSourceFile().fileName) !== null
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ npmStaticPackageOfPath(ts.getSourceFileOfNode(decl).fileName) !== null
) {
type = DYN;
}
@@ -5169,17 +5294,17 @@ export function lowerVarDecl(
init.type.kind === "dyn" &&
(inferredObjectType?.kind === "record" ||
inferredObjectType?.kind === "array" ||
- (isJsSourceFile(decl.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
inferredObjectArms.some((arm) => arm.kind === "record") &&
inferredObjectArms.every((arm) => isUnitType(arm) || arm.kind === "record")) ||
- (isJsSourceFile(decl.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
inferredObjectType !== null &&
(inferredObjectType.kind === "date" ||
inferredObjectType.kind === "func" ||
inferredObjectType.kind === "object" ||
jsOpenObjectType(decl, inferredObjectType, lowerer.shapes, lowerer.unions).kind ===
"dyn")) ||
- (isJsSourceFile(decl.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
inferredObjectType !== null &&
(inferredObjectType.kind === "void" ||
isUnitType(inferredObjectType) ||
@@ -5197,14 +5322,14 @@ export function lowerVarDecl(
expandsObject ||
preservesObjectIdentity ||
keepsAssertedObject ||
- (isJsSourceFile(decl.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
isNativeBuiltinValueInitializer(lowerer, decl.initializer))
) {
type = DYN;
}
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
init.type.kind === "array" &&
init.type.elem.kind === "dyn" &&
@@ -5220,14 +5345,14 @@ export function lowerVarDecl(
// cannot hold the binding's future. The binding is checked-dynamic
// instead (irTypeOf's JS story): writes dynFrom, typed exits dynCheck.
// Const keeps the static empty record — no reassignment exists.
- if (isLet && type?.kind === "record" && isJsSourceFile(decl.getSourceFile())) {
+ if (isLet && type?.kind === "record" && isJsSourceFile(ts.getSourceFileOfNode(decl))) {
const shape = lowerer.shapes.get(type.shapeId);
if (shape && shape.fields.length === 0 && !shape.indexValue && !shape.tuple) type = DYN;
}
if (
isLet &&
type !== null &&
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!hasJsTypeAnnotation(decl) &&
(type.kind === "object" || type.kind === "symbol" || jsBindingHasOpenWrites(lowerer, decl))
@@ -5251,7 +5376,7 @@ export function lowerVarDecl(
if (
returnedEmptyArrayType === null &&
(type === null || type.kind === "array") &&
- isJsSourceFile(decl.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(decl))
) {
let initExpr: ts.Expression = decl.initializer;
while (ts.isParenthesizedExpression(initExpr)) initExpr = initExpr.expression;
@@ -5314,7 +5439,7 @@ export function lowerVarDecl(
if (
type?.kind === "func" &&
init.type.kind === "dyn" &&
- (isJsSourceFile(decl.getSourceFile()) ||
+ (isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
(!decl.type &&
!hasJsTypeAnnotation(decl) &&
type.rest === true &&
@@ -5323,7 +5448,7 @@ export function lowerVarDecl(
type = DYN;
}
if (
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!decl.type &&
!hasJsTypeAnnotation(decl) &&
init.type.kind === "dyn" &&
@@ -5350,7 +5475,7 @@ export function lowerVarDecl(
// type (a union with a fence-folded arm — the typeof-'bigint'
// dual-mode ternary) adopts the initializer's static type, the
// same rule genuine `any` consts take.
- (type === null && isJsSourceFile(decl.getSourceFile()))) &&
+ (type === null && isJsSourceFile(ts.getSourceFileOfNode(decl)))) &&
init.type.kind !== "void" &&
init.type.kind !== "caught" &&
!isUnitType(init.type)
@@ -5371,7 +5496,7 @@ export function lowerVarDecl(
let settledType: IrType = type;
if (
!isLet &&
- isJsSourceFile(decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
!hasJsTypeAnnotation(decl) &&
decl.initializer &&
ts.isNewExpression(decl.initializer)
@@ -5643,7 +5768,7 @@ function jsObjectBindingExpands(
decl.type ||
!decl.initializer ||
!ts.isIdentifier(decl.name) ||
- !isJsSourceFile(decl.getSourceFile())
+ !isJsSourceFile(ts.getSourceFileOfNode(decl))
)
return false;
let initializer = decl.initializer;
@@ -5753,7 +5878,10 @@ function jsObjectBindingExpands(
export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean {
const statement = decl.parent?.parent;
return (
- !!statement && /@type\b/.test(decl.getSourceFile().text.slice(statement.pos, decl.getStart()))
+ !!statement &&
+ /@type\b/.test(
+ ts.getSourceFileOfNode(decl).text.slice(statement.pos, ts.tokenStartOfNode(decl)),
+ )
);
}
@@ -5761,7 +5889,7 @@ export function hasJsTypeAnnotation(decl: ts.VariableDeclaration): boolean {
* an any-valued callback. Keep those bindings open, including captured writes. */
export function jsBindingHasOpenWrites(lowerer: Lowerer, decl: ts.VariableDeclaration): boolean {
if (
- !isJsSourceFile(decl.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
decl.type ||
hasJsTypeAnnotation(decl) ||
!ts.isIdentifier(decl.name)
@@ -6263,7 +6391,7 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt
}
if (accessor && errorBase) return { kind: "exprStmt", expr: obj, loc };
}
- if (isJsSourceFile(expr.getSourceFile()) && obj.type.kind === "object") {
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr)) && obj.type.kind === "object") {
const info = lowerer.classes.get(obj.type.className);
const keyType = ts.isElementAccessExpression(target)
? lowerer.typeOf(target.argumentExpression)
@@ -6284,7 +6412,7 @@ function lowerDeleteStatement(lowerer: Lowerer, expr: ts.DeleteExpression): IrSt
}
}
if (
- isJsSourceFile(expr.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
obj.type.kind === "func" &&
lowerer.dynConvertible(obj.type)
) {
@@ -6395,7 +6523,7 @@ function lowerCjsExportStatement(
const symbol =
lowerer.checker.getSymbolAtLocation(nameNode) ??
(ts.isIdentifier(nameNode) || ts.isPrivateIdentifier(nameNode)
- ? lowerer.cjsModuleExportSymbol(cjs.expr.getSourceFile(), nameNode.text)
+ ? lowerer.cjsModuleExportSymbol(ts.getSourceFileOfNode(cjs.expr), nameNode.text)
: undefined);
const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined;
if (!g) {
@@ -6470,7 +6598,7 @@ function lowerCjsExportStatement(
// later real assignment keep the ordinary path (the export IS
// undefined then).
if (cjsVoidInitPreambleReals(cjs) !== null) return { kind: "block", body: [], loc };
- const sf = cjs.expr.getSourceFile();
+ const sf = ts.getSourceFileOfNode(cjs.expr);
let root: IrGlobal | undefined;
for (const stmt of sf.statements.nodes) {
const whole = cjsExportAssignmentOf(stmt);
@@ -6533,7 +6661,7 @@ function lowerCjsExportStatement(
// were already evaluated by the const's own declaration, so this
// statement may not RE-evaluate anything: expression-valued entries
// (which the direct-table lowering assigns here) keep a fence.
- tableObj = cjsExportTargetLiteral(cjs.expr.right, cjs.expr.getSourceFile());
+ tableObj = cjsExportTargetLiteral(cjs.expr.right, ts.getSourceFileOfNode(cjs.expr));
resolvedTarget = tableObj !== null;
if (!tableObj) {
const single = lowerCjsSingleValueExport(lowerer, cjs.expr, loc);
@@ -6595,7 +6723,7 @@ function lowerCjsExportStatement(
lowerer.rejectThisInObjectMethod(method.body);
const symbol =
lowerer.checker.getSymbolAtLocation(method.name) ??
- lowerer.cjsModuleExportSymbol(cjs.expr.getSourceFile(), method.name.text);
+ lowerer.cjsModuleExportSymbol(ts.getSourceFileOfNode(cjs.expr), method.name.text);
const g = symbol ? lowerer.globalsBySymbol.get(symbol) : undefined;
if (g) {
const fn = lowerer.lowerLambda(method);
@@ -6870,7 +6998,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
// global — the expression path owns it (lowerCreateRequireCall).
createRequireCalleeFileOf(lowerer, expr.expression) === null
) {
- const sf = expr.getSourceFile();
+ const sf = ts.getSourceFileOfNode(expr);
const spec = requireSpecOf(expr)!;
if (lowerer.externalTypes.has(spec)) lowerer.externalHostFence(spec, expr, false);
// A bare require of a package NOTHING INSTALLED resolves, in ANY
@@ -6910,6 +7038,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(ts.getSourceFileOfNode(expr)) &&
+ 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) };
@@ -7401,7 +7548,7 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
if (
recv &&
(recv.type.kind === "dyn" ||
- (isJsSourceFile(expr.getSourceFile()) &&
+ (isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
recv.type.kind === "union" &&
lowerer.dynConvertible(recv.type)))
) {
@@ -7422,6 +7569,34 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
loc,
};
}
+ // `err.code = v` on an Error value (`err as Error & { code?:
+ // string }`): typed reads come from the runtime error's code
+ // slot, so the write lands there. A value that is not statically
+ // a string checks at runtime: strings take the slot, anything
+ // else clears it and stays a dynamic own property. The
+ // system-error members (errno, syscall, path, dest) read
+ // through the checked-dynamic node and keep the dynamic write, as
+ // do JavaScript files, whose reads use that node too.
+ if (
+ !isJsSourceFile(ts.getSourceFileOfNode(expr)) &&
+ recv?.type.kind === "object" &&
+ lowerer.isErrorRootedClass(recv.type.className) &&
+ expr.left.name.text === "code"
+ ) {
+ const loc = locOf(expr);
+ const stringValue = isStringTyped(lowerer.typeOf(expr.right));
+ return {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: stringValue ? "error.setCode" : "error.setCodeDyn",
+ args: [recv, lowerer.lowerExprExpecting(expr.right, stringValue ? STRING : DYN)],
+ type: VOID,
+ loc,
+ },
+ loc,
+ };
+ }
if (recv?.type.kind === "object" && recv.type.className === "%Error") {
const loc = locOf(expr);
return {
@@ -7475,6 +7650,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(
@@ -7651,8 +7851,8 @@ function lowerDiscardedLogical(lowerer: Lowerer, expr: ts.BinaryExpression): IrS
// picks another lowering, exactly as before.
if (!(e instanceof PoisonError) || lowerer.diagSink !== null) throw e;
const added = lowerer.diags.slice(diagsBefore);
- const file = expr.getSourceFile().fileName;
- const start = expr.getStart();
+ const file = ts.getSourceFileOfNode(expr).fileName;
+ const start = ts.tokenStartOfNode(expr);
const local =
added.length > 0 &&
added.every(
@@ -7802,16 +8002,42 @@ export function lowerDestructuringAssign(
/** V8's spelling of a destructuring RHS in its TypeError message
* ("Cannot destructure 'a' as it is undefined.") — plain identifiers
- * spell their name (parens unwrap), and calls append an ellipsis to a
- * supported callee spelling. Other CallPrinter forms keep a fence. */
+ * spell their name (parens unwrap), `this`, property chains and
+ * identifier or numeric element reads spell their source, awaited and
+ * yielded values spell "(intermediate value)", and calls append an
+ * ellipsis to a supported callee spelling. Other CallPrinter forms keep
+ * a fence. */
function destrSpellingOf(rhs: ts.Expression): string | null {
let e = rhs;
- while (ts.isParenthesizedExpression(e)) e = e.expression;
+ // Parentheses and the type-only wrappers are not part of the emitted
+ // JavaScript expression V8 prints.
+ while (
+ ts.isParenthesizedExpression(e) ||
+ ts.isAsExpression(e) ||
+ ts.isSatisfiesExpression(e) ||
+ ts.isTypeAssertion(e) ||
+ ts.isNonNullExpression(e)
+ ) {
+ e = e.expression;
+ }
if (ts.isIdentifier(e)) return e.text;
+ if (e.kind === ts.SyntaxKind.ThisKeyword) return "this";
+ // V8 prints an awaited or yielded operand as "(intermediate value)".
+ if (ts.isAwaitExpression(e) || ts.isYieldExpression(e)) return "(intermediate value)";
if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
const base = destrSpellingOf(e.expression);
return base === null ? null : `${base}.${e.name.text}`;
}
+ // `table[key]` and `list[0]`; V8 prints a numeric key in its canonical
+ // number form (`o[1.50]` reads "o[1.5]").
+ if (ts.isElementAccessExpression(e) && !e.questionDotToken) {
+ const base = destrSpellingOf(e.expression);
+ const key = e.argumentExpression;
+ if (base === null) return null;
+ if (ts.isIdentifier(key)) return `${base}[${key.text}]`;
+ if (ts.isNumericLiteral(key)) return `${base}[${String(Number(key.text))}]`;
+ return null;
+ }
if (ts.isCallExpression(e) && !e.questionDotToken) {
const callee = destrSpellingOf(e.expression);
return callee === null ? null : `${callee}(...)`;
@@ -7897,7 +8123,7 @@ function destructuringAssignInto(
const patternEmpty = ts.isObjectLiteralExpression(target)
? target.properties.nodes.length === 0
: target.elements.nodes.length === 0;
- if (!patternEmpty && rhs !== null && isJsSourceFile(rhs.getSourceFile())) {
+ if (!patternEmpty && rhs !== null && isJsSourceFile(ts.getSourceFileOfNode(rhs))) {
const globalName = stdlibGlobalNameOf(lowerer, rhs);
if (globalName !== null) {
lowerer.unsupported(
@@ -8914,8 +9140,15 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
) {
return lowerForAwaitStdin(lowerer, stmt);
}
+ // A checked-dynamic NodeJS.ReadableStream iterates its native stream.
+ const dynStream = lowerForAwaitDynStream(lowerer, stmt, () =>
+ lowerForAwaitBuiltin(lowerer, stmt),
+ );
+ if (dynStream) return dynStream;
const builtin = lowerForAwaitBuiltin(lowerer, stmt);
if (builtin) return builtin;
+ // A NodeJS.ReadableStream iterates as the general Readable.
+ requestGeneralStreamSurface(lowerer, stmt.expression);
lowerer.unsupported(
"SC1070",
stmt,
@@ -9149,6 +9382,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);
@@ -10675,7 +10918,7 @@ function lowerForIn(lowerer: Lowerer, stmt: ts.ForInStatement): IrStmt {
while (root?.base) root = root.base;
if (
info?.decl &&
- isJsSourceFile(info.decl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
!info.def.runtime &&
!root?.def.runtime &&
!info.builtinError &&
@@ -11205,6 +11448,141 @@ export function stdlibBufferVarAlias(lowerer: Lowerer, decl: ts.VariableDeclarat
);
}
+/** A checker type that is a string or string literal (or a union of them). */
+function isStringTyped(t: ts.Type): boolean {
+ if (t.isUnionType()) return ts.constituentTypes(t).every(isStringTyped);
+ return (t.flags & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral)) !== 0;
+}
+
+/** The Error-subclass prototype reset of transpiled code, inside a class
+ * constructor:
+ *
+ * const proto = new.target.prototype;
+ * if (Object.setPrototypeOf) Object.setPrototypeOf(this, proto);
+ * else this.__proto__ = proto;
+ *
+ * (also `Object.setPrototypeOf(this, new.target.prototype)` and
+ * `Object.setPrototypeOf(this, C.prototype)` in C's own constructor when no
+ * class extends C). A compiled instance already has exactly that prototype,
+ * so each statement is a no-op. True for those statements. */
+function ownPrototypeResetStatement(lowerer: Lowerer, stmt: ts.Statement): boolean {
+ if (ts.isVariableStatement(stmt)) {
+ const decls = stmt.declarationList.declarations.nodes;
+ if (decls.length !== 1 || (stmt.declarationList.flags & ts.NodeFlags.Const) === 0) return false;
+ const decl = decls[0]!;
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) return false;
+ const ctor = enclosingConstructor(decl);
+ if (!ctor || !ownPrototypeRef(lowerer, decl.initializer, ctor)) return false;
+ const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
+ if (!symbol) return false;
+ // Every other use of the binding must be one of the reset statements.
+ let onlyResets = true;
+ ts.walkPreorder(ctor, (n) => {
+ if (!onlyResets) return "stop";
+ if (!ts.isIdentifier(n) || n === decl.name) return undefined;
+ if (lowerer.checker.getSymbolAtLocation(n) !== symbol) return undefined;
+ const statement = n.parent?.parent;
+ if (
+ !statement ||
+ !ts.isExpressionStatement(statement) ||
+ resetTarget(lowerer, statement.expression) !== n
+ )
+ onlyResets = false;
+ return undefined;
+ });
+ return onlyResets;
+ }
+ if (!ts.isExpressionStatement(stmt)) return false;
+ const ctor = enclosingConstructor(stmt);
+ if (!ctor) return false;
+ const target = resetTarget(lowerer, stmt.expression);
+ return target !== null && ownPrototypeRef(lowerer, target, ctor);
+}
+
+/** The prototype operand of `Object.setPrototypeOf(this, P)` or
+ * `this.__proto__ = P`, or null. */
+function resetTarget(lowerer: Lowerer, expr: ts.Expression): ts.Expression | null {
+ if (
+ ts.isCallExpression(expr) &&
+ expr.arguments.nodes.length === 2 &&
+ expr.arguments.nodes[0]!.kind === ts.SyntaxKind.ThisKeyword &&
+ ts.isPropertyAccessExpression(expr.expression) &&
+ expr.expression.name.text === "setPrototypeOf" &&
+ lowerer.isStdlibGlobal(expr.expression.expression, "Object")
+ )
+ return expr.arguments.nodes[1]!;
+ if (
+ ts.isBinaryExpression(expr) &&
+ expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
+ ts.isPropertyAccessExpression(expr.left) &&
+ expr.left.expression.kind === ts.SyntaxKind.ThisKeyword &&
+ expr.left.name.text === "__proto__"
+ )
+ return expr.right;
+ return null;
+}
+
+/** The class constructor whose body holds `node`, crossing only statements
+ * and blocks (any nested function rebinds `this`). */
+function enclosingConstructor(node: ts.Node): ts.ConstructorDeclaration | null {
+ for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
+ if (ts.isConstructorDeclaration(cur)) return cur;
+ if (
+ ts.isFunctionLike(cur) ||
+ ts.isClassDeclaration(cur) ||
+ ts.isClassExpression(cur) ||
+ ts.isSourceFile(cur)
+ )
+ return null;
+ }
+ return null;
+}
+
+/** Whether `node` denotes the prototype of the object `ctor` constructs:
+ * `new.target.prototype`, a const holding it, or the class's own
+ * `C.prototype` when no class extends C. */
+function ownPrototypeRef(
+ lowerer: Lowerer,
+ node: ts.Expression,
+ ctor: ts.ConstructorDeclaration,
+): boolean {
+ while (ts.isParenthesizedExpression(node)) node = node.expression;
+ if (ts.isPropertyAccessExpression(node) && node.name.text === "prototype") {
+ const base = node.expression;
+ if (
+ ts.isMetaProperty(base) &&
+ base.keywordToken === ts.SyntaxKind.NewKeyword &&
+ base.name.text === "target"
+ )
+ return true;
+ const cls = ctor.parent;
+ if (!ts.isIdentifier(base) || !(ts.isClassDeclaration(cls) || ts.isClassExpression(cls)))
+ return false;
+ const symbol = lowerer.resolveValueSymbol(base);
+ const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
+ if (!info || symbol!.valueDeclaration !== cls) return false;
+ for (const other of lowerer.classes.values()) {
+ if (other !== info && lowerer.isSubclassOf(other.def.name, info.def.name)) return false;
+ }
+ return true;
+ }
+ if (ts.isIdentifier(node)) {
+ const symbol = lowerer.resolveValueSymbol(node);
+ const decl = symbol?.valueDeclaration;
+ return (
+ decl !== undefined &&
+ ts.isVariableDeclaration(decl) &&
+ decl.initializer !== undefined &&
+ ts.isVariableDeclarationList(decl.parent) &&
+ (decl.parent.flags & ts.NodeFlags.Const) !== 0 &&
+ enclosingConstructor(decl) === ctor &&
+ !ts.isIdentifier(decl.initializer) &&
+ ownPrototypeRef(lowerer, decl.initializer, ctor)
+ );
+ }
+ return false;
+}
+
function isAssertionInitializer(node: ts.Expression): boolean {
let e = node;
while (ts.isParenthesizedExpression(e)) e = e.expression;
diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts
index dd0ddcc0e..b9add95a6 100644
--- a/packages/compiler/src/frontend/lowering/lower-stream.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stream.ts
@@ -49,6 +49,8 @@ import { sideEffectFreeOptionValue } from "./surfaces.js";
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
import { dynUndefinedExpr, varRef } from "../../ir/build.js";
import { genResultRecord } from "../type-mapper.js";
+import { noteUntypedStreamFlow, requestGeneralStreamSurface } from "./general-streams.js";
+import { dynNativeBranch } from "./dyn-streams.js";
import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js";
import {
BOOL,
@@ -169,6 +171,16 @@ export const UNDERSCORE_METHODS: ReadonlyMap = new Map([
["flush", "_flush"],
]);
+/** The completion callback's parameter position in each underscore
+ * method that takes one (cbTuples' "fn" positions). */
+export const STREAM_COMPLETION_CALLBACK_INDEX: ReadonlyMap = new Map([
+ ["_write", 2],
+ ["_final", 0],
+ ["_destroy", 1],
+ ["_transform", 2],
+ ["_flush", 0],
+]);
+
/** The accepted option-callback names (canonical flag order) and the
* duplex-shaped-head answer for one runtime stream class. */
export function streamCtorShape(cls: string): {
@@ -586,6 +598,33 @@ function dynOptionsValue(lowerer: Lowerer, arg: ts.Expression): IrExpr | null {
return v.type.kind === "dyn" ? v : null;
}
+/** @types/node declares the transform/flush completion callback as
+ * `(error?: Error | null, data?: any) => void`. Outside objectMode Node
+ * accepts only a Buffer, a string or nothing as `data`, so the callback
+ * parameter is typed with that union while the option callback lowers: a
+ * checked-dynamic value passed as `data` converts at the call. Returns the
+ * pinned parameter symbol (the caller unpins it), or null when the
+ * declared type needs no pin. */
+function pinAnyDataCompletion(
+ lowerer: Lowerer,
+ which: string,
+ fnNode: ts.MethodDeclaration | ts.FunctionExpression | ts.ArrowFunction,
+): ts.Symbol | null {
+ const index = which === "transform" ? 2 : which === "flush" ? 0 : -1;
+ const param = index < 0 ? undefined : fnNode.parameters.nodes[index];
+ if (param === undefined || !ts.isIdentifier(param.name) || param.dotDotDotToken) return null;
+ const symbol = lowerer.checker.getSymbolAtLocation(param.name);
+ if (!symbol || lowerer.stageSourceTypes.has(symbol)) return null;
+ const declared = lowerer.paramShape(param).type;
+ if (declared.kind !== "func" || declared.rest || declared.params[1]?.kind !== "dyn") return null;
+ const data = unionOf(lowerer, [BYTES, STRING, { kind: "undefinedT" }]);
+ lowerer.stageSourceTypes.set(
+ symbol,
+ funcOf([declared.params[0]!, data, ...declared.params.slice(2)], declared.ret),
+ );
+ return symbol;
+}
+
/** Parses the options object literal of a stream constructor. `which`
* names the callbacks this class accepts, in canonical flag order. */
function parseStreamOptions(
@@ -671,22 +710,27 @@ function parseStreamOptions(
continue;
}
const full = tuples[name]!.map((t) => (t === "fn" ? null : t));
- // The completion-callback tail: typed by the checker contextually;
- // accept the user's declared func type as the tuple's entry so the
- // prefix check compares like with like.
- const { funcType } = lowerer.lambdaSignature(fnNode);
- const tuple: IrType[] = full.map((t, i) => {
- if (t !== null) return t;
- const declared = funcType.params[i];
- if (declared !== undefined && declared.kind !== "func" && declared.kind !== "dyn") {
- lowerer.noLowering(
- `a '${name}' callback whose parameter ${i} is '${lowerer.fmt(declared)}' where Node passes the completion callback`,
- fnNode!,
- );
- }
- return declared ?? funcOf([], VOID);
- });
- present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple));
+ const pinned = pinAnyDataCompletion(lowerer, name, fnNode);
+ try {
+ // The completion-callback tail: typed by the checker contextually;
+ // accept the user's declared func type as the tuple's entry so the
+ // prefix check compares like with like.
+ const { funcType } = lowerer.lambdaSignature(fnNode);
+ const tuple: IrType[] = full.map((t, i) => {
+ if (t !== null) return t;
+ const declared = funcType.params[i];
+ if (declared !== undefined && declared.kind !== "func" && declared.kind !== "dyn") {
+ lowerer.noLowering(
+ `a '${name}' callback whose parameter ${i} is '${lowerer.fmt(declared)}' where Node passes the completion callback`,
+ fnNode!,
+ );
+ }
+ return declared ?? funcOf([], VOID);
+ });
+ present.set(name, lowerStreamCallback(lowerer, name, fnNode, thisType, tuple));
+ } finally {
+ if (pinned) lowerer.stageSourceTypes.delete(pinned);
+ }
continue;
}
// `{ signal }` shorthand: the binding is the value.
@@ -1431,7 +1475,7 @@ export function lowerStreamStaticCall(
member === "toWeb" &&
cls === "%Readable" &&
call.arguments.nodes.length === 2 &&
- isJsSourceFile(call.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(call)) &&
ts.isObjectLiteralExpression(call.arguments.nodes[1]!)
) {
for (const p of call.arguments.nodes[1]!.properties.nodes) {
@@ -1465,7 +1509,7 @@ export function lowerStreamStaticCall(
);
}
const type: IrType = { kind: "object", className: "%Readable" };
- const js = isJsSourceFile(call.getSourceFile());
+ const js = isJsSourceFile(ts.getSourceFileOfNode(call));
const srcNode = args[0];
const optsNode = args[1];
const srcT = srcNode ? lowerer.mapTypeOf(lowerer.typeOf(srcNode)) : null;
@@ -2044,6 +2088,27 @@ function lowerStreamArg(
const v = isHashValueType(lowered.type)
? hashStreamOf(lowered, what === "pipeline" || what === "compose")
: lowered;
+ // A checked-dynamic pipeline stage (an untyped source or destination):
+ // a boxed native stream unwraps at run time; other values (iterables,
+ // functions) throw the not-supported Error there.
+ if (v.type.kind === "dyn" && what === "pipeline") {
+ noteUntypedStreamFlow(lowerer, null);
+ return {
+ kind: "libCall",
+ fn: "stream.unboxDyn",
+ args: [
+ v,
+ strLit(
+ lowerer.dynNodeStreams
+ ? "pipeline stages other than native streams (JavaScript stream objects, iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet"
+ : "pipeline stages other than native streams (iterables, generators, plain functions, and { readable, writable } pairs) are not supported yet",
+ locOf(node),
+ ),
+ ],
+ type: { kind: "object", className: "%Duplex" },
+ loc: locOf(node),
+ };
+ }
const info = v.type.kind === "object" ? lowerer.classes.get(v.type.className) : undefined;
if (!streamSidesOf(lowerer, info)) {
// A provably-non-stream value in a JS source (the invalid-input
@@ -2062,7 +2127,7 @@ function lowerStreamArg(
loc: locOf(node),
});
};
- if (isJsSourceFile(node.getSourceFile()) && lowerer.dynConvertible(v.type)) {
+ if (isJsSourceFile(ts.getSourceFileOfNode(node)) && lowerer.dynConvertible(v.type)) {
if (what === "pipeline") {
// Strings and arrays are iterables — VALID pipeline stages in
// Node — and a record carrying a readable/writable member may be
@@ -3076,7 +3141,7 @@ function lowerOperatorCallback(
if (v.type.kind === "dyn") return v;
if (
v.type.kind !== "func" &&
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
lowerer.dynConvertible(v.type)
) {
// JavaScript passes any value; Node's validateFunction throws.
@@ -3296,6 +3361,235 @@ function pipeEndOption(lowerer: Lowerer, options: ts.Expression | undefined, loc
return end;
}
+/** `readable.pipe(process.stdout)`: the process stream joins the pipe as
+ * an fd-backed Writable (stream.stdioWritable), and — like Node, which
+ * never ends process.stdout or process.stderr from a pipe — the source's
+ * end does not end it. pipe() answers the process stream itself. */
+function pipeIntoStdio(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+ target: IrExpr,
+): IrExpr {
+ const loc = locOf(call);
+ // JS order: the receiver, then the destination, then the options.
+ const receiverValue = lowerer.lowerExpr(access.expression);
+ const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
+ const stdio = lowerer.declareHiddenLocal("%pipeStdio", target.type);
+ const options = call.arguments.nodes[1];
+ const stmts: IrStmt[] = [
+ { kind: "varDecl", localId: source.id, init: receiverValue, loc },
+ { kind: "varDecl", localId: stdio.id, init: target, loc },
+ ];
+ // The options literal still evaluates; its end flag has no effect here.
+ if (options !== undefined) {
+ stmts.push({ kind: "exprStmt", expr: pipeEndOption(lowerer, options, loc), loc });
+ }
+ const receiver: IrExpr = { kind: "varRef", localId: source.id, type: receiverValue.type, loc };
+ const writable: IrType = { kind: "object", className: "%Writable" };
+ stmts.push({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "readable.pipe",
+ args: [
+ receiver,
+ {
+ kind: "libCall",
+ fn: "stream.stdioWritable",
+ args: [{ kind: "varRef", localId: stdio.id, type: target.type, loc }],
+ type: writable,
+ loc,
+ },
+ boolLit(false, loc),
+ ],
+ type: writable,
+ loc,
+ },
+ loc,
+ });
+ return {
+ kind: "seqExpr",
+ stmts,
+ result: { kind: "varRef", localId: stdio.id, type: target.type, loc },
+ type: target.type,
+ loc,
+ };
+}
+
+/** `readable.unpipe(untyped)`: a native destination (boxed) unpipes as a
+ * typed one does; any other value unpipes the JavaScript destination a
+ * pipe into it registered (scr_stream_dyn.c), or does nothing when there
+ * is none, as Node's unpipe does for a destination it is not piped into.
+ * The call answers the source. */
+function unpipeDyn(lowerer: Lowerer, receiver: IrExpr, dst: IrExpr, loc: SrcLoc): IrExpr {
+ noteUntypedStreamFlow(lowerer, null);
+ const source = lowerer.declareHiddenLocal("%unpipeSource", receiver.type);
+ const destination = lowerer.declareHiddenLocal("%unpipeDestination", dst.type);
+ const sourceRef = varRef(source.id, receiver.type, loc);
+ const destinationRef = varRef(destination.id, dst.type, loc);
+ const stmts: IrStmt[] = [
+ { kind: "varDecl", localId: source.id, init: receiver, loc },
+ { kind: "varDecl", localId: destination.id, init: dst, loc },
+ ];
+ const unpipeNative = (native: IrExpr): IrStmt => ({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "readable.unpipe",
+ args: [sourceRef, native],
+ type: receiver.type,
+ loc,
+ },
+ loc,
+ });
+ if (lowerer.dynNodeStreams) {
+ stmts.push(
+ dynNativeBranch(lowerer, destinationRef, "w", unpipeNative, {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "stream.unpipeDyn",
+ args: [sourceRef, destinationRef],
+ type: VOID,
+ loc,
+ },
+ loc,
+ }),
+ );
+ } else {
+ stmts.push(
+ unpipeNative({
+ kind: "libCall",
+ fn: "stream.unboxDyn",
+ args: [
+ destinationRef,
+ strLit("unpipe destinations other than native streams are not supported yet", loc),
+ ],
+ type: { kind: "object", className: "%Duplex" },
+ loc,
+ }),
+ );
+ }
+ return { kind: "seqExpr", stmts, result: sourceRef, type: receiver.type, loc };
+}
+
+/** The pipe() libCall for a destination of type `dst`. Under the general
+ * stream representation a Writable slot can hold the converted
+ * process.stdout / process.stderr, which a pipe never ends (as in Node), so
+ * the pipe takes readable.pipeGeneral, which checks for them. */
+function generalPipe(lowerer: Lowerer, dst: IrType): "readable.pipe" | "readable.pipeGeneral" {
+ return lowerer.generalNodeStreams && dst.kind === "object" && dst.className === "%Writable"
+ ? "readable.pipeGeneral"
+ : "readable.pipe";
+}
+
+/** `readable.pipe(untyped)`: a checked-dynamic destination unwraps a boxed
+ * native stream at run time (stream.unboxDyn; other values throw the
+ * not-supported Error), and pipe() answers the destination value itself. */
+function pipeIntoDyn(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+ target: IrExpr,
+): IrExpr {
+ const loc = locOf(call);
+ noteUntypedStreamFlow(lowerer, null);
+ const receiverValue = lowerer.lowerExpr(access.expression);
+ const source = lowerer.declareHiddenLocal("%pipeSource", receiverValue.type);
+ const destination = lowerer.declareHiddenLocal("%pipeDestination", target.type);
+ const stmts: IrStmt[] = [
+ { kind: "varDecl", localId: source.id, init: receiverValue, loc },
+ { kind: "varDecl", localId: destination.id, init: target, loc },
+ ];
+ const end = pipeEndOption(lowerer, call.arguments.nodes[1], loc);
+ const duplex: IrType = { kind: "object", className: "%Duplex" };
+ // Checked-dynamic stream slots (dyn-streams.ts): a JavaScript stream
+ // object is a destination too, piped with Node's listener protocol.
+ if (lowerer.dynNodeStreams) {
+ const sourceRef: IrExpr = { kind: "varRef", localId: source.id, type: receiverValue.type, loc };
+ const destinationRef: IrExpr = {
+ kind: "varRef",
+ localId: destination.id,
+ type: target.type,
+ loc,
+ };
+ const ending = lowerer.declareHiddenLocal("%pipeEnd", BOOL);
+ stmts.push({ kind: "varDecl", localId: ending.id, init: end, loc });
+ const endRef: IrExpr = { kind: "varRef", localId: ending.id, type: BOOL, loc };
+ stmts.push(
+ dynNativeBranch(
+ lowerer,
+ destinationRef,
+ "w",
+ (native) => ({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ // Checked-dynamic slots come with the general representation,
+ // where a boxed native destination can be the converted
+ // process.stdout (generalPipe).
+ fn: "readable.pipeGeneral",
+ args: [sourceRef, native, endRef],
+ type: native.type,
+ loc,
+ },
+ loc,
+ }),
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "stream.pipeDyn",
+ args: [sourceRef, lowerer.coerceToExpected(sourceRef, DYN), destinationRef, endRef],
+ type: VOID,
+ loc,
+ },
+ loc,
+ },
+ ),
+ );
+ return {
+ kind: "seqExpr",
+ stmts,
+ result: destinationRef,
+ type: target.type,
+ loc,
+ };
+ }
+ stmts.push({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: lowerer.generalNodeStreams ? "readable.pipeGeneral" : "readable.pipe",
+ args: [
+ { kind: "varRef", localId: source.id, type: receiverValue.type, loc },
+ {
+ kind: "libCall",
+ fn: "stream.unboxDyn",
+ args: [
+ { kind: "varRef", localId: destination.id, type: target.type, loc },
+ strLit("pipe destinations other than native streams are not supported yet", loc),
+ ],
+ type: duplex,
+ loc,
+ },
+ end,
+ ],
+ type: duplex,
+ loc,
+ },
+ loc,
+ });
+ return {
+ kind: "seqExpr",
+ stmts,
+ result: { kind: "varRef", localId: destination.id, type: target.type, loc },
+ type: target.type,
+ loc,
+ };
+}
+
export function lowerStreamMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -3545,22 +3839,26 @@ export function lowerStreamMethodCall(
loc,
};
}
- const dst = lowerer.lowerExpr(args[0]!);
+ const target = lowerer.lowerExpr(args[0]!);
+ if (target.type.kind === "procStream") return pipeIntoStdio(lowerer, call, access, target);
+ if (target.type.kind === "dyn") return pipeIntoDyn(lowerer, call, access, target);
+ const dst = target;
const dstInfo =
dst.type.kind === "object" ? lowerer.classes.get(dst.type.className) : undefined;
const dstSides = streamSidesOf(lowerer, dstInfo);
if (dstSides !== "w" && dstSides !== "rw") {
+ requestGeneralStreamSurface(lowerer, args[0]!);
lowerer.noLowering(
`pipe into a '${lowerer.fmt(dst.type)}'`,
args[0]!,
- "the destination must be a stream Writable/Duplex (process streams and sockets are not pipe destinations yet)",
+ "the destination must be a stream Writable/Duplex, process.stdout or process.stderr (sockets are not pipe destinations yet)",
);
}
const end = pipeEndOption(lowerer, args[1], loc);
const receiver = lowerer.lowerExpr(access.expression);
return {
kind: "libCall",
- fn: "readable.pipe",
+ fn: generalPipe(lowerer, dst.type),
args: [receiver, dst, end],
type: dst.type,
loc,
@@ -3573,6 +3871,7 @@ export function lowerStreamMethodCall(
const receiver = lowerer.lowerExpr(access.expression);
if (args[0] !== undefined) {
const dst = lowerer.lowerExpr(args[0]);
+ if (dst.type.kind === "dyn") return unpipeDyn(lowerer, receiver, dst, loc);
const dstInfo =
dst.type.kind === "object" ? lowerer.classes.get(dst.type.className) : undefined;
const dstSides = streamSidesOf(lowerer, dstInfo);
@@ -3587,6 +3886,44 @@ export function lowerStreamMethodCall(
loc,
};
}
+ // Checked-dynamic slots: unpipe() also unpipes the JavaScript
+ // destinations stream.pipeDyn registered (stream.unpipeDyn with an
+ // undefined destination), as Node's unpipe() empties every pipe.
+ if (lowerer.dynNodeStreams) {
+ const source = lowerer.declareHiddenLocal("%unpipeSource", receiver.type);
+ const sourceRef = varRef(source.id, receiver.type, loc);
+ return {
+ kind: "seqExpr",
+ stmts: [
+ { kind: "varDecl", localId: source.id, init: receiver, loc },
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "readable.unpipe",
+ args: [sourceRef],
+ type: receiver.type,
+ loc,
+ },
+ loc,
+ },
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "stream.unpipeDyn",
+ args: [sourceRef, dynUndefinedExpr(loc)],
+ type: VOID,
+ loc,
+ },
+ loc,
+ },
+ ],
+ result: sourceRef,
+ type: receiver.type,
+ loc,
+ };
+ }
return { kind: "libCall", fn: "readable.unpipe", args: [receiver], type: receiver.type, loc };
}
@@ -3674,8 +4011,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 +4071,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 +4339,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
+ );
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-test.ts b/packages/compiler/src/frontend/lowering/lower-test.ts
index 32870c2cf..391a77245 100644
--- a/packages/compiler/src/frontend/lowering/lower-test.ts
+++ b/packages/compiler/src/frontend/lowering/lower-test.ts
@@ -86,8 +86,8 @@ function requireStatementPosition(lowerer: Lowerer, call: ts.CallExpression, wha
* reports the `test` property's column), at the call for plain ones. */
function atStringOf(expr: ts.CallExpression): string {
const target = ts.isPropertyAccessExpression(expr.expression) ? expr.expression.name : expr;
- const sf = expr.getSourceFile();
- const pos = sf.getLineAndCharacterOfPosition(target.getStart());
+ const sf = ts.getSourceFileOfNode(expr);
+ const pos = sf.getLineAndCharacterOfPosition(ts.tokenStartOfNode(target));
return `${sf.fileName}:${pos.line + 1}:${pos.character + 1}`;
}
diff --git a/packages/compiler/src/frontend/lowering/lower-timers.ts b/packages/compiler/src/frontend/lowering/lower-timers.ts
index 22983fbb4..6b6a1b5d1 100644
--- a/packages/compiler/src/frontend/lowering/lower-timers.ts
+++ b/packages/compiler/src/frontend/lowering/lower-timers.ts
@@ -420,7 +420,7 @@ function lowerTimerCall(
lowerer.noLowering(`clearTimeout with ${expr.arguments.nodes.length} arguments`, expr);
}
let handle = lowerer.lowerExpr(expr.arguments.nodes[0]!);
- if (handle.type.kind === "dyn" && isJsSourceFile(expr.getSourceFile())) {
+ if (handle.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) {
const stored = lowerer.declareHiddenLocal("%timer.clear", DYN);
const ref = varRef(stored.id, DYN, loc);
handle = {
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 1361b346f..e2236f592 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -1,5 +1,12 @@
+import { onlyForwarded } from "./typed-arguments.js";
+import {
+ needsGeneralNodeStreams,
+ noteUntypedStreamFlow,
+ stdioIntoGeneralStream,
+} from "./general-streams.js";
import { extendAssertedRecordMembers } from "./assertion-members.js";
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
+import { builtinClassValue, builtinMemberClassKind } from "./builtins/builtin-class-dyn.js";
import {
lifetimeRegionType,
lowerLifetimeRegionCall,
@@ -30,8 +37,13 @@ import { isSettledIteratorValueRead } from "./lower-generators.js";
import { concreteCollectionNarrow } from "./collection-narrowing.js";
import { lowerWorkerMetadata } from "./builtins/workers.js";
import { fenceStaticBlobMember, lowerBlobNew, lowerStaticBlobCall } from "./builtins/blob.js";
-import { refuseWebAssemblyUse } from "./builtins/webassembly.js";
+import {
+ lowerWebAssemblyCall,
+ lowerWebAssemblyNew,
+ refuseWebAssemblyUse,
+} from "./builtins/webassembly.js";
import { discoverWasmModules, type WasmModuleReport } from "../../wasm/discover.js";
+import { encodeBase64 } from "../../wasm/base64.js";
import { resolveRequireRuntime } from "../runtime-resolve.js";
import { lowerKeyObjectCall } from "./builtins/key-object.js";
import { fenceWebCryptoMember, lowerWebCryptoCall } from "./builtins/webcrypto.js";
@@ -185,6 +197,7 @@ import type {
IrGlobal,
IrLocal,
IrModule,
+ IrWasmModule,
IrParam,
IrRecordShape,
IrStmt,
@@ -877,6 +890,15 @@ export interface LowererMode {
/** Statically resolved child_process.fork program roots, in stable target-id order. */
forkTargets?: readonly ts.SourceFile[];
frontendServices?: FrontendServices | undefined;
+ /** The program's general stream representation (TypeMapperCtx.
+ * generalNodeStreams). lowerToIr turns it on only after a pass without
+ * it saw a stream flow that needs it (requiresGeneralNodeStreams). */
+ generalNodeStreams?: boolean;
+ /** With generalNodeStreams, the checked-dynamic stream slots
+ * (TypeMapperCtx.dynNodeStreams). lowerToIr turns it on only after a
+ * pass saw an untyped value flow into a stream slot
+ * (requiresDynNodeStreams). */
+ dynNodeStreams?: boolean;
/** LoadResult.inlineRequireCycleCalls. */
inlineRequireCycleCalls?: ReadonlySet | undefined;
/** LowerOptions.libraryCallbacks (see there). */
@@ -1033,10 +1055,53 @@ export function lowerToIr(
for (const d of extraModuleCycleDiags) reachableEmit.pushDiag(d);
for (const d of ffiValidation.diagnostics) reachableEmit.pushDiag(d);
const emitted = reachableEmit.emitReachable(options.libRoots);
- const { reachable } = emitted;
+ let { reachable } = emitted;
let result = emitted.result;
let resultLowerer = reachableEmit;
timing("reachable-emit", { reachable: reachable.size });
+ // The general stream representation (TypeMapperCtx.generalNodeStreams)
+ // is a per-program decision: only a program whose reachable code moved
+ // a general stream into a stdio-represented stream slot lowers again
+ // with it. Every other program keeps the single pass above unchanged.
+ const generalNodeStreams = reachableEmit.requiresGeneralNodeStreams;
+ // Checked-dynamic stream slots (dyn-streams.ts) are a further per-program
+ // decision under the general representation: only a program that moves an
+ // untyped value into a stream slot lowers again with them.
+ let dynNodeStreams = reachableEmit.requiresDynNodeStreams;
+ const streamPass = (dyn: boolean): Lowerer => {
+ const general = new Lowerer(program, entry, moduleOrder, {
+ frontendServices: options.frontendServices,
+ projectCheckJs: options.projectCheckJs ?? false,
+ targetPlatform,
+ startupCrash,
+ inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ ffiImports,
+ libraryCallbacks,
+ forkTargets,
+ workerTargets,
+ externalTypes,
+ externalTypeSpecifiersByFile,
+ tierMode,
+ ffiBindingSymbols: ffiValidation.symbolsByName,
+ generalNodeStreams: true,
+ ...(dyn ? { dynNodeStreams: true } : {}),
+ });
+ for (const d of extraModuleCycleDiags) general.pushDiag(d);
+ for (const d of ffiValidation.diagnostics) general.pushDiag(d);
+ const generalEmitted = general.emitReachable(options.libRoots);
+ reachable = generalEmitted.reachable;
+ result = generalEmitted.result;
+ resultLowerer = general;
+ timing(dyn ? "dyn-streams-relower" : "general-streams-relower", { reachable: reachable.size });
+ return general;
+ };
+ if (generalNodeStreams) {
+ const general = streamPass(dynNodeStreams);
+ if (!dynNodeStreams && general.requiresDynNodeStreams) {
+ dynNodeStreams = true;
+ streamPass(true);
+ }
+ }
// A generic class-rest support decision can depend on record metadata
// whose historical owner is discovered only while retained bodies lower.
// If the settled answer is a fence, rerun the ordinary reachable emit so
@@ -1044,10 +1109,12 @@ export function lowerToIr(
// declarators stay unvisited, bindings block, cascades and stats match the
// historical compiler. This is a rare compatibility fallback; programs
// without such a settled fence retain checker-backed IR exactly once.
- if (reachableEmit.requiresHistoricalOrderRelower) {
+ if (resultLowerer.requiresHistoricalOrderRelower) {
const emit = new Lowerer(program, entry, moduleOrder, {
frontendServices: options.frontendServices,
projectCheckJs: options.projectCheckJs ?? false,
+ ...(generalNodeStreams ? { generalNodeStreams: true } : {}),
+ ...(dynNodeStreams ? { dynNodeStreams: true } : {}),
reachable,
targetPlatform,
startupCrash,
@@ -1103,25 +1170,75 @@ export function lowerToIr(
diagnostics: [...result.diagnostics, ...contracts.diagnostics],
};
else if (contracts.count > 0) result = { ...result, contracts };
+ const discoverModules = (
+ onModule?: (sha256: string, bytes: Uint8Array) => void,
+ ): WasmModuleReport[] =>
+ discoverWasmModules({
+ files: moduleOrder.includes(entry) ? moduleOrder : [...moduleOrder, entry],
+ entry: entry.fileName,
+ configured: options.wasmFiles ?? [],
+ resolveRequest: (fromFile, specifier) => {
+ const resolved = resolveRequireRuntime(
+ fromFile,
+ specifier,
+ targetPlatform,
+ undefined,
+ options.frontendServices,
+ );
+ return resolved.ok ? resolved.value : null;
+ },
+ ...(onModule === undefined ? {} : { onModule }),
+ });
+ // A program that uses the WebAssembly API carries every module the build
+ // can see and translate (wasm/translate.ts); the rest fail at run time
+ // with the CompileError for untranslated bytes.
+ if (result.module !== null && resultLowerer.usesWebAssembly) {
+ const bytesByHash = new Map();
+ const reports = discoverModules((sha256, bytes) => bytesByHash.set(sha256, bytes));
+ const wasmModules: IrWasmModule[] = [];
+ for (const report of reports) {
+ const bytes = bytesByHash.get(report.sha256);
+ if (report.status !== "valid" || bytes === undefined) continue;
+ wasmModules.push({ sha256: report.sha256, bytes: encodeBase64(bytes) });
+ }
+ result.module.wasmModules = wasmModules;
+ if (resultLowerer.usesWasmCustomSections) result.module.wasmCustomSections = true;
+ timing("wasm-discovery", { modules: wasmModules.length });
+ }
if (options.coverage !== true) return result;
- const remainder = new Lowerer(program, entry, moduleOrder, {
- frontendServices: options.frontendServices,
- projectCheckJs: options.projectCheckJs ?? false,
- reachable,
- remainder: true,
- alreadyFlushed: resultLowerer.flushedSymbols,
- inlineRequireCycleCalls: options.inlineRequireCycleCalls,
- targetPlatform,
- ffiImports,
- libraryCallbacks,
- forkTargets,
- workerTargets,
- ffiBindingSymbols: ffiValidation.symbolsByName,
- externalTypes,
- externalTypeSpecifiersByFile,
- tierMode,
- });
- const rem = remainder.run();
+ const remainderPass = (general: boolean, dyn = dynNodeStreams): Lowerer =>
+ new Lowerer(program, entry, moduleOrder, {
+ frontendServices: options.frontendServices,
+ projectCheckJs: options.projectCheckJs ?? false,
+ ...(general ? { generalNodeStreams: true } : {}),
+ ...(general && dyn ? { dynNodeStreams: true } : {}),
+ reachable,
+ remainder: true,
+ alreadyFlushed: resultLowerer.flushedSymbols,
+ inlineRequireCycleCalls: options.inlineRequireCycleCalls,
+ targetPlatform,
+ ffiImports,
+ libraryCallbacks,
+ forkTargets,
+ workerTargets,
+ ffiBindingSymbols: ffiValidation.symbolsByName,
+ externalTypes,
+ externalTypeSpecifiersByFile,
+ tierMode,
+ });
+ let remainder = remainderPass(generalNodeStreams);
+ let rem = remainder.run();
+ // Unreached code reports what it would compile to once reached: a
+ // stream flow there that needs the general representation lowers the
+ // remainder again with it, as reachable emit would.
+ if (!generalNodeStreams && remainder.requiresGeneralNodeStreams) {
+ remainder = remainderPass(true, remainder.requiresDynNodeStreams);
+ rem = remainder.run();
+ }
+ if (!dynNodeStreams && remainder.generalNodeStreams && remainder.requiresDynNodeStreams) {
+ remainder = remainderPass(true, true);
+ rem = remainder.run();
+ }
timing("coverage-remainder");
// Tiers (tier-analysis.ts): reached statements in dynamic-tier functions,
// and with tierReport the report-only demotion analysis.
@@ -1156,21 +1273,7 @@ export function lowerToIr(
},
};
if (tiers !== undefined) covered.tiers = tiers;
- const wasmModules = discoverWasmModules({
- files: moduleOrder.includes(entry) ? moduleOrder : [...moduleOrder, entry],
- entry: entry.fileName,
- configured: options.wasmFiles ?? [],
- resolveRequest: (fromFile, specifier) => {
- const resolved = resolveRequireRuntime(
- fromFile,
- specifier,
- targetPlatform,
- undefined,
- options.frontendServices,
- );
- return resolved.ok ? resolved.value : null;
- },
- });
+ const wasmModules = discoverModules();
timing("wasm-discovery", { modules: wasmModules.length });
if (wasmModules.length > 0) covered.wasmModules = wasmModules;
return covered;
@@ -1246,7 +1349,7 @@ export function uncheckedOverloadHandleCall(
* `never` at the ROOT stays out (`for (const v of [])`'s loop var — the
* dead read the f64 mapping is FOR). */
export function neverTaintedJsType(lowerer: Lowerer, node: ts.Node, t: ts.Type): boolean {
- if (!isJsSourceFile(node.getSourceFile())) return false;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(node))) return false;
const walk = (x: ts.Type, depth: number): boolean => {
if (depth === 0) return false;
if (x.isUnionType()) return ts.constituentTypes(x).some((a) => walk(a, depth - 1));
@@ -1276,7 +1379,7 @@ export function ladderFenceExpr(
const loc = locOf(node);
const d = noLoweringDiag(surface, loc, hint);
lowerer.runtimeFences.push(d);
- const sf = node.getSourceFile();
+ const sf = ts.getSourceFileOfNode(node);
const pos = ts.getLineAndCharacterOfPosition(sf, loc.start);
return {
kind: "strLit",
@@ -1288,7 +1391,7 @@ export function ladderFenceExpr(
export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): IrType | null {
if (t.flags & ts.TypeFlags.Void) return null;
- if (!isJsSourceFile(node.getSourceFile())) {
+ if (!isJsSourceFile(ts.getSourceFileOfNode(node))) {
if (t.flags & ts.TypeFlags.Any) return DYN;
const symbol = t.getSymbol();
const args =
@@ -1303,16 +1406,17 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir
ts.TypeFlags.Any) !==
0;
if (
- symbol?.name === "Promise" &&
+ symbol !== undefined &&
+ ts.symbolName(symbol) === "Promise" &&
args[0] &&
(args[0].flags & ts.TypeFlags.Any || anyArray(args[0]))
)
- return symbol.name === "Promise" && anyArray(args[0])
+ return ts.symbolName(symbol) === "Promise" && anyArray(args[0])
? { kind: "promise", inner: DYN }
: jsFallbackPromiseType(lowerer, t);
if (
symbol &&
- ["Generator", "AsyncGenerator", "IterableIterator"].includes(symbol.name) &&
+ ["Generator", "AsyncGenerator", "IterableIterator"].includes(ts.symbolName(symbol)) &&
args.some((arg) => arg.flags & ts.TypeFlags.Any)
)
return jsFallbackGeneratorType(lowerer, t);
@@ -1334,7 +1438,8 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir
if (generator) return generator;
const symbol = t.getSymbol();
if (
- (symbol?.name === "Map" || symbol?.name === "ReadonlyMap") &&
+ ((symbol !== undefined && ts.symbolName(symbol) === "Map") ||
+ (symbol !== undefined && ts.symbolName(symbol) === "ReadonlyMap")) &&
lowerer.isStdlibSymbol(symbol)
) {
const args = lowerer.checker.getTypeArguments(t as ts.TypeReference);
@@ -1423,7 +1528,12 @@ export function dynFallbackType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir
* has no fixed shape. The settled value crosses the checked native boundary. */
function jsFallbackPromiseType(lowerer: Lowerer, t: ts.Type): IrType | null {
const symbol = t.getSymbol();
- if (symbol?.name !== "Promise" || !lowerer.isStdlibSymbol(symbol)) return null;
+ if (
+ symbol === undefined ||
+ ts.symbolName(symbol) !== "Promise" ||
+ !lowerer.isStdlibSymbol(symbol)
+ )
+ return null;
const inner = lowerer.checker.getTypeArguments(t as ts.TypeReference)[0];
return inner ? { kind: "promise", inner: lowerer.mapTypeOf(inner) ?? DYN } : null;
}
@@ -1434,7 +1544,7 @@ function jsFallbackGeneratorType(lowerer: Lowerer, t: ts.Type): IrType | null {
const symbol = t.getSymbol();
if (
!symbol ||
- !["Generator", "AsyncGenerator", "IterableIterator"].includes(symbol.name) ||
+ !["Generator", "AsyncGenerator", "IterableIterator"].includes(ts.symbolName(symbol)) ||
!lowerer.isStdlibSymbol(symbol)
)
return null;
@@ -1442,7 +1552,7 @@ function jsFallbackGeneratorType(lowerer: Lowerer, t: ts.Type): IrType | null {
const unit = ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never;
return {
kind: "generator",
- ...(symbol.name === "AsyncGenerator" ? { async: true as const } : {}),
+ ...(ts.symbolName(symbol) === "AsyncGenerator" ? { async: true as const } : {}),
yieldT: yielded && yielded.flags & ts.TypeFlags.Never ? VOID : DYN,
retT: !returned || returned.flags & unit ? VOID : DYN,
nextT: !resumed || resumed.flags & unit ? UNDEFINED_T : DYN,
@@ -1476,7 +1586,7 @@ function jsFallbackFunctionType(lowerer: Lowerer, node: ts.Node, t: ts.Type): Ir
const sigDecl = sig.declaration;
const jsUnitReturn =
sigDecl !== undefined &&
- isJsSourceFile(sigDecl.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(sigDecl)) &&
(retT.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0;
const ret: IrType = jsUnitReturn
? DYN
@@ -1510,7 +1620,7 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null {
ts.isFunctionExpression(decl) ||
ts.isArrowFunction(decl)
) ||
- !isJsSourceFile(decl.getSourceFile())
+ !isJsSourceFile(ts.getSourceFileOfNode(decl))
)
return null;
const hasRest = decl.parameters.nodes.some((p) => p.dotDotDotToken !== undefined);
@@ -1518,7 +1628,8 @@ function jsArgumentsFunctionType(lowerer: Lowerer, t: ts.Type): IrType | null {
!hasRest &&
(ts.isArrowFunction(decl) ||
!bodyReadsArguments(decl) ||
- (decl.parameters.nodes.length > 0 && !isNodeEsmFile(decl.getSourceFile(), lowerer.program)))
+ (decl.parameters.nodes.length > 0 &&
+ !isNodeEsmFile(ts.getSourceFileOfNode(decl), lowerer.program)))
)
return null;
const shapes = paramShapes(lowerer, decl.parameters.nodes);
@@ -1921,6 +2032,18 @@ export class Lowerer {
* type parameters are bound to `unknown`, so lambdaSignature admits
* them. */
readonly erasingLambdas = new Set();
+ /** Function expressions lowered against a fixed target signature with a
+ * tuple-typed `arguments` (typed-arguments.ts). */
+ readonly typedArgumentsLambdas = new Set();
+ /** Const bindings holding a forwarded fixed tuple (`const args =
+ * arguments as any` in a typed-arguments function) → the tuple type. */
+ readonly tupleAliasSymbols = new Map();
+ /** `??` expressions whose result is the optional callback's own function
+ * type (an adapted function default); their bindings take that type. */
+ readonly nullishFuncResults = new Set();
+ /** The static names of the current generic instance's name-keyed
+ * function parameters (static-function-names.ts). */
+ staticFnNames: Map | null = null;
/** Nesting depth of erased generic bodies being lowered (erased lambdas
* and erased instances). */
erasingDepth = 0;
@@ -1950,6 +2073,10 @@ export class Lowerer {
* setEncoding call whose receiver is a socket or an untyped value.
* Computed once on first use (null until then). */
socketEncodingReachable: boolean | null = null;
+ /** Whether some server in this program may listen on a Unix socket: a
+ * listen call whose first argument is a string. Computed once on first
+ * use (null until then); address() answers the path only when true. */
+ unixListenReachable: boolean | null = null;
/** IMPLICIT-ANY instances lowered EAGERLY at first demand (their return
* types are inferred from the body — the call site needs them settled),
* collected here for run()'s function list (the liftedFns discipline). */
@@ -2018,7 +2145,9 @@ export class Lowerer {
/** A pipeline() function stage's `source` parameter: the IR type of what
* the pipeline passes it (the previous stage as an async iterable), set
* while the stage lowers. Parameter typing and `for await` heads read
- * it before the checker's `AsyncIterable`. */
+ * it before the checker's `AsyncIterable`. A stream option callback's
+ * completion parameter declared with an any-typed `data` is pinned here
+ * too while the callback lowers (pinAnyDataCompletion). */
readonly stageSourceTypes = new Map();
/** Set when a match result lowers to execGroups/matchGroups, the inputs
* of regex span folding. */
@@ -2144,7 +2273,7 @@ export class Lowerer {
return (
sym !== undefined &&
sym.declarations.some((d) => {
- if (!isJsSourceFile(d.getSourceFile())) return false;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(d))) return false;
// The pattern itself, or the `= {}` default of a pattern parameter.
const pattern =
ts.isObjectLiteralExpression(d) &&
@@ -2260,7 +2389,7 @@ export class Lowerer {
// A checked JavaScript indexed read can already be boxed. The checker
// calls the element present, but that inference is not a runtime guard.
if (
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
value.kind === "dynCheck" &&
value.value.type.kind === "dyn"
)
@@ -2496,6 +2625,21 @@ export class Lowerer {
/** Settled generic class-rest metadata requires the historical emit
* fallback so its fence can poison the original statement atomically. */
requiresHistoricalOrderRelower = false;
+ /** A general stream value (a runtime stream class instance) reached a
+ * slot the child/process stdio handles represent — NodeJS.ReadableStream,
+ * NodeJS.WritableStream, or @types/node's stream.Readable/Writable — or
+ * a stdio handle reached an 'unknown' slot. lowerToIr then lowers the
+ * program again under the general stream representation. */
+ requiresGeneralNodeStreams = false;
+ /** An untyped value reached a stream slot (a checked unwrap into a
+ * runtime stream class, or an untyped pipe/pipeline stage). lowerToIr
+ * then lowers the program again with checked-dynamic stream slots
+ * (dyn-streams.ts). */
+ requiresDynNodeStreams = false;
+ /** Receiver representations forced by the per-value stream dispatch
+ * (dyn-streams.ts): the native arm lowers a member call on the unboxed
+ * stream through the ordinary class-method path. */
+ readonly receiverTypeOverrides = new Map();
/** Helpers that snapshot shape declaration order into their bodies.
* Reachability lowers inits before the declarations they discover, so
* these rebuild after the worklist restores historical shape metadata. */
@@ -2574,6 +2718,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();
+ /** Accessor exports' link-time values for ES-module importers, and the
+ * per-module flag that the copy already ran (cjsAccessorSnapshotStores). */
+ readonly cjsAccessorSnapshots = new Map();
+ readonly cjsSnapshotReady = new Map();
/** Interned `%iter.drain.` helpers (classIteratorDrainCall): one per
* receiver class — the eager drain of a class iterable's protocol into
* a fresh element array, behind array/call spreads. */
@@ -2653,6 +2801,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();
+ /** 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();
/** Typed calls through class-backed protocols, by signature; class
* dynamic dispatch adds their native arms (class-protocol-dispatch.ts). */
readonly protocolDispatches = new Map();
@@ -2742,6 +2894,12 @@ export class Lowerer {
readonly workerTargets: readonly ts.SourceFile[];
readonly workerTargetIdByPath = new Map();
usesWorkers = false;
+ /** The program uses the WebAssembly API (builtins/webassembly.ts): the
+ * build translates the modules it discovers. */
+ usesWebAssembly = false;
+ /** The program calls WebAssembly.Module.customSections: translated
+ * modules keep their custom sections. */
+ usesWasmCustomSections = false;
/** The program constructs a LifetimeRegion (lifetime-regions.ts). */
usesLifetimeRegions = false;
/** Per-directory answers of experimental-import.ts's declaration-file test. */
@@ -2943,6 +3101,8 @@ export class Lowerer {
* refuse and fence exactly as before. Stage S6 demotes the units of
* committed refusals in "auto" and keeps them refusals in "static-only". */
readonly tierMode: TierMode;
+ readonly generalNodeStreams: boolean;
+ readonly dynNodeStreams: boolean;
/** Functions finishModule placed in the dynamic tier (ir/tiers.ts). */
dynamicFunctions: DynamicFunction[] = [];
/** Their IR, for coverage's dynamic statement count. */
@@ -3071,6 +3231,8 @@ export class Lowerer {
this.startupCrash = mode.startupCrash ?? null;
this.targetPlatform = mode.targetPlatform ?? process.platform;
this.tierMode = mode.tierMode ?? "auto";
+ this.generalNodeStreams = mode.generalNodeStreams ?? false;
+ this.dynNodeStreams = this.generalNodeStreams && (mode.dynNodeStreams ?? false);
this.workerTargets = mode.workerTargets ?? [];
this.workerTargets.forEach((sf, id) => {
this.workerTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id);
@@ -3151,7 +3313,8 @@ export class Lowerer {
if (this.localClassInstantiations.some((entry) => entry.owner === parent)) break;
if (
parent.typeParameters?.nodes.length ||
- (implicitMonoFile(parent.getSourceFile()) && monomorphizedImplicit(this, parent))
+ (implicitMonoFile(ts.getSourceFileOfNode(parent)) &&
+ monomorphizedImplicit(this, parent))
)
return null;
}
@@ -3167,6 +3330,8 @@ export class Lowerer {
isExternalTypeFile: (sf) =>
this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))),
typeMemo: this.typeMemo,
+ ...(this.generalNodeStreams ? { generalNodeStreams: true } : {}),
+ ...(this.dynNodeStreams ? { dynNodeStreams: true } : {}),
canMemoizeType: () =>
this.typeParamBindings === null &&
this.typeParamTsBindings === null &&
@@ -3175,15 +3340,15 @@ export class Lowerer {
// lowering, long after the constructor completes.
isProgramFile: (sf) => this.fileTag.has(sf),
lifetimeRegionClass: (decl) =>
- isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles)
+ isExperimentalDeclarationFile(ts.getSourceFileOfNode(decl), this.experimentalFiles)
? lifetimeRegionType(this)
: null,
internTableClass: (decl) =>
- isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles)
+ isExperimentalDeclarationFile(ts.getSourceFileOfNode(decl), this.experimentalFiles)
? internTableType(this)
: null,
threadSlotsClass: (decl) =>
- isExperimentalDeclarationFile(decl.getSourceFile(), this.experimentalFiles)
+ isExperimentalDeclarationFile(ts.getSourceFileOfNode(decl), this.experimentalFiles)
? threadSlotsType(this)
: null,
moduleNamespaceId: (type) => this.moduleNamespaceIdOfType(type),
@@ -3249,9 +3414,10 @@ export class Lowerer {
const weak = (part: ts.Type): boolean => {
const symbol = part.getSymbol();
return (
- (symbol?.name === "WeakMap" || symbol?.name === "WeakSet") &&
+ ((symbol !== undefined && ts.symbolName(symbol) === "WeakMap") ||
+ (symbol !== undefined && ts.symbolName(symbol) === "WeakSet")) &&
symbol.declarations.some(
- (d) => ts.isInterfaceDeclaration(d) && this.isStdlibFile(d.getSourceFile()),
+ (d) => ts.isInterfaceDeclaration(d) && this.isStdlibFile(ts.getSourceFileOfNode(d)),
)
);
};
@@ -3360,9 +3526,12 @@ export class Lowerer {
}
return false;
})()
- ? this.qualify(decl.getSourceFile(), `%cx${decl.getStart()}.${decl.name?.text ?? ""}`)
+ ? this.qualify(
+ ts.getSourceFileOfNode(decl),
+ `%cx${ts.tokenStartOfNode(decl)}.${decl.name?.text ?? ""}`,
+ )
: this.qualify(
- decl.getSourceFile(),
+ ts.getSourceFileOfNode(decl),
nsPathPrefix(decl) + (decl.name ? decl.name.text : "%anon"),
);
for (let index = this.localClassInstantiations.length - 1; index >= 0; index--) {
@@ -3606,7 +3775,10 @@ export class Lowerer {
return null;
if (this.externalTypes.has(importDecl.moduleSpecifier.text))
return importDecl.moduleSpecifier.text;
- const dep = this.moduleSourceFileOf(importDecl.getSourceFile(), importDecl.moduleSpecifier);
+ const dep = this.moduleSourceFileOf(
+ ts.getSourceFileOfNode(importDecl),
+ importDecl.moduleSpecifier,
+ );
return dep !== null && !dep.isDeclarationFile
? this.externalTypeSpecifierOfModuleExport(dep, member, new Set(), new Set())
: null;
@@ -3654,7 +3826,7 @@ export class Lowerer {
// A namespace OBJECT from a project module is not wholly external;
// property accesses resolve their selected member separately above.
if (importedName === null) return null;
- const dep = this.moduleSourceFileOf(decl.getSourceFile(), specNode);
+ const dep = this.moduleSourceFileOf(ts.getSourceFileOfNode(decl), specNode);
return dep !== null && !dep.isDeclarationFile
? this.externalTypeSpecifierOfModuleExport(dep, importedName, seenSymbols, seenExports)
: null;
@@ -3662,7 +3834,7 @@ export class Lowerer {
for (const decl of declarations) {
const owners = this.externalTypeSpecifiersByFile.get(
- tsgoPath(resolve(decl.getSourceFile().fileName)),
+ tsgoPath(resolve(ts.getSourceFileOfNode(decl).fileName)),
);
if (owners !== undefined && owners.length > 0) return owners[0]!;
}
@@ -3707,7 +3879,7 @@ export class Lowerer {
}
if (d === undefined || spec === undefined || !ts.isStringLiteral(spec) || name !== "default")
return null;
- const dep = resolveImport(this.program, d.getSourceFile(), spec.text);
+ const dep = resolveImport(this.program, ts.getSourceFileOfNode(d), spec.text);
if (!dep) return null;
sym = defaultExportSymbolOf(this, dep) ?? undefined;
}
@@ -3823,8 +3995,8 @@ export class Lowerer {
// Project aliases/self-references resolve through the same entry point as
// relative imports; an opted-in --npm-static package is the fallback.
const dep =
- resolveImport(this.program, importDecl.getSourceFile(), spec) ??
- npmStaticDepSf7(this.program, importDecl.getSourceFile(), spec);
+ resolveImport(this.program, ts.getSourceFileOfNode(importDecl), spec) ??
+ npmStaticDepSf7(this.program, ts.getSourceFileOfNode(importDecl), spec);
if (!dep || !isJsSourceFile(dep) || isNodeEsmFile(dep, this.program)) return null;
return dep;
}
@@ -3864,8 +4036,8 @@ export class Lowerer {
if (directSpec !== null) {
if (isJsonRequireDecl(this, decl)) return null;
return (
- resolveImport(this.program, decl.getSourceFile(), directSpec, "require") ??
- npmStaticDepSf7(this.program, decl.getSourceFile(), directSpec, "require")
+ resolveImport(this.program, ts.getSourceFileOfNode(decl), directSpec, "require") ??
+ npmStaticDepSf7(this.program, ts.getSourceFileOfNode(decl), directSpec, "require")
);
}
return createRequireProgramModuleOf(this, decl.initializer)?.dep ?? null;
@@ -4205,7 +4377,7 @@ export class Lowerer {
* report on the next tick. Later loads are cache hits either way. */
punycodeRequireWarning(node: ts.Node): IrStmt {
const loc = locOf(node);
- const fromDependency = /[\\/]node_modules[\\/]/.test(node.getSourceFile().fileName);
+ const fromDependency = /[\\/]node_modules[\\/]/.test(ts.getSourceFileOfNode(node).fileName);
return {
kind: "exprStmt",
expr: {
@@ -4276,8 +4448,8 @@ export class Lowerer {
// those globals stay uninitialized: the dep's module body would never
// run.
const dep =
- resolveImport(this.program, node.getSourceFile(), spec, "require") ??
- npmStaticDepSf7(this.program, node.getSourceFile(), spec, "require");
+ resolveImport(this.program, ts.getSourceFileOfNode(node), spec, "require") ??
+ npmStaticDepSf7(this.program, ts.getSourceFileOfNode(node), spec, "require");
if (!dep || dep.fileName.endsWith(".json")) return null;
if (this.asyncInitFiles.has(dep)) {
this.unsupported(
@@ -4294,7 +4466,7 @@ export class Lowerer {
expr: { kind: "call", callee: initName, args: [], type: VOID, loc },
loc,
};
- const parentModule = this.cjsModuleIdOf.get(node.getSourceFile());
+ const parentModule = this.cjsModuleIdOf.get(ts.getSourceFileOfNode(node));
const childModule = this.cjsModuleIdOf.get(dep);
if (parentModule === undefined || childModule === undefined) return init;
return {
@@ -5055,7 +5227,7 @@ export class Lowerer {
const decl = symbol.valueDeclaration;
if (!decl || !ts.isPropertyDeclaration(decl) || !ts.isIdentifier(decl.name)) return false;
if (ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Static) return false;
- const sf = decl.getSourceFile();
+ const sf = ts.getSourceFileOfNode(decl);
if (isJsSourceFile(sf) || sf.isDeclarationFile) return false;
const declared = typeOrNull(decl.name);
const mapped = declared ? this.mapTypeOf(declared) : null;
@@ -5145,7 +5317,8 @@ export class Lowerer {
const initSymbol = initType.getAliasSymbol() ?? initType.getSymbol();
const matchResult =
initSymbol !== undefined &&
- (initSymbol.name === "RegExpExecArray" || initSymbol.name === "RegExpMatchArray");
+ (ts.symbolName(initSymbol) === "RegExpExecArray" ||
+ ts.symbolName(initSymbol) === "RegExpMatchArray");
if (arrayType?.kind !== "array" || matchResult) return scanRecordPattern(name, init);
const positions = name.elements.nodes.filter((el) => {
const key = el.propertyName;
@@ -5321,7 +5494,7 @@ export class Lowerer {
};
for (const [symbol, signature] of signatureBySymbol) {
const declaration = functionDeclBySymbol.get(symbol);
- if (!declaration || !isJsSourceFile(declaration.getSourceFile())) continue;
+ if (!declaration || !isJsSourceFile(ts.getSourceFileOfNode(declaration))) continue;
const checked = this.checker.getSignatureFromDeclaration(declaration);
const checkedReturn = checked ? this.checker.getReturnTypeOfSignature(checked) : null;
// Checker-known optional results already narrow at each use. Only
@@ -6163,7 +6336,7 @@ export class Lowerer {
const callee = functionDeclBySymbol.get(symbol);
if (
callee &&
- isJsSourceFile(callee.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(callee)) &&
!node.arguments.nodes.some(ts.isSpreadElement)
) {
for (let i = 0; i < sig.params.length; i++) {
@@ -6379,7 +6552,7 @@ export class Lowerer {
for (const [symbol, decl] of functionDeclBySymbol)
if (scanReturns(symbol, decl)) changed = true;
const noteGenericReturn = (symbol: ts.Symbol, decl: ts.FunctionLikeDeclaration): void => {
- if (optionalReturns.has(symbol) || isJsSourceFile(decl.getSourceFile())) return;
+ if (optionalReturns.has(symbol) || isJsSourceFile(ts.getSourceFileOfNode(decl))) return;
if (!returnsOf(decl).some(genericMayBeOptional)) return;
optionalReturns.add(symbol);
this.runtimeOptionalGenericReturns.add(decl);
@@ -6656,7 +6829,7 @@ export class Lowerer {
if (
global.type.kind !== "func" ||
!ts.isBinaryExpression(declaration) ||
- !isJsSourceFile(declaration.getSourceFile())
+ !isJsSourceFile(ts.getSourceFileOfNode(declaration))
)
continue;
const value = peel(declaration.right);
@@ -7132,7 +7305,7 @@ export class Lowerer {
if (n === 0) return;
this.stats.statementsTotal += n;
this.stats.statementsFailed += n;
- const file = decl.getSourceFile().fileName;
+ const file = ts.getSourceFileOfNode(decl).fileName;
for (let i = 0; i < n; i++) {
this.bumpFileStat(file, "total");
this.bumpFileStat(file, "failed");
@@ -7751,8 +7924,8 @@ export class Lowerer {
"this statement uses a construct with no static lowering";
const messageLoc = first?.loc ?? loc;
const source = first
- ? (this.program.getSourceFile(messageLoc.file) ?? node.getSourceFile())
- : node.getSourceFile();
+ ? (this.program.getSourceFile(messageLoc.file) ?? ts.getSourceFileOfNode(node))
+ : ts.getSourceFileOfNode(node);
const pos = ts.getLineAndCharacterOfPosition(source, messageLoc.start);
return {
kind: "runtimeFence",
@@ -7871,10 +8044,12 @@ export class Lowerer {
// here; constructor objects such as Headers still can.
if (
typeSym &&
- (FETCH_AMBIENT_TYPES as readonly string[]).includes(typeSym.name) &&
+ (FETCH_AMBIENT_TYPES as readonly string[]).includes(ts.symbolName(typeSym)) &&
this.isStdlibSymbol(typeSym)
) {
- this.pushDiag(unsupportedNativeApiDiag(`a value of type '${typeSym.name}'`, locOf(node)));
+ this.pushDiag(
+ unsupportedNativeApiDiag(`a value of type '${ts.symbolName(typeSym)}'`, locOf(node)),
+ );
throw new PoisonError();
}
// ENUM OBJECTS (`typeof e` — a numeric enum's type carries the
@@ -7888,7 +8063,7 @@ export class Lowerer {
unsupportedDiag(
"SC1090",
locOf(node),
- `enum objects as values ('${typeSym.name}' — member reads like '${typeSym.name}.X' compile to constants; the object itself has no runtime representation)`,
+ `enum objects as values ('${ts.symbolName(typeSym)}' — member reads like '${ts.symbolName(typeSym)}.X' compile to constants; the object itself has no runtime representation)`,
),
);
throw new PoisonError();
@@ -7918,7 +8093,7 @@ export class Lowerer {
ownSym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
- this.isStdlibFile(d.getSourceFile()),
+ this.isStdlibFile(ts.getSourceFileOfNode(d)),
)
) {
this.pushDiag(noLoweringDiag(this.checker.typeToString(type), locOf(node)));
@@ -7964,7 +8139,7 @@ export class Lowerer {
stdlibOwnSym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
- this.isStdlibFile(d.getSourceFile()),
+ this.isStdlibFile(ts.getSourceFileOfNode(d)),
);
// OVERLOADED call signatures (SC2007) — after the generic branch, so a
// generic overload set keeps the monomorphization story: a value of a
@@ -8026,7 +8201,7 @@ export class Lowerer {
noLoweringDiag(
this.checker.typeToString(type),
locOf(node),
- typeHints[stdlibOwnSym?.name ?? ""],
+ typeHints[ts.symbolNameOf(stdlibOwnSym) ?? ""],
),
);
throw new PoisonError();
@@ -8103,7 +8278,7 @@ export class Lowerer {
}
typeOf(node: ts.Node): ts.Type {
- if (ts.isPropertyAccessExpression(node) && isJsSourceFile(node.getSourceFile())) {
+ if (ts.isPropertyAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) {
const receiver =
node.expression.kind === ts.SyntaxKind.ThisKeyword && this.fnStack.length > 0
? this.resolveThis()?.type
@@ -8124,7 +8299,7 @@ export class Lowerer {
}
// A symbol-keyed field read through a CommonJS key-table key reads as
// the field's layout type (tableKeyFieldType).
- if (ts.isElementAccessExpression(node) && isJsSourceFile(node.getSourceFile())) {
+ if (ts.isElementAccessExpression(node) && isJsSourceFile(ts.getSourceFileOfNode(node))) {
const field = tableKeyFieldType(this, node);
if (field) {
if (field.kind === "f64") return this.checker.getNumberType();
@@ -8138,7 +8313,7 @@ export class Lowerer {
// into native field loads when the method is borrowed by another object.
if (
node.kind === ts.SyntaxKind.ThisKeyword &&
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
this.fnStack.length > 0 &&
this.resolveThis()?.type.kind === "dyn"
)
@@ -8412,7 +8587,7 @@ export class Lowerer {
// locals at null/undefined despite later writes. Keep mutable native
// storage instead of a slot restricted to the initializer's unit value.
if (
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
(t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) !== 0
)
return DYN;
@@ -8451,6 +8626,12 @@ export class Lowerer {
* validator ICEs on them as the usual backstop. */
requireExactShape(node: ts.Node, actual: IrType, expected: IrType): void {
if (typeEquals(actual, expected)) return;
+ if (!this.generalNodeStreams && needsGeneralNodeStreams(this, actual, expected)) {
+ this.requiresGeneralNodeStreams = true;
+ // An untyped value into a stream slot also needs the checked-dynamic
+ // slots (dyn-streams.ts).
+ if (actual.kind === "dyn") this.requiresDynNodeStreams = true;
+ }
// dyn mismatches first. A dyn ('unknown') value flowing into a typed
// slot needs a CHECKED cast — the hint points at `as `; tsc
// usually rejects this before we do, so the fence is mostly defensive.
@@ -8649,6 +8830,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) {
@@ -8657,6 +8849,9 @@ export class Lowerer {
}
if (expr.type.kind === "void" && expected.kind !== "void" && this.neverValued.has(expr))
return this.divergentValue(expr, expected);
+ // process.stdout/stderr into a general Writable slot (general-streams.ts).
+ const stdioStream = stdioIntoGeneralStream(this, expr, expected);
+ if (stdioStream) return this.coerceToExpected(stdioStream, expected);
// Iterator-typed slots: native iterators and differently typed
// generators step through an adapter generator.
if (expected.kind === "generator" && !typeEquals(expr.type, expected)) {
@@ -8828,6 +9023,7 @@ export class Lowerer {
(id) => this.unions.get(id),
);
const handleOk = DYN_HANDLE_KINDS.has(expected.kind);
+ noteUntypedStreamFlow(this, expected);
if (
canDynCheckTo(
expected,
@@ -9023,6 +9219,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;
@@ -9360,6 +9560,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
@@ -9417,7 +9618,9 @@ export class Lowerer {
: [];
const errorOptions = parts.some((part) => {
const sym = part.getAliasSymbol() ?? part.getSymbol();
- return sym?.name === "ErrorOptions" && this.isStdlibSymbol(sym);
+ return (
+ sym !== undefined && ts.symbolName(sym) === "ErrorOptions" && this.isStdlibSymbol(sym)
+ );
});
if (errorOptions) {
const actual = this.typeOf(node);
@@ -9453,7 +9656,7 @@ export class Lowerer {
let literal = node;
while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
if (
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
!ts.isArrayLiteralExpression(literal) &&
e.kind === "dynFrom" &&
e.value.type.kind === "array"
@@ -9485,7 +9688,7 @@ export class Lowerer {
if (
e.type.kind === "union" &&
!typeEquals(e.type, expected) &&
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
this.dynConvertible(e.type)
) {
const sourceArms = this.unions.get(e.type.unionId)?.arms ?? [];
@@ -9540,7 +9743,7 @@ export class Lowerer {
!ts.isIdentifier(d.name) ||
!ts.isVariableDeclarationList(d.parent) ||
!ts.isVariableStatement(d.parent.parent) ||
- d.getSourceFile().isDeclarationFile ||
+ ts.getSourceFileOfNode(d).isDeclarationFile ||
(ts.getCombinedModifierFlags(d) & ts.ModifierFlags.Ambient) !== 0
) {
return null;
@@ -9687,6 +9890,8 @@ export class Lowerer {
node = node.expression;
if (expected) {
if (ts.isConditionalExpression(node)) return lowerTernary(this, node, expected);
+ const escaped = this.neverAssertionInto(node, expected);
+ if (escaped) return escaped;
const fresh = this.emptyCollectionFor(node, expected);
if (fresh) return this.coerceInto(node, fresh, expected);
}
@@ -9757,11 +9962,11 @@ export class Lowerer {
const builtinOption = (name: string): boolean =>
parts.some((part) => {
const sym = part.getAliasSymbol() ?? part.getSymbol();
- return sym?.name === name && this.isStdlibSymbol(sym);
+ return ts.symbolNameOf(sym) === name && this.isStdlibSymbol(sym);
});
if (ts.isObjectLiteralExpression(x)) {
if (
- isJsSourceFile(x.getSourceFile()) ||
+ isJsSourceFile(ts.getSourceFileOfNode(x)) ||
builtinOption("RequestInit") ||
builtinOption("ErrorOptions") ||
builtinOption("SpawnOptions") ||
@@ -9844,7 +10049,7 @@ export class Lowerer {
coerceExistingInto(node: ts.Expression, e: IrExpr, expected: IrType): IrExpr {
if (
(expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) ||
- (expected.kind === "dyn" && isJsSourceFile(node.getSourceFile()))
+ (expected.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(node)))
) {
// A destination that accepts undefined must retain an unchecked
// read's storage value even when the checker still calls it present.
@@ -9867,7 +10072,8 @@ export class Lowerer {
return null;
const symbol = this.resolveValueSymbol(value.expression);
if (!symbol || !this.isStdlibSymbol(symbol)) return null;
- const kind = symbol.name === "Map" ? "map" : symbol.name === "Set" ? "set" : null;
+ const kind =
+ ts.symbolName(symbol) === "Map" ? "map" : ts.symbolName(symbol) === "Set" ? "set" : null;
if (!kind) return null;
const arms =
expected.kind === "union" ? (this.unions.get(expected.unionId)?.arms ?? []) : [expected];
@@ -9992,7 +10198,7 @@ export class Lowerer {
while (ts.isParenthesizedExpression(literal)) literal = literal.expression;
return (
expected.kind === "dyn" &&
- isJsSourceFile(node.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(node)) &&
ts.isObjectLiteralExpression(literal)
);
}
@@ -10097,6 +10303,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",
@@ -10449,7 +10664,7 @@ export class Lowerer {
}
}
}
- if (!isJsSourceFile(decl.getSourceFile())) continue;
+ if (!isJsSourceFile(ts.getSourceFileOfNode(decl))) continue;
for (const member of decl.members.nodes) {
if (ts.isConstructorDeclaration(member)) {
for (const param of member.parameters.nodes) {
@@ -10482,7 +10697,7 @@ export class Lowerer {
}
// npm-static implicit-any classification resolves body parameter
// references while collecting the method's shape.
- if (implicitMonoFile(decl.getSourceFile()) && member.body) {
+ if (implicitMonoFile(ts.getSourceFileOfNode(decl)) && member.body) {
symbolRoots.push(member.body);
}
}
@@ -10591,6 +10806,14 @@ export class Lowerer {
return findMethodOn(this, info, name);
}
+ /** A class whose root is the runtime's %Error (builtin errors and their
+ * program subclasses). */
+ isErrorRootedClass(className: string): boolean {
+ let info = this.classes.get(className) ?? null;
+ while (info && info.base) info = info.base;
+ return info?.def.name === "%Error";
+ }
+
isSubclassOf(sub: string, sup: string): boolean {
return isSubclassOf(this, sub, sup);
}
@@ -10877,6 +11100,108 @@ export class Lowerer {
return declareContextLocal(this.ctx, name, type, false, undefined, undefined);
}
+ /** The body binding of a fixed-tuple rest parameter: one native slot per
+ * position (the flattened ABI), rebuilt into the tuple the source names.
+ * The empty tuple rides the empty-array representation. */
+ declareRestTupleParam(
+ decl: ts.ParameterDeclaration,
+ nameNode: ts.Identifier,
+ elemShapes: readonly ParamShape[],
+ params: IrParam[],
+ prologue: IrStmt[],
+ ): void {
+ const loc = locOf(decl);
+ const name = nameNode.text;
+ const fn = decl.parent;
+ const symbol = this.checker.getSymbolAtLocation(nameNode);
+ const isRead = (id: ts.Identifier): boolean =>
+ id !== nameNode && this.checker.getSymbolAtLocation(id) === symbol;
+ const pure = fn !== undefined && symbol !== undefined && onlyForwarded(this, fn, isRead);
+ // A forwarded-only tuple is never built: a tuple with optional
+ // positions (`Parameters<(n?: T) => R>`, whose length varies per call)
+ // forwards its slots, an omitted position as undefined — which a
+ // callee's default treats exactly as an omitted argument. Its length
+ // is the one read that would observe the difference.
+ const tupleType =
+ pure &&
+ elemShapes.length > 0 &&
+ this.mapTypeOf(this.typeOf(nameNode)) === null &&
+ !readsLength(fn!, isRead)
+ ? this.forwardedSlotsTuple(elemShapes)
+ : this.irTypeOf(nameNode);
+ const slots = elemShapes.map((shape, i) => {
+ const slot = this.declareHiddenLocal(`%${name}.${i}`, shape.type);
+ params.push({ localId: slot.id, name: `%${name}.${i}`, type: shape.type });
+ return slot;
+ });
+ const body = this.declareLocal(nameNode, name, tupleType, true);
+ if (pure) {
+ this.ctx.pureTuples.set(body.id, slots);
+ return;
+ }
+ const init = this.tupleOfSlots(slots, tupleType, nameNode, loc);
+ prologue.push({ kind: "varDecl", localId: body.id, init, loc });
+ }
+
+ /** `value as never` flowing into a typed slot: `never` is assignable to
+ * every type, so the assertion promises the slot's type. A dynamic value
+ * takes the checked extraction to it, as `value as T` would. Null for
+ * every other form, and for numeric, dynamic and union slots, which
+ * keep the existing lowering. */
+ private neverAssertionInto(node: ts.Expression, expected: IrType): IrExpr | null {
+ if (!(ts.isAsExpression(node) || ts.isTypeAssertion(node))) return null;
+ if (expected.kind === "f64" || expected.kind === "dyn" || expected.kind === "union")
+ return null;
+ if (!(this.checker.getTypeFromTypeNode(node.type).flags & ts.TypeFlags.Never)) return null;
+ const innerTs = this.typeOf(node.expression);
+ if (!(innerTs.flags & ts.TypeFlags.Any) && this.mapTypeOf(innerTs)?.kind !== "dyn") return null;
+ if (
+ !canDynCheckTo(
+ expected,
+ (id) => this.shapes.get(id),
+ (id) => this.unions.get(id),
+ )
+ )
+ return null;
+ const inner = this.lowerExpr(node.expression);
+ if (inner.type.kind !== "dyn") return null;
+ return { kind: "dynCheck", value: inner, type: expected, loc: locOf(node) };
+ }
+
+ /** The tuple record of a forwarded-only rest tuple's native slots. */
+ private forwardedSlotsTuple(elemShapes: readonly ParamShape[]): IrType {
+ const fields = elemShapes.map((shape, i) => ({ name: String(i), type: shape.type }));
+ fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
+ return { kind: "record", shapeId: this.shapes.intern(fields, true) };
+ }
+
+ /** A fresh tuple holding the given positional slots, typed `tupleType`
+ * (a tuple record, or the empty-array form of `[]`). */
+ tupleOfSlots(slots: readonly IrLocal[], tupleType: IrType, blame: ts.Node, loc: SrcLoc): IrExpr {
+ if (slots.length === 0) {
+ if (tupleType.kind !== "array") this.badType(blame, this.typeOf(blame));
+ return { kind: "arrayLit", elems: [], type: tupleType, loc };
+ }
+ const shape = tupleType.kind === "record" ? this.shapes.get(tupleType.shapeId) : undefined;
+ if (!shape?.tuple || shape.fields.length !== slots.length)
+ this.badType(blame, this.typeOf(blame));
+ return {
+ kind: "recordLit",
+ fields: slots.map((slot, i) => {
+ const field = shape.fields.find((f) => f.name === String(i))!;
+ return {
+ name: field.name,
+ value: this.coerceToExpected(
+ { kind: "varRef", localId: slot.id, type: slot.type, loc },
+ field.type,
+ ),
+ };
+ }),
+ type: tupleType,
+ loc,
+ };
+ }
+
/** Declares a callee's parameter locals from its ParamShapes and builds
* the DEFAULT-PARAM PROLOGUE. Required/optional/rest params bind their
* symbol directly (one local of the ABI type). A defaulted param `x: T = e`
@@ -10913,6 +11238,17 @@ export class Lowerer {
return defaultAfterUndefined(undefinedValue, this.coerceInto(node, absent, expected));
};
decls.forEach((decl, i) => {
+ // A fixed-tuple rest parameter owns every remaining (flattened)
+ // shape, possibly none for `[]` (restTupleShapes).
+ if (
+ decl.dotDotDotToken !== undefined &&
+ ts.isIdentifier(decl.name) &&
+ i === decls.length - 1 &&
+ (shapes[i] === undefined ? shapes.length === i : shapes[i]!.restTuple === true)
+ ) {
+ this.declareRestTupleParam(decl, decl.name, shapes.slice(i), params, prologue);
+ return;
+ }
const shape = shapes[i]!;
if (ts.isArrayBindingPattern(decl.name) || ts.isObjectBindingPattern(decl.name)) {
// Pattern parameter: one hidden ABI slot carries the source value;
@@ -11244,6 +11580,13 @@ export class Lowerer {
}
if (!symbol) return null;
const direct = this.resolveKey(symbol, ident);
+ if (direct && this.ctx.pureTuples.has(direct.id)) {
+ // onlyForwarded proved every read forwards; a plain read here means
+ // the analysis and the lowering disagree.
+ throw new InternalCompilerError(
+ `lowerer bug: forwarded-only tuple '${ident.text}' read as a value`,
+ );
+ }
if (direct) return direct;
// A PARAMETER PROPERTY declares two symbols: references resolve to the
// PARAMETER symbol, while the declaration's name binds the PROPERTY
@@ -11273,8 +11616,8 @@ export class Lowerer {
/** Lexical `this` — the enclosing method's this-param, possibly captured
* through arrows (function expressions/declarations reset `this` in JS;
* their bodies never see an enclosing method's binding). */
- resolveThis(): IrLocal | null {
- return this.resolveKey(undefined);
+ resolveThis(blame?: ts.Node): IrLocal | null {
+ return this.resolveKey(undefined, blame);
}
peekLocal(ident: ts.Identifier): IrLocal | null {
@@ -11914,6 +12257,8 @@ export class Lowerer {
// dispatch. The general identifier-call paths have no lowering for a
// promise-returning ambient global, and `import` is a keyword callee no
// identifier path matches.
+ const webAssembly = lowerWebAssemblyCall(this, expr);
+ if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(this, expr.expression);
return (
lowerLifetimeRegionCall(this, expr) ??
@@ -12142,6 +12487,8 @@ export class Lowerer {
}
lowerNew(expr: ts.NewExpression): IrExpr {
+ const webAssembly = lowerWebAssemblyNew(this, expr);
+ if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(this, expr.expression, "new ");
const builtin =
lowerLifetimeRegionNew(this, expr) ??
@@ -12546,13 +12893,25 @@ 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(ts.getSourceFileOfNode(expr))) {
+ 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);
+ // `stream.Readable` as a JavaScript value: the runtime's untyped
+ // constructor, the base a constructor function inherits.
+ if (isJsSourceFile(ts.getSourceFileOfNode(expr))) {
+ const classKind = builtinMemberClassKind(bi.module, bi.member);
+ if (classKind !== null) return builtinClassValue(classKind, loc);
+ }
if (builtinModuleFnOf(this, bi.module, bi.member)) {
this.unsupported(
"SC1090",
expr,
- `library functions as values (call '${expr.getText()}' directly)`,
+ `library functions as values (call '${ts.getTextOfNode(expr)}' directly)`,
);
}
this.noLowering(
@@ -13096,3 +13455,205 @@ export class Lowerer {
return lowerStdlibMethodCall(this, call, access);
}
}
+
+/** Whether some read of a binding (identified by `isRead`) in `fn` takes
+ * its `.length`. */
+function readsLength(fn: ts.Node, isRead: (id: ts.Identifier) => boolean): boolean {
+ let found = false;
+ ts.walkPreorder(fn, (n) => {
+ if (found) return "stop";
+ if (!ts.isIdentifier(n) || !isRead(n)) return undefined;
+ let top: ts.Node = n;
+ while (
+ top.parent &&
+ (ts.isParenthesizedExpression(top.parent) ||
+ ts.isAsExpression(top.parent) ||
+ ts.isTypeAssertion(top.parent) ||
+ ts.isSatisfiesExpression(top.parent) ||
+ ts.isNonNullExpression(top.parent))
+ )
+ top = top.parent;
+ const p = top.parent;
+ if (p && ts.isPropertyAccessExpression(p) && p.expression === top && p.name.text === "length")
+ found = true;
+ return undefined;
+ });
+ return found;
+}
+
+/** 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`) 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[] {
+ 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;
+}
diff --git a/packages/compiler/src/frontend/lowering/module-member-this.ts b/packages/compiler/src/frontend/lowering/module-member-this.ts
index 592136bec..8d1592b47 100644
--- a/packages/compiler/src/frontend/lowering/module-member-this.ts
+++ b/packages/compiler/src/frontend/lowering/module-member-this.ts
@@ -70,9 +70,9 @@ function ignoresOwnThis(lowerer: Lowerer, fn: ts.Node, bindings: Bindings, depth
for (const arg of node.arguments.nodes.slice(1)) visit(arg);
return;
}
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- fn.forEachChild(visit);
+ ts.forEachChild(fn, visit);
return ignores;
}
@@ -176,7 +176,7 @@ function mayRebind(lowerer: Lowerer, fn: ts.Node, param: ts.Identifier): boolean
found = true;
return;
}
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
// The function's own parameters declare the name; their defaults can write it.
const params = (fn as { parameters?: ts.NodeArray }).parameters;
@@ -195,9 +195,9 @@ function returnedExpressions(fn: ts.Node): ts.Expression[] | null {
const visit = (node: ts.Node): void => {
if (ts.isFunctionLike(node) || ts.isClassLikeDeclaration(node)) return;
if (ts.isReturnStatement(node) && node.expression) result.push(node.expression);
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- body.forEachChild(visit);
+ ts.forEachChild(body, visit);
return result;
}
diff --git a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts
index 4270457d5..65efe5b2a 100644
--- a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts
+++ b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts
@@ -365,20 +365,22 @@ export class NanCodedReads {
private removalReason(slot: Slot): string {
const badSource = slot.sources.find((s) => s === null || this.sourceInfo(s) === null);
if (badSource !== undefined)
- return `source ${badSource ? badSource.getText().slice(0, 60) : ""}`;
+ return `source ${badSource ? ts.getTextOfNode(badSource).slice(0, 60) : ""}`;
const nan = this.genuineNaN.has(slot);
const badUse = slot.uses.find((u) => !this.useSafe(u) && (nan || !this.reboxConsumer(u)));
- if (badUse)
- return `${nan ? "nan-use" : "use"} ${transparentParent(badUse).parent?.getText().slice(0, 80) ?? "?"}`;
+ if (badUse) {
+ const parent = transparentParent(badUse).parent;
+ return `${nan ? "nan-use" : "use"} ${parent === undefined ? "?" : ts.getTextOfNode(parent).slice(0, 80)}`;
+ }
return "?";
}
private debugReport(reached: Set): void {
const name = (slot: Slot): string => {
const decl = this.host.declarationOf(slot.symbol);
- const sf = decl?.getSourceFile();
- const where = sf && decl ? `${sf.fileName}:${decl.getStart()}` : "?";
- return `${slot.kind} ${slot.symbol.name} @ ${where}`;
+ const sf = decl === undefined ? undefined : ts.getSourceFileOfNode(decl);
+ const where = sf && decl ? `${sf.fileName}:${ts.tokenStartOfNode(decl)}` : "?";
+ return `${slot.kind} ${ts.symbolName(slot.symbol)} @ ${where}`;
};
for (const slot of reached) {
const ok = this.eligible.has(slot);
@@ -483,7 +485,11 @@ export class NanCodedReads {
return true;
}
const decl = this.host.declarationOf(symbol);
- if (!decl || isJsSourceFile(decl.getSourceFile()) || decl.getSourceFile().isDeclarationFile)
+ if (
+ !decl ||
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
+ ts.getSourceFileOfNode(decl).isDeclarationFile
+ )
return false;
let name: ts.Identifier;
if (ts.isParameter(decl)) {
@@ -565,7 +571,7 @@ export class NanCodedReads {
!nonNegativeIntegerLiteral(decl.initializer) ||
!ts.isVariableDeclarationList(decl.parent) ||
!(decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) ||
- isJsSourceFile(decl.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(decl))
)
return false;
const name = decl.name;
@@ -685,7 +691,7 @@ export class NanCodedReads {
!(decl.parent.flags & ts.NodeFlags.Const) ||
// A module binding can escape through an export.
(ts.isVariableStatement(decl.parent.parent) && ts.isSourceFile(decl.parent.parent.parent)) ||
- isJsSourceFile(decl.getSourceFile()) ||
+ isJsSourceFile(ts.getSourceFileOfNode(decl)) ||
this.host.bytesElemOf(decl.name) === null
)
return null;
@@ -755,7 +761,7 @@ export class NanCodedReads {
if (!decl || (!ts.isFunctionDeclaration(decl) && !ts.isMethodDeclaration(decl))) return null;
if (!decl.body || decl.asteriskToken || decl.typeParameters?.nodes || !decl.name) return null;
if (!ts.isIdentifier(decl.name)) return null;
- const sf = decl.getSourceFile();
+ const sf = ts.getSourceFileOfNode(decl);
if (isJsSourceFile(sf) || sf.isDeclarationFile) return null;
const flags = ts.getCombinedModifierFlags(decl);
if (flags & (ts.ModifierFlags.Async | ts.ModifierFlags.Abstract | ts.ModifierFlags.Ambient))
@@ -831,7 +837,7 @@ export class NanCodedReads {
private scanBindingSlot(symbol: ts.Symbol): BindingSlot | null {
const decl = this.host.declarationOf(symbol);
if (!decl) return null;
- const sf = decl.getSourceFile();
+ const sf = ts.getSourceFileOfNode(decl);
if (isJsSourceFile(sf) || sf.isDeclarationFile) return null;
let nameNode: ts.Identifier;
const slot: BindingSlot = {
@@ -1362,7 +1368,7 @@ export class NanCodedReads {
decl.initializer &&
ts.isVariableDeclarationList(decl.parent) &&
decl.parent.flags & ts.NodeFlags.Const &&
- !isJsSourceFile(decl.getSourceFile())
+ !isJsSourceFile(ts.getSourceFileOfNode(decl))
)
return this.plainNumber(decl.initializer, depth + 1);
return null;
diff --git a/packages/compiler/src/frontend/lowering/narrowing-stability.ts b/packages/compiler/src/frontend/lowering/narrowing-stability.ts
index 01801470d..1c991c6ea 100644
--- a/packages/compiler/src/frontend/lowering/narrowing-stability.ts
+++ b/packages/compiler/src/frontend/lowering/narrowing-stability.ts
@@ -191,7 +191,8 @@ export class NarrowingStability {
const checker = this.lowerer.checker;
const signature = checker.getResolvedSignature(call);
const declaration = signature ? signature.declaration : undefined;
- if (!declaration || !this.lowerer.isStdlibFile(declaration.getSourceFile())) return true;
+ if (!declaration || !this.lowerer.isStdlibFile(ts.getSourceFileOfNode(declaration)))
+ return true;
const callee = skipOuter(call.expression);
if (
ts.isPropertyAccessExpression(callee) &&
@@ -287,11 +288,11 @@ export class NarrowingStability {
const last = chain.links[chain.links.length - 1];
const refKey = last ? `.${last.name}` : (chain.root as ts.Identifier).text;
const candidates = facts.refs.get(refKey) ?? [];
- const useStart = node.getStart();
+ const useStart = ts.tokenStartOfNode(node);
let start = -1;
for (const candidate of candidates) {
if (candidate === node) continue;
- const candidateStart = candidate.getStart();
+ const candidateStart = ts.tokenStartOfNode(candidate);
if (candidateStart >= useStart) break;
if (referenceKey(candidate) !== chain.key) continue;
start = candidateStart;
@@ -313,7 +314,7 @@ export class NarrowingStability {
// between.
let end = useStart;
for (const loop of facts.loops) {
- const loopStart = loop.getStart();
+ const loopStart = ts.tokenStartOfNode(loop);
if (loopStart >= start && loopStart <= useStart && loop.end >= node.end)
end = Math.max(end, loop.end);
}
@@ -376,7 +377,9 @@ export class NarrowingStability {
if (decl !== undefined && ts.isVariableDeclaration(decl)) {
if (decl.initializer !== undefined && this.mayBeStale(decl.initializer)) result = true;
else {
- for (const write of this.writesIn(decl.getSourceFile()).get(symbol.name) ?? []) {
+ for (const write of this.writesIn(ts.getSourceFileOfNode(decl)).get(
+ ts.symbolName(symbol),
+ ) ?? []) {
const assignment = write.parent;
if (
ts.isBinaryExpression(assignment) &&
@@ -430,7 +433,7 @@ export class NarrowingStability {
ts.isParameter(decl));
if (decl && mutableBinding) {
const home = flowBoundary(decl);
- const writes = this.writesIn(decl.getSourceFile()).get(symbol.name) ?? [];
+ const writes = this.writesIn(ts.getSourceFileOfNode(decl)).get(ts.symbolName(symbol)) ?? [];
for (const write of writes) {
if (flowBoundary(write) === home) continue;
if (this.lowerer.resolveValueSymbol(write) === symbol) {
@@ -497,7 +500,7 @@ export class NarrowingStability {
if (name === "toString" || name === "valueOf" || name === "toJSON")
facts.conversion = true;
if (ts.isComputedPropertyName(node.name)) {
- const text = node.name.expression.getText();
+ const text = ts.getTextOfNode(node.name.expression);
if (text.includes("toPrimitive")) facts.conversion = true;
if (text.includes("iterator") || text.includes("asyncIterator")) facts.iteration = true;
}
@@ -711,7 +714,7 @@ export class NarrowingStability {
const symbol = type.getSymbol();
return (
symbol !== undefined &&
- BUILTIN_COLLECTIONS.has(symbol.name) &&
+ BUILTIN_COLLECTIONS.has(ts.symbolName(symbol)) &&
this.lowerer.isStdlibSymbol(symbol)
);
}
@@ -1062,7 +1065,7 @@ function memberUse(
function flowBoundary(node: ts.Node): ts.Node {
let n: ts.Node | undefined = node.parent;
while (n && !isFlowBoundary(n)) n = n.parent;
- return n ?? node.getSourceFile();
+ return n ?? ts.getSourceFileOfNode(node);
}
function isFlowBoundary(node: ts.Node): boolean {
diff --git a/packages/compiler/src/frontend/lowering/native-ffi.ts b/packages/compiler/src/frontend/lowering/native-ffi.ts
index c9ff3300d..0f932ba93 100644
--- a/packages/compiler/src/frontend/lowering/native-ffi.ts
+++ b/packages/compiler/src/frontend/lowering/native-ffi.ts
@@ -393,7 +393,7 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr
(decl) =>
ts.isFunctionDeclaration(decl) &&
decl.body === undefined &&
- libraryCallbackOwnsFile(lowerer, decl.getSourceFile()),
+ libraryCallbackOwnsFile(lowerer, ts.getSourceFileOfNode(decl)),
);
if (callbackShaped) {
lowerer.pushDiag(
@@ -429,13 +429,13 @@ export function lowerFfiCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr
if (lowerer.libraryCallbacks) {
const declarations = symbol.declarations;
const programDeclarations = declarations.filter((decl) =>
- libraryCallbackOwnsFile(lowerer, decl.getSourceFile()),
+ libraryCallbackOwnsFile(lowerer, ts.getSourceFileOfNode(decl)),
);
const programAmbient =
programDeclarations.length > 0 &&
programDeclarations.every(
(decl) =>
- decl.getSourceFile().isDeclarationFile ||
+ ts.getSourceFileOfNode(decl).isDeclarationFile ||
(ts.getCombinedModifierFlags(decl as ts.Declaration) & ts.ModifierFlags.Ambient) !==
0,
);
diff --git a/packages/compiler/src/frontend/lowering/object-factory-new.ts b/packages/compiler/src/frontend/lowering/object-factory-new.ts
index ddd8d6d56..bcb675692 100644
--- a/packages/compiler/src/frontend/lowering/object-factory-new.ts
+++ b/packages/compiler/src/frontend/lowering/object-factory-new.ts
@@ -41,7 +41,7 @@ function isObjectFactoryNew(lowerer: Lowerer, expr: ts.NewExpression): boolean {
!owner ||
!declaration?.body ||
!bindingNeverReassigned(lowerer, symbol, owner) ||
- !isJsSourceFile(declaration.getSourceFile()) ||
+ !isJsSourceFile(ts.getSourceFileOfNode(declaration)) ||
declaration.asteriskToken ||
declaration.modifiers?.nodes.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)
)
diff --git a/packages/compiler/src/frontend/lowering/private-brands.ts b/packages/compiler/src/frontend/lowering/private-brands.ts
index e7b3edbd8..8507cc81a 100644
--- a/packages/compiler/src/frontend/lowering/private-brands.ts
+++ b/packages/compiler/src/frontend/lowering/private-brands.ts
@@ -30,7 +30,7 @@ export function perEvaluationPrivateNames(info: ClassInfo): boolean {
const declaration = info.localClass ? info.decl : null;
return (
!!declaration &&
- isJsSourceFile(declaration.getSourceFile()) &&
+ isJsSourceFile(ts.getSourceFileOfNode(declaration)) &&
declaration.members.nodes.some(
(member) => member.name !== undefined && ts.isPrivateIdentifier(member.name),
)
diff --git a/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts
new file mode 100644
index 000000000..09d228b05
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/promise-executor-adapter.ts
@@ -0,0 +1,102 @@
+import { varRef } from "../../ir/build.js";
+import {
+ type IrExpr,
+ type IrLocal,
+ type IrParam,
+ type IrType,
+ type SrcLoc,
+ VOID,
+ funcOf,
+ typeKey,
+} from "../../ir/ir.js";
+import { newFnCtx, type Lowerer } from "./lowerer.js";
+
+/** An executor VALUE whose parameters are checked-dynamic (a JavaScript
+ * function declaration: `function run(resolve, reject) {...}; return new
+ * Promise(run)`): the promise's native executor is an adapter closure that
+ * receives the native (resolve, reject) pair and calls the original with
+ * both boxed into the checked-dynamic tree, so the JavaScript body calls
+ * them like any function value. Null when the executor's shape is not
+ * adaptable (more than two parameters, a rest parameter, or a parameter
+ * that neither is dyn nor already the native closure). */
+export function adaptPromiseExecutor(
+ lowerer: Lowerer,
+ executor: IrExpr,
+ inner: IrType,
+ loc: SrcLoc,
+): IrExpr | null {
+ const execT = executor.type;
+ if (execT.kind !== "func" || execT.rest || execT.params.length > 2) return null;
+ if (!execT.params.some((p) => p.kind === "dyn")) return null;
+ const resolveT = funcOf(inner.kind === "void" ? [] : [inner], VOID);
+ const rejectT = funcOf([{ kind: "object", className: "%Error" }], VOID);
+ const native = [resolveT, rejectT];
+ if (
+ !execT.params.every((p, i) => p.kind === "dyn" || typeKey(p) === typeKey(native[i]!)) ||
+ !native.slice(0, execT.params.length).every((t) => lowerer.dynConvertible(t))
+ )
+ return null;
+ const adaptT = funcOf(native, VOID);
+ const key = `promise.executor:${typeKey(execT)}:${typeKey(inner)}`;
+ let name = lowerer.arrHofHelpers.get(key);
+ if (name === undefined) {
+ name = `%promise.executor.${lowerer.arrHofHelpers.size}`;
+ lowerer.arrHofHelpers.set(key, name);
+ const impl = `${name}.impl`;
+ const captured: IrLocal = { id: "f.0", name: "f", type: execT, mutable: false, boxed: true };
+ const params: IrParam[] = [
+ { localId: "resolve.0", name: "resolve", type: resolveT },
+ { localId: "reject.0", name: "reject", type: rejectT },
+ ];
+ const ctx = newFnCtx(false, null, null, VOID);
+ lowerer.fnStack.push(ctx);
+ let args: IrExpr[];
+ try {
+ args = execT.params.map((p, i) =>
+ lowerer.coerceToExpected(varRef(params[i]!.localId, native[i]!, loc), p),
+ );
+ } finally {
+ lowerer.fnStack.pop();
+ }
+ lowerer.liftedFns.push({
+ name: impl,
+ params,
+ returnType: VOID,
+ captures: [{ localId: captured.id, name: captured.name, type: execT }],
+ locals: [
+ captured,
+ ...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
+ ...ctx.locals,
+ ],
+ body: [
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "callValue",
+ callee: varRef(captured.id, execT, loc),
+ args,
+ type: execT.ret,
+ loc,
+ },
+ loc,
+ },
+ ],
+ loc,
+ });
+ lowerer.liftedFns.push({
+ name,
+ params: [{ localId: captured.id, name: captured.name, type: execT }],
+ returnType: adaptT,
+ locals: [captured],
+ body: [
+ {
+ kind: "return",
+ value: { kind: "closure", fnName: impl, captures: [captured.id], type: adaptT, loc },
+ loc,
+ },
+ ],
+ loc,
+ });
+ }
+ return { kind: "call", callee: name, args: [executor], type: adaptT, loc };
+}
diff --git a/packages/compiler/src/frontend/lowering/require-initialization.ts b/packages/compiler/src/frontend/lowering/require-initialization.ts
index 2f84e988a..d979423b0 100644
--- a/packages/compiler/src/frontend/lowering/require-initialization.ts
+++ b/packages/compiler/src/frontend/lowering/require-initialization.ts
@@ -119,7 +119,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void {
const tag = rawTag === "" ? "e." : rawTag.replace(/^%/, "");
const bySymbol = new Map();
for (const { decl } of declared) {
- const flagId = `%g.${tag}%required.${decl.getStart(sf)}`;
+ const flagId = `%g.${tag}%required.${ts.tokenStartOfNode(decl)}`;
lowerer.globalsList.push({ id: flagId, name: "%initialized", type: BOOL, mutable: true });
const flag: RequireInitFlag = { flagId, names: new Map() };
for (const id of boundNames(decl.name)) {
@@ -148,7 +148,7 @@ export function markRequireEarlyBindings(lowerer: Lowerer): void {
if (owner === undefined) continue;
if (boundNames(owner.decl.name).includes(id)) continue;
// Code after the require runs after it, except inside functions.
- if (id.getStart(sf) >= owner.decl.getEnd() && !inDeferredPosition(id)) continue;
+ if (ts.tokenStartOfNode(id) >= owner.decl.end && !inDeferredPosition(id)) continue;
const body = enclosingBody(id);
lowerer.requireEarlyReferences.push({
flag: owner.flag,
diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts
index d3acaff6b..f332d0f51 100644
--- a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts
+++ b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts
@@ -287,7 +287,7 @@ function assertsSlot(node: ts.Node, operands: Operands): boolean {
return;
}
if ((n.flags & ts.NodeFlags.OptionalChain) !== 0) return;
- n.forEachChild(visit);
+ ts.forEachChild(n, visit);
};
if (ts.isVariableStatement(node)) {
for (const declaration of node.declarationList.declarations.nodes)
@@ -323,7 +323,7 @@ function unchanged(nodes: readonly ts.Node[], { array, index }: Operands): boole
let hit = false;
const walk = (n: ts.Node): void => {
if (ts.isIdentifier(n) && roots.includes(n.text)) hit = true;
- n.forEachChild(walk);
+ ts.forEachChild(n, walk);
};
walk(e);
return hit;
@@ -372,7 +372,7 @@ function unchanged(nodes: readonly ts.Node[], { array, index }: Operands): boole
ok = false;
return;
}
- n.forEachChild(visit);
+ ts.forEachChild(n, visit);
};
for (const node of nodes) visit(node);
return ok;
diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts
index 2fa8785c9..5ed011ce1 100644
--- a/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts
+++ b/packages/compiler/src/frontend/lowering/runtime-optional-elements.ts
@@ -611,7 +611,7 @@ export class ArrayElementStates {
if (this.host.markIterationBinding(name, known)) changed = true;
return;
}
- name.forEachChild(mark);
+ ts.forEachChild(name, mark);
};
if (ts.isVariableDeclarationList(initializer)) {
for (const declaration of initializer.declarations.nodes) mark(declaration.name);
diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts b/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts
index 768932ad3..3e4605f94 100644
--- a/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts
+++ b/packages/compiler/src/frontend/lowering/runtime-optional-guards.ts
@@ -21,7 +21,7 @@ export function reassignedBindings(
else if (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e)) {
const walk = (n: ts.Node): void => {
if (ts.isIdentifier(n)) mark(n);
- else n.forEachChild(walk);
+ else ts.forEachChild(n, walk);
};
walk(e);
}
diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts b/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts
index adecfa5a9..dd1dce04d 100644
--- a/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts
+++ b/packages/compiler/src/frontend/lowering/runtime-optional-ownership.ts
@@ -378,9 +378,9 @@ function usesArguments(fn: ts.FunctionExpression): boolean {
if (found) return;
if (ts.isIdentifier(node) && node.text === "arguments") found = true;
else if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node))
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- fn.body?.forEachChild(visit);
+ if (fn.body !== undefined) ts.forEachChild(fn.body, visit);
return found;
}
diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts b/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts
index f150e314b..3363f197d 100644
--- a/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts
+++ b/packages/compiler/src/frontend/lowering/runtime-optional-provenance.ts
@@ -33,8 +33,8 @@ export class RuntimeOptionalProvenance {
function describe(node: ts.Node | undefined): string {
if (!node) return "?";
- const sf = node.getSourceFile();
- const start = node.getStart();
+ const sf = ts.getSourceFileOfNode(node);
+ const start = ts.tokenStartOfNode(node);
const pos = ts.getLineAndCharacterOfPosition(sf, start);
const text = sf.text.slice(start, node.end).replace(/\s+/g, " ");
const short = text.length > 60 ? `${text.slice(0, 57)}...` : text;
diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts b/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts
index f95694624..50b508a52 100644
--- a/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts
+++ b/packages/compiler/src/frontend/lowering/runtime-optional-records.test.ts
@@ -21,7 +21,7 @@ function classes(source: string): {
const symbolFor = (name: string): ts.Symbol => {
let symbol = symbols.get(name);
if (!symbol) {
- symbol = { name, declarations: [] } as unknown as ts.Symbol;
+ symbol = { escapedName: name, declarations: [] } as unknown as ts.Symbol;
symbols.set(name, symbol);
}
return symbol;
diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-records.ts b/packages/compiler/src/frontend/lowering/runtime-optional-records.ts
index bad2523c8..18e136d69 100644
--- a/packages/compiler/src/frontend/lowering/runtime-optional-records.ts
+++ b/packages/compiler/src/frontend/lowering/runtime-optional-records.ts
@@ -351,7 +351,7 @@ export class RecordFlowClasses {
private referencesOf(symbol: ts.Symbol): readonly ts.Identifier[] {
let list = this.references.get(symbol);
if (!list) {
- list = (this.names.get(symbol.name) ?? []).filter(
+ list = (this.names.get(ts.symbolName(symbol)) ?? []).filter(
(node) => this.host.symbolOf(node) === symbol,
);
this.references.set(symbol, list);
@@ -412,7 +412,7 @@ export class RecordFlowClasses {
const symbol = ts.isIdentifier(base) ? this.host.symbolOf(base) : null;
const target = symbol ? this.host.declarationOf(symbol) : undefined;
if (!target || !ts.isClassDeclaration(target)) return true;
- const file = target.getSourceFile();
+ const file = ts.getSourceFileOfNode(target);
if (file.isDeclarationFile || isJsFile(file)) return true;
current = target;
}
@@ -591,7 +591,7 @@ export class RecordFlowClasses {
} else {
this.markOpen(slot);
}
- if (this.shorthandNames.has(symbol.name)) this.markOpen(slot);
+ if (this.shorthandNames.has(ts.symbolName(symbol))) this.markOpen(slot);
for (const reference of this.referencesOf(symbol)) {
if (declaration && (declaration as { name?: ts.Node }).name === reference) continue;
if (!this.use(reference, slot)) this.markOpen(slot);
@@ -824,9 +824,9 @@ export function returnExpressions(fn: ts.FunctionLikeDeclaration): ts.Expression
}
if (ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node))
return;
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- fn.body?.forEachChild(visit);
+ if (fn.body !== undefined) ts.forEachChild(fn.body, visit);
return out;
}
@@ -865,9 +865,9 @@ function usesArguments(body: ts.Node): boolean {
if (found) return;
if (ts.isIdentifier(node) && node.text === "arguments") found = true;
else if (!ts.isFunctionDeclaration(node) && !ts.isFunctionExpression(node))
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- body.forEachChild(visit);
+ ts.forEachChild(body, visit);
return found;
}
diff --git a/packages/compiler/src/frontend/lowering/split-consumers.ts b/packages/compiler/src/frontend/lowering/split-consumers.ts
index 9624ac0aa..8195d2792 100644
--- a/packages/compiler/src/frontend/lowering/split-consumers.ts
+++ b/packages/compiler/src/frontend/lowering/split-consumers.ts
@@ -45,7 +45,7 @@ export function isPrivateSplitConsumer(
)
return false;
- for (const use of identifiersNamed(lowerer, decl.getSourceFile(), name.text)) {
+ for (const use of identifiersNamed(lowerer, ts.getSourceFileOfNode(decl), name.text)) {
if (use === decl.name || use === name) continue;
if (lowerer.resolveValueSymbol(use) === symbol) return false;
}
diff --git a/packages/compiler/src/frontend/lowering/static-function-names.ts b/packages/compiler/src/frontend/lowering/static-function-names.ts
new file mode 100644
index 000000000..8868d6745
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/static-function-names.ts
@@ -0,0 +1,219 @@
+import * as ts from "../ts7/adapter.js";
+import { isJsSourceFile } from "../program.js";
+import type { Lowerer } from "./lowerer.js";
+import type { GenericFnInfo } from "./generic-functions.js";
+import { stripTypeWrappers } from "./typed-arguments.js";
+
+/** `fn.name` inside generic higher-order functions.
+ *
+ * Native closures carry no name, but a generic wrapper is instantiated per
+ * call site anyway: when its body reads `.name` of a function parameter,
+ * the instantiation key also carries the static name of that call's
+ * argument (the function declaration's name, a const binding's inferred
+ * name, the name of the function a wrapper call returns), and the read
+ * folds to that string. A call whose argument's name is only known at run
+ * time keeps a precise refusal at the read. */
+
+/** Parameter index → symbol for the parameters whose `.name` the generic
+ * body reads (and never reassigns). Computed once per generic. */
+export function nameReadParams(lowerer: Lowerer, info: GenericFnInfo): Map {
+ if (info.nameReadParams) return info.nameReadParams;
+ const out = new Map();
+ // JavaScript generics read function names through the checked-value
+ // box, which carries the runtime name already.
+ if (isJsSourceFile(ts.getSourceFileOfNode(info.decl))) {
+ info.nameReadParams = out;
+ return out;
+ }
+ const bySymbol = new Map();
+ info.decl.parameters.nodes.forEach((param, i) => {
+ if (!ts.isIdentifier(param.name) || param.dotDotDotToken) return;
+ if (!functionTypedParameter(lowerer, param)) return;
+ const symbol = lowerer.checker.getSymbolAtLocation(param.name);
+ if (symbol) bySymbol.set(symbol, i);
+ });
+ if (bySymbol.size > 0) {
+ const written = new Set();
+ const read = new Set();
+ ts.walkPreorder(info.decl, (node) => {
+ if (ts.isTypeNode(node)) return "skip";
+ if (
+ ts.isPropertyAccessExpression(node) &&
+ node.name.text === "name" &&
+ ts.isIdentifier(node.expression)
+ ) {
+ const symbol = lowerer.checker.getSymbolAtLocation(node.expression);
+ if (symbol && bySymbol.has(symbol)) read.add(symbol);
+ }
+ if (
+ ts.isBinaryExpression(node) &&
+ node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
+ node.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
+ ts.isIdentifier(node.left)
+ ) {
+ const symbol = lowerer.checker.getSymbolAtLocation(node.left);
+ if (symbol) written.add(symbol);
+ }
+ return undefined;
+ });
+ for (const symbol of read) {
+ if (!written.has(symbol)) out.set(bySymbol.get(symbol)!, symbol);
+ }
+ }
+ info.nameReadParams = out;
+ return out;
+}
+
+/** A parameter declared with a function type, or with a type parameter
+ * constrained to one (`fn: F` where `F extends (...args: any) => any`). */
+function functionTypedParameter(lowerer: Lowerer, param: ts.ParameterDeclaration): boolean {
+ const t = lowerer.typeOf(param.name);
+ if (lowerer.checker.getCallSignatures(t).length > 0) return true;
+ if (!(t.flags & ts.TypeFlags.TypeParameter)) return false;
+ const symbol = t.getSymbol();
+ const decl = symbol ? symbol.declarations.find(ts.isTypeParameterDeclaration) : undefined;
+ if (!decl?.constraint) return false;
+ const constraint = lowerer.checker.getTypeFromTypeNode(decl.constraint);
+ return lowerer.checker.getCallSignatures(constraint).length > 0;
+}
+
+/** The instantiation key suffix and name bindings of one call of `info`,
+ * or null when the body reads no parameter's name. */
+export function staticNameBindings(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ info: GenericFnInfo,
+): { key: string; names: Map } | null {
+ const params = nameReadParams(lowerer, info);
+ if (params.size === 0) return null;
+ const names = new Map();
+ const parts: string[] = [];
+ for (const [index, symbol] of params) {
+ const arg = call.arguments.nodes[index];
+ const name = arg && !ts.isSpreadElement(arg) ? staticFunctionName(lowerer, arg) : null;
+ names.set(symbol, name);
+ parts.push(name === null ? "?" : JSON.stringify(name));
+ }
+ return { key: `@name(${parts.join(",")})`, names };
+}
+
+/** The `name` JavaScript gives the function `expr` evaluates to, when it is
+ * statically known; null otherwise. */
+export function staticFunctionName(lowerer: Lowerer, expr: ts.Expression): string | null {
+ return staticNameOf(lowerer, expr, new Set());
+}
+
+function staticNameOf(lowerer: Lowerer, expr: ts.Expression, seen: Set): string | null {
+ const e = stripTypeWrappers(expr);
+ if (seen.has(e)) return null;
+ seen.add(e);
+ // An anonymous function as a call argument gets no inferred name.
+ if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) {
+ return !ts.isArrowFunction(e) && e.name ? e.name.text : "";
+ }
+ if (ts.isIdentifier(e)) {
+ const symbol = lowerer.resolveValueSymbol(e);
+ if (!symbol) return null;
+ const bound = lowerer.staticFnNames?.get(symbol);
+ if (bound !== undefined) return bound;
+ const decl = symbol.valueDeclaration;
+ if (decl === undefined) return null;
+ if (ts.isFunctionDeclaration(decl)) return decl.name ? decl.name.text : "default";
+ if (
+ ts.isVariableDeclaration(decl) &&
+ ts.isIdentifier(decl.name) &&
+ decl.initializer !== undefined &&
+ (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0
+ ) {
+ const init = stripTypeWrappers(decl.initializer);
+ // `const f = () => …` / `const f = function () {}`: the binding names
+ // an anonymous function definition.
+ if (ts.isArrowFunction(init)) return decl.name.text;
+ if (ts.isFunctionExpression(init)) return init.name ? init.name.text : decl.name.text;
+ return staticNameOf(lowerer, init, seen);
+ }
+ return null;
+ }
+ if (ts.isCallExpression(e) && ts.isIdentifier(e.expression)) {
+ // A call of a generic higher-order function returns the function its
+ // body returns: every `return` names the same anonymous definition or
+ // the same local const binding.
+ const symbol = lowerer.resolveValueSymbol(e.expression);
+ const info = symbol ? lowerer.genericFnsBySymbol.get(symbol) : undefined;
+ if (!info) return null;
+ return returnedFunctionName(lowerer, info.decl, seen);
+ }
+ if (ts.isPropertyAccessExpression(e)) {
+ const symbol = lowerer.checker.getSymbolAtLocation(e.name);
+ const decl = symbol ? symbol.valueDeclaration : undefined;
+ if (decl && ts.isMethodDeclaration(decl) && ts.isIdentifier(decl.name)) return decl.name.text;
+ return null;
+ }
+ return null;
+}
+
+function returnedFunctionName(
+ lowerer: Lowerer,
+ decl: GenericFnInfo["decl"],
+ seen: Set,
+): string | null {
+ const body = decl.body;
+ if (body === undefined) return null;
+ if (!ts.isBlock(body)) return functionValueName(lowerer, body, seen);
+ let name: string | null | undefined;
+ let ok = true;
+ ts.walkPreorder(body, (node) => {
+ if (!ok) return "stop";
+ if (ts.isFunctionLike(node)) return "skip";
+ if (!ts.isReturnStatement(node)) return undefined;
+ const here = node.expression ? functionValueName(lowerer, node.expression, seen) : null;
+ if (here === null || (name !== undefined && name !== here)) ok = false;
+ else name = here;
+ return undefined;
+ });
+ return ok && name !== undefined ? name : null;
+}
+
+/** The name of a returned function value: an anonymous definition returned
+ * directly is nameless; a local const binding names its definition. */
+function functionValueName(
+ lowerer: Lowerer,
+ expr: ts.Expression,
+ seen: Set,
+): string | null {
+ const e = stripTypeWrappers(expr);
+ if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) {
+ return !ts.isArrowFunction(e) && e.name ? e.name.text : "";
+ }
+ if (ts.isIdentifier(e)) return staticNameOf(lowerer, e, seen);
+ return null;
+}
+
+/** `fn.name` where `fn` is a name-keyed parameter of the generic instance
+ * being lowered: the folded name, or a precise refusal when this call
+ * site's argument has no static name. Null for every other read. */
+export function staticFunctionNameRead(
+ lowerer: Lowerer,
+ expr: ts.PropertyAccessExpression,
+): string | null {
+ const names = lowerer.staticFnNames;
+ if (
+ names === null ||
+ expr.name.text !== "name" ||
+ expr.questionDotToken !== undefined ||
+ !ts.isIdentifier(expr.expression)
+ ) {
+ return null;
+ }
+ const symbol = lowerer.checker.getSymbolAtLocation(expr.expression);
+ if (!symbol || !names.has(symbol)) return null;
+ const name = names.get(symbol);
+ if (name === null || name === undefined) {
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ `reading '${expr.expression.text}.name' when the call passes a function whose name is only known at run time (generic wrappers fold the name of a function declaration, a const-bound function, or a wrapper call's result)`,
+ );
+ }
+ return name;
+}
diff --git a/packages/compiler/src/frontend/lowering/static-this.ts b/packages/compiler/src/frontend/lowering/static-this.ts
index 004a72375..5c26ef0c9 100644
--- a/packages/compiler/src/frontend/lowering/static-this.ts
+++ b/packages/compiler/src/frontend/lowering/static-this.ts
@@ -31,10 +31,10 @@ export function rejectStaticThis(
const keyword = node.kind === ts.SyntaxKind.ThisKeyword ? "this" : "super";
lowerer.unsupported("SC1090", node, message(keyword));
}
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
if (includeRoot) visit(root);
- else root.forEachChild(visit);
+ else ts.forEachChild(root, visit);
}
export function hasStaticThis(root: ts.Node): boolean {
@@ -47,9 +47,9 @@ export function hasStaticThis(root: ts.Node): boolean {
ts.isClassExpression(node)
)
return;
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- root.forEachChild(visit);
+ ts.forEachChild(root, visit);
return found;
}
@@ -60,8 +60,8 @@ export function hasLexicalPrivateReference(root: ts.Node): boolean {
const visit = (node: ts.Node): void => {
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) return;
if (ts.isPrivateIdentifier(node)) found = true;
- node.forEachChild(visit);
+ ts.forEachChild(node, visit);
};
- root.forEachChild(visit);
+ ts.forEachChild(root, visit);
return found;
}
diff --git a/packages/compiler/src/frontend/lowering/string-index-bounds.ts b/packages/compiler/src/frontend/lowering/string-index-bounds.ts
index acb41514f..6e422b231 100644
--- a/packages/compiler/src/frontend/lowering/string-index-bounds.ts
+++ b/packages/compiler/src/frontend/lowering/string-index-bounds.ts
@@ -73,7 +73,7 @@ function isBoundary(node: ts.Node): boolean {
function containerOf(node: ts.Node): ts.Node {
let n = node.parent;
while (n && !isBoundary(n)) n = n.parent;
- return n ?? node.getSourceFile();
+ return n ?? ts.getSourceFileOfNode(node);
}
function isLoop(node: ts.Node): boolean {
@@ -130,7 +130,7 @@ function writeOf(id: ts.Identifier): Write | null {
const k = p.operatorToken.kind;
if (p.left !== n || k < ts.SyntaxKind.FirstAssignment || k > ts.SyntaxKind.LastAssignment)
return null;
- const at = id.getStart();
+ const at = ts.tokenStartOfNode(id);
if (destructured) return { at, step: null, value: null };
if (k === ts.SyntaxKind.EqualsToken) return { at, step: null, value: p.right };
const amount = integerLiteral(p.right);
@@ -146,15 +146,15 @@ function writeOf(id: ts.Identifier): Write | null {
(p.operator !== ts.SyntaxKind.PlusPlusToken && p.operator !== ts.SyntaxKind.MinusMinusToken)
)
return null;
- if (destructured) return { at: id.getStart(), step: null, value: null };
+ if (destructured) return { at: ts.tokenStartOfNode(id), step: null, value: null };
return {
- at: id.getStart(),
+ at: ts.tokenStartOfNode(id),
step: p.operator === ts.SyntaxKind.PlusPlusToken ? 1 : -1,
value: null,
};
}
if (ts.isForOfStatement(p) || ts.isForInStatement(p))
- return p.initializer === n ? { at: id.getStart(), step: null, value: null } : null;
+ return p.initializer === n ? { at: ts.tokenStartOfNode(id), step: null, value: null } : null;
if (ts.isParenthesizedExpression(p)) {
n = p;
continue;
@@ -199,7 +199,7 @@ export class StringIndexBounds {
private prove(read: ts.ElementAccessExpression): boolean {
// JavaScript declarations are not checked against callers.
- if (read.questionDotToken || isJsSourceFile(read.getSourceFile())) return false;
+ if (read.questionDotToken || isJsSourceFile(ts.getSourceFileOfNode(read))) return false;
const textNode = read.expression;
const indexNode = read.argumentExpression;
if (!ts.isIdentifier(textNode) || !ts.isIdentifier(indexNode)) return false;
@@ -268,7 +268,7 @@ export class StringIndexBounds {
if (!(list.flags & ts.NodeFlags.Const)) return null;
} else if (declaration.initializer) {
writes.push({
- at: declaration.name.getStart(),
+ at: ts.tokenStartOfNode(declaration.name),
step: null,
value: declaration.initializer,
});
@@ -280,7 +280,7 @@ export class StringIndexBounds {
const container = containerOf(declaration);
let escapes = false;
let assigned = 0;
- for (const id of this.namesIn(container).get(symbol.name) ?? []) {
+ for (const id of this.namesIn(container).get(ts.symbolName(symbol)) ?? []) {
if (id === nameNode) continue;
const write = writeOf(id);
if (!write) continue;
diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index 6fbbe3be4..a884b39e1 100644
--- a/packages/compiler/src/frontend/lowering/surfaces.ts
+++ b/packages/compiler/src/frontend/lowering/surfaces.ts
@@ -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",
@@ -2431,6 +2452,27 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
name: "perf_hooks.performance.clearMeasures",
fns: ["perf.clearMeasures"],
},
+ {
+ id: "node-builtin.perf_hooks.performance.eventLoopUtilization",
+ kind: "node-builtin",
+ name: "perf_hooks.performance.eventLoopUtilization",
+ fns: ["perf.loopStart", "perf.idleTime"],
+ note: "active time is the main thread's CPU time since the loop started; the one- and two-argument difference forms are supported",
+ },
+ {
+ id: "node-builtin.perf_hooks.monitorEventLoopDelay",
+ kind: "node-builtin",
+ name: "perf_hooks.monitorEventLoopDelay",
+ fns: [
+ "perf.eldNew",
+ "perf.eldEnable",
+ "perf.eldDisable",
+ "perf.eldReset",
+ "perf.eldPercentile",
+ "perf.eldStat",
+ ],
+ note: "an unref'd sampling interval records the delay histogram; enable, disable, reset, percentile and the min, max, mean, stddev, count and exceeds readings are supported",
+ },
// ── the process global's ambient reads and authority calls
// (lowerProcessProperty/lowerProcessMethodCall — process is a
// provenance-checked global here, not an importable module).
@@ -2803,7 +2845,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
@@ -3266,7 +3312,7 @@ export function isChildSurfaceMember(
const decl = sym ? sym.declarations[0] : undefined;
const iface = decl?.parent;
if (!decl || !iface || !ts.isInterfaceDeclaration(iface)) return false;
- if (lowerer.isStdlibFile(decl.getSourceFile())) return false;
+ if (lowerer.isStdlibFile(ts.getSourceFileOfNode(decl))) return false;
return lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(iface.name))?.kind === "child";
}
@@ -3276,7 +3322,9 @@ export function isChildSurfaceMember(
* with lib interfaces, so a member can carry declarations from both. A
* user's own declaration is in neither, so shadowing never matches. */
export function isStdlibSymbol(lowerer: Lowerer, symbol: ts.Symbol | undefined): boolean {
- return !!symbol && symbol.declarations.some((d) => lowerer.isStdlibFile(d.getSourceFile()));
+ return (
+ !!symbol && symbol.declarations.some((d) => lowerer.isStdlibFile(ts.getSourceFileOfNode(d)))
+ );
}
/** The canonical stdlib-global name `expr` denotes, or null. Three
@@ -3334,20 +3382,20 @@ export function stdlibGlobalNameOf(lowerer: Lowerer, expr: ts.Expression): strin
if (
decl !== undefined &&
(ts.isVariableDeclaration(decl) || ts.isBindingElement(decl)) &&
- isJsSourceFile(decl.getSourceFile())
+ isJsSourceFile(ts.getSourceFileOfNode(decl))
) {
const imported = lowerer.builtinImportOf(expr);
if (imported?.module === "buffer" && imported.member === "Buffer") return "Buffer";
}
return null;
}
- return resolved.name === "global" ? "globalThis" : resolved.name;
+ return ts.symbolName(resolved) === "global" ? "globalThis" : ts.symbolName(resolved);
}
if (ts.isPropertyAccessExpression(expr) && !expr.questionDotToken) {
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "globalThis") return null;
const symbol = lowerer.checker.getSymbolAtLocation(expr.name);
if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
- return symbol.name === "global" ? "globalThis" : symbol.name;
+ return ts.symbolName(symbol) === "global" ? "globalThis" : ts.symbolName(symbol);
}
return null;
}
@@ -3433,6 +3481,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;
@@ -3477,7 +3529,7 @@ export function nodeTypesOnlySymbol(lowerer: Lowerer, sym: ts.Symbol | null | un
if (!decls || decls.length === 0) return false;
let viaNode = false;
for (const d of decls) {
- const sf = d.getSourceFile();
+ const sf = ts.getSourceFileOfNode(d);
if (sf.isDeclarationFile && isNodeTypesPath(sf.fileName)) viaNode = true;
else if (lowerer.isStdlibFile(sf)) return false;
}
diff --git a/packages/compiler/src/frontend/lowering/symbol-fields.ts b/packages/compiler/src/frontend/lowering/symbol-fields.ts
index 0791fc6cd..56718615e 100644
--- a/packages/compiler/src/frontend/lowering/symbol-fields.ts
+++ b/packages/compiler/src/frontend/lowering/symbol-fields.ts
@@ -118,13 +118,13 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS
const init = decl.initializer;
if (init && (ts.isIdentifier(init) || ts.isPropertyAccessExpression(init))) {
const target = classSymbolKeyOf(lowerer, init);
- if (!target || bindingWritten(lowerer, sym, decl.getSourceFile())) return null;
+ if (!target || bindingWritten(lowerer, sym, ts.getSourceFileOfNode(decl))) return null;
const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl));
if (
bindingEarlyUse7(
lowerer.program,
- decl.getSourceFile(),
- decl.getSourceFile().statements.nodes.indexOf(list.parent),
+ ts.getSourceFileOfNode(decl),
+ ts.getSourceFileOfNode(decl).statements.nodes.indexOf(list.parent),
decl,
preceding,
) !== null
@@ -139,7 +139,7 @@ export function classSymbolKeyOfSymbol(lowerer: Lowerer, sym: ts.Symbol): ClassS
const { registered, description } = call;
// Ordinary Symbol() is a fresh identity per evaluation, so only the
// module-scope declaration above can name one fixed layout slot.
- const sf = decl.getSourceFile();
+ const sf = ts.getSourceFileOfNode(decl);
if (bindingWritten(lowerer, sym, sf)) return null;
const preceding = list.declarations.nodes.slice(0, list.declarations.nodes.indexOf(decl));
// Creating a function value does not execute its body. The general
@@ -268,7 +268,7 @@ function cjsTableSymbolKeyOf(
)
return null;
const stmt = assign.parent;
- const sf = stmt.getSourceFile();
+ const sf = ts.getSourceFileOfNode(stmt);
if (cjsExportDiscardReason(stmt) !== null) return null;
const call = literalSymbolCallOf(lowerer, decl.initializer);
if (!call) return null;
@@ -330,7 +330,7 @@ function tablePropertyWritten(
if (node.name.text !== name) return false;
const recv = node.expression;
if (
- node.getSourceFile() === home &&
+ ts.getSourceFileOfNode(node) === home &&
((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv))
)
return true;
@@ -381,7 +381,7 @@ function bindingWritten(lowerer: Lowerer, sym: ts.Symbol, sf: ts.SourceFile): bo
// Assignment to obj[key] changes the property, never the key's
// binding. The same applies to these leaves in destructuring.
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) return "skip";
- if (!ts.isIdentifier(node) || node.text !== sym.name) return;
+ if (!ts.isIdentifier(node) || node.text !== ts.symbolName(sym)) return;
const targetSym = ts.isShorthandPropertyAssignment(node.parent)
? lowerer.checker.getShorthandAssignmentValueSymbol(node.parent)
: lowerer.checker.getSymbolAtLocation(node);
diff --git a/packages/compiler/src/frontend/lowering/symbol-methods.ts b/packages/compiler/src/frontend/lowering/symbol-methods.ts
index f6a8e68e4..b02e95641 100644
--- a/packages/compiler/src/frontend/lowering/symbol-methods.ts
+++ b/packages/compiler/src/frontend/lowering/symbol-methods.ts
@@ -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:` 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;
+}
diff --git a/packages/compiler/src/frontend/lowering/typed-arguments.ts b/packages/compiler/src/frontend/lowering/typed-arguments.ts
new file mode 100644
index 000000000..2fbd83482
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/typed-arguments.ts
@@ -0,0 +1,516 @@
+import * as ts from "../ts7/adapter.js";
+import { bodyReadsArguments } from "../arguments-usage.js";
+import { isJsSourceFile, locOf } from "../program.js";
+import { restTupleElementTypes, unitOnlyUnion } from "../type-mapper.js";
+import { DYN, type IrExpr, type IrLocal, type IrStmt, type IrType, arrayOf } from "../../ir/ir.js";
+import type { Lowerer } from "./lowerer.js";
+import { captureContextArguments } from "./function-context.js";
+import { isThisParameter, type ParamShape } from "./call-signatures.js";
+import { tryLowerExpression } from "./expressions/try-lower-expression.js";
+import { omittedArgFor } from "./call-arguments.js";
+
+/** Variadic forwarding through a FIXED signature.
+ *
+ * A TypeScript function expression that declares no parameters and reads
+ * `arguments` (`function (this: unknown) { return fn.apply(this, arguments) }
+ * as F`) is lowered against the fixed function type its value is asserted
+ * to: one native slot per position of that type, and `arguments` is the
+ * tuple of those slots. Inside a generic instantiation F is the call
+ * site's concrete signature, so a higher-order wrapper specializes to the
+ * wrapped function's exact ABI. Forwarding (`f.apply(thisArg, args)`,
+ * `f(...args)`) then calls with the tuple's positions directly. */
+
+/** `e`, with the type-only wrappers that never change its value removed. */
+export function stripTypeWrappers(e: ts.Expression): ts.Expression {
+ let cur = e;
+ for (;;) {
+ if (ts.isParenthesizedExpression(cur)) cur = cur.expression;
+ else if (ts.isAsExpression(cur) || ts.isTypeAssertion(cur) || ts.isSatisfiesExpression(cur))
+ cur = cur.expression;
+ else if (ts.isNonNullExpression(cur)) cur = cur.expression;
+ else return cur;
+ }
+}
+
+/** The outermost type assertion around `node` (`node as unknown as F` → F),
+ * looking through parentheses; null when the value is not asserted. */
+function assertedTypeNode(node: ts.Node): ts.TypeNode | null {
+ let cur: ts.Node = node;
+ let found: ts.TypeNode | null = null;
+ for (let parent = cur.parent; parent !== undefined; parent = cur.parent) {
+ if (ts.isParenthesizedExpression(parent)) {
+ cur = parent;
+ continue;
+ }
+ if ((ts.isAsExpression(parent) || ts.isTypeAssertion(parent)) && parent.expression === cur) {
+ found = parent.type;
+ cur = parent;
+ continue;
+ }
+ break;
+ }
+ return found;
+}
+
+/** The fixed function type a value is asserted to, or null. */
+function assertedFuncType(lowerer: Lowerer, node: ts.Node): (IrType & { kind: "func" }) | null {
+ const typeNode = assertedTypeNode(node);
+ if (!typeNode) return null;
+ const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeFromTypeNode(typeNode));
+ return mapped?.kind === "func" ? mapped : null;
+}
+
+function sameFuncType(a: IrType & { kind: "func" }, b: IrType & { kind: "func" }): boolean {
+ return JSON.stringify(a) === JSON.stringify(b);
+}
+
+/** A function expression whose `arguments` can be typed: TypeScript source,
+ * no runtime parameters (a `this` parameter is type-only), an ordinary
+ * (non-generator) function that reads its own `arguments`. */
+export function typedArgumentsCandidate(node: ts.Node): node is ts.FunctionExpression {
+ return (
+ ts.isFunctionExpression(node) &&
+ node.asteriskToken === undefined &&
+ !isJsSourceFile(ts.getSourceFileOfNode(node)) &&
+ node.parameters.nodes.every(isThisParameter) &&
+ bodyReadsArguments(node)
+ );
+}
+
+/** The fixed signature a typed-arguments function expression is lowered
+ * against: the type its value is asserted to (`function () {…} as F`), the
+ * one type every use of its const binding asserts it to (`const w =
+ * function () {…}; return w as unknown as F`), or its contextual function
+ * type. Null when none names one fixed-arity signature. */
+export function typedArgumentsTarget(
+ lowerer: Lowerer,
+ node: ts.FunctionExpression,
+): (IrType & { kind: "func" }) | null {
+ const fixed = (t: (IrType & { kind: "func" }) | null) => (t && t.rest !== true ? t : null);
+ const direct = assertedFuncType(lowerer, node);
+ if (direct) return fixed(direct);
+ let top: ts.Node = node;
+ while (top.parent && ts.isParenthesizedExpression(top.parent)) top = top.parent;
+ const decl = top.parent;
+ if (
+ decl &&
+ ts.isVariableDeclaration(decl) &&
+ decl.initializer === top &&
+ decl.type === undefined &&
+ ts.isIdentifier(decl.name) &&
+ (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0
+ ) {
+ const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
+ const scope = enclosingScope(decl);
+ if (symbol && scope) {
+ let target: (IrType & { kind: "func" }) | null = null;
+ let ok = true;
+ let uses = 0;
+ ts.walkPreorder(scope, (n) => {
+ if (!ok) return "stop";
+ if (n === decl.name || !ts.isIdentifier(n) || n.text !== decl.name.text) return undefined;
+ if (lowerer.checker.getSymbolAtLocation(n) !== symbol) return undefined;
+ uses++;
+ const asserted = assertedFuncType(lowerer, n);
+ if (!asserted || (target && !sameFuncType(target, asserted))) ok = false;
+ else target = asserted;
+ return undefined;
+ });
+ if (ok && uses > 0) return fixed(target);
+ }
+ }
+ const ctxType = lowerer.checker.getContextualType(node);
+ if (!ctxType) return null;
+ const sigs = lowerer.checker.getCallSignatures(lowerer.checker.getNonNullableType(ctxType));
+ if (sigs.length !== 1) return null;
+ const mapped = lowerer.mapTypeOf(lowerer.checker.getNonNullableType(ctxType));
+ return mapped?.kind === "func" ? fixed(mapped) : null;
+}
+
+function enclosingScope(node: ts.Node): ts.Node | null {
+ for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
+ if (ts.isSourceFile(cur) || (ts.isFunctionLike(cur) && cur.body !== undefined)) return cur;
+ }
+ return null;
+}
+
+/** The tuple type of a fixed parameter list, interned exactly as the
+ * spelled tuple (and `Parameters`) map: positional fields, or the
+ * empty-array form for no parameters. */
+export function tupleTypeOfParams(lowerer: Lowerer, params: readonly IrType[]): IrType {
+ if (params.length === 0) return arrayOf(unitOnlyUnion(lowerer.unions));
+ const fields = params.map((type, i) => ({ name: String(i), type }));
+ fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
+ return { kind: "record", shapeId: lowerer.shapes.intern(fields, true) };
+}
+
+/** The ABI shapes of a typed-arguments function: one required slot per
+ * position of its target signature. */
+export function typedArgumentsShapes(target: IrType & { kind: "func" }): ParamShape[] {
+ return target.params.map((type) => ({ type, mode: "required" }));
+}
+
+/** Declares the hidden positional slots of a typed-arguments function and
+ * binds its `arguments` to their tuple. */
+export function declareTypedArguments(
+ lowerer: Lowerer,
+ node: ts.FunctionExpression,
+ shapes: readonly ParamShape[],
+ params: { localId: string; name: string; type: IrType }[],
+ prologue: IrStmt[],
+): void {
+ const loc = locOf(node);
+ const slots = shapes.map((shape, i) => {
+ const slot = lowerer.declareHiddenLocal(`%arguments.${i}`, shape.type);
+ params.push({ localId: slot.id, name: `%arguments.${i}`, type: shape.type });
+ return slot;
+ });
+ const tupleType = tupleTypeOfParams(
+ lowerer,
+ shapes.map((s) => s.type),
+ );
+ const local = lowerer.declareHiddenLocal("%arguments", tupleType);
+ lowerer.ctx.argumentsLocal = local;
+ // Pure forwarding (every read is forwarded, counted, or aliased by a
+ // binding that is itself only forwarded): no tuple exists at all.
+ if (
+ onlyForwarded(lowerer, node, (id) => id.text === "arguments" && argumentsOwner(id) === node)
+ ) {
+ lowerer.ctx.pureTuples.set(local.id, slots);
+ return;
+ }
+ prologue.push({
+ kind: "varDecl",
+ localId: local.id,
+ init: lowerer.tupleOfSlots(slots, tupleType, node, loc),
+ loc,
+ });
+}
+
+/** Whether every read of a tuple binding in `fn` (identified by `isRoot`)
+ * only forwards it: `f.apply(t, x)`, `g(...x)` into a program function,
+ * `x.length`, or a const alias whose own reads only forward. Reads inside
+ * nested functions (arrows included) capture the binding and need the
+ * tuple. Type assertions around a read never matter. */
+export function onlyForwarded(
+ lowerer: Lowerer,
+ fn: ts.Node,
+ isRoot: (id: ts.Identifier) => boolean,
+): boolean {
+ const body = (fn as ts.Node & { body?: ts.Node }).body;
+ if (body === undefined) return false;
+ const roots: ((id: ts.Identifier) => boolean)[] = [isRoot];
+ let ok = true;
+ const visit = (scope: ts.Node, test: (id: ts.Identifier) => boolean): void => {
+ ts.walkPreorder(scope, (n) => {
+ if (!ok) return "stop";
+ if (ts.isTypeNode(n)) return "skip";
+ if (!ts.isIdentifier(n) || !test(n)) return undefined;
+ for (let cur = n.parent; cur !== undefined && cur !== fn; cur = cur.parent) {
+ if (ts.isFunctionLike(cur)) {
+ ok = false;
+ return "stop";
+ }
+ }
+ let top: ts.Node = n;
+ while (
+ top.parent &&
+ (ts.isParenthesizedExpression(top.parent) ||
+ ts.isAsExpression(top.parent) ||
+ ts.isTypeAssertion(top.parent) ||
+ ts.isSatisfiesExpression(top.parent) ||
+ ts.isNonNullExpression(top.parent))
+ )
+ top = top.parent;
+ const p = top.parent;
+ if (!p) {
+ ok = false;
+ return "stop";
+ }
+ if (ts.isPropertyAccessExpression(p) && p.expression === top && p.name.text === "length")
+ return undefined;
+ if (
+ ts.isCallExpression(p) &&
+ p.arguments.nodes.length === 2 &&
+ p.arguments.nodes[1] === top &&
+ ts.isPropertyAccessExpression(p.expression) &&
+ p.expression.name.text === "apply" &&
+ p.expression.questionDotToken === undefined
+ )
+ return undefined;
+ if (ts.isSpreadElement(p) && p.parent && ts.isCallExpression(p.parent)) {
+ const sigs = lowerer.checker.getCallSignatures(
+ lowerer.checker.getNonNullableType(lowerer.typeOf(p.parent.expression)),
+ );
+ const decl = sigs.length === 1 ? sigs[0]!.declaration : undefined;
+ if (decl && !ts.getSourceFileOfNode(decl).isDeclarationFile) return undefined;
+ }
+ if (
+ ts.isVariableDeclaration(p) &&
+ p.initializer === top &&
+ ts.isIdentifier(p.name) &&
+ (ts.getCombinedNodeFlags(p) & ts.NodeFlags.Const) !== 0
+ ) {
+ const alias = lowerer.checker.getSymbolAtLocation(p.name);
+ const aliasName = p.name;
+ if (alias) {
+ roots.push((id) => id !== aliasName && lowerer.checker.getSymbolAtLocation(id) === alias);
+ return undefined;
+ }
+ }
+ ok = false;
+ return "stop";
+ });
+ };
+ for (let i = 0; i < roots.length && ok; i++) visit(body, roots[i]!);
+ return ok;
+}
+
+/** The native slots of a pure-forwarded tuple binding read at `node` in
+ * the current function, or null when the tuple is materialized. */
+function pureSlotsOf(lowerer: Lowerer, node: ts.Expression): IrLocal[] | null {
+ const operand = stripTypeWrappers(node);
+ if (!ts.isIdentifier(operand)) return null;
+ const local =
+ operand.text === "arguments" ? lowerer.ctx.argumentsLocal : lowerer.peekLocal(operand);
+ return local ? (lowerer.ctx.pureTuples.get(local.id) ?? null) : null;
+}
+
+/** The positional values `node` forwards when it is a pure-forwarded tuple
+ * (slot reads, in order), or null. */
+export function pureForwardedValues(lowerer: Lowerer, node: ts.Expression): IrExpr[] | null {
+ const slots = pureSlotsOf(lowerer, node);
+ if (!slots) return null;
+ const loc = locOf(node);
+ return slots.map((slot) => ({ kind: "varRef", localId: slot.id, type: slot.type, loc }));
+}
+
+/** Whether `node` reads a tuple-typed `arguments` binding: the nearest
+ * ordinary function (arrows inherit it) is a typed-arguments function.
+ * Looks the binding up without allocating captures. */
+function isTypedArgumentsRead(lowerer: Lowerer, node: ts.Expression): boolean {
+ if (!ts.isIdentifier(node) || node.text !== "arguments") return false;
+ const stack = lowerer.fnStack;
+ let owner = stack.length - 1;
+ while (owner > 0 && stack[owner]!.inheritsArguments) owner--;
+ const own = owner >= 0 ? stack[owner]!.argumentsLocal : undefined;
+ return own !== undefined && own !== null && own.type.kind !== "dyn";
+}
+
+/** The fixed tuple `node` forwards, or null: the typed `arguments`, a
+ * fixed-tuple rest parameter, or a const alias of either — seen through
+ * type assertions, which never change the value. */
+export function forwardedTupleType(lowerer: Lowerer, node: ts.Expression): IrType | null {
+ const operand = stripTypeWrappers(node);
+ if (!ts.isIdentifier(operand)) return null;
+ if (operand.text === "arguments") {
+ if (!isTypedArgumentsRead(lowerer, operand)) return null;
+ return captureContextArguments(lowerer.fnStack)!.type;
+ }
+ const symbol = lowerer.resolveValueSymbol(operand);
+ if (!symbol) return null;
+ const alias = lowerer.tupleAliasSymbols.get(symbol);
+ if (alias) return alias;
+ const decl = symbol.valueDeclaration;
+ if (
+ decl !== undefined &&
+ ts.isParameter(decl) &&
+ decl.dotDotDotToken !== undefined &&
+ ts.isIdentifier(decl.name) &&
+ !isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
+ // Checker-only probe first: an ordinary array rest never maps here.
+ restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx) !== null
+ ) {
+ // A tuple with optional positions has no value type of its own; its
+ // forwarded-only binding carries the slots' tuple (declareRestTupleParam).
+ const t = lowerer.mapTypeOf(lowerer.typeOf(operand)) ?? lowerer.peekLocal(operand)?.type;
+ if (t && isTupleValueType(lowerer, t)) return t;
+ }
+ return null;
+}
+
+function isTupleValueType(lowerer: Lowerer, t: IrType): boolean {
+ if (t.kind === "record") return lowerer.shapes.get(t.shapeId)?.tuple === true;
+ // The empty tuple's representation (mapType's `[]` rule): an array over
+ // the unit-only union. Looked up, never interned, so probing a value
+ // leaves the registries untouched.
+ if (t.kind !== "array" || t.elem.kind !== "union") return false;
+ const arms = lowerer.unions.get(t.elem.unionId)?.arms;
+ return (
+ arms !== undefined &&
+ arms.length === 2 &&
+ arms.some((a) => a.kind === "nullT") &&
+ arms.some((a) => a.kind === "undefinedT")
+ );
+}
+
+/** The positional reads of a forwarded tuple value (already saved in a
+ * local), in order. */
+function tupleReads(
+ lowerer: Lowerer,
+ ref: () => IrExpr,
+ tupleType: IrType,
+ loc: IrExpr["loc"],
+): IrExpr[] {
+ if (tupleType.kind !== "record") return [];
+ const shape = lowerer.shapes.get(tupleType.shapeId)!;
+ return [...shape.fields]
+ .sort((a, b) => Number(a.name) - Number(b.name))
+ .map((field) => ({
+ kind: "recordGet" as const,
+ obj: ref(),
+ shapeId: tupleType.shapeId,
+ field: field.name,
+ type: field.type,
+ loc,
+ }));
+}
+
+/** `f.apply(thisArg, args)` where `args` forwards a fixed tuple and `f` is a
+ * fixed-arity function value: a direct call of `f` with the tuple's
+ * positions and `thisArg` as the call-time receiver. Null for every other
+ * form (the existing Function.prototype paths keep them). */
+export function lowerForwardingApply(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+): IrExpr | null {
+ if (access.name.text !== "apply" || access.questionDotToken !== undefined) return null;
+ const argNodes = call.arguments.nodes;
+ if (argNodes.length !== 2 || argNodes.some(ts.isSpreadElement)) return null;
+ const tupleType = forwardedTupleType(lowerer, argNodes[1]!);
+ if (!tupleType) return null;
+ const callee = tryLowerExpression(lowerer, access.expression);
+ if (callee?.type.kind !== "func") return null;
+ const fnType = callee.type;
+ const loc = locOf(call);
+ if (fnType.rest === true) {
+ lowerer.unsupported(
+ "SC1090",
+ call,
+ "forwarding a fixed argument tuple through 'apply' to a variadic function value",
+ );
+ }
+ const savedFn = lowerer.declareHiddenLocal("%applyFn", fnType);
+ const savedThis = lowerer.declareHiddenLocal("%applyThis", DYN);
+ const stmts: IrStmt[] = [
+ { kind: "varDecl", localId: savedFn.id, init: callee, loc },
+ {
+ kind: "varDecl",
+ localId: savedThis.id,
+ init: lowerer.lowerExprExpecting(argNodes[0]!, DYN),
+ loc,
+ },
+ ];
+ let reads = pureForwardedValues(lowerer, argNodes[1]!);
+ if (!reads) {
+ const savedArgs = lowerer.declareHiddenLocal("%applyArgs", tupleType);
+ stmts.push({
+ kind: "varDecl",
+ localId: savedArgs.id,
+ init: lowerer.lowerExpr(stripTypeWrappers(argNodes[1]!)),
+ loc,
+ });
+ reads = tupleReads(
+ lowerer,
+ () => ({ kind: "varRef", localId: savedArgs.id, type: tupleType, loc }),
+ tupleType,
+ loc,
+ );
+ }
+ // JS drops surplus positions and fills missing ones with undefined.
+ const args = fnType.params.map((param, i): IrExpr => {
+ const read = reads[i];
+ if (read) return lowerer.coerceInto(call, read, param);
+ const absent = omittedArgFor(lowerer, param, loc);
+ if (!absent) {
+ lowerer.unsupported(
+ "SC1090",
+ call,
+ "forwarding fewer arguments through 'apply' than the callee's required parameters",
+ );
+ }
+ return absent;
+ });
+ const invoked: IrExpr = {
+ kind: "callValue",
+ callee: { kind: "varRef", localId: savedFn.id, type: fnType, loc },
+ receiver: { kind: "varRef", localId: savedThis.id, type: DYN, loc },
+ args,
+ type: fnType.ret,
+ loc,
+ };
+ return { kind: "seqExpr", stmts, result: invoked, type: fnType.ret, loc };
+}
+
+/** `const args = arguments as …` (or an alias of a rest tuple) inside a
+ * typed-arguments function: the binding holds the tuple itself — the
+ * assertion only re-describes the value — and stays a forwarding source. */
+export function lowerTupleAliasDecl(lowerer: Lowerer, decl: ts.VariableDeclaration): IrStmt | null {
+ if (
+ !decl.initializer ||
+ !ts.isIdentifier(decl.name) ||
+ (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) === 0 ||
+ lowerer.fnStack.length === 0
+ ) {
+ return null;
+ }
+ const tupleType = forwardedTupleType(lowerer, decl.initializer);
+ if (!tupleType) return null;
+ const symbol = lowerer.checker.getSymbolAtLocation(decl.name);
+ if (!symbol) return null;
+ const declared = lowerer.mapTypeOf(lowerer.typeOf(decl.name));
+ if (declared && declared.kind !== "dyn" && JSON.stringify(declared) !== JSON.stringify(tupleType))
+ return null;
+ lowerer.tupleAliasSymbols.set(symbol, tupleType);
+ const slots = pureSlotsOf(lowerer, decl.initializer);
+ if (slots) {
+ // An alias of a pure-forwarded tuple is itself only forwarded
+ // (onlyForwarded followed it): it names the same slots.
+ const local = lowerer.declareLocal(decl.name, decl.name.text, tupleType, false);
+ lowerer.ctx.pureTuples.set(local.id, slots);
+ return { kind: "block", body: [], loc: locOf(decl) };
+ }
+ const init = lowerer.lowerExpr(stripTypeWrappers(decl.initializer));
+ const local = lowerer.declareLocal(decl.name, decl.name.text, tupleType, false);
+ return { kind: "varDecl", localId: local.id, init, loc: locOf(decl) };
+}
+
+/** A spread operand forwarding a fixed tuple (forwardedTupleType),
+ * including one that resolved to no positions at all. */
+export function isForwardedTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean {
+ return forwardedTupleType(lowerer, node) !== null;
+}
+
+/** `arguments` read inside a typed-arguments function: its fixed arity. */
+export function typedArgumentsArity(lowerer: Lowerer, node: ts.Expression): number | null {
+ if (!ts.isIdentifier(node)) return null;
+ const t = forwardedTupleType(lowerer, node);
+ if (!t) return null;
+ if (t.kind !== "record") return 0;
+ const fields = lowerer.shapes.get(t.shapeId)?.fields ?? [];
+ // A position that may be undefined may also be absent at the call: the
+ // native signature always passes it, so the count is not static.
+ if (
+ fields.some(
+ (f) => f.type.kind === "union" && lowerer.armTag(f.type.unionId, { kind: "undefinedT" }) >= 0,
+ )
+ ) {
+ lowerer.unsupported(
+ "SC1090",
+ node.parent ?? node,
+ "'length' of an argument tuple whose positions may be omitted (the native signature always passes every position, so the count is not static)",
+ );
+ }
+ return fields.length;
+}
+
+/** The function whose `arguments` an identifier reads: the nearest
+ * enclosing non-arrow function. */
+export function argumentsOwner(node: ts.Node): ts.Node | null {
+ for (let cur = node.parent; cur !== undefined; cur = cur.parent) {
+ if (ts.isFunctionLike(cur) && !ts.isArrowFunction(cur)) return cur;
+ if (ts.isSourceFile(cur)) return null;
+ }
+ return null;
+}
diff --git a/packages/compiler/src/frontend/lowering/var-hoisting.ts b/packages/compiler/src/frontend/lowering/var-hoisting.ts
index c445f1751..6276ab671 100644
--- a/packages/compiler/src/frontend/lowering/var-hoisting.ts
+++ b/packages/compiler/src/frontend/lowering/var-hoisting.ts
@@ -97,7 +97,7 @@ function hoistingScopeOf(node: ts.Node): Scope {
)
return p;
}
- return node.getSourceFile();
+ return ts.getSourceFileOfNode(node);
}
function statementsOf(scope: Scope): readonly ts.Statement[] {
@@ -221,7 +221,7 @@ class ScopeAnalysis {
* the statement at index `settle`. `read`: `ref` reads the binding
* itself, so tsc vouches for one in the scope's own code. */
settles(scope: Scope, settle: number, ref: ts.Identifier, read = true): boolean {
- if (ref.getSourceFile() !== this.sf) return true;
+ if (ts.getSourceFileOfNode(ref) !== this.sf) return true;
const statements = statementsOf(scope);
let deferred: ts.Node | null = null;
let top: ts.Node | null = null;
diff --git a/packages/compiler/src/frontend/lowering/width-lift.ts b/packages/compiler/src/frontend/lowering/width-lift.ts
index d90f9ed35..e3722bc32 100644
--- a/packages/compiler/src/frontend/lowering/width-lift.ts
+++ b/packages/compiler/src/frontend/lowering/width-lift.ts
@@ -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" }
diff --git a/packages/compiler/src/frontend/module-source-candidates.test.ts b/packages/compiler/src/frontend/module-source-candidates.test.ts
index 4ff20decc..8fccef000 100644
--- a/packages/compiler/src/frontend/module-source-candidates.test.ts
+++ b/packages/compiler/src/frontend/module-source-candidates.test.ts
@@ -6,6 +6,7 @@ import * as ast from "./ts7/ast.js";
import { Ts7Api } from "./ts7/rpc-api.js";
import { Ts7Host } from "./ts7/program-host.js";
import { moduleSourceCandidates } from "./module-source-candidates.js";
+import { getTextOfNode } from "./ts7/node-functions.js";
test("module candidates preserve nested call order and belong to one program snapshot", () => {
const directory = mkdtempSync(join(tmpdir(), "scriptc-module-candidates-"));
@@ -24,7 +25,7 @@ test("module candidates preserve nested call order and belong to one program sna
const source = first.getSourceFile(entry)!;
const candidates = moduleSourceCandidates(first, source);
expect(candidates.specifiers).toEqual(["first", "second", "third", "fourth"]);
- expect(candidates.calls.map((call) => call.expression.getText())).toEqual([
+ expect(candidates.calls.map((call) => getTextOfNode(call.expression))).toEqual([
"outer",
"require",
"import",
diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts
index 6bc830d74..e6dfe9a56 100644
--- a/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts
+++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.test.ts
@@ -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');
+});
diff --git a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts
index 33ff865ec..b625c456d 100644
--- a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts
+++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts
@@ -23,6 +23,8 @@
* 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";
+import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js";
export interface BundledChunkSplit {
/** The bundle with chunk declarations blanked and calls respelled. */
@@ -31,7 +33,7 @@ export interface BundledChunkSplit {
chunks: { path: string; source: string }[];
}
-interface Edit {
+export interface Edit {
start: number;
end: number;
text: string;
@@ -42,11 +44,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 +56,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 +85,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 {
+export function declaredNames(root: ts.Node): Set {
const names = new Set();
// Binding patterns nest BindingElements, which the walk visits too.
ts.walkPreorder(root, (n) => {
@@ -272,7 +274,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 +287,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)) {
@@ -365,16 +367,16 @@ function helperForm(
const ret = stmts[0];
if (stmts.length !== 1 || ret === undefined || !ts.isReturnStatement(ret) || !ret.expression)
return null;
- const actual = helperTokens(ret.expression.getText(sf));
+ const actual = helperTokens(getTextOfNode(ret.expression));
if (actual === null) return null;
// The own-key reader: esbuild's alias or the 0.11 spelling.
- const keysRef = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(ret.expression.getText(sf));
+ const keysRef = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(getTextOfNode(ret.expression));
const keys = keysRef?.[1];
if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null;
return actual === template(KEYED_RETURN, cb, mod, keys) ? "keyed" : null;
}
if (ts.isArrowFunction(body) && body.parameters.nodes.length === 0 && !ts.isBlock(body.body)) {
- const actual = helperTokens(body.body.getText(sf));
+ const actual = helperTokens(getTextOfNode(body.body));
return actual !== null && actual === template(ARROW_BODY, cb, mod, "") ? "arrow" : null;
}
return null;
@@ -471,7 +473,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;
@@ -538,7 +540,7 @@ export function splitBundledChunks(
const init = t.node.initializer;
return (
init !== undefined &&
- helperTokens(init.getText(sf)) === helperTokens("Object.getOwnPropertyNames")
+ helperTokens(getTextOfNode(init)) === helperTokens("Object.getOwnPropertyNames")
);
};
if (helperNames.length === 0) {
@@ -637,10 +639,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)})`,
+ start: tokenStartOfNode(call),
+ end: call.end,
+ text: `require(${JSON.stringify(forwarded ?? chunk.spec)})`,
});
}
for (const name of chunks.keys()) {
@@ -652,6 +655,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: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" });
+ continue;
+ }
const result = chunkSource(
sf,
chunk,
@@ -665,12 +673,12 @@ export function splitBundledChunks(
);
if (typeof result !== "string") return result;
out.push({ path: chunk.path, source: result });
- bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" });
+ bundleEdits.push({ start: tokenStartOfNode(chunk.stmt), end: chunk.stmt.end, text: "" });
}
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 +702,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 +720,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("")` (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 +829,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);
@@ -799,8 +849,8 @@ function chunkSource(
const p = r.parent;
edits.push(
ts.isShorthandPropertyAssignment(p)
- ? { start: r.getStart(sf), end: r.getEnd(), text: `${name}: ${global}` }
- : { start: r.getStart(sf), end: r.getEnd(), text: global },
+ ? { start: tokenStartOfNode(r), end: r.end, text: `${name}: ${global}` }
+ : { start: tokenStartOfNode(r), end: r.end, text: global },
);
}
continue;
@@ -844,15 +894,17 @@ function chunkSource(
}
// Declaration order of the bundle: a helper reads only earlier ones.
const ordered = [...copied].sort(
- (a, b) => top.get(a)!.node.getStart(sf) - top.get(b)!.node.getStart(sf),
+ (a, b) => tokenStartOfNode(top.get(a)!.node) - tokenStartOfNode(top.get(b)!.node),
);
for (const name of ordered) {
const node = top.get(name)!.node;
- header.push(ts.isFunctionDeclaration(node) ? node.getText(sf) : `var ${node.getText(sf)};`);
+ header.push(
+ ts.isFunctionDeclaration(node) ? getTextOfNode(node) : `var ${getTextOfNode(node)};`,
+ );
}
const bodyStrict = strictPrologue(body.statements.nodes);
- const text = applyEdits(sf.text, body.getStart(sf) + 1, body.getEnd() - 1, edits);
+ const text = applyEdits(sf.text, tokenStartOfNode(body) + 1, body.end - 1, edits);
const prologue = bundleStrict || bodyStrict ? '"use strict";\n' : "";
return prologue + (header.length > 0 ? header.join("\n") + "\n" : "") + text + "\n";
}
diff --git a/packages/compiler/src/frontend/npm-static-bundled-cjs.ts b/packages/compiler/src/frontend/npm-static-bundled-cjs.ts
index 89cd0fda1..be43284a6 100644
--- a/packages/compiler/src/frontend/npm-static-bundled-cjs.ts
+++ b/packages/compiler/src/frontend/npm-static-bundled-cjs.ts
@@ -1,5 +1,6 @@
import * as ts from "./ts7/syntax.js";
import { helperTokens } from "./helper-tokens.js";
+import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js";
// Bun's cached node-mode interop helper. Match the complete token stream,
// including its dependencies: helper names alone are not a semantic contract.
@@ -117,14 +118,14 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile)
}
for (const [name, shape] of helperShapes) {
const actual = declarations.get(name);
- if (!actual || helperTokens(actual.stmt.getText(sf)) !== shape) return null;
+ if (!actual || helperTokens(getTextOfNode(actual.stmt)) !== shape) return null;
}
// These initializers must execute before any factory or namespace. The
// original bundle would throw if a var helper were still undefined.
let helperEnd = 0;
for (const name of helperShapes.keys()) {
const stmt = declarations.get(name)!.stmt;
- if (stmt.getStart(sf) < helperEnd) return null;
+ if (tokenStartOfNode(stmt) < helperEnd) return null;
helperEnd = stmt.end;
}
const edits: Edit[] = [];
@@ -140,7 +141,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile)
const call = decl.initializer;
if (!ts.isIdentifier(call.expression) || call.expression.text !== "__commonJS") continue;
if (
- stmt.getStart(sf) < helperEnd ||
+ tokenStartOfNode(stmt) < helperEnd ||
(ts.canHaveModifiers(stmt) &&
ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
)
@@ -193,7 +194,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile)
if (unsafe) return null;
// The comma expression prevents inferred naming of an anonymous export.
edits.push(
- { start: call.getStart(sf), end: value.getStart(sf), text: "(0," },
+ { start: tokenStartOfNode(call), end: tokenStartOfNode(value), text: "(0," },
{ start: value.end, end: call.end, text: ")" },
);
factories.set(name, stmt);
@@ -212,7 +213,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile)
const call = decl.initializer;
if (!ts.isIdentifier(call.expression) || call.expression.text !== "__toESM") continue;
if (
- stmt.getStart(sf) < helperEnd ||
+ tokenStartOfNode(stmt) < helperEnd ||
(ts.canHaveModifiers(stmt) &&
ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
)
@@ -230,9 +231,9 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile)
)
return null;
const factory = factories.get(requireCall.expression.text);
- if (!factory || factory.end > stmt.getStart(sf)) return null;
+ if (!factory || factory.end > tokenStartOfNode(stmt)) return null;
edits.push({
- start: call.getStart(sf),
+ start: tokenStartOfNode(call),
end: call.end,
text: `{default:${requireCall.expression.text}}`,
});
@@ -286,7 +287,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile)
// Remove only helpers that became dead. Other bundle features may share
// __defProp, so a recognized declaration alone does not justify erasure.
const dead = (n: ts.Node): boolean =>
- edits.some((e) => n.getStart(sf) >= e.start && n.end <= e.end);
+ edits.some((e) => tokenStartOfNode(n) >= e.start && n.end <= e.end);
const removed = new Set();
for (let changed = true; changed;) {
changed = false;
@@ -304,7 +305,7 @@ export function rewriteBundledFunctionImports(source: string, sf: ts.SourceFile)
live = true;
});
if (!live) {
- edits.push({ start: stmt.getStart(sf), end: stmt.end, text: "" });
+ edits.push({ start: tokenStartOfNode(stmt), end: stmt.end, text: "" });
removed.add(name);
changed = true;
}
diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts
new file mode 100644
index 000000000..381538087
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-bundled-esm.test.ts
@@ -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();
+});
diff --git a/packages/compiler/src/frontend/npm-static-bundled-esm.ts b/packages/compiler/src/frontend/npm-static-bundled-esm.ts
new file mode 100644
index 000000000..ac572000d
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-bundled-esm.ts
@@ -0,0 +1,826 @@
+/* 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";
+import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.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
+ * ; }` with a one-method object argument, and arrow
+ * `(FN, RES) => () => ()` 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 = getTextOfNode(ret.expression);
+ 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(getTextOfNode(body.body));
+ 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): Edit[] {
+ const edits: Edit[] = [];
+ const stmts = new Set();
+ 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: tokenStartOfNode(stmt), end: stmt.end, text: "" });
+ continue;
+ }
+ const lastLive = deadAt.lastIndexOf(false);
+ for (let i = 0; i < decls.length; i++) {
+ if (!deadAt[i]) continue;
+ edits.push(
+ i < lastLive
+ ? { start: tokenStartOfNode(decls[i]!), end: tokenStartOfNode(decls[i + 1]!), text: "" }
+ : { start: decls[lastLive]!.end, end: decls[i]!.end, 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();
+ const duplicate = new Set();
+ 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();
+ 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(getTextOfNode(init)) === helperTokens("Object.getOwnPropertyNames")
+ );
+ };
+ const forms = new Map();
+ 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();
+ 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();
+ 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();
+ 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 = getTextOfNode(rhs);
+ 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: tokenStartOfNode(access), end: access.end, text: `(${req})` });
+ else uses.push({ start: tokenStartOfNode(r), end: r.end, text: req });
+ }
+ if (!ok) continue;
+ edits.push({ start: tokenStartOfNode(el), end: el.end, 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();
+ 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();
+ /** 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();
+ 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, tokenStartOfNode(value), value.end, 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, 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 ? "" : `${getTextOfNode(initCall)}, `;
+ edits.push({
+ start: tokenStartOfNode(access),
+ end: access.end,
+ text: `(${init}${getterText(value)})`,
+ });
+ }
+ deadDeclarators.add(d);
+ deadDeclarators.add(memoDecl);
+ loaderSpans.push({ start: tokenStartOfNode(d), end: d.end });
+ lowered.add(target);
+ }
+
+ for (const r of allRefs) {
+ const table = tables.get(r.text);
+ if (table === undefined) continue;
+ if (loaderSpans.some((span) => tokenStartOfNode(r) >= span.start && r.end <= 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
+ ? `${getTextOfNode(unparen(inner.left))}; `
+ : "";
+ 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: tokenStartOfNode(holder.parent),
+ end: holder.parent.end,
+ 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: tokenStartOfNode(p), end: p.end, 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 !(tokenStartOfNode(x) >= tokenStartOfNode(table.stmt) && x.end <= table.stmt.end);
+ });
+ 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: tokenStartOfNode(table.stmt), end: table.stmt.end, 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: tokenStartOfNode(call),
+ end: tokenStartOfNode(body) + 1,
+ text: "(() => { let __scriptc_done = false; function __scriptc_init() {",
+ },
+ {
+ start: body.end - 1,
+ end: call.end,
+ text: "} return function () { if (__scriptc_done) return; __scriptc_done = true; __scriptc_init(); }; })()",
+ },
+ );
+ }
+ return applyEdits(source, 0, source.length, edits);
+}
diff --git a/packages/compiler/src/frontend/npm-static-cjs-exports-alias.test.ts b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.test.ts
new file mode 100644
index 000000000..c3704ea29
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.test.ts
@@ -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 }");
+});
diff --git a/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts
new file mode 100644
index 000000000..bf753be8c
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts
@@ -0,0 +1,192 @@
+/* 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";
+import { tokenStartOfNode } from "./ts7/node-functions.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 || tokenStartOfNode(r) < whole.stmt.end) {
+ 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: tokenStartOfNode(r),
+ end: r.end,
+ 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 (tokenStartOfNode(access) < whole.stmt.end && !insideFunction(access)) unsafe = true;
+ edits.push({ start: tokenStartOfNode(access), end: access.end, 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: tokenStartOfNode(e), end: tokenStartOfNode(unparen(e.right)), 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;
+}
diff --git a/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts b/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts
new file mode 100644
index 000000000..1e7489595
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts
@@ -0,0 +1,239 @@
+/* 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.`, 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";
+import { tokenStartOfNode } from "./ts7/node-functions.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: tokenStartOfNode(n), end: n.end, 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: tokenStartOfNode(alias.stmt), end: alias.stmt.end, 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);
+}
diff --git a/packages/compiler/src/frontend/npm-static-declaration-syntax.ts b/packages/compiler/src/frontend/npm-static-declaration-syntax.ts
index 4a84dcfeb..0d713dcff 100644
--- a/packages/compiler/src/frontend/npm-static-declaration-syntax.ts
+++ b/packages/compiler/src/frontend/npm-static-declaration-syntax.ts
@@ -10,6 +10,7 @@
* too; inferred checked values validate at the ordinary return boundary. */
import * as ts from "./ts7/syntax.js";
+import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js";
export interface NpmStaticOverloadParameter {
name: string;
@@ -56,11 +57,11 @@ export function applyNpmStaticJsDocNamepaths(
for (const tag of doc.tags?.nodes ?? []) {
const expression = tag.typeExpression;
if (expression?.kind !== ts.SyntaxKind.JSDocTypeExpression) continue;
- const start = expression.getStart(sourceFile);
+ const start = tokenStartOfNode(expression);
if (source[start] !== "{") continue;
let depth = 1;
let quote = "";
- for (let i = start + 1; i < tag.getEnd() && depth > 0; i++) {
+ for (let i = start + 1; i < tag.end && depth > 0; i++) {
const char = source[i]!;
if (quote !== "") {
if (char === "\\") i++;
@@ -80,7 +81,7 @@ export function applyNpmStaticJsDocNamepaths(
continue;
}
const name = /^[$A-Z_a-z][$\w]*(?:[.#~][$A-Z_a-z][$\w]*)*/.exec(
- source.slice(i, tag.getEnd()),
+ source.slice(i, tag.end),
)?.[0];
if (!name) continue;
if (name.includes("~") || name.includes("#")) replacements.set(i, name.length);
@@ -171,7 +172,7 @@ export function applyNpmStaticNullableClassFields(
hasModifier(member, ts.SyntaxKind.StaticKeyword)
)
continue;
- const leading = source.slice(member.getFullStart(), member.getStart(sourceFile));
+ const leading = source.slice(member.pos, tokenStartOfNode(member));
if (jsDocTypeOf(member) !== undefined || leading.includes("@type")) continue;
fields.set(member.name.text, { member, invalid: false });
}
@@ -285,7 +286,7 @@ export function applyNpmStaticNullableClassFields(
for (const field of fields.values()) {
if (field.invalid || field.className === undefined) continue;
inserts.push({
- offset: field.member.getStart(sourceFile),
+ offset: tokenStartOfNode(field.member),
text: `/** @type {${field.className} | null} */ `,
});
}
@@ -358,7 +359,7 @@ export function applyNpmStaticFindReturnWidening(
(returnType.typeArguments?.nodes.length ?? 0) !== 0
)
continue;
- insertions.push({ offset: returnType.getEnd(), length: " | undefined".length });
+ insertions.push({ offset: returnType.end, length: " | undefined".length });
}
}
if (insertions.length === 0) return null;
@@ -428,26 +429,22 @@ function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
);
}
-function safeTypeText(
- node: ts.TypeNode,
- sourceFile: ts.SourceFile,
- className: string,
-): string | null {
- if (SAFE_KEYWORD_TYPES.has(node.kind) || ts.isThisTypeNode(node)) return node.getText(sourceFile);
+function safeTypeText(node: ts.TypeNode, className: string): string | null {
+ if (SAFE_KEYWORD_TYPES.has(node.kind) || ts.isThisTypeNode(node)) return getTextOfNode(node);
if (ts.isLiteralTypeNode(node) && node.literal.kind === ts.SyntaxKind.NullKeyword) return "null";
if (ts.isParenthesizedTypeNode(node)) {
- const inner = safeTypeText(node.type, sourceFile, className);
+ const inner = safeTypeText(node.type, className);
return inner === null ? null : `(${inner})`;
}
if (ts.isArrayTypeNode(node)) {
- const element = safeTypeText(node.elementType, sourceFile, className);
+ const element = safeTypeText(node.elementType, className);
return element === null ? null : `${element}[]`;
}
if (ts.isTypeOperatorNode(node) && node.operator === ts.SyntaxKind.ReadonlyKeyword) {
- return safeTypeText(node.type, sourceFile, className);
+ return safeTypeText(node.type, className);
}
if (ts.isUnionTypeNode(node)) {
- const arms = node.types.nodes.map((type) => safeTypeText(type, sourceFile, className));
+ const arms = node.types.nodes.map((type) => safeTypeText(type, className));
return arms.some((arm) => arm === null) ? null : arms.join(" | ");
}
if (
@@ -483,7 +480,7 @@ function overloadSignature(
) {
return null;
}
- const returnType = safeTypeText(method.type, sourceFile, className);
+ const returnType = safeTypeText(method.type, className);
if (returnType === null) return null;
const parameters: NpmStaticOverloadParameter[] = [];
for (const parameter of method.parameters.nodes) {
@@ -496,7 +493,7 @@ function overloadSignature(
) {
return null;
}
- const type = safeTypeText(parameter.type, sourceFile, className);
+ const type = safeTypeText(parameter.type, className);
if (type === null) return null;
parameters.push({
name: parameter.name.text,
@@ -553,13 +550,9 @@ export function parseNpmStaticDeclarationOverloads(
return classes;
}
-function nullableSelfType(
- node: ts.TypeNode,
- sourceFile: ts.SourceFile,
- className: string,
-): string | null {
+function nullableSelfType(node: ts.TypeNode, className: string): string | null {
if (!ts.isUnionTypeNode(node) || node.types.nodes.length !== 2) return null;
- const arms = node.types.nodes.map((type) => safeTypeText(type, sourceFile, className));
+ const arms = node.types.nodes.map((type) => safeTypeText(type, className));
return arms.includes(className) && arms.includes("null") ? `${className} | null` : null;
}
@@ -613,8 +606,7 @@ export function parseNpmStaticDeclarationProperties(
continue;
}
const type =
- nullableSelfType(member.type, sourceFile, className) ??
- nullableVoidCallbackType(member.type);
+ nullableSelfType(member.type, className) ?? nullableVoidCallbackType(member.type);
if (type !== null) properties.set(member.name.text, type);
}
if (properties.size > 0) classes.set(className, properties);
@@ -1012,13 +1004,10 @@ export function applyNpmStaticDeclarationOverloads(
) {
continue;
}
- const leading = source.slice(
- bodyStatement.getFullStart(),
- bodyStatement.getStart(sourceFile),
- );
+ const leading = source.slice(bodyStatement.pos, tokenStartOfNode(bodyStatement));
if (!leading.includes("@type")) {
inserts.push({
- offset: bodyStatement.getStart(sourceFile),
+ offset: tokenStartOfNode(bodyStatement),
text: `/** @type {${backing.type}} */ `,
});
}
@@ -1036,13 +1025,13 @@ export function applyNpmStaticDeclarationOverloads(
(parameter) => parameter.optional && !existing.includes(`[${parameter.name}]`),
);
if (missingOptional)
- inserts.push({ offset: member.getStart(sourceFile), text: `${implementation} ` });
+ inserts.push({ offset: tokenStartOfNode(member), text: `${implementation} ` });
else if (
jsDocs.length === 0 &&
member.parameters.nodes.length === 0 &&
SCALAR_METHOD_RETURNS.has(signatures[0]!.returnType)
) {
- inserts.push({ offset: member.getStart(sourceFile), text: `${implementation} ` });
+ inserts.push({ offset: tokenStartOfNode(member), text: `${implementation} ` });
}
}
continue;
@@ -1050,7 +1039,9 @@ export function applyNpmStaticDeclarationOverloads(
const implementation =
jsDocs.length === 0 ? implementationComment(statement.name.text, member, signatures) : null;
if (jsDocs.length === 0 && implementation === null) continue;
- const offset = jsDocs[0]?.getStart(sourceFile) ?? member.getStart(sourceFile);
+ const offset =
+ (jsDocs[0] === undefined ? undefined : tokenStartOfNode(jsDocs[0])) ??
+ tokenStartOfNode(member);
const text = `${signatures.map(overloadComment).join(" ")} ${implementation === null ? "" : implementation + " "}`;
inserts.push({ offset, text });
}
@@ -1158,9 +1149,9 @@ export function applyNpmStaticDeclarationProperties(
!callbackInitializer(member.initializer))
)
continue;
- const leading = source.slice(member.getFullStart(), member.getStart(sourceFile));
+ const leading = source.slice(member.pos, tokenStartOfNode(member));
if (jsDocTypeOf(member) !== undefined || leading.includes("@type")) continue;
- inserts.push({ offset: member.getStart(sourceFile), text: `/** @type {${type}} */ ` });
+ inserts.push({ offset: tokenStartOfNode(member), text: `/** @type {${type}} */ ` });
}
const constructor = statement.members.nodes.find(
(member): member is ts.ConstructorDeclaration =>
@@ -1184,12 +1175,9 @@ export function applyNpmStaticDeclarationProperties(
}
const type = properties.get(left.name.text);
if (type === undefined) continue;
- const leading = source.slice(
- bodyStatement.getFullStart(),
- bodyStatement.getStart(sourceFile),
- );
+ const leading = source.slice(bodyStatement.pos, tokenStartOfNode(bodyStatement));
if (leading.includes("@type")) continue;
- inserts.push({ offset: bodyStatement.getStart(sourceFile), text: `/** @type {${type}} */ ` });
+ inserts.push({ offset: tokenStartOfNode(bodyStatement), text: `/** @type {${type}} */ ` });
}
}
if (inserts.length === 0) return null;
diff --git a/packages/compiler/src/frontend/npm-static-rewrite-host.ts b/packages/compiler/src/frontend/npm-static-rewrite-host.ts
index 524239901..4e0c92e31 100644
--- a/packages/compiler/src/frontend/npm-static-rewrite-host.ts
+++ b/packages/compiler/src/frontend/npm-static-rewrite-host.ts
@@ -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 {
}
}
+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) => {
diff --git a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts
index 1730e1153..c48420625 100644
--- a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts
+++ b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts
@@ -6,6 +6,7 @@ import {
isModuleExports,
} from "./cjs-syntax.js";
import { rewriteBundledFunctionImports } from "./npm-static-bundled-cjs.js";
+import { getTextOfNode, tokenStartOfNode } from "./ts7/node-functions.js";
/** Resolution stays with the compilation host. Syntax never reads disk or
* starts a parser implicitly, so native and Node hosts run the same pass. */
@@ -22,6 +23,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)`
@@ -54,7 +58,7 @@ function liveExportTableFirst(
};
let sawTable = false;
for (const stmt of sf.statements.nodes) {
- if (stmt.getStart(sf) >= installAt) return sawTable;
+ if (tokenStartOfNode(stmt) >= installAt) return sawTable;
if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) continue;
if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) continue;
if (ts.isVariableStatement(stmt)) {
@@ -119,8 +123,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;
}
@@ -139,7 +147,7 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string }
cur.kind === ts.SyntaxKind.FalseKeyword ||
cur.kind === ts.SyntaxKind.NullKeyword
) {
- return { kind: "hoist", text: cur.getText() };
+ return { kind: "hoist", text: getTextOfNode(cur) };
}
// ident-rooted member chains: this.x / a.b.c / a["k"].d — side-effect
// free reads the tail const can evaluate once.
@@ -154,10 +162,30 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string }
) {
walk = walk.expression;
}
- if (ts.isIdentifier(walk)) return { kind: "hoist", text: cur.getText() };
+ if (ts.isIdentifier(walk)) return { kind: "hoist", text: getTextOfNode(cur) };
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: getTextOfNode(cur) };
+}
+
/** 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 }`). */
@@ -299,7 +327,7 @@ function recognizedToEsmDecl(d: ts.VariableDeclaration): boolean {
let to: ts.Expression = expr.arguments.nodes[0]!;
while (ts.isParenthesizedExpression(to)) to = to.expression;
if (!ts.isConditionalExpression(to)) return false;
- const condText = to.condition.getText();
+ const condText = getTextOfNode(to.condition);
if (!condText.includes(isNodeMode) || !condText.includes("__esModule")) return false;
let stamp: ts.Expression = to.whenTrue;
while (ts.isParenthesizedExpression(stamp)) stamp = stamp.expression;
@@ -416,7 +444,7 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH
) {
if (parent.name.text === "default" && defaultIsModule) {
if (!readOnlyAccess(parent)) return fail(TO_ESM_ESCAPE_DEGRADE);
- pads.push({ start: child.getEnd(), end: parent.getEnd() });
+ pads.push({ start: child.end, end: parent.end });
}
} else {
return fail(TO_ESM_ESCAPE_DEGRADE);
@@ -424,8 +452,8 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH
// the wrapper itself: callee + '(' down, and everything after the
// require argument (`, 1` included) — the bare require survives at its
// original offsets.
- pads.push({ start: call.getStart(sf), end: arg0!.getStart(sf) });
- pads.push({ start: arg0!.getEnd(), end: call.getEnd() });
+ pads.push({ start: tokenStartOfNode(call), end: tokenStartOfNode(arg0!) });
+ pads.push({ start: arg0!.end, end: call.end });
}
if (bindings.size > 0) {
@@ -486,7 +514,7 @@ function planToEsmInterop(sf: ts.SourceFile, filePath: string, host: CjsRewriteH
violated = true;
return;
}
- pads.push({ start: n.expression.getEnd(), end: n.getEnd() });
+ pads.push({ start: n.expression.end, end: n.end });
}
};
ts.walkPreorder(sf, padDefaults);
@@ -601,8 +629,8 @@ export function rewriteBundlerCjsSyntax(
ts.isIdentifier(rhs.arguments.nodes[0]!)
) {
esbuildMainTarget = (rhs.arguments.nodes[0] as ts.Identifier).text;
- tablePos = stmt.getStart(sf);
- neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
+ tablePos = tokenStartOfNode(stmt);
+ neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end });
sawBundleShape = true;
continue;
}
@@ -635,9 +663,9 @@ export function rewriteBundlerCjsSyntax(
names,
finalRhs: cur,
viaBareExports,
- pos: stmt.getStart(sf),
- start: stmt.getStart(sf),
- end: stmt.getEnd(),
+ pos: tokenStartOfNode(stmt),
+ start: tokenStartOfNode(stmt),
+ end: stmt.end,
});
continue; // classified later (only when a rewrite actually happens)
}
@@ -657,7 +685,7 @@ export function rewriteBundlerCjsSyntax(
isModuleExports(inner.left) &&
ts.isObjectLiteralExpression(inner.right)
) {
- neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
+ neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end });
sawBundleShape = true;
continue;
}
@@ -668,7 +696,7 @@ export function rewriteBundlerCjsSyntax(
const table = exportCallOf(e);
if (table !== null) {
exportTables.push(table);
- neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
+ neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end });
sawBundleShape = true;
continue;
}
@@ -678,7 +706,7 @@ export function rewriteBundlerCjsSyntax(
const spec = bareRequireSpecOf(e.arguments.nodes[0]!);
if (spec !== null) {
addStarSpec(spec);
- neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
+ neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end });
sawBundleShape = true;
continue;
}
@@ -689,7 +717,7 @@ export function rewriteBundlerCjsSyntax(
const spec = bareRequireSpecOf(e.arguments.nodes[1]!);
if (spec !== null) {
addStarSpec(spec);
- neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
+ neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end });
sawBundleShape = true;
continue;
}
@@ -698,7 +726,7 @@ export function rewriteBundlerCjsSyntax(
const def = definePropertyExportOf(e);
if (def !== null) {
if (def.esModuleStamp) {
- neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
+ neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end });
// the canonical table re-stamps it (lexer-visibly — merve detects
// this defineProperty form, so parity wants a visible entry)
continue;
@@ -706,7 +734,7 @@ export function rewriteBundlerCjsSyntax(
const chased = def.value !== null ? chaseValue(def.value) : null;
if (chased !== null) {
definePropEntries.push({ name: def.name, value: def.value });
- neutralize.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
+ neutralize.push({ start: tokenStartOfNode(stmt), end: stmt.end });
sawBundleShape = true;
}
// unchaseable descriptor: LEAVE the statement (its runtime fence
@@ -760,7 +788,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: getTextOfNode(value) });
+ else claim(name, spec);
+ }
}
for (const d of definePropEntries) claim(d.name, classify(d.name, d.value));
@@ -792,7 +827,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 +928,7 @@ export function rewriteBundlerCjsSyntax(
"__toESM",
"__toCommonJS",
"__commonJS",
+ "__esm",
"__exportStar",
"__createBinding",
"__setModuleDefault",
@@ -916,8 +952,8 @@ export function rewriteBundlerCjsSyntax(
helperDecls.set(
d.name.text,
decls.length === 1
- ? { start: stmt.getStart(sf), end: stmt.getEnd() }
- : { start: d.getStart(sf), end: d.getEnd() },
+ ? { start: tokenStartOfNode(stmt), end: stmt.end }
+ : { start: tokenStartOfNode(d), end: d.end },
);
}
if (anyHelper && decls.length > 1) helperStmts.push(stmt);
@@ -931,8 +967,8 @@ export function rewriteBundlerCjsSyntax(
if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.name === node)
return;
const list = refs.get(node.text);
- if (list === undefined) refs.set(node.text, [node.getStart(sf)]);
- else list.push(node.getStart(sf));
+ if (list === undefined) refs.set(node.text, [tokenStartOfNode(node)]);
+ else list.push(tokenStartOfNode(node));
}
};
ts.walkPreorder(sf, record);
@@ -964,12 +1000,12 @@ export function rewriteBundlerCjsSyntax(
const deadAt = decls.map((d) => {
if (!ts.isIdentifier(d.name) || !removed.has(d.name.text)) return false;
const span = helperDecls.get(d.name.text)!;
- if (span.start !== d.getStart(sf)) return false;
+ if (span.start !== tokenStartOfNode(d)) return false;
declaratorSpans.add(span);
return true;
});
if (deadAt.every((x) => x)) {
- declaratorPads.push({ start: stmt.getStart(sf), end: stmt.getEnd() });
+ declaratorPads.push({ start: tokenStartOfNode(stmt), end: stmt.end });
continue;
}
// Dead declarators before the last live one take their trailing
@@ -980,8 +1016,8 @@ export function rewriteBundlerCjsSyntax(
const d = decls[i]!;
declaratorPads.push(
i < lastLive
- ? { start: d.getStart(sf), end: decls[i + 1]!.getStart(sf) }
- : { start: decls[lastLive]!.getEnd(), end: d.getEnd() },
+ ? { start: tokenStartOfNode(d), end: tokenStartOfNode(decls[i + 1]!) }
+ : { start: decls[lastLive]!.end, end: d.end },
);
}
}
@@ -1038,6 +1074,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 +1101,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";
}
diff --git a/packages/compiler/src/frontend/npm-static-subtyping.ts b/packages/compiler/src/frontend/npm-static-subtyping.ts
index 04c66a559..18d69d4c4 100644
--- a/packages/compiler/src/frontend/npm-static-subtyping.ts
+++ b/packages/compiler/src/frontend/npm-static-subtyping.ts
@@ -19,8 +19,8 @@ export function isNpmStaticSubclassArgument(
symbol.declarations.some(
(declaration) =>
(ts.isClassDeclaration(declaration) || ts.isClassExpression(declaration)) &&
- /\.[cm]?js$/.test(declaration.getSourceFile().fileName) &&
- npmStaticPackageOfPath(declaration.getSourceFile().fileName) !== null,
+ /\.[cm]?js$/.test(ts.getSourceFileOfNode(declaration).fileName) &&
+ npmStaticPackageOfPath(ts.getSourceFileOfNode(declaration).fileName) !== null,
)
);
};
@@ -50,12 +50,12 @@ export function isNpmStaticSubclassArgument(
};
let accepted = false;
ts.walkPreorder(source, (node) => {
- if (node.end < diagnostic.pos || node.getStart(source) > diagnostic.end) return "skip";
+ if (node.end < diagnostic.pos || ts.tokenStartOfNode(node) > diagnostic.end) return "skip";
if (!ts.isCallExpression(node) && !ts.isNewExpression(node)) return undefined;
const index =
node.arguments?.nodes.findIndex(
(argument) =>
- argument.getStart(source) === diagnostic.pos && argument.end === diagnostic.end,
+ ts.tokenStartOfNode(argument) === diagnostic.pos && argument.end === diagnostic.end,
) ?? -1;
if (index < 0) return undefined;
const parameter = checker.getResolvedSignature(node)?.getParameters()[index];
diff --git a/packages/compiler/src/frontend/npm-static.ts b/packages/compiler/src/frontend/npm-static.ts
index 7dffd05f7..ae8a4a8f4 100644
--- a/packages/compiler/src/frontend/npm-static.ts
+++ b/packages/compiler/src/frontend/npm-static.ts
@@ -20,6 +20,7 @@ import {
trackedReadFile,
trackedRealpath,
} from "./input-tracker.js";
+import { npmBuiltinAliasOf } from "./builtin-modules.js";
let activePackages: ReadonlySet = new Set();
let declarationOverloads: ReadonlyMap = 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();
+
+export function npmStaticLexerVisibleNames(path: string): readonly string[] {
+ return lexerNames.get(path.split("\\").join("/")) ?? [];
+}
+
export function setNpmStaticPackages>(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();
+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/`). 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);
diff --git a/packages/compiler/src/frontend/pipeline.ts b/packages/compiler/src/frontend/pipeline.ts
index 676b2bb3e..16c96247f 100644
--- a/packages/compiler/src/frontend/pipeline.ts
+++ b/packages/compiler/src/frontend/pipeline.ts
@@ -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
diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts
index 192a36ef9..2e75542d7 100644
--- a/packages/compiler/src/frontend/program.ts
+++ b/packages/compiler/src/frontend/program.ts
@@ -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);
@@ -1183,7 +1236,7 @@ export function isEsModuleStamp(expr: ts.Expression): boolean {
function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean {
const cjs = cjsExportAssignmentOf(stmt);
if (cjs === null) return false;
- const sf = stmt.getSourceFile();
+ const sf = ts.getSourceFileOfNode(stmt);
const plainMember = (left: ts.Expression): boolean => {
if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return false;
// A replacement export can have setters or non-writable function
@@ -1238,7 +1291,7 @@ function sourceBinding7(program: ts.Program, ident: ts.Identifier): boolean {
return (
symbol !== undefined &&
symbol.declarations.some(
- (decl) => !decl.getSourceFile().isDeclarationFile && !ts.isSourceFile(decl),
+ (decl) => !ts.getSourceFileOfNode(decl).isDeclarationFile && !ts.isSourceFile(decl),
)
);
}
@@ -1265,7 +1318,7 @@ function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean {
if (ts.isEmptyStatement(s) || ts.isFunctionDeclaration(s)) return true;
if (ts.isExpressionStatement(s) && ts.isStringLiteral(s.expression)) return true; // directive
if (isRequireStatement7(s)) return true;
- if (isCjsJsFile7(s.getSourceFile(), program)) {
+ if (isCjsJsFile7(ts.getSourceFileOfNode(s), program)) {
if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) {
const call = s.expression as ts.CallExpression;
const object = (call.expression as ts.PropertyAccessExpression).expression as ts.Identifier;
@@ -1392,7 +1445,7 @@ function requireEarlyReference7(
if (cached !== undefined) return cached;
let found: ts.Identifier | null = null;
const stmt = decl.parent?.parent;
- const sf = decl.getSourceFile();
+ const sf = ts.getSourceFileOfNode(decl);
if (stmt !== undefined && ts.isVariableStatement(stmt) && ts.isSourceFile(stmt.parent)) {
const stmts = sf.statements.nodes;
const k = stmts.indexOf(stmt);
@@ -1968,7 +2021,7 @@ function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): bool
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
const target = checker.getAliasedSymbol(symbol);
const targetDecl = target.declarations[0];
- if (targetDecl !== undefined && targetDecl.getSourceFile().isDeclarationFile) {
+ if (targetDecl !== undefined && ts.getSourceFileOfNode(targetDecl).isDeclarationFile) {
for (
let p: ts.Node | undefined = targetDecl.parent;
p !== undefined && !ts.isSourceFile(p);
@@ -1976,7 +2029,8 @@ function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): bool
) {
if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) {
return (
- canonicalBuiltinModule(p.name.text) === "module" && target.name === "createRequire"
+ canonicalBuiltinModule(p.name.text) === "module" &&
+ ts.symbolName(target) === "createRequire"
);
}
}
@@ -2060,7 +2114,7 @@ function stableCreateRequireBindingReason7(
return "its createRequire binding is redeclared";
if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0)
return "its createRequire result escapes through an exported binding";
- const sf = decl.getSourceFile();
+ const sf = ts.getSourceFileOfNode(decl);
const refs = identifierOccurrences7(sf, decl.name.text);
checker.prefetchSymbolNodesExact(refs);
for (const ref of refs) {
@@ -2127,7 +2181,7 @@ function npmStaticModuleImportReason7(
}
if (!bindings || !ts.isNamedImports(bindings)) return null;
const checker = program.getTypeChecker();
- const sf = stmt.getSourceFile();
+ const sf = ts.getSourceFileOfNode(stmt);
for (const specifier of bindings.elements.nodes) {
if ((specifier.propertyName?.text ?? specifier.name.text) !== "createRequire") continue;
const imported = checker.getSymbolAtLocation(specifier.name);
@@ -2207,7 +2261,7 @@ function requireCallBindingKind7(program: ts.Program, node: ts.Node): RequireCal
const symbol = checker.getSymbolAtLocation(call.expression);
if (symbol === undefined) return "ambient";
const decls = symbol.declarations;
- if (decls.every((d) => d.getSourceFile().isDeclarationFile)) return "ambient";
+ if (decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return "ambient";
return isCreateRequireBinding7(program, call.expression) ? "createRequire" : "source";
}
@@ -2362,15 +2416,15 @@ function selfImportTdzFences7(
// are initialized. Hoisted functions have none.
const tdzEndOf = (aliased: ts.Symbol): number | null => {
const d = aliased.valueDeclaration ?? aliased.declarations[0];
- if (d === undefined || d.getSourceFile() !== sf) return null;
+ if (d === undefined || ts.getSourceFileOfNode(d) !== sf) return null;
if (ts.isFunctionDeclaration(d)) return null;
- return d.getEnd();
+ return d.end;
};
const fence = (ref: ts.Identifier, name: string): void => {
diags.push(
unsupportedDiag(
"SC1030",
- { file: sf.fileName, start: ref.getStart(sf), end: ref.getEnd() },
+ { file: sf.fileName, start: ts.tokenStartOfNode(ref), end: ref.end },
`the self-import binding '${name}' read above its declaration (the import aliases this module's OWN binding — Node initializes it when the declaration executes, so this read throws ReferenceError; move the read below the declaration)`,
),
);
@@ -2386,7 +2440,7 @@ function selfImportTdzFences7(
ts.isIdentifier(node) &&
node !== bindingName &&
node.text === bindingName.text &&
- node.getStart(sf) < end &&
+ ts.tokenStartOfNode(node) < end &&
!insideFunction(node) &&
checker.getSymbolAtLocation(node) === sym
) {
@@ -2419,7 +2473,7 @@ function selfImportTdzFences7(
const memberSym = checker.getSymbolAtLocation(node.name);
if (memberSym !== undefined) {
const end = tdzEndOf(memberSym);
- if (end !== null && node.getStart(sf) < end)
+ if (end !== null && ts.tokenStartOfNode(node) < end)
fence(node.expression, `${nsName.text}.${node.name.text}`);
}
}
@@ -2619,7 +2673,7 @@ function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Nod
if (sym === undefined) return false;
if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym);
const decls = sym.declarations;
- return decls.length > 0 && decls.every((d) => d.getSourceFile().isDeclarationFile);
+ return decls.length > 0 && decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile);
};
const hasDecorator = (n: ts.Node): boolean =>
((n as { modifiers?: ts.NodeArray }).modifiers?.nodes ?? []).some(
@@ -2893,8 +2947,8 @@ export function makeCycleAdmission(
}
};
const lineOf = (node: ts.Node): string => {
- const nsf = node.getSourceFile();
- return `${nsf.fileName}:${ts.getLineAndCharacterOfPosition(nsf, node.getStart(nsf)).line + 1}`;
+ const nsf = ts.getSourceFileOfNode(node);
+ return `${nsf.fileName}:${ts.getLineAndCharacterOfPosition(nsf, ts.tokenStartOfNode(node)).line + 1}`;
};
// Cluster verdict memo (keyed by the component array identity): true
// when every member is an ES module passing the inert-top-level bar.
@@ -3401,7 +3455,7 @@ function exactCycleRefusal7(
const targetOf = (stmt: ts.ImportDeclaration | ts.ExportDeclaration): ts.SourceFile | null => {
const spec = stmt.moduleSpecifier;
if (spec === undefined || !ts.isStringLiteral(spec)) return null;
- const sf = stmt.getSourceFile();
+ const sf = ts.getSourceFileOfNode(stmt);
return resolveImport7(program, sf, spec.text) ?? npmStaticDepSf7(program, sf, spec.text);
};
for (const m of comp) {
@@ -3635,8 +3689,8 @@ function computeCycleEarlyBindings7(
module: m,
exposed,
rank,
- start: stmt.getStart(m),
- end: stmt.getEnd(),
+ start: ts.tokenStartOfNode(stmt),
+ end: stmt.end,
reference: null,
folded: null,
});
@@ -3681,13 +3735,13 @@ function computeCycleEarlyBindings7(
if (
c.kind === "class" &&
sf === c.module &&
- ref.getStart(sf) >= c.start &&
- ref.getEnd() <= c.end
+ ts.tokenStartOfNode(ref) >= c.start &&
+ ref.end <= c.end
)
continue;
let early: boolean;
if (inDeferredPosition7(ref)) early = c.exposed;
- else if (sf === c.module) early = ref.getStart(sf) < c.start;
+ else if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start;
else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false;
else early = !known || rankOf.get(sf)! < c.rank;
if (early) c.reference = ref;
@@ -3826,7 +3880,7 @@ function moduleEarlyBindingsOf7(
store(stmt.name, [stmt]);
} else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) {
store(stmt.name, [stmt]);
- candidates.push({ name: stmt.name, index, start: stmt.getStart(sf), cls: stmt });
+ candidates.push({ name: stmt.name, index, start: ts.tokenStartOfNode(stmt), cls: stmt });
candidateNames.add(stmt.name.text);
} else if (ts.isVariableStatement(stmt)) {
const lexical = (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0;
@@ -3838,7 +3892,7 @@ function moduleEarlyBindingsOf7(
}
if (!lexical) continue;
for (const name of boundNames7(d.name)) {
- candidates.push({ name, index, start: stmt.getStart(sf) });
+ candidates.push({ name, index, start: ts.tokenStartOfNode(stmt) });
candidateNames.add(name.text);
}
}
@@ -3930,9 +3984,10 @@ function moduleEarlyBindingsOf7(
c !== undefined &&
node !== c.name &&
!reachedRefs.has(symbol) &&
- !(c.cls !== undefined && node.getStart(sf) >= c.cls.pos && node.end <= c.cls.end)
+ !(c.cls !== undefined && ts.tokenStartOfNode(node) >= c.cls.pos && node.end <= c.cls.end)
) {
- if (inDeferredPosition7(node) || node.getStart(sf) < c.start) reachedRefs.set(symbol, node);
+ if (inDeferredPosition7(node) || ts.tokenStartOfNode(node) < c.start)
+ reachedRefs.set(symbol, node);
}
for (const unit of unitsBySymbol.get(symbol) ?? []) {
if (reachedUnits.has(unit)) continue;
@@ -3951,8 +4006,8 @@ function moduleEarlyBindingsOf7(
const report = (declaration: ts.Node): void => {
while (
next < candidates.length &&
- candidates[next]!.name.getStart(sf) >= declaration.getStart(sf) &&
- candidates[next]!.name.getEnd() <= declaration.getEnd()
+ ts.tokenStartOfNode(candidates[next]!.name) >= ts.tokenStartOfNode(declaration) &&
+ candidates[next]!.name.end <= declaration.end
) {
const c = candidates[next++]!;
const symbol = checker.getSymbolAtLocation(c.name);
@@ -4012,7 +4067,7 @@ function returnsOnlyClosuresOver7(
const decls = symbol === undefined ? [] : symbol.declarations;
if (decls.length !== 1) return false;
const decl = decls[0]!;
- if (decl.getSourceFile().isDeclarationFile) return false;
+ if (ts.getSourceFileOfNode(decl).isDeclarationFile) return false;
let fn: ts.Node | undefined;
if (ts.isFunctionDeclaration(decl)) fn = decl;
else if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined) {
@@ -4133,12 +4188,13 @@ function laterJobCallee7(checker: ts.TypeChecker, callee: ts.Expression): boolea
}
if (symbol === undefined) return false;
const decls = symbol.declarations;
- if (decls.length === 0 || !decls.every((d) => d.getSourceFile().isDeclarationFile)) return false;
+ if (decls.length === 0 || !decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile))
+ return false;
if (LATER_JOB_BUILTINS7.has(name.text)) return true;
// The JavaScript standard library's callbacks mostly run synchronously;
// package declarations describe user code.
return decls.every((d) => {
- const file = d.getSourceFile().fileName;
+ const file = ts.getSourceFileOfNode(d).fileName;
return isNodeTypesPath(file) || file === fallbackDtsPath();
});
}
@@ -4281,8 +4337,8 @@ function resolveNpmImport7(
}
function locOf7(node: ts.Node): { file: string; start: number; end: number } {
- const sf = node.getSourceFile();
- return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() };
+ const sf = ts.getSourceFileOfNode(node);
+ return { file: sf.fileName, start: ts.tokenStartOfNode(node), end: node.end };
}
/** The program file holding an opted-in package's resolved runtime JS:
@@ -4814,7 +4870,7 @@ function preflight7(load: LoadResult): {
}
if (reDep && !reDep.fileName.endsWith(".json")) {
deps.push({ dep: reDep, stmt });
- depPositions.get(sf)!.push({ dep: reDep, pos: stmt.getStart(sf) });
+ depPositions.get(sf)!.push({ dep: reDep, pos: ts.tokenStartOfNode(stmt) });
}
continue;
}
@@ -4862,7 +4918,7 @@ function preflight7(load: LoadResult): {
refusals.get(sf)!.push({
crash: { message, className, loc: locOf7(stmt) },
fallback: unsupportedDiag("SC1010", locOf7(stmt), fallbackFeature),
- pos: stmt.getStart(sf),
+ pos: ts.tokenStartOfNode(stmt),
});
};
// Project modules win before npm fallback: this is the source graph
@@ -5075,7 +5131,7 @@ function preflight7(load: LoadResult): {
}
if (dep && !isJson) {
deps.push({ dep, stmt });
- depPositions.get(sf)!.push({ dep, pos: stmt.getStart(sf) });
+ depPositions.get(sf)!.push({ dep, pos: ts.tokenStartOfNode(stmt) });
}
}
{
@@ -5115,7 +5171,11 @@ function preflight7(load: LoadResult): {
for (let k = 0; k < stmts.length; k++) {
const stmt = stmts[k]!;
for (const req of requiresOf7(stmt)) {
- const loc = { file: sf.fileName, start: req.node.getStart(sf), end: req.node.getEnd() };
+ const loc = {
+ file: sf.fileName,
+ start: ts.tokenStartOfNode(req.node),
+ end: req.node.end,
+ };
const bindingKind = requireCallBindingKind7(program, req.node);
// TypeScript source files join this scan only through the
// canonical createRequire bridge. Their ordinary require-shaped
@@ -5207,6 +5267,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) {
@@ -5258,7 +5321,7 @@ function preflight7(load: LoadResult): {
);
for (const call of nestedBareRequires) {
const spec = requireSpecOf7(call)!;
- const loc = { file: sf.fileName, start: call.getStart(sf), end: call.getEnd() };
+ const loc = { file: sf.fileName, start: ts.tokenStartOfNode(call), end: call.end };
const bindingKind = requireCallBindingKind7(program, call);
if (!jsRequireFile && bindingKind !== "createRequire") continue;
if (isNodeEsmFile7(sf, program)) {
@@ -5270,6 +5333,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 +5584,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 => {
+ 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. */
@@ -5667,7 +5737,7 @@ function cjsNamedImportLinkCheck(
// swaps ` as ` for `: ` — reproduced exactly, quirks included
// (multi-line import statements simply lose the hint).
const text = bad.sf.text;
- const pos = bad.nameNode.getStart(bad.sf);
+ const pos = ts.tokenStartOfNode(bad.nameNode);
const lineStart = text.lastIndexOf("\n", pos - 1) + 1;
const lineEnd = text.indexOf("\n", pos);
let line = text.slice(lineStart, lineEnd < 0 ? text.length : lineEnd);
@@ -5981,7 +6051,7 @@ function earlierLinkCrash(
if (!element.isTypeOnly) {
entries.push({
local: element.name.text,
- start: (element.propertyName ?? element.name).getStart(sf),
+ start: ts.tokenStartOfNode(element.propertyName ?? element.name),
kind: 0,
});
}
@@ -6005,7 +6075,7 @@ function earlierLinkCrash(
for (const element of exportClause.elements.nodes) {
if (
!element.isTypeOnly &&
- (element.propertyName ?? element.name).getStart(sf) === crash.loc.start
+ ts.tokenStartOfNode(element.propertyName ?? element.name) === crash.loc.start
) {
return [1, reexport];
}
@@ -6039,6 +6109,7 @@ export { ambientDtsPath, fallbackDtsPath, isNodeTypesPath, overridesDtsPath } fr
export {
builtinDefaultImportModule,
canonicalBuiltinModule,
+ npmBuiltinAliasOf,
SUPPORTED_BUILTIN_MODULES,
} from "./builtin-modules.js";
@@ -6070,7 +6141,11 @@ export function isJsSourceFile(sf: ts.SourceFile): boolean {
export function isCheckedJsSourceFile(sf: ts.SourceFile, projectCheckJs: boolean): boolean {
if (!isJsSourceFile(sf) || isNodeModulesPath(sf.fileName)) return false;
let checked = projectCheckJs;
- const head = sf.text.slice(0, sf.statements.nodes[0]?.getStart(sf) ?? sf.text.length);
+ const head = sf.text.slice(
+ 0,
+ (sf.statements.nodes.length === 0 ? undefined : ts.tokenStartOfNode(sf.statements.nodes[0]!)) ??
+ sf.text.length,
+ );
for (const line of head.split("\n")) {
const directive = /^\s*\/\/\s*@ts-(no)?check\b/.exec(line);
if (directive) checked = directive[1] === undefined;
@@ -6269,8 +6344,8 @@ export function cjsVoidInitPreambleReals(
tail = tail.right;
}
if (!isVoid(tail)) return null;
- const sf = cjs.expr.getSourceFile();
- const myPos = cjs.expr.getStart(sf);
+ const sf = ts.getSourceFileOfNode(cjs.expr);
+ const myPos = ts.tokenStartOfNode(cjs.expr);
const reals = new Map<
string,
Extract, { kind: "member" }>
@@ -6283,7 +6358,7 @@ export function cjsVoidInitPreambleReals(
other?.kind === "member" &&
ts.isIdentifier(other.name) &&
other.name.text === name &&
- other.expr.getStart(sf) > myPos &&
+ ts.tokenStartOfNode(other.expr) > myPos &&
!isVoid(other.value),
);
if (real?.kind !== "member") return null;
@@ -6441,7 +6516,7 @@ export function isCjsExportTableLiteral(obj: ts.ObjectLiteralExpression): boolea
}
const decl = obj.parent;
if (!ts.isVariableDeclaration(decl) || decl.initializer !== obj) return false;
- const sf = obj.getSourceFile();
+ const sf = ts.getSourceFileOfNode(obj);
if (!isJsSourceFile(sf)) return false;
for (const stmt of sf.statements.nodes) {
const cjs = cjsExportAssignmentOf(stmt);
diff --git a/packages/compiler/src/frontend/resolve.ts b/packages/compiler/src/frontend/resolve.ts
index 463400475..36470a643 100644
--- a/packages/compiler/src/frontend/resolve.ts
+++ b/packages/compiler/src/frontend/resolve.ts
@@ -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//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,
diff --git a/packages/compiler/src/frontend/services.test.ts b/packages/compiler/src/frontend/services.test.ts
index bd172bbc5..e491b0ac5 100644
--- a/packages/compiler/src/frontend/services.test.ts
+++ b/packages/compiler/src/frontend/services.test.ts
@@ -7,6 +7,7 @@ import { Ts7Api } from "./ts7/rpc-api.js";
import { checkPreflight, isNodeEsmFile, loadProgram } from "./program.js";
import { clearResolveCaches } from "./resolve.js";
import type { Ts7ApiOptions } from "./ts7/program-host.js";
+import { getTextOfNode } from "./ts7/node-functions.js";
const directories: string[] = [];
afterEach(() => {
@@ -80,7 +81,7 @@ test("syntax sessions reuse one connection and returned ASTs survive edits and c
services.close();
services.close();
expect(connections[0]!.close).toHaveBeenCalledTimes(1);
- expect(before.statements.nodes[0]!.getText()).toBe(before.text);
+ expect(getTextOfNode(before.statements.nodes[0]!)).toBe(before.text);
});
test("program hosts remain usable after their creating services close", () => {
diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts
index c6bb24ae5..07bc8ca69 100644
--- a/packages/compiler/src/frontend/services.ts
+++ b/packages/compiler/src/frontend/services.ts
@@ -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);
}
diff --git a/packages/compiler/src/frontend/tier-analysis.ts b/packages/compiler/src/frontend/tier-analysis.ts
index 2ed5c8c9b..b75e491a8 100644
--- a/packages/compiler/src/frontend/tier-analysis.ts
+++ b/packages/compiler/src/frontend/tier-analysis.ts
@@ -107,8 +107,8 @@ function nodeAt(sf: ts.SourceFile, start: number, end: number): ts.Node {
let node: ts.Node = sf;
for (;;) {
let next: ts.Node | undefined;
- node.forEachChild((child) => {
- if (child.getStart(sf) <= start && end <= child.getEnd()) {
+ ts.forEachChild(node, (child) => {
+ if (ts.tokenStartOfNode(child) <= start && end <= child.end) {
next = child;
return true;
}
@@ -263,23 +263,23 @@ function functionName(node: ts.Node): string {
if (ts.isBinaryExpression(parent) && parent.right === node) {
const left = parent.left;
if (ts.isIdentifier(left)) return left.text;
- if (ts.isPropertyAccessExpression(left)) return left.getText();
+ if (ts.isPropertyAccessExpression(left)) return ts.getTextOfNode(left);
}
}
- const sf = node.getSourceFile();
- const pos = ts.getLineAndCharacterOfPosition(sf, node.getStart(sf));
+ const sf = ts.getSourceFileOfNode(node);
+ const pos = ts.getLineAndCharacterOfPosition(sf, ts.tokenStartOfNode(node));
return ``;
}
function locOfNode(node: ts.Node): SrcLoc {
- const sf = node.getSourceFile();
- return { file: sf.fileName, start: node.getStart(sf), end: node.getEnd() };
+ const sf = ts.getSourceFileOfNode(node);
+ return { file: sf.fileName, start: ts.tokenStartOfNode(node), end: node.end };
}
function enclosingFunction(node: ts.Node): ts.Node {
let current: ts.Node | undefined = node.parent;
while (current !== undefined && !isFunctionLikeOrFile(current)) current = current.parent;
- return current ?? node.getSourceFile();
+ return current ?? ts.getSourceFileOfNode(node);
}
function functionUnit(node: ts.Node, instance: string | null): DemotionUnit {
@@ -423,7 +423,10 @@ class UnitResolver {
return this.declarationUnit(binding);
}
if (ts.isVariableDeclaration(decl) || ts.isParameter(decl) || ts.isBindingElement(decl)) {
- const name = nameText(decl.name) ?? decl.name?.getText() ?? "";
+ const name =
+ nameText(decl.name) ??
+ (decl.name === undefined ? undefined : ts.getTextOfNode(decl.name)) ??
+ "";
const scope = enclosingFunction(decl);
const unit: DemotionUnit = { kind: "binding", name, loc: locOfNode(decl) };
if (ts.isParameter(decl) && ts.isFunctionLikeDeclaration(decl.parent))
@@ -437,7 +440,7 @@ class UnitResolver {
/** The unit an operand expression denotes. */
valueUnit(expr: ts.Node): { unit: DemotionUnit; reflection: boolean } | null {
const { root, viaPrototype } = rootOf(expr);
- const sf = root.getSourceFile();
+ const sf = ts.getSourceFileOfNode(root);
if (root.kind === ts.SyntaxKind.ThisKeyword) {
const owner = enclosingFunction(root);
if (ts.isSourceFile(owner))
@@ -475,7 +478,7 @@ class UnitResolver {
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0)
symbol = this.checker.getAliasedSymbol(symbol);
const decls = symbol === undefined ? [] : symbol.declarations;
- const decl = decls.find((d) => !d.getSourceFile().isDeclarationFile);
+ const decl = decls.find((d) => !ts.getSourceFileOfNode(d).isDeclarationFile);
if (decl === undefined) {
// CommonJS `exports`/`module` as values: the module object.
if (isModuleObjectName(root))
diff --git a/packages/compiler/src/frontend/ts7/api-conformance.ts b/packages/compiler/src/frontend/ts7/api-conformance.ts
new file mode 100644
index 000000000..304b4f32f
--- /dev/null
+++ b/packages/compiler/src/frontend/ts7/api-conformance.ts
@@ -0,0 +1,297 @@
+/* Compile-time checks that scriptc's AST and semantic objects have the shapes
+ * of tsc-ts/api's interfaces. The frontend keeps its own types, whose nodes
+ * extend the AstNode class, because native code reads class fields directly
+ * and reads through a structural interface dynamically. This module only
+ * declares types and emits nothing. */
+
+import type * as api from "tsc-ts/api";
+import type { AstNode, AstNodeList } from "./ast-node.js";
+import type * as ast from "./ast-types.js";
+
+type Conforms = [T, U];
+
+/* Members where tsgo's wire, which the RPC model reads, differs from tsc-ts:
+ * - SourceFile.externalModuleIndicator is `true` where moduleDetection makes a
+ * file a module without an import or export.
+ * - JSDocTypeLiteral.jsdocPropertyTags is one tag.
+ * - SyntaxList.children is a node list.
+ * - SyntheticExpression's isSpread and type are factory fields that a parsed
+ * tree never has. */
+type Without = Omit;
+
+export interface AstConformance {
+ Node: Conforms;
+ NodeList: Conforms;
+ SourceFile: Conforms<
+ Without,
+ Without