diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8a726aee8..fbc921b74 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -498,6 +498,30 @@ jobs:
- name: TypeScript 7 full parity sweep
run: pnpm test:ts7
+ # tsc-ts, the default checker, and tsgo (SCRIPTC_CHECKER=tsgo) serve the same TypeScript 7 API. Every corpus
+ # program must compile to the same LLVM IR, with the same diagnostics, with either checker.
+ checker_identity:
+ needs: [validation_scope, lanes]
+ if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
+ name: test (tsgo and tsc-ts give the same IR)
+ runs-on: ubuntu-24.04
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v4
+ - uses: pnpm/action-setup@v4
+ with:
+ version: 11.1.3
+ - uses: actions/setup-node@v4
+ with:
+ node-version-file: .node-version
+ cache: pnpm
+ - run: pnpm install --frozen-lockfile
+ - run: pnpm build
+ - name: Corpus LLVM IR with tsgo and with tsc-ts (release)
+ run: pnpm test:checker-identity --optimization release
+ - name: Corpus LLVM IR with tsgo and with tsc-ts (dev)
+ run: pnpm test:checker-identity --optimization dev
+
llvm_artifacts_linux_x64:
needs: [validation_scope, lanes]
if: ${{ needs.validation_scope.outputs.full == 'true' && needs.lanes.outputs.linux == 'true' }}
@@ -863,7 +887,28 @@ jobs:
# Require this check on the stack base. Upper layers must not pass before
# full validation, so untested groups cannot merge. Merge bottom-up.
test:
- needs: [validation_scope, lanes, code_quality, native_macos, tests, tests_macos, native_linux, effects, bootstrap_seed, bootstrap, linux_host_clang, ts7_parity, llvm_artifacts_linux_x64, llvm_artifacts_macos_arm64, llvm_artifacts_macos_x64, windows_cli, llvm_artifacts_windows_x64, mobile_runtime_packs]
+ needs:
+ [
+ validation_scope,
+ lanes,
+ code_quality,
+ native_macos,
+ tests,
+ tests_macos,
+ native_linux,
+ effects,
+ bootstrap_seed,
+ bootstrap,
+ linux_host_clang,
+ ts7_parity,
+ checker_identity,
+ llvm_artifacts_linux_x64,
+ llvm_artifacts_macos_arm64,
+ llvm_artifacts_macos_x64,
+ windows_cli,
+ llvm_artifacts_windows_x64,
+ mobile_runtime_packs,
+ ]
# Run even after failed or cancelled lanes: skipped required jobs allow merging.
if: ${{ always() }}
runs-on: ubuntu-latest
@@ -887,6 +932,7 @@ jobs:
lane bootstrap "${{ needs.bootstrap.result }}" "${{ needs.lanes.outputs.bootstrap }}"
lane linux_host_clang "${{ needs.linux_host_clang.result }}" "${{ needs.lanes.outputs.linux }}"
lane ts7_parity "${{ needs.ts7_parity.result }}" "${{ needs.lanes.outputs.linux }}"
+ lane checker_identity "${{ needs.checker_identity.result }}" "${{ needs.lanes.outputs.linux }}"
lane llvm_artifacts_linux_x64 "${{ needs.llvm_artifacts_linux_x64.result }}" "${{ needs.lanes.outputs.linux }}"
lane llvm_artifacts_macos_arm64 "${{ needs.llvm_artifacts_macos_arm64.result }}" "${{ needs.lanes.outputs.macos }}"
lane llvm_artifacts_macos_x64 "${{ needs.llvm_artifacts_macos_x64.result }}" "${{ needs.lanes.outputs.macos }}"
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/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..541c9fb2b 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,11 @@ 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.
+Calling `Readable`, `Writable`, `Duplex`, `Transform`, or `PassThrough` with a constructor function's receiver throws `SC2020` where the call runs.
+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..411291d94 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,
@@ -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..b6a18db06 100644
--- a/packages/compiler/package.json
+++ b/packages/compiler/package.json
@@ -39,6 +39,7 @@
},
"dependencies": {
"@scriptc/runtime": "workspace:*",
+ "tsc-ts": "0.0.3",
"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..9cbf030e7 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,21 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
dc: features.dc,
dynAsync: features.dynAsync,
...(features.dynTier ? { dynTier: true } : {}),
+ ...(features.streamDyn ? { streamDyn: true } : {}),
events: features.processEvents,
emitter: features.emitter,
symbol: features.symbol,
bigint: features.bigint,
searchParams: features.searchParams,
qs: features.qs,
+ dynBuiltinClass: features.dynBuiltinClass,
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 +48,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..42275c5af 100644
--- a/packages/compiler/src/backend/library-pack.ts
+++ b/packages/compiler/src/backend/library-pack.ts
@@ -42,18 +42,21 @@ export async function compilePackedLibrary(
bigint: options.bigint ?? false,
searchParams: options.searchParams ?? false,
qs: false,
+ dynBuiltinClass: 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 86b4f5be8..7e414daee 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,
@@ -613,6 +614,18 @@ export class LlDyn {
* tracking does not count the read's reference. Returns the value and
* the matching release. */
private memberRead(B: BlockBuilder, key: string, t: IrType): { value: string; release: string } {
+ // An accessor slot reads the getter or setter of the accessor property
+ // the checked-dynamic copy holds for it (see accessorSlotProp).
+ const slot = accessorSlotProp(key);
+ if (slot !== null) {
+ this.host.declare(`declare ptr @scr_dyn_obj_accessor_fn(ptr, ptr, ${this.S}, i1 zeroext)`);
+ this.host.declare(`declare void @scr_dyn_release(ptr)`);
+ const fn = B.tmp();
+ B.line(
+ `${fn} = call ptr @scr_dyn_obj_accessor_fn(ptr %d, ptr ${this.host.cstr(slot.prop)}, ${this.S} ${Buffer.byteLength(slot.prop, "utf8")}, i1 ${slot.kind === "set" ? "true" : "false"})`,
+ );
+ return { value: fn, release: "scr_dyn_release" };
+ }
const held = this.isRecursiveShape(t) || this.holdsRecursiveShape(t);
const release = held ? "scr_dyn_release_held" : "scr_dyn_release";
this.host.declare(`declare void @${release}(ptr)`);
@@ -2687,13 +2700,53 @@ export class LlDyn {
// Keys insert in DECLARED order (JS insertion order); internal
// '%'-fields follow so a record→dyn→record round trip keeps them.
const byName = new Map(shape.fields.map((f) => [f.name, f]));
- const order = shape.declaredOrder ?? shape.fields.map((f) => f.name);
+ // An accessor property is one key whose getter and setter are the
+ // slot closures (see accessorSlotProp), in accessorOrder's place.
+ const order = shape.accessorOrder ?? shape.declaredOrder ?? shape.fields.map((f) => f.name);
const inOrder = new Set(order);
- const dynFields = [
- ...order.map((n) => byName.get(n)).filter((f) => f !== undefined),
- ...shape.fields.filter((f) => !inOrder.has(f.name)),
+ const accessors = new Set(
+ shape.accessorOrder === undefined
+ ? []
+ : shape.fields.flatMap((f) => {
+ const slot = accessorSlotProp(f.name);
+ return slot === null ? [] : [slot.prop];
+ }),
+ );
+ type DynField = { name: string; type: IrType } | { accessor: string };
+ const dynFields: DynField[] = [
+ ...order.flatMap((n) => {
+ if (accessors.has(n)) return [{ accessor: n } as DynField];
+ const f = byName.get(n);
+ return f === undefined ? [] : [f as DynField];
+ }),
+ ...shape.fields.filter(
+ (f) =>
+ !inOrder.has(f.name) && !(accessors.size > 0 && accessorSlotProp(f.name) !== null),
+ ),
];
- for (const f of dynFields) {
+ for (const entry of dynFields) {
+ if ("accessor" in entry) {
+ host.declare(
+ `declare void @scr_dyn_obj_set_accessor(ptr, ptr, ${host.sizeType}, ptr, ptr)`,
+ );
+ const boxed = (kind: "get" | "set"): string => {
+ const slot = byName.get(`%${kind}:${entry.accessor}`);
+ if (slot === undefined) return "null";
+ const fv = loadFieldOf(slot.name, slot.type);
+ const conv = B.tmp();
+ B.line(
+ `${conv} = call ptr @${this.toDynHelper(slot.type)}(${this.valTy(slot.type)} ${fv})`,
+ );
+ return conv;
+ };
+ const getter = boxed("get");
+ const setter = boxed("set");
+ B.line(
+ `call void @scr_dyn_obj_set_accessor(ptr ${d}, ptr ${host.cstr(entry.accessor)}, ${host.sizeType} ${Buffer.byteLength(entry.accessor, "utf8")}, ptr ${getter}, ptr ${setter}) ; ${llvmCommentText(entry.accessor)}`,
+ );
+ continue;
+ }
+ const f = entry;
const klen = Buffer.byteLength(f.name, "utf8");
const fv = loadFieldOf(f.name, f.type);
// An ABSENT optional field contributes no key.
diff --git a/packages/compiler/src/backend/llvm/emit-module.ts b/packages/compiler/src/backend/llvm/emit-module.ts
index 7d60205e5..4e4f11828 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()`);
}
@@ -728,6 +732,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
@@ -837,6 +844,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()`] : []),
@@ -870,6 +878,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 f7b9f2517..b72a6e226 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";
@@ -181,6 +182,7 @@ import {
genResultThunkFor,
childExitThunkFor,
childExitSignalThunkFor,
+ childDataDynThunkFor,
childDataThunkFor,
execFileThunkFor,
ipcMessageThunkFor,
@@ -413,6 +415,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 {
@@ -682,6 +687,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();
@@ -1175,6 +1181,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;
@@ -4201,6 +4214,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 &&
@@ -5564,6 +5591,10 @@ 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);
}
diff --git a/packages/compiler/src/backend/llvm/expr-callbacks.ts b/packages/compiler/src/backend/llvm/expr-callbacks.ts
index db1dca1af..28bd15759 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" },
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-stream-bridges.ts b/packages/compiler/src/backend/llvm/expr-stream-bridges.ts
index 134c74b92..847ed1e20 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";
@@ -941,6 +942,18 @@ export function streamTypedRefMaterializeAdapter(
B.line(`${copied} = call ptr @scr_dyn_copy_property_descriptors(ptr ${out}, ptr ${bag})`);
B.line(`call void @scr_dyn_release_v(ptr ${copied})`);
}
+ // An Error subclass instance's own `name` sits where its class's
+ // construction creates it: after a field, or after every other
+ // property when no constructor sets it.
+ const nameAfter = meta.def.errorNameAfter;
+ if (meta.root.def.name === "%Error" && !meta.def.runtime && nameAfter !== null) {
+ host.declare(`declare void @scr_error_dyn_place_name(ptr, ptr, ${host.sizeType})`);
+ B.line(
+ nameAfter === undefined
+ ? `call void @scr_error_dyn_place_name(ptr ${out}, ptr null, ${host.sizeType} 0)`
+ : `call void @scr_error_dyn_place_name(ptr ${out}, ptr ${host.cstr(nameAfter)}, ${host.sizeType} ${Buffer.byteLength(nameAfter, "utf8")})`,
+ );
+ }
// A frozen, sealed or non-extensible instance's view says so.
if (withAttributes) {
host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`);
@@ -965,6 +978,7 @@ export function streamTypedRefMaterializeAdapter(
* Declared-field records and tuples cover the live stream values. */
const ordinary =
shape.indexValue ||
+ shapeHasAccessorSlots(shape) ||
shape.fields.some((field) => field.name === "handleEvent" && field.type.kind === "func");
if (ordinary) {
const out = B.tmp();
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..6cec9d403 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -857,6 +857,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..4a7be1a35 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]!);
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index 294128107..f9deb553e 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",
@@ -612,6 +626,9 @@ export const LIB_FN_SYMS: Record = {
"insp.regex": "scr_insp_regex",
"insp.date": "scr_insp_date",
"insp.error": "scr_insp_error",
+ "insp.errorHasCause": "scr_insp_error_has_cause",
+ "insp.errorBagHasProperties": "scr_insp_error_bag_has_properties",
+ "insp.errorCause": "scr_insp_error_cause",
"insp.begin": "scr_insp_begin",
"insp.layout": "scr_insp_layout",
"insp.entry": "scr_insp_entry",
@@ -726,6 +743,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 +879,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 +965,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 +981,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 +1148,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 +1196,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 +1463,9 @@ 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.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",
@@ -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..704945c95 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,24 @@ 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: boolean;
emitter: boolean;
symbol: boolean;
bigint: boolean;
searchParams: boolean;
qs: boolean;
+ dynBuiltinClass: 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 +68,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..c7371077f 100644
--- a/packages/compiler/src/backend/native/contracts.ts
+++ b/packages/compiler/src/backend/native/contracts.ts
@@ -113,11 +113,16 @@ 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;
/** 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 +155,11 @@ 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 native util parsing, comparison, or styling. Optional
* utility units also require their symbol, inspection, and warning features. */
parseArgs?: boolean;
@@ -177,6 +187,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 +224,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..5ee6b6abd 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"),
@@ -235,11 +238,23 @@ 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.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 +267,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..748de64fc 100644
--- a/packages/compiler/src/backend/runtime-pack-core.ts
+++ b/packages/compiler/src/backend/runtime-pack-core.ts
@@ -82,7 +82,9 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
workers: boolean;
youngNursery: boolean;
eventTarget: boolean;
+ wasm: boolean;
dynTier: boolean;
+ streamDyn: boolean;
warningReplace: boolean;
stdioExtended: boolean;
nativeFetch: boolean;
@@ -272,7 +274,9 @@ 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,
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 fd57cb49d..a39ce78e2 100644
--- a/packages/compiler/src/backend/runtime-pack-targets.test.ts
+++ b/packages/compiler/src/backend/runtime-pack-targets.test.ts
@@ -107,12 +107,14 @@ const features = {
bigint: false,
searchParams: false,
qs: false,
+ dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
+ dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -120,6 +122,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 61d1dba5e..1681626b7 100644
--- a/packages/compiler/src/backend/runtime-pack.test.ts
+++ b/packages/compiler/src/backend/runtime-pack.test.ts
@@ -44,12 +44,14 @@ const BASE: NativeLinkFeatures = {
bigint: false,
searchParams: false,
qs: false,
+ dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
+ dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -57,6 +59,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/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..9a505d517 100644
--- a/packages/compiler/src/backend/warm-cache.ts
+++ b/packages/compiler/src/backend/warm-cache.ts
@@ -50,18 +50,21 @@ export async function warmNativeCaches(
bigint: false,
searchParams: false,
qs: false,
+ dynBuiltinClass: 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..5b0982dc1 100644
--- a/packages/compiler/src/executable/executable-cache.test.ts
+++ b/packages/compiler/src/executable/executable-cache.test.ts
@@ -45,12 +45,14 @@ const native: EarlyExecutableNativeFeatures = {
bigint: false,
searchParams: false,
qs: false,
+ dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
+ dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -58,6 +60,7 @@ const native: EarlyExecutableNativeFeatures = {
tlsCa: false,
cipher: false,
webcrypto: false,
+ perfLoop: false,
};
async function fixture(): Promise<{
@@ -199,7 +202,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 +219,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..f0be7e11f 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,27 @@ export interface EarlyExecutableNativeFeatures {
dc: boolean;
dynAsync: boolean;
dynTier?: boolean;
+ streamDyn?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
bigint: boolean;
searchParams: boolean;
qs: boolean;
+ dynBuiltinClass: 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 +121,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 +193,14 @@ const BOOLEAN_NATIVE_KEYS = [
"bigint",
"searchParams",
"qs",
+ "dynBuiltinClass",
"parseArgs",
"stream",
"net",
"http",
"http2",
"dgram",
+ "dnsPromises",
"watch",
"foreignFfi",
"nodeTest",
@@ -198,6 +208,7 @@ const BOOLEAN_NATIVE_KEYS = [
"tlsCa",
"cipher",
"webcrypto",
+ "perfLoop",
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures {
@@ -230,6 +241,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 +282,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/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/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/builtins/builtin-class-dyn.ts b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts
new file mode 100644
index 000000000..ac73924ad
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/builtin-class-dyn.ts
@@ -0,0 +1,149 @@
+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,
+ };
+}
+
+export function builtinClassValue(kind: number, loc: SrcLoc): IrExpr {
+ return { kind: "libCall", fn: "dyn.builtinClass", 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: callee.getText(),
+ 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/child-process-methods.ts b/packages/compiler/src/frontend/lowering/builtins/child-process-methods.ts
index a435bf740..e1a25d169 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";
@@ -131,8 +132,16 @@ export function lowerChildMethodCall(
) {
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 (
+ narrowed.length > 0 ||
(isJsSourceFile(listenerNode.getSourceFile()) && cb.type.kind === "dyn") ||
(cb.type.kind === "func" &&
(cb.type.rest ||
@@ -301,10 +310,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 +346,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 +354,7 @@ export function lowerChildStreamMethodCall(
);
}
if (!ts.isExpressionStatement(call.parent)) {
+ requestGeneralStreamSurface(lowerer, access.expression);
lowerer.unsupported(
"SC1090",
call,
@@ -376,23 +389,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 +440,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 +484,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 +531,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 +539,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(listener.getSourceFile())) 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/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/errors.ts b/packages/compiler/src/frontend/lowering/builtins/errors.ts
index a8b4dbcd6..439c9fca3 100644
--- a/packages/compiler/src/frontend/lowering/builtins/errors.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/errors.ts
@@ -52,6 +52,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 +149,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/module-bindings.ts b/packages/compiler/src/frontend/lowering/builtins/module-bindings.ts
index 86d93de2c..8a5365d18 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
diff --git a/packages/compiler/src/frontend/lowering/builtins/module-calls.ts b/packages/compiler/src/frontend/lowering/builtins/module-calls.ts
index 956eddb93..895c1a9d6 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,
@@ -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/performance.ts b/packages/compiler/src/frontend/lowering/builtins/performance.ts
index 2a9962b0f..7aac3b80c 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;
@@ -513,3 +520,305 @@ 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?.name === "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..9ac1fc059 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";
@@ -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,
@@ -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(expr.getSourceFile())) {
+ return builtinFunctionValue("process.nextTick", loc);
+ }
if (member === "getBuiltinModule") {
// Keep the native loader's argument validation when the function is
// stored. The declaration's string signature must not insert a checked
diff --git a/packages/compiler/src/frontend/lowering/builtins/promises.ts b/packages/compiler/src/frontend/lowering/builtins/promises.ts
index 48106d082..55290f720 100644
--- a/packages/compiler/src/frontend/lowering/builtins/promises.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/promises.ts
@@ -1,7 +1,8 @@
-import { checkedPromiseAll } from "../checked-promise-all.js";
+import { checkedPromiseAll, thenReader } from "../checked-promise-all.js";
import { InternalCompilerError } from "../../../errors.js";
import * as ts from "../../ts7/adapter.js";
-import { type Lowerer } from "../lowerer.js";
+import { newFnCtx, type Lowerer } from "../lowerer.js";
+import { varRef } from "../../../ir/build.js";
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
import { isJsSourceFile, locOf } from "../../program.js";
import {
@@ -10,6 +11,7 @@ import {
type IrFunction,
type IrStmt,
type IrType,
+ UNDEFINED_T,
VOID,
arrayOf,
isUnitType,
@@ -17,6 +19,168 @@ import {
typeKey,
} from "../../../ir/ir.js";
+/** `Promise.all([p, q, r])` over DIFFERENT promise types — the checker's
+ * tuple overload, `Promise<[A, B, C]>`. One interned async helper per
+ * entry/result signature takes the entries as arguments, awaits the
+ * runtime combinator over the same promises (each widened to
+ * Promise, 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
@@ -74,6 +238,17 @@ export function lowerPromiseStaticCall(
}
return resultArms.some((a) => typeEquals(a, inner));
};
+ // 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 =
+ t.getSymbol()?.name === "Promise"
+ ? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
+ : undefined;
+ return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
+ };
const entries = argNode.elements.nodes.map((el) => {
const entry = lowerer.lowerExpr(el);
if (entry.type.kind !== "promise") {
@@ -83,7 +258,7 @@ export function lowerPromiseStaticCall(
"JS would resolve plain values — wrap them: new Promise((r) => r(v))",
);
}
- if (!compatible(entry.type.inner)) {
+ if (!compatible(entry.type.inner) && !(entry.type.inner.kind === "void" && neverEntry(el))) {
lowerer.unsupported(
"SC1090",
el,
@@ -143,6 +318,10 @@ export function lowerPromiseStaticCall(
return { kind: "intrinsic", name: "promise.all", args: [entriesArr], type: resultT, loc };
}
+ if (!isJsSourceFile(call.getSourceFile())) {
+ const tuple = lowerTuplePromiseAll(lowerer, call, elems);
+ if (tuple) return tuple;
+ }
if (
isJsSourceFile(call.getSourceFile()) &&
elems.every((entry) => lowerer.dynConvertible(entry.type))
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/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/call-arguments.ts b/packages/compiler/src/frontend/lowering/call-arguments.ts
index f5600b033..db7691a1c 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();
@@ -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) {
diff --git a/packages/compiler/src/frontend/lowering/call-signatures.ts b/packages/compiler/src/frontend/lowering/call-signatures.ts
index 7570fff88..954122a16 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 {
@@ -373,6 +383,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(param.getSourceFile())
+ ) {
+ 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 +412,47 @@ 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(declParam.getSourceFile()) &&
+ (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(declParam.getSourceFile())
+ ? DYN
+ : lowerer.irTypeOf(declParam.name)),
),
lowerer.shapes,
lowerer.unions,
@@ -428,7 +486,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
@@ -828,6 +911,16 @@ 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.
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/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..5fc2a3cec 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";
@@ -2070,6 +2071,49 @@ export class ClassDynamicDispatch {
// A read compares the bag with the receiver and then reads it:
// evaluate it once, so each read retains the receiver once.
const bagRead = varRef("bag.0", DYN, loc);
+ const bagGet: IrExpr | null = read
+ ? {
+ kind: "libCall",
+ fn: "dyn.bagGet",
+ args: [bagRead, read.key, varRef("p.0", DYN, loc)],
+ type: DYN,
+ loc,
+ }
+ : null;
+ // A class instance whose class chain declares no member of an
+ // Object.prototype method's name (classes that do answer in
+ // their own branches) inherits it unless an own property
+ // shadows it; a class constructor's static bag reads as before.
+ const instanceRead: IrExpr | null =
+ read && INHERITED_OBJECT_METHODS.has(name)
+ ? {
+ kind: "ternary",
+ cond: {
+ kind: "dynTest",
+ test: "function",
+ value: varRef("p.0", DYN, loc),
+ type: BOOL,
+ loc,
+ },
+ then: bagGet!,
+ else_: {
+ kind: "ternary",
+ cond: this.readBagHas(lowerer, "dyn.hasKey", bagRead, read.key, loc),
+ then: bagGet!,
+ else_: {
+ kind: "libCall",
+ fn: "dyn.builtinMethod",
+ args: [{ kind: "strLit", value: "Object", type: STRING, loc }, read.key],
+ type: DYN,
+ loc,
+ },
+ type: DYN,
+ loc,
+ },
+ type: DYN,
+ loc,
+ }
+ : bagGet;
const fallback: IrExpr = read
? {
kind: "ternary",
@@ -2081,13 +2125,7 @@ export class ClassDynamicDispatch {
loc,
},
then: { ...read, value: bagRead },
- else_: {
- kind: "libCall",
- fn: "dyn.bagGet",
- args: [bagRead, read.key, varRef("p.0", DYN, loc)],
- type: DYN,
- loc,
- },
+ else_: instanceRead!,
type: DYN,
loc,
}
@@ -2112,7 +2150,20 @@ export class ClassDynamicDispatch {
]
: [
{ kind: "varDecl", localId: "bag.0", init: bag, loc },
- { kind: "return", value: fallback, loc },
+ {
+ kind: "return",
+ // Primitives answer their wrapper constructors.
+ value:
+ name === "constructor"
+ ? primitiveConstructorOr(
+ lowerer,
+ varRef("p.0", DYN, loc),
+ fallback,
+ loc,
+ )
+ : fallback,
+ loc,
+ },
],
loc,
};
@@ -4820,19 +4871,40 @@ export class ClassDynamicDispatch {
if (!field && !accessor) {
const instance = lowerer.coerceToExpected(receiver, DYN);
const bag = this.readPropertyBagOrEmpty(lowerer, instance, loc);
- return [
- {
- kind: "return",
- value: {
- kind: "libCall",
- fn: "dyn.bagGet",
- args: [bag, { kind: "strLit", value: name, type: STRING, loc }, instance],
- type: DYN,
+ const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
+ const own: IrExpr = {
+ kind: "libCall",
+ fn: "dyn.bagGet",
+ args: [bag, key, instance],
+ type: DYN,
+ loc,
+ };
+ // An Object.prototype method no class in the chain declares: an
+ // own property shadows it, otherwise the read answers the same
+ // interned value `Object.prototype.` 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..981cd0c1a 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
@@ -555,6 +565,19 @@ export function collectClassMethodMutations(
lowerer.prototypeMethodAccesses.set(name, site);
}
for (const { key, receiver, site } of computedWrites) {
+ // A class method declared with a well-known symbol key is not a write;
+ // any other write with one can replace that method.
+ const symbolName = key ? wellKnownMethodName(unwrap(key)) : null;
+ if (symbolName) {
+ const declaration =
+ (ts.isMethodDeclaration(site) ||
+ ts.isGetAccessorDeclaration(site) ||
+ ts.isSetAccessorDeclaration(site)) &&
+ (ts.isClassDeclaration(site.parent) || ts.isClassExpression(site.parent));
+ if (declaration) symbolMethods.add(symbolName);
+ else lowerer.replacedSymbolMethods.add(symbolName);
+ continue;
+ }
const type = key ? lowerer.typeOf(key) : undefined;
const names = type && (literalValues(type) ?? (key ? literalLoopKeys(unwrap(key)) : null));
if (names) {
@@ -585,8 +608,10 @@ export function collectClassMethodMutations(
if (String(Number(name)) === name) markName(name, numericKey);
}
}
- if (unknownKey)
+ if (unknownKey) {
for (const method of methods) lowerer.prototypeMethodAccesses.set(method, unknownKey);
+ for (const method of symbolMethods) lowerer.replacedSymbolMethods.add(method);
+ }
if (!unknownKey && !unknownOwnField)
for (const field of arrowFields) {
if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field);
diff --git a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts
index 0c9a24f24..298ff45dc 100644
--- a/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts
+++ b/packages/compiler/src/frontend/lowering/class-protocol-dispatch.ts
@@ -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;
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/code-generation.ts b/packages/compiler/src/frontend/lowering/code-generation.ts
new file mode 100644
index 000000000..b3bd9243a
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/code-generation.ts
@@ -0,0 +1,142 @@
+/* Code generation from strings: `eval(...)`, `new Function(...)` and
+ * `Function(...)`. A compiled binary embeds no JavaScript engine, so scriptc
+ * answers exactly as `node --disallow-code-generation-from-strings` does:
+ * every call throws the catchable EvalError "Code generation from strings
+ * disallowed for this context". Packages that probe for code generation
+ * (zod 4's `allowsEval`, template engines, CSP-aware JITs) then take their
+ * interpreted paths. The arguments still evaluate first, in order, and the
+ * Function constructor converts each to a string before it refuses, as
+ * V8's CreateDynamicFunction does; eval refuses without converting. */
+import * as ts from "../ts7/adapter.js";
+import type { Lowerer } from "./lowerer.js";
+import { isJsSourceFile, locOf } from "../program.js";
+import { DYN, F64, type IrExpr, type IrStmt, type IrType, STRING } from "../../ir/ir.js";
+import { nodeThrowExpr } from "../../ir/build.js";
+import { ambientUndefReadType, contextualUndefReadType } from "./lower-namespaces.js";
+import { stdlibGlobalNameOf } from "./surfaces.js";
+
+export const CODE_GENERATION_DISALLOWED =
+ "Code generation from strings disallowed for this context";
+
+/** The code-generation entry point a callee names: the stdlib `eval` or
+ * `Function` global (directly, through `globalThis.`, or through a stable
+ * alias), or the indirect-eval spelling `(0, eval)`. Null otherwise. */
+export function codeGenerationCalleeOf(
+ lowerer: Lowerer,
+ callee: ts.Expression,
+): "eval" | "Function" | null {
+ let e = callee;
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.CommaToken) {
+ return codeGenerationCalleeOf(lowerer, e.right) === "eval" ? "eval" : null;
+ }
+ // Syntactic pre-check first: every call and construction asks, and only
+ // these spellings can name the globals (aliases are stdlib-resolved
+ // symbols, checked below only when the spelling could match).
+ const text = ts.isIdentifier(e) ? e.text : ts.isPropertyAccessExpression(e) ? e.name.text : null;
+ if (text === null) return null;
+ if (text !== "eval" && text !== "Function" && !hasCodeGenerationAlias(lowerer)) return null;
+ const name = stdlibGlobalNameOf(lowerer, e);
+ return name === "eval" || name === "Function" ? name : null;
+}
+
+/** Some stable binding aliases `eval` or `Function` (`const F = Function`,
+ * zod 4's spelling). */
+function hasCodeGenerationAlias(lowerer: Lowerer): boolean {
+ for (const name of lowerer.stdlibGlobalAliases.values()) {
+ if (name === "eval" || name === "Function") return true;
+ }
+ return false;
+}
+
+/** The comma operands an indirect eval evaluates before the call: `a` in
+ * `(a, eval)(...)`, outermost first. */
+function indirectEvalPrefix(callee: ts.Expression): ts.Expression[] {
+ let e = callee;
+ while (ts.isParenthesizedExpression(e)) e = e.expression;
+ if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.CommaToken) return [];
+ return [...indirectEvalPrefix(e.left), e.left, ...indirectEvalPrefix(e.right)].filter(
+ (operand) => {
+ let o = operand;
+ while (ts.isParenthesizedExpression(o)) o = o.expression;
+ return !(ts.isBinaryExpression(o) && o.operatorToken.kind === ts.SyntaxKind.CommaToken);
+ },
+ );
+}
+
+/** Lowers a code-generation call or construction to Node's refusal under
+ * --disallow-code-generation-from-strings: the callee's own operands, then
+ * the arguments in order (converted to strings for the Function
+ * constructor), then the EvalError. The result carries the use site's type
+ * (the throw always fires, so the value is never observed). */
+export function lowerCodeGenerationCall(
+ lowerer: Lowerer,
+ expr: ts.CallExpression | ts.NewExpression,
+ kind: "eval" | "Function",
+): IrExpr {
+ const loc = locOf(expr);
+ const stmts: IrStmt[] = [];
+ for (const operand of indirectEvalPrefix(expr.expression)) {
+ stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(operand), loc: locOf(operand) });
+ }
+ for (const arg of expr.arguments?.nodes ?? []) {
+ if (ts.isSpreadElement(arg)) {
+ lowerer.noLowering(
+ `${kind === "eval" ? "eval" : "the Function constructor"} with spread arguments`,
+ arg,
+ "code generation from strings throws EvalError in compiled programs, as under node --disallow-code-generation-from-strings; pass the arguments directly",
+ );
+ }
+ const value = lowerer.lowerExpr(arg);
+ const argLoc = locOf(arg);
+ if (kind === "Function" && !isPrimitiveIrType(value.type)) {
+ stmts.push({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "dyn.toStringCoerce",
+ args: [lowerer.coerceToExpected(value, DYN)],
+ type: STRING,
+ loc: argLoc,
+ },
+ loc: argLoc,
+ });
+ } else {
+ stmts.push({ kind: "exprStmt", expr: value, loc: argLoc });
+ }
+ }
+ const type = useSiteType(lowerer, expr);
+ const refusal = nodeThrowExpr(6, "", CODE_GENERATION_DISALLOWED, type, loc);
+ return stmts.length === 0 ? refusal : { kind: "seqExpr", stmts, result: refusal, type, loc };
+}
+
+function isPrimitiveIrType(t: IrType): boolean {
+ return (
+ t.kind === "string" ||
+ t.kind === "f64" ||
+ t.kind === "bool" ||
+ t.kind === "void" ||
+ t.kind === "bigint"
+ );
+}
+
+function useSiteType(lowerer: Lowerer, expr: ts.Expression): IrType {
+ // The Function interface has no mapping (mapping it would fence): the
+ // constructed value is checked-dynamic in JavaScript, and a typed dummy
+ // elsewhere — the throw always fires first.
+ const type = lowerer.typeOf(expr);
+ const symbol = type.getSymbol();
+ if (symbol?.name === "Function" && lowerer.isStdlibSymbol(symbol)) {
+ return isJsSourceFile(expr.getSourceFile()) ? DYN : F64;
+ }
+ const mapped = lowerer.mapTypeOf(type);
+ if (
+ mapped &&
+ mapped.kind !== "void" &&
+ mapped.kind !== "func" &&
+ !lowerer.typeNamesUnregisteredClass(mapped)
+ ) {
+ return mapped;
+ }
+ return ambientUndefReadType(lowerer, expr) ?? contextualUndefReadType(lowerer, expr) ?? F64;
+}
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/containers/array-callbacks.ts b/packages/compiler/src/frontend/lowering/containers/array-callbacks.ts
index 2e93e2fd5..735361f54 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
diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
index 6072b58b7..110af2b56 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,
@@ -295,6 +312,15 @@ export function lowerArrayConstructor(
const contextual = lowerer.checker.getContextualType(expr);
if (contextual) result = lowerer.mapTypeOf(contextual);
}
+ // `Array(n).fill(v)`: the checker types the length-constructed array as
+ // any[], but fill gives every element v's type.
+ if (result?.kind !== "array") {
+ const fillValue = fillArgumentOfReceiver(expr);
+ const elem = fillValue
+ ? lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(fillValue)))
+ : null;
+ if (elem !== null && isSupportedArrayElem(elem)) result = arrayOf(elem);
+ }
if (isJsSourceFile(expr.getSourceFile()) && (result === null || result.kind === "dyn"))
result = arrayOf(DYN);
if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) {
@@ -913,7 +939,7 @@ export function lowerArrayFromCall(
// and materializes holes while keeping element reference identity.
if ((args.length === 1 || args.length === 2) && !ts.isObjectLiteralExpression(args[0]!)) {
const source = lowerer.lowerExpr(args[0]!);
- const src =
+ let src =
source.type.kind === "string" && args.length === 2
? strCharsCall(lowerer, source, loc)
: source;
@@ -1008,6 +1034,9 @@ export function lowerArrayFromCall(
if (src.type.kind === "searchParams" && args.length === 1) {
return lowerCheckedArrayFrom(lowerer, lowerer.coerceInto(args[0]!, src, DYN), loc);
}
+ // A class whose [Symbol.iterator] is a generator method iterates the
+ // generator that method returns.
+ src = classGeneratorIterable(lowerer, src) ?? src;
// A generator (or an iterator adapted into one) drains in order.
if (src.type.kind === "generator" && args.length === 1) {
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
diff --git a/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts b/packages/compiler/src/frontend/lowering/containers/callback-arguments.ts
index e87a5e1bc..dcd9934bd 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(argNode.getSourceFile()) &&
+ lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined)
+ ) {
+ const valueT = lead[0];
+ if (valueT === undefined || valueT.kind === "void" || isUnitType(valueT))
+ lowerer.badType(argNode, lowerer.typeOf(argNode));
+ if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode);
+ return { fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), arity: 1 };
+ }
const full = [...lead, F64, arrT];
// A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`):
// the contextual signature types the unannotated param `any` (the
@@ -248,3 +272,39 @@ function runtimeOptionalPairVariant(lowerer: Lowerer, expected: IrType, mapped:
: t;
return lowerer.isRuntimeOptionalRecordVariant(strip(expected), strip(mapped));
}
+
+/** A native closure for the ambient Boolean callback. Reifying the
+ * JavaScript constructor itself is unnecessary; Array#filter only observes
+ * its one-argument ToBoolean result. */
+export function truthyFilterCallback(
+ lowerer: Lowerer,
+ valueT: IrType,
+ loc: SrcLoc,
+): IrExpr & { type: IrType & { kind: "func" } } {
+ const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: BOOL };
+ const key = `filterBoolean:${typeKey(valueT)}`;
+ let name = lowerer.arrHofHelpers.get(key);
+ if (!name) {
+ name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`;
+ lowerer.arrHofHelpers.set(key, name);
+ const value = varRef("v.0", valueT, loc);
+ lowerer.liftedFns.push({
+ name,
+ params: [{ localId: "v.0", name: "v", type: valueT }],
+ returnType: BOOL,
+ locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }],
+ body: [
+ {
+ kind: "return",
+ value:
+ valueT.kind === "dyn"
+ ? { kind: "dynTest", test: "truthy", value, type: BOOL, loc }
+ : { kind: "toBool", operand: value, type: BOOL, loc },
+ loc,
+ },
+ ],
+ loc,
+ });
+ }
+ return { kind: "closure", fnName: name, captures: [], type: fnT, loc };
+}
diff --git a/packages/compiler/src/frontend/lowering/containers/collection-methods.ts b/packages/compiler/src/frontend/lowering/containers/collection-methods.ts
index a8b782b51..6a5290c0f 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
@@ -1048,6 +1050,14 @@ function setFromSeedValue(
}
: null;
}
+ // A generator, or a class whose [Symbol.iterator] is a generator method:
+ // the Set takes every value it yields, in order.
+ const generator =
+ classGeneratorIterable(lowerer, source) ?? (source.type.kind === "generator" ? source : null);
+ if (generator) {
+ const drained = generatorDrain(lowerer, generator, setT.elem, loc);
+ return drained ? { kind: "setNew", seed: drained, type: setT, loc } : null;
+ }
let seed: IrExpr;
if (source.type.kind === "set" && typeEquals(source.type, setT)) {
return { kind: "setIntrinsic", method: "clone", receiver: source, args: [], type: setT, loc };
@@ -1246,6 +1256,24 @@ export function mapFromSeedValue(
}
: null;
}
+ // A generator of entries, or a class whose [Symbol.iterator] is one: the
+ // Map takes every [key, value] pair it yields, in order.
+ const generator =
+ classGeneratorIterable(lowerer, seed) ?? (seed.type.kind === "generator" ? seed : null);
+ if (generator) {
+ const entry: IrType = {
+ kind: "record",
+ shapeId: lowerer.shapes.intern(
+ [
+ { name: "0", type: mapT.key },
+ { name: "1", type: mapT.value },
+ ],
+ true,
+ ),
+ };
+ const drained = generatorDrain(lowerer, generator, entry, loc);
+ return drained ? mapFromSeedValue(lowerer, drained, mapT) : null;
+ }
if (seed.type.kind === "map") {
if (!typeEquals(seed.type, mapT)) return null;
return { kind: "mapIntrinsic", method: "clone", receiver: seed, args: [], type: mapT, loc };
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..4017335ae 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")
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/divergences.ts b/packages/compiler/src/frontend/lowering/divergences.ts
index 36fbf48b9..a77f33c6b 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)) {
@@ -399,55 +386,9 @@ export function scanDivergences(
}
}
- // ── Date strings the native parser does not accept ────────────────
- for (const { call, arg } of dates) {
- const callee = ts.isNewExpression(call)
- ? call.expression
- : ts.isPropertyAccessExpression(call.expression)
- ? call.expression.expression
- : undefined;
- if (callee === undefined || !lowerer.isStdlibGlobal(callee, "Date")) continue;
- for (const literal of dateStringLiterals(checker, arg)) {
- const text = stringLiteralText(literal)!;
- if (nativeDateParses(text) || !/\d/.test(text)) continue;
- out.push(dateLiteralDivergenceDiag(text, locOf(literal)));
- }
- }
-
return out;
}
-/** The string literals a Date argument can be: the literal itself, or
- * each element of a const array of literals a for-of binding iterates. */
-function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Expression[] {
- if (stringLiteralText(arg) !== null) return [arg];
- if (!ts.isIdentifier(arg)) return [];
- const symbol = safely(() => checker.getSymbolAtLocation(arg));
- const decl = symbol === undefined ? undefined : symbol.valueDeclaration;
- const list = decl?.parent;
- const loop = list?.parent;
- if (
- decl === undefined ||
- !ts.isVariableDeclaration(decl) ||
- list === undefined ||
- loop === undefined ||
- !ts.isForOfStatement(loop) ||
- loop.initializer !== list
- )
- return [];
- let iterated: ts.Expression = loop.expression;
- if (ts.isIdentifier(iterated)) {
- const source = safely(() => checker.getSymbolAtLocation(iterated));
- const init = source === undefined ? undefined : source.valueDeclaration;
- if (init === undefined || !ts.isVariableDeclaration(init) || init.initializer === undefined)
- return [];
- if (init.parent === undefined || (init.parent.flags & ts.NodeFlags.Const) === 0) return [];
- iterated = init.initializer;
- }
- if (!ts.isArrayLiteralExpression(iterated)) return [];
- return iterated.elements.nodes.filter((e) => stringLiteralText(e) !== null);
-}
-
/** A checker query that fails here only loses a warning. */
function safely(f: () => T): T | undefined {
try {
@@ -870,20 +811,3 @@ function classExtends(
function className(decl: ts.ClassLikeDeclaration): string {
return decl.name?.text ?? "(anonymous class)";
}
-
-const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
-
-/** The date-string grammar the native runtime accepts (scr_lib.c
- * scr_date_parse_get_time): ECMAScript's date-time format with an explicit
- * offset (date-only forms are UTC) and the certificate-time shape
- * "Mon DD HH:MM:SS YYYY GMT". Everything else parses to NaN natively. */
-export function nativeDateParses(text: string): boolean {
- const iso =
- /^(?:[+-]\d{6}|\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.test(text) ||
- /^(?:[+-]\d{6}|\d{4})-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
- text,
- );
- if (iso) return true;
- const cert = /^([A-Za-z]{3}) {1,2}\d{1,2} \d{2}:\d{2}:\d{2} \d{4} GMT$/.exec(text);
- return cert !== null && MONTHS.includes(cert[1]!.toLowerCase());
-}
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..9694e3820
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/dyn-streams.ts
@@ -0,0 +1,314 @@
+/* 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?.name !== "ReadableStream" && psym?.name !== "WritableStream") continue;
+ const declared = psym.declarations.some(
+ (d) =>
+ ts.isInterfaceDeclaration(d) &&
+ lowerer.isStdlibFile(d.getSourceFile()) &&
+ isDeclaredInAmbientNamespace(d, "NodeJS"),
+ );
+ if (declared) return psym.name === "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: access.getText(),
+ 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 5aabf42e1..38aa4f8aa 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
@@ -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..8ef631f60 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,
diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts
index 144742257..f39b7c428 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]`
@@ -1417,6 +1419,11 @@ 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())) {
const stmts: IrStmt[] = [];
diff --git a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
index d7a58800e..c2e5ddcbb 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
@@ -530,12 +553,21 @@ export function lowerIdentifierExpr(lowerer: Lowerer, expr: ts.Identifier, loc:
// the same identity-token story as stdlib globals above (the
// harness adds worker_threads.Worker to its identity Set).
if (isJsSourceFile(expr.getSourceFile())) {
+ // 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,
};
@@ -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") {
+ const canonical = stdlibGlobalNameOf(lowerer, expr) ?? expr.text;
+ // Builtin constructors have real function values in both source
+ // languages (TypeScript reads them as dynamic values).
+ const builtinValue = builtinGlobalValueAt(
+ lowerer,
+ expr,
+ canonical,
+ loc,
+ isJsSourceFile(expr.getSourceFile()),
+ );
+ if (builtinValue !== null) return builtinValue;
if (isJsSourceFile(expr.getSourceFile())) {
- const canonical = stdlibGlobalNameOf(lowerer, expr) ?? expr.text;
return { kind: "strLit", value: `[builtin ${canonical}]`, type: STRING, loc };
}
// The families with a WHY: each hint states what makes the
@@ -930,5 +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(binding.getSourceFile(), lowerer.program)
+ ) {
+ const snapshot = cjsAccessorSnapshotOf(lowerer, getter);
+ if (snapshot !== null)
+ return { kind: "varRef", localId: snapshot.id, type: snapshot.type, loc: locOf(ident) };
+ }
return cjsAccessorCall(lowerer, getter, locOf(ident));
}
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..7a4f9c557
--- /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(access.getSourceFile()) ||
+ 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..9f98ac2db 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";
@@ -1186,6 +1187,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 +1288,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 +1376,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 +1431,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 +1511,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/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..77c4c8fc5 100644
--- a/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/object-literals.ts
@@ -640,6 +640,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,
@@ -1372,7 +1440,7 @@ export function lowerObjectLiteral(
vNode,
);
contributors.push({
- kind: "field",
+ kind: "condField",
name: csProp.name.text,
value: {
kind: "ternary",
@@ -1387,6 +1455,7 @@ export function lowerObjectLiteral(
}
let src = lowerer.lowerExpr(prop.expression);
src = lowerOptionalIndexSpreadSource(lowerer, src, locOf(prop)) ?? src;
+ src = dynIndexSpreadSource(lowerer, src, type, locOf(prop)) ?? src;
if (src.type.kind !== "record") {
lowerer.unsupported(
"SC1090",
@@ -1631,7 +1700,11 @@ export function lowerObjectLiteral(
}
shapeMismatch(prop);
}
- const absent = lowerer.absentFieldValue(fieldType, locOf(prop));
+ // An 'unknown' slot holds the checked-dynamic undefined for the
+ // empty arm, the absent-property read the width copy uses too.
+ const absent =
+ lowerer.absentFieldValue(fieldType, locOf(prop)) ??
+ (fieldType.kind === "dyn" ? dynUndefinedExpr(locOf(prop)) : null);
if (!absent) {
lowerer.unsupported(
"SC1090",
@@ -2559,6 +2632,14 @@ export function andSpreadOf(
while (ts.isParenthesizedExpression(right)) right = right.expression;
if (!ts.isObjectLiteralExpression(right)) return null;
const spreadType = lowerer.mapTypeOf(lowerer.typeOf(e));
+ // Every falsy value is a primitive, which spreads nothing, so `c && {
+ // ... }` contributes the literal's members exactly when c is truthy,
+ // whatever c's type. The checker's union of the two decides the
+ // representation where it maps; where it does not (a member typed by a
+ // checked-dynamic value such as an AbortSignal), the syntax decides.
+ if (spreadType === null || spreadType.kind === "dyn") {
+ return syntacticAndSpread(right, e.left);
+ }
if (spreadType?.kind !== "union") return null;
const arms = lowerer.unions.get(spreadType.unionId)?.arms ?? [];
if (
@@ -2590,6 +2671,26 @@ export function andSpreadOf(
return { cond: e.left, props, whenTrue: true };
}
+function syntacticAndSpread(
+ right: ts.ObjectLiteralExpression,
+ cond: ts.Expression,
+): { cond: ts.Expression; props: CondSpreadProp[]; whenTrue: boolean } | "unsupported" {
+ if (right.properties.nodes.length === 0) return "unsupported";
+ const props: CondSpreadProp[] = [];
+ for (const p of right.properties.nodes) {
+ if (
+ (ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
+ p.name &&
+ (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
+ ) {
+ props.push(p as CondSpreadProp);
+ } else {
+ return "unsupported";
+ }
+ }
+ return { cond, props, whenTrue: true };
+}
+
/** A conditional spread's carrier property: `name: value` or shorthand,
* with an identifier/string-literal name. */
export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & {
diff --git a/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts b/packages/compiler/src/frontend/lowering/expressions/optional-chains.ts
index 930e1a7cb..995514a4d 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";
@@ -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..22bcb6609 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).
@@ -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).
@@ -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,
};
@@ -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.
@@ -775,6 +807,8 @@ export function lowerPropertyAccessExpr(
const canonical = stdlibGlobalNameOf(lowerer, expr);
if (canonical !== null) {
if (canonical === "globalThis") return lowerGlobalValue(lowerer, expr);
+ // Typed for the programs that name it, absent in Node 24.
+ if (canonical === TEMPORAL_GLOBAL) return dynUndefinedExpr(loc);
if (canonical === "ArrayBuffer")
return { kind: "libCall", fn: "arrayBuffer.constructor", args: [], type: DYN, loc };
if (canonical === "fetch")
@@ -787,7 +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
@@ -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
@@ -1064,6 +1142,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.
@@ -1255,6 +1350,8 @@ export function lowerPropertyAccessExpr(
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
return mapped && mapped.kind !== "void" ? lowerer.coerceInto(expr, read, mapped) : read;
}
+ const asserted = assertedMemberRead(lowerer, expr, recvLowered, loc);
+ if (asserted) return asserted;
lowerer.unsupported(
"SC1090",
expr,
@@ -1262,6 +1359,101 @@ export function lowerPropertyAccessExpr(
);
}
+/** `(source as { repoName: string }).repoName` where the assertion names a
+ * member some arms of the stored union of records do not declare: Node
+ * reads the property off whichever object it is, so arms that declare it
+ * answer their slot, and the others answer undefined (native records are
+ * closed, so they cannot hold it). The result keeps that undefined arm
+ * whatever the assertion claims; a consumer that needs the asserted type
+ * checks it like any other narrowing. The arms that declare the member
+ * must agree on its type. Null when the receiver is not such an
+ * assertion over records, or no arm declares the member. */
+function assertedMemberRead(
+ lowerer: Lowerer,
+ expr: ts.PropertyAccessExpression,
+ recv: IrExpr,
+ loc: SrcLoc,
+): IrExpr | null {
+ let receiverNode: ts.Expression = expr.expression;
+ while (ts.isParenthesizedExpression(receiverNode)) receiverNode = receiverNode.expression;
+ if (!ts.isAsExpression(receiverNode) && !ts.isTypeAssertion(receiverNode)) return null;
+ if (recv.type.kind !== "union") return null;
+ const unionId = recv.type.unionId;
+ const arms = lowerer.unions.get(unionId)?.arms ?? [];
+ const name = expr.name.text;
+ let resultType: IrType | null = null;
+ let records = 0;
+ const declaring: { tag: number; shapeId: string; type: IrType }[] = [];
+ const units: number[] = [];
+ for (let tag = 0; tag < arms.length; tag++) {
+ const arm = arms[tag]!;
+ if (isUnitType(arm)) {
+ units.push(tag);
+ continue;
+ }
+ if (arm.kind !== "record") return null;
+ records++;
+ const shape = lowerer.shapes.get(arm.shapeId);
+ if (!shape || shape.tuple || shape.indexValue) return null;
+ const field = shape.fields.find((f) => f.name === name);
+ if (!field) continue;
+ // The declaring arms agree up to an undefined arm (`string` beside
+ // `string | undefined`).
+ const armResult = lowerer.withUndefinedArmOf(field.type);
+ if (!armResult || (resultType && !typeEquals(resultType, armResult))) return null;
+ resultType = armResult;
+ declaring.push({ tag, shapeId: arm.shapeId, type: field.type });
+ }
+ if (!resultType || declaring.length === records) return null;
+ const stable = lowerer.declareHiddenLocal("%assertedRecv", recv.type);
+ const ref = (): IrExpr => ({ kind: "varRef", localId: stable.id, type: recv.type, loc });
+ let result: IrExpr | null = lowerer.wrappedUndefined(resultType, loc);
+ if (!result) return null;
+ // A unit arm the checker narrowed away reads like Node would read it.
+ for (const tag of units) {
+ const unit = arms[tag]!.kind === "nullT" ? "null" : "undefined";
+ result = {
+ kind: "ternary",
+ cond: { kind: "unionIsTag", unionId, tag, negated: false, value: ref(), type: BOOL, loc },
+ then: nodeThrowExpr(
+ 1,
+ "",
+ `Cannot read properties of ${unit} (reading '${name}')`,
+ resultType,
+ loc,
+ ),
+ else_: result,
+ type: resultType,
+ loc,
+ };
+ }
+ for (const { tag, shapeId, type: fieldType } of declaring) {
+ const read: IrExpr = {
+ kind: "recordGet",
+ obj: { kind: "unionNarrow", unionId, tag, value: ref(), type: arms[tag]!, loc },
+ shapeId,
+ field: name,
+ type: fieldType,
+ loc,
+ };
+ result = {
+ kind: "ternary",
+ cond: { kind: "unionIsTag", unionId, tag, negated: false, value: ref(), type: BOOL, loc },
+ then: lowerer.coerceToExpected(read, resultType),
+ else_: result,
+ type: resultType,
+ loc,
+ };
+ }
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "varDecl", localId: stable.id, init: recv, loc }],
+ result,
+ type: resultType,
+ loc,
+ };
+}
+
function importMetaProperty(lowerer: Lowerer, expr: ts.PropertyAccessExpression): IrExpr | null {
return isImportMeta(expr.expression) ? importMetaField(lowerer, expr, expr.name.text) : null;
}
@@ -1305,6 +1497,13 @@ export function lowerIntrinsicProperty(
return { kind: "dynFrom", value, fnName: "ownKeys", type: DYN, loc: locOf(expr) };
}
if (lowerer.chainBlocked(expr)) return null;
+ // Object.prototype.constructor: the Object constructor value.
+ if (
+ expr.name.text === "constructor" &&
+ ts.isPropertyAccessExpression(expr.expression) &&
+ lowerer.stdlibGlobalMember(expr.expression, "Object") === "prototype"
+ )
+ return builtinGlobalValue("Object", locOf(expr));
const builtinPrototype = builtinPrototypeMethod(lowerer, expr);
if (builtinPrototype)
return {
@@ -2584,6 +2783,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,
@@ -3330,6 +3550,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,
@@ -3557,6 +3778,223 @@ function lowerStreamObjectProperty(
return lowerStreamProperty(lowerer, expr, info);
}
+/** The builtin constructors JavaScript sources read as real function
+ * values rather than identity tokens: the runtime interns one function per
+ * constructor, and dynamic values answer `.constructor` with the same one,
+ * so `o.constructor === Object` is an exact identity test. */
+const GLOBAL_CONSTRUCTOR_VALUES: ReadonlySet = 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}` : callee.getText().replace(/\s+/g, "");
+ lowerer.noLowering(
+ `${name}(${canonical}, ...)`,
+ expr,
+ "builtin constructor and function values model only name, length and constructor, so reflecting on or redefining their members has no lowering",
+ );
+ }
+ }
+ return value;
+}
+
+/** Whether the stdlib global `expr` is read for its members (`Map.prototype`,
+ * `Date["UTC"]`, `"prototype" in Map`) rather than as a value. Those reads
+ * keep the global's member lowerings and fences: its interned function value
+ * models only BUILTIN_VALUE_MEMBERS. */
+function readsBuiltinGlobalMembers(expr: ts.Expression): boolean {
+ let node: ts.Node = expr;
+ while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
+ const parent = node.parent;
+ if (parent === undefined) return false;
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === node)
+ return !BUILTIN_VALUE_MEMBERS.has(parent.name.text);
+ if (ts.isElementAccessExpression(parent) && parent.expression === node) return true;
+ return (
+ ts.isBinaryExpression(parent) &&
+ parent.operatorToken.kind === ts.SyntaxKind.InKeyword &&
+ parent.right === node
+ );
+}
+
+/** Reports SC2020 when `access` reads a stdlib static member or the
+ * prototype of a builtin constructor with an interned function value through
+ * a receiver the checker types as the constructor's static side
+ * (`MapConstructor`); null otherwise. Calls that lower statically never
+ * reach the member read. */
+function fenceBuiltinConstructorStaticRead(
+ lowerer: Lowerer,
+ access: ts.PropertyAccessExpression,
+): null {
+ if (BUILTIN_VALUE_MEMBERS.has(access.name.text)) return null;
+ const symbol = lowerer.typeOf(access.expression).getSymbol();
+ if (
+ symbol !== undefined &&
+ symbol.name.endsWith("Constructor") &&
+ GLOBAL_CONSTRUCTOR_VALUES.has(symbol.name.slice(0, -"Constructor".length)) &&
+ lowerer.isStdlibSymbol(symbol)
+ )
+ lowerer.stdlibMemberFence(access);
+ return null;
+}
+
+/** Object.prototype members a plain record inherits, read as values. */
+const OBJECT_PROTOTYPE_VALUE_READS: ReadonlySet = new Set([
+ "constructor",
+ "valueOf",
+ "toString",
+ "hasOwnProperty",
+ "propertyIsEnumerable",
+]);
+
+/** Whether `expr` is the callee of a call (`o.toString()`), which keeps its
+ * call lowering rather than reading the member as a value. */
+function isCalleeOf(expr: ts.PropertyAccessExpression): boolean {
+ let node: ts.Node = expr;
+ while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
+ const parent = node.parent;
+ return (
+ parent !== undefined &&
+ (ts.isCallExpression(parent) || ts.isNewExpression(parent)) &&
+ parent.expression === node
+ );
+}
+
+/** The constructor a primitive dynamic value answers for `.constructor`:
+ * the same interned closure the `Number`/`String`/`Boolean`/`BigInt`
+ * identifiers read (dequal's `(ctor = foo.constructor) === bar.constructor`
+ * over numbers), or `otherwise` for non-primitives. `value` must be safe to
+ * read repeatedly. */
+export function primitiveConstructorOr(
+ lowerer: Lowerer,
+ value: IrExpr,
+ otherwise: IrExpr,
+ loc: SrcLoc,
+): IrExpr {
+ let result = otherwise;
+ for (const [test, name] of [
+ ["bigint", "BigInt"],
+ ["boolean", "Boolean"],
+ ["string", "String"],
+ ["number", "Number"],
+ ] as const) {
+ result = {
+ kind: "ternary",
+ cond: { kind: "dynTest", test, value, type: BOOL, loc },
+ then: {
+ kind: "dynFrom",
+ value: primitiveCtorClosure(lowerer, name, loc, true),
+ type: DYN,
+ loc,
+ },
+ else_: result,
+ type: DYN,
+ loc,
+ };
+ }
+ return result;
+}
+
/** True when `access` reads a member that JavaScript sources attach to a
* function by assignment (`fn.member = value`): the receiver's type is
* callable and every declaration of the member is such an assignment in a
@@ -3581,3 +4019,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..91d1468c2 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.
@@ -471,6 +474,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
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(expr.getSourceFile()) &&
+ 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",
@@ -1144,6 +1171,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__",
@@ -1360,9 +1405,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 +1430,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..41a05ac2b 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);
@@ -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,6 +193,11 @@ export function lowerTypeofExpr(lowerer: Lowerer, expr: ts.TypeOfExpression, loc
if (!sym) {
return { kind: "strLit", value: "undefined", type: STRING, loc };
}
+ // Node 24 defines no temporal global: typeof answers "undefined"
+ // without reading it, like any undeclared name.
+ if (isTemporalGlobalRoot(lowerer, tested, sym)) {
+ return { kind: "strLit", value: "undefined", type: STRING, loc };
+ }
// `const { inspect } = require("util")` / `import { inspect } from
// "node:util"`: a binding of a builtin module's exported function,
// class or object. JavaScript sources represent such a value without
diff --git a/packages/compiler/src/frontend/lowering/expressions/updates.ts b/packages/compiler/src/frontend/lowering/expressions/updates.ts
index 2270f6043..41d8f6332 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. */
@@ -337,10 +338,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,
@@ -617,6 +619,7 @@ export function fieldSetStmt(
};
}
}
+ if (target.container === "recordKeyed") return keyedFieldWrite(lowerer, target, value, loc);
if (target.container === "dynamic")
return {
kind: "exprStmt",
@@ -872,12 +875,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);
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/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..b586884c5 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
@@ -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(info.decl.getSourceFile())
+ ? 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
@@ -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,11 @@ 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(info.decl.getSourceFile())
+ ? 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.
@@ -824,6 +854,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 +905,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 +1018,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;
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 dbb7fd0a9..2a35d2a81 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
@@ -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) ??
@@ -1893,6 +1916,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;
@@ -3362,7 +3398,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 (
@@ -3687,6 +3729,8 @@ 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",
@@ -4394,6 +4438,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",
@@ -6937,6 +6989,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 };
@@ -6951,12 +7004,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);
@@ -7504,6 +7558,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
@@ -7596,7 +7652,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);
@@ -7614,6 +7675,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
@@ -8844,6 +8936,9 @@ export function lowerRecordFieldCall(
};
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
@@ -9342,6 +9437,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
@@ -9690,6 +9786,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(declarerDecl.getSourceFile())) &&
+ !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();
@@ -10300,3 +10420,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(call.getSourceFile()) ||
+ 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..182a73f14 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -16,6 +16,8 @@ import { InternalCompilerError } from "../../errors.js";
* accessor access, upcasts, `new` expressions, and the builtin Error
* hierarchy registration. */
import { fsStreamClassOf, lowerFsStreamNew } from "./builtins/fs-streams.js";
+import { codeGenerationCalleeOf, lowerCodeGenerationCall } from "./code-generation.js";
+import { TEMPORAL_GLOBAL } from "../builtin-modules.js";
import * as ts from "../ts7/adapter.js";
import { lowerZlibStreamNew, zlibClassName, zlibStreamModeOf } from "./builtins/compression.js";
import { everyStmtList } from "../../ir/traverse.js";
@@ -28,6 +30,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 +73,7 @@ import {
} from "./instantiation-path.js";
import { appendImplicitUndefinedReturn, hasExplicitJsDocReturn } from "./function-returns.js";
import { bodyReadsArguments } from "../arguments-usage.js";
+import { errorNameAfterField, errorViewFields } from "./error-name-order.js";
import {
checkedJsDocumentedParameter,
documentedParameter,
@@ -144,6 +148,7 @@ import {
ambientUndefReadType,
ambientUndefVarRootOf,
ambientUndefinedFnSymbolOf,
+ contextualUndefReadType,
fenceEarlyAliasUse,
fenceEarlyNsMemberRef,
nsMemberIdentOf,
@@ -495,6 +500,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
@@ -2213,11 +2224,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 +2256,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();
@@ -3434,7 +3442,67 @@ export function collectClassShapeInner(
overridden.declarer.decl !== null &&
isJsSourceFile(overridden.declarer.decl.getSourceFile()) &&
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(member.getSourceFile()) &&
+ 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,7 +3557,34 @@ 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(member.getSourceFile()) &&
+ !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()))
methodEntryFences.set(mName, unsupportedDiag("SC1090", locOf(member.name), message));
@@ -3520,7 +3615,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 },
);
@@ -4238,6 +4333,7 @@ export function collectClassShapeInner(
fieldOrder,
methods,
...(methodEntryFences.size > 0 ? { methodEntryFences } : {}),
+ ...(returnAdapters.size > 0 ? { returnAdapters } : {}),
decl,
...(emitOverride !== undefined ? { emitOverride } : {}),
ctor,
@@ -4260,6 +4356,12 @@ export function collectClassShapeInner(
...(deferredInitFields.size > 0 ? { deferredInitFields } : {}),
...(escape.escapes ? { constructionEscapes: true as const } : {}),
};
+ let root: ClassInfo | null = info;
+ while (root?.base) root = root.base;
+ if (root !== info && root?.def.name === "%Error") {
+ const after = errorNameAfterField(info, errorViewFields(info.def.fields));
+ if (after !== undefined) info.def.errorNameAfter = after;
+ }
// GENERIC members get their declaring-class backlink now that the
// info exists (instance lowering reads it for `this` typing and the
// generic-class binding merge).
@@ -5183,16 +5285,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,
@@ -6615,6 +6712,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,
@@ -7837,6 +7956,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 +8073,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,
@@ -8737,6 +8932,60 @@ function inheritsBuiltinStreamCtor(lowerer: Lowerer, info: ClassInfo): boolean {
return false;
}
+/** The run-time form of refuseInheritedBuiltinCtorArgs, for construction
+ * from checked-dynamic code (`new F(options)` with F an untyped value
+ * holding C), whose argument count is known only to the caller: a
+ * runtimeFence located at C, or null when C's constructor takes its
+ * arguments. */
+export function inheritedBuiltinCtorFence(
+ lowerer: Lowerer,
+ info: ClassInfo,
+ loc: SrcLoc,
+): IrStmt | null {
+ let feature: string | null = null;
+ if (inheritsBuiltinStreamCtor(lowerer, info))
+ feature = `new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor (declare a constructor that passes an inline options object to super(...))`;
+ else if (lowerer.inheritsBuiltinEmitterCtor(info))
+ feature = "EventEmitter constructor options ('captureRejections')";
+ if (feature === null) return null;
+ // Located at the class, whose constructor takes no arguments.
+ const at = info.decl !== null ? locOf(info.decl) : loc;
+ const diag = unsupportedDiag("SC1090", at, feature);
+ lowerer.runtimeFences.push(diag);
+ const line =
+ info.decl !== null
+ ? ts.getLineAndCharacterOfPosition(info.decl.getSourceFile(), at.start).line + 1
+ : null;
+ const where = line !== null ? ` [${diag.code} at ${at.file}:${line}]` : "";
+ return { kind: "runtimeFence", code: diag.code, message: `${diag.message}${where}`, loc };
+}
+
+/** `new C(args)` where C's constructor is the inherited runtime stream or
+ * EventEmitter constructor (a ctor-less chain into the base): that
+ * constructor takes zero arguments here (the options bag fences, like the
+ * super() form), so arguments report at the first one. Applies to direct
+ * construction and to construction through a class value of C. Stream
+ * subclasses come first: their chain roots at the emitter too, but the
+ * message should name the stream story. */
+function refuseInheritedBuiltinCtorArgs(
+ lowerer: Lowerer,
+ info: ClassInfo,
+ expr: ts.NewExpression,
+): void {
+ const first = expr.arguments?.nodes[0];
+ if (first === undefined) return;
+ if (inheritsBuiltinStreamCtor(lowerer, info)) {
+ lowerer.noLowering(
+ `new ${info.def.name.replace(/^%/, "")} with arguments through an inherited stream constructor`,
+ first,
+ "declare a constructor that passes an inline options object to super(...)",
+ );
+ }
+ if (lowerer.inheritsBuiltinEmitterCtor(info)) {
+ lowerer.unsupported("SC1090", first, "EventEmitter constructor options ('captureRejections')");
+ }
+}
+
/** Preserve argument evaluation order and option presence through builtin
* and inherited Error constructors. Checked-dynamic conversion retains
* Error identity and distinguishes a missing cause from undefined. */
@@ -8861,24 +9110,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 +9157,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) {
@@ -9469,18 +9715,57 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
// fenced, so copying the scalar cannot create an observable lie.
if (symbol && symbol.name === "Date" && lowerer.isStdlibSymbol(symbol)) {
const args = expr.arguments?.nodes ?? [];
- if (args.some(ts.isSpreadElement) || args.length > 1) {
+ if (args.some(ts.isSpreadElement)) {
lowerer.noLowering(
- `new Date with ${args.length} arguments`,
+ `new Date with spread arguments`,
expr,
- "new Date(), new Date(milliseconds), and new Date(dateString) are supported; the local-time year/month field constructor has no lowering",
+ "new Date(), new Date(milliseconds), new Date(dateString), and new Date(year, monthIndex, ...) with listed arguments are supported",
symbol,
);
}
+ // new Date(year, monthIndex[, date[, hours[, minutes[, seconds[, ms]]]]]):
+ // local-time fields, converted to UTC in the host zone at run time.
+ // JavaScript callers pass their arguments to the runtime's ToNumber
+ // (in order, exactly the constructor's coercions); typed callers
+ // pass numbers, completed with the spec's defaults (date 1, time
+ // parts 0) for the seven-f64 ABI.
+ if (args.length > 1) {
+ if (isJsSourceFile(expr.getSourceFile())) {
+ return {
+ kind: "libCall",
+ fn: "date.nativeNewFields",
+ args: [
+ {
+ kind: "dynArrLit",
+ elems: args.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
+ type: DYN,
+ loc,
+ },
+ ],
+ type: DYN,
+ loc,
+ };
+ }
+ if (args.length > 7) {
+ lowerer.noLowering(`new Date with ${args.length} arguments`, expr, undefined, symbol);
+ }
+ const defaults = [0, 0, 1, 0, 0, 0, 0];
+ const fields: IrExpr[] = [];
+ for (let i = 0; i < 7; i++) {
+ const arg = args[i];
+ fields.push(
+ arg !== undefined
+ ? lowerer.lowerExprExpecting(arg, F64)
+ : { kind: "numLit", value: defaults[i]!, type: F64, loc },
+ );
+ }
+ return { kind: "libCall", fn: "date.newLocal", args: fields, type: DATE_T, loc };
+ }
if (isJsSourceFile(expr.getSourceFile())) {
return {
kind: "libCall",
- fn: "date.nativeNew",
+ // One argument: a string parses with the full date parser.
+ fn: args.length === 1 ? "date.nativeNewValue" : "date.nativeNew",
args: [
{
kind: "dynArrLit",
@@ -10126,6 +10411,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 ?? [],
diff --git a/packages/compiler/src/frontend/lowering/lower-comptime.ts b/packages/compiler/src/frontend/lowering/lower-comptime.ts
index 41c6704c5..f239eea82 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.
@@ -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;
}
@@ -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-dgram.ts b/packages/compiler/src/frontend/lowering/lower-dgram.ts
index 54f9d158b..17522971d 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") {
@@ -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 => {
diff --git a/packages/compiler/src/frontend/lowering/lower-enums.ts b/packages/compiler/src/frontend/lowering/lower-enums.ts
index 159d72f32..cc2797fb9 100644
--- a/packages/compiler/src/frontend/lowering/lower-enums.ts
+++ b/packages/compiler/src/frontend/lowering/lower-enums.ts
@@ -29,7 +29,8 @@
import * as ts from "../ts7/adapter.js";
import type { Lowerer } from "./lowerer.js";
import { locOf } from "../program.js";
-import { F64, type IrExpr, STRING } from "../../ir/ir.js";
+import { BOOL, F64, type IrExpr, STRING, UNDEFINED_T } from "../../ir/ir.js";
+import { nodeThrowExpr } from "../../ir/build.js";
/** The member's source-name text (identifiers and string-literal names —
* the only name forms tsc allows on enum members). */
@@ -182,6 +183,15 @@ function lowerEnumReverseRead(
// existence tracking; nothing needs it — fence through the generic path.
if (decls.some((d) => isAmbientEnumDecl(d))) return null;
const idx = foldConstIndex(lowerer, key);
+ const keyType = lowerer.typeOf(key);
+ if (
+ idx === null &&
+ ((keyType.flags & ts.TypeFlags.StringLike) !== 0 ||
+ (keyType.isUnionType() &&
+ ts.constituentTypes(keyType).every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0)))
+ ) {
+ return lowerEnumKeyRead(lowerer, expr, decls, key);
+ }
if (idx === null) {
lowerer.unsupported(
"SC1090",
@@ -206,6 +216,109 @@ function lowerEnumReverseRead(
return { kind: "strLit", value: name, type: STRING, loc };
}
+/** Object.prototype members every enum object inherits: a runtime key
+ * naming one reads a function, which the member value types cannot hold. */
+const OBJECT_PROTOTYPE_KEYS = new Set([
+ "__defineGetter__",
+ "__defineSetter__",
+ "__lookupGetter__",
+ "__lookupSetter__",
+ "__proto__",
+ "constructor",
+ "hasOwnProperty",
+ "isPrototypeOf",
+ "propertyIsEnumerable",
+ "toLocaleString",
+ "toString",
+ "valueOf",
+]);
+
+/** `E[key]` with a RUNTIME string key (`Color[name as keyof typeof
+ * Color]` — the name-to-value lookup): the key evaluates once and is
+ * compared against every member NAME, each hit answering that member's
+ * constant, exactly the forward entries tsc's emit stores. A key naming no
+ * member is Node's undefined, so the read carries an undefined arm. Keys
+ * Node would answer from elsewhere on the object throw a named runtime
+ * refusal instead of a wrong value: a numeric member's reverse entry (its
+ * value spelled as a string — Node answers the member NAME) and the
+ * inherited Object.prototype members. */
+function lowerEnumKeyRead(
+ lowerer: Lowerer,
+ expr: ts.ElementAccessExpression,
+ decls: readonly ts.EnumDeclaration[],
+ key: ts.Expression,
+): IrExpr {
+ const loc = locOf(expr);
+ const enumName = decls[0]!.name.text;
+ const valueType = lowerer.mapTypeOf(lowerer.typeOf(expr));
+ if (valueType === null) lowerer.badType(expr, lowerer.typeOf(expr));
+ const resultType = lowerer.withUndefinedArm(valueType);
+ const keyValue = lowerer.coerceToExpected(lowerer.lowerExpr(key), STRING);
+ const keyLocal = lowerer.declareHiddenLocal("%enumKey", STRING);
+ const keyRef: IrExpr = { kind: "varRef", localId: keyLocal.id, type: STRING, loc };
+ const matches = (text: string): IrExpr => ({
+ kind: "strEq",
+ negated: false,
+ left: keyRef,
+ right: { kind: "strLit", value: text, type: STRING, loc },
+ type: BOOL,
+ loc,
+ });
+ const refusal = (what: string): IrExpr =>
+ nodeThrowExpr(
+ 0,
+ "SC1090",
+ `${what} through a runtime key ('${enumName}[key]') is not supported yet`,
+ resultType,
+ loc,
+ );
+ const arms: { cond: IrExpr; value: IrExpr }[] = [];
+ const names = new Set();
+ 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..4b8b9ea14 100644
--- a/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
+++ b/packages/compiler/src/frontend/lowering/lower-event-emitter.ts
@@ -1245,7 +1245,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]!,
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index 6cbe5d7a4..e770b8d58 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
@@ -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)) {
@@ -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`
@@ -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",
@@ -2057,6 +2094,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(getter.getSourceFile()).includes(getter)) return null;
+ const call = cjsAccessorCall(lowerer, getter, locOf(getter));
+ if (call === null) return null;
+ const snapshot = { id: `%g.cjsSnapshot.${lowerer.cjsAccessorSnapshots.size}`, type: call.type };
+ lowerer.globalsList.push({
+ id: snapshot.id,
+ name: "%cjsSnapshot",
+ type: call.type,
+ mutable: true,
+ });
+ lowerer.cjsAccessorSnapshots.set(getter, snapshot);
+ return snapshot;
+}
+
+/** Node's ES-module loader reads every lexer-visible export of a CommonJS
+ * module once, when an importer first links it, and the import bindings
+ * keep those values. For the accessors a bundler rewrite introduced, the
+ * importer's link point runs each getter once, in the lexer's order, and
+ * stores the values its bindings read. Empty when the module has none. */
+export function cjsAccessorSnapshotStores(
+ lowerer: Lowerer,
+ dep: ts.SourceFile,
+ loc: SrcLoc,
+): IrStmt[] {
+ const getters = lexerVisibleAccessors(dep);
+ if (getters.length === 0) return [];
+ let readyId = lowerer.cjsSnapshotReady.get(dep);
+ if (readyId === undefined) {
+ readyId = `%g.cjsSnapshotReady.${lowerer.cjsSnapshotReady.size}`;
+ lowerer.globalsList.push({ id: readyId, name: "%cjsSnapshotReady", type: BOOL, mutable: true });
+ lowerer.cjsSnapshotReady.set(dep, readyId);
+ }
+ const stores: IrStmt[] = [
+ {
+ kind: "assign",
+ localId: readyId,
+ value: { kind: "boolLit", value: true, type: BOOL, loc },
+ loc,
+ },
+ ];
+ for (const getter of getters) {
+ const snapshot = cjsAccessorSnapshotOf(lowerer, getter);
+ const call = cjsAccessorCall(lowerer, getter, loc);
+ if (snapshot === null || call === null) return [];
+ stores.push({ kind: "assign", localId: snapshot.id, value: call, loc });
+ }
+ return [
+ {
+ kind: "if",
+ cond: {
+ kind: "unary",
+ op: "!",
+ operand: { kind: "varRef", localId: readyId, type: BOOL, loc },
+ type: BOOL,
+ loc,
+ },
+ then: stores,
+ else_: null,
+ loc,
+ },
+ ];
+}
+
/** The interned lifted-getter call both accessor-read paths share. */
export function cjsAccessorCall(
lowerer: Lowerer,
@@ -2133,7 +2266,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);
@@ -2588,6 +2739,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 +2788,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..f0f2c45ea 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";
@@ -309,6 +310,8 @@ export function lowerLambda(
: 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,
diff --git a/packages/compiler/src/frontend/lowering/lower-global-value.ts b/packages/compiler/src/frontend/lowering/lower-global-value.ts
index a5f516e86..d0b251e84 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) => property.name)
+ .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 4eda3207b..2ac6e779c 100644
--- a/packages/compiler/src/frontend/lowering/lower-inspect.ts
+++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts
@@ -7,6 +7,7 @@ import {
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";
@@ -134,6 +135,31 @@ function errorRenderable(info: ClassInfo): boolean {
return info.subclasses.every((s) => errorRenderable(s));
}
+/** The accessors that change what Node reads for an error's header. The
+ * runtime renders the instance's own name and message slots. */
+const ERROR_HEADER_ACCESSORS = [
+ "get:name",
+ "set:name",
+ "get:message",
+ "set:message",
+ "get:stack",
+ "set:stack",
+];
+
+/** Whether Node's header for an instance of `info`'s hierarchy can differ
+ * from the runtime's slots: a program class among its ancestors or
+ * descendants declares a `name`, `message` or `stack` accessor, or the
+ * program writes a prototype's `name` or `message`. */
+function errorHeaderOverridden(lowerer: Lowerer, info: ClassInfo): boolean {
+ if (lowerer.prototypeMethodAccesses.has("name") || lowerer.prototypeMethodAccesses.has("message"))
+ return true;
+ const declares = (c: ClassInfo): boolean =>
+ !c.builtinError && !c.def.runtime && ERROR_HEADER_ACCESSORS.some((m) => c.methods.has(m));
+ for (let c = info.base; c; c = c.base) if (declares(c)) return true;
+ const below = (c: ClassInfo): boolean => declares(c) || c.subclasses.some(below);
+ return below(info);
+}
+
/** Error keys Node's error inspector folds into the base or hides. */
const ERROR_SPECIAL_KEYS = new Set(["name", "message", "stack", "cause", "errors"]);
@@ -180,6 +206,16 @@ function fieldedErrorReason(
}
visiting.add(key);
try {
+ for (let c: ClassInfo | null = info; c && !c.builtinError && !c.def.runtime; c = c.base) {
+ // Node prints the own properties in creation order, which puts a
+ // declared `cause` among the fields; the cause renders after them.
+ if (c.fieldOrder.some((f) => f.errorCause))
+ return "its field 'cause' is one Node's error inspector treats specially";
+ // A JavaScript class's fields exist once its constructor creates them,
+ // which can be conditional; the layout reads every field.
+ if (c.decl && isJsSourceFile(c.decl.getSourceFile()) && ownErrorFields(c).length > 0)
+ return "its fields are JavaScript properties, which its constructor may not create";
+ }
for (const f of ownErrorFields(info)) {
if (ERROR_SPECIAL_KEYS.has(f.name))
return `its field '${f.name}' is one Node's error inspector treats specially`;
@@ -213,21 +249,23 @@ function errorInstanceReachesItself(lowerer: Lowerer, c: ClassInfo): boolean {
return walk({ kind: "object", className: c.def.name });
}
-/** An error value whose runtime class may declare fields
- * (`class Bailable extends Error { bail = false }`): Node prints the error
- * base (`[Bailable [Error]: m]`, the stackless form compiled errors use)
- * followed by the own properties, `[Bailable [Error]: m] { bail: true }`,
- * with the usual single-line/multi-line choice counting the base. Each
- * fielded class (most-derived first) is an instanceof arm; other values
- * keep the runtime's error rendering. Beyond the depth budget a fielded
- * instance prints `[Name]`, as Node does for any object with keys. */
-function fieldedErrorInspectHelper(
+/** An error value whose runtime class may be a program-declared subclass,
+ * which may declare fields (`class Bailable extends Error { bail = false }`)
+ * or carry a `cause`: Node prints the error base (`[Bailable [Error]: m]`,
+ * the stackless form compiled errors use) followed by the own properties,
+ * `[Bailable [Error]: m] { bail: true }`, with the usual single-line/
+ * multi-line choice counting the base. Each program class (most-derived
+ * first) is an instanceof arm; other values keep the runtime's error
+ * rendering. Beyond the depth budget an instance with own properties
+ * prints `[Name]`, as Node does for any object with keys; one without
+ * prints its header. */
+function errorInspectHelper(
lowerer: Lowerer,
t: IrType & { kind: "object" },
info: ClassInfo,
loc: SrcLoc,
): string {
- const key = `insp.fieldedError:${typeKey(t)}`;
+ const key = `insp.userError:${typeKey(t)}`;
const existing = lowerer.inspectHelpers.get(key);
if (existing) return existing;
const name = `%util.insp.${lowerer.inspectHelpers.size}`;
@@ -250,55 +288,34 @@ function fieldedErrorInspectHelper(
type: STRING,
loc,
});
- const body: IrStmt[] = [];
- for (const c of userErrorClasses(info)) {
+ const error = (): IrExpr => lowerer.upcastTo(v(), "%Error");
+ const hasCause = (): IrExpr => ({
+ kind: "libCall",
+ fn: "insp.errorHasCause",
+ args: [error()],
+ type: BOOL,
+ loc,
+ });
+ const entry = (text: IrExpr): IrStmt => ({
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "insp.entry",
+ args: [text, boolLit(false, loc)],
+ type: VOID,
+ loc,
+ },
+ loc,
+ });
+ /** The own properties of an instance of `c`: its fields, then its
+ * `cause` (an assignment in the constructor or later, or the
+ * constructor's options, which Node lists last as `[cause]`). */
+ const propertiesArm = (
+ c: ClassInfo,
+ display: string,
+ fields: { name: string; type: IrType }[],
+ ): IrStmt[] => {
const cls: IrType & { kind: "object" } = { kind: "object", className: c.def.name };
- const display = c.def.jsName ?? c.decl?.name?.text ?? c.def.name.replace(/^%/, "");
- if (ownErrorFields(c).length === 0) {
- // A field-less program class: the header rule alone.
- const header: IrStmt = {
- kind: "return",
- value: improvedErrorBase(lowerer, v(), base(), display, loc),
- loc,
- };
- if (c.def.name === t.className) {
- body.push(header);
- break;
- }
- body.push({
- kind: "if",
- cond: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc },
- then: [header],
- else_: null,
- loc,
- });
- continue;
- }
- const why = fieldedErrorReason(lowerer, c);
- if (why !== null) {
- // An instance of a subclass whose own properties have no rendering
- // throws a named refusal instead of printing a partial object.
- body.push({
- kind: "if",
- cond: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc },
- then: [
- {
- kind: "return",
- value: nodeThrowExpr(
- 0,
- "SC2020",
- `inspecting a '${display}' error (${why}) is not supported yet`,
- STRING,
- loc,
- ),
- loc,
- },
- ],
- else_: null,
- loc,
- });
- continue;
- }
const instance: IrExpr =
c.def.name === t.className ? v() : { kind: "downcast", value: v(), type: cls, loc };
// A class whose fields can hold the instance itself (`next: Chain`)
@@ -373,7 +390,7 @@ function fieldedErrorInspectHelper(
] satisfies IrStmt[])
: []),
];
- for (const f of ownErrorFields(c)) {
+ for (const f of fields) {
const read: IrExpr = {
kind: "fieldGet",
obj: instance,
@@ -382,27 +399,33 @@ function fieldedErrorInspectHelper(
type: f.type,
loc,
};
- arm.push({
- kind: "exprStmt",
- expr: {
- kind: "libCall",
- fn: "insp.entry",
- args: [
- concatAll(
- [
- strLit(`${inspectKey(f.name)}: `, loc),
- inspectExpr(lowerer, f.type, read, rPlus1(), d(), loc),
- ],
- loc,
- ),
- boolLit(false, loc),
- ],
- type: VOID,
- loc,
- },
- loc,
- });
+ arm.push(
+ entry(
+ concatAll(
+ [
+ strLit(`${inspectKey(f.name)}: `, loc),
+ inspectExpr(lowerer, f.type, read, rPlus1(), d(), loc),
+ ],
+ loc,
+ ),
+ ),
+ );
}
+ arm.push({
+ kind: "if",
+ cond: hasCause(),
+ then: [
+ entry({
+ kind: "libCall",
+ fn: "insp.errorCause",
+ args: [error(), rPlus1(), d()],
+ type: STRING,
+ loc,
+ }),
+ ],
+ else_: null,
+ loc,
+ });
const ended: IrExpr = {
kind: "libCall",
fn: "insp.end",
@@ -424,22 +447,62 @@ function fieldedErrorInspectHelper(
: ended,
loc,
});
- body.push(
- c.def.name === t.className
- ? { kind: "block", body: arm, loc }
- : {
- kind: "if",
- cond: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc },
- then: arm,
- else_: null,
+ return arm;
+ };
+ const body: IrStmt[] = [];
+ for (const c of userErrorClasses(info)) {
+ const display = c.def.jsName ?? c.decl?.name?.text ?? c.def.name.replace(/^%/, "");
+ const fields = ownErrorFields(c);
+ const why = fields.length > 0 ? fieldedErrorReason(lowerer, c) : null;
+ let arm: IrStmt[];
+ if (why !== null) {
+ // An instance of a subclass whose own properties have no rendering
+ // throws a named refusal instead of printing a partial object.
+ arm = [
+ {
+ kind: "return",
+ value: nodeThrowExpr(
+ 0,
+ "SC2020",
+ `inspecting a '${display}' error (${why}) is not supported yet`,
+ STRING,
loc,
- },
- );
- if (c.def.name === t.className) break;
+ ),
+ loc,
+ },
+ ];
+ } else if (fields.length === 0) {
+ // A field-less program class: without a `cause` it has no own
+ // properties, and Node prints the header alone at any depth.
+ arm = [
+ {
+ kind: "if",
+ cond: { kind: "unary", op: "!", operand: hasCause(), type: BOOL, loc },
+ then: [
+ { kind: "return", value: improvedErrorBase(lowerer, v(), base(), display, loc), loc },
+ ],
+ else_: null,
+ loc,
+ },
+ ...propertiesArm(c, display, fields),
+ ];
+ } else {
+ arm = propertiesArm(c, display, fields);
+ }
+ if (c.def.name === t.className) {
+ body.push({ kind: "block", body: arm, loc });
+ break;
+ }
+ body.push({
+ kind: "if",
+ cond: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc },
+ then: arm,
+ else_: null,
+ loc,
+ });
}
- if (body.at(-1)?.kind !== "return" && body.at(-1)?.kind !== "block")
- body.push({ kind: "return", value: base(), loc });
- lowerer.liftedFns.push({
+ if (body.at(-1)?.kind !== "block") body.push({ kind: "return", value: base(), loc });
+ const fn: IrFunction = {
name,
params: [
{ localId: "v.0", name: "v", type: t },
@@ -458,7 +521,74 @@ function fieldedErrorInspectHelper(
],
body,
loc,
+ };
+ // Dynamic stores (a JavaScript class's `this.x = ...` outside its
+ // declared fields, or a write through an untyped reference) add a
+ // property bag to a class after this helper was built. The rendering
+ // reads only a `cause` from it, so an instance whose bag holds another
+ // property throws a named refusal instead of printing a partial object.
+ lowerer.shapeOrderHelperFinalizers.push(() => {
+ const bagged = [info, ...userErrorClasses(info)].filter(
+ (c) =>
+ !c.builtinError &&
+ !c.def.runtime &&
+ c.def.fields.some((f) => f.name === DYN_CLASS_PROPERTIES),
+ );
+ if (bagged.length === 0) return;
+ fn.body = [
+ ...bagged.map((c): IrStmt => ({
+ kind: "if",
+ cond: {
+ kind: "logical",
+ op: "&&",
+ left: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc },
+ right: {
+ kind: "libCall",
+ fn: "insp.errorBagHasProperties",
+ args: [
+ {
+ kind: "fieldGet",
+ obj:
+ c.def.name === t.className
+ ? v()
+ : {
+ kind: "downcast",
+ value: v(),
+ type: { kind: "object", className: c.def.name },
+ loc,
+ },
+ className: c.def.name,
+ field: DYN_CLASS_PROPERTIES,
+ type: DYN,
+ loc,
+ },
+ ],
+ type: BOOL,
+ loc,
+ },
+ type: BOOL,
+ loc,
+ },
+ then: [
+ {
+ kind: "return",
+ value: nodeThrowExpr(
+ 0,
+ "SC2020",
+ `inspecting a '${c.def.jsName ?? c.def.name.replace(/^%/, "")}' error with properties created at run time is not supported yet`,
+ STRING,
+ loc,
+ ),
+ loc,
+ },
+ ],
+ else_: null,
+ loc,
+ })),
+ ...body,
+ ];
});
+ lowerer.liftedFns.push(fn);
return name;
}
@@ -475,73 +605,6 @@ function userErrorClasses(info: ClassInfo): ClassInfo[] {
return out;
}
-/** An error value whose runtime class may be a program-declared subclass:
- * the runtime renders the stackless base from the instance's `name`
- * (`[Error: m]`), and Node's improveStack then names the constructor too
- * (`[Plain [Error]: m]`). Each program class is an instanceof arm (the
- * static class itself unconditionally) applying that header rule; other
- * values keep the runtime rendering. */
-function userErrorInspectHelper(
- lowerer: Lowerer,
- t: IrType & { kind: "object" },
- info: ClassInfo,
- loc: SrcLoc,
-): string {
- const key = `insp.userError:${typeKey(t)}`;
- const existing = lowerer.inspectHelpers.get(key);
- if (existing) return existing;
- const name = `%util.insp.${lowerer.inspectHelpers.size}`;
- lowerer.inspectHelpers.set(key, name);
- const v = (): IrExpr => varRef("v.0", t, loc);
- const base = (): IrExpr => ({
- kind: "libCall",
- fn: "insp.error",
- args: [v(), varRef("r.0", F64, loc), varRef("d.0", F64, loc)],
- type: STRING,
- loc,
- });
- const body: IrStmt[] = [];
- for (const c of userErrorClasses(info)) {
- const display = c.def.jsName ?? c.decl?.name?.text ?? c.def.name.replace(/^%/, "");
- const ret: IrStmt = {
- kind: "return",
- value: improvedErrorBase(lowerer, v(), base(), display, loc),
- loc,
- };
- if (c === info) {
- body.push(ret);
- break;
- }
- body.push({
- kind: "if",
- cond: { kind: "instanceOf", value: v(), className: c.def.name, type: BOOL, loc },
- then: [ret],
- else_: null,
- loc,
- });
- }
- if (body.at(-1)?.kind !== "return") body.push({ kind: "return", value: base(), loc });
- lowerer.liftedFns.push({
- name,
- params: [
- { localId: "v.0", name: "v", type: t },
- { localId: "r.0", name: "r", type: F64 },
- { localId: "d.0", name: "d", type: F64 },
- ],
- returnType: STRING,
- locals: [
- { id: "v.0", name: "v", type: t, mutable: false },
- { id: "r.0", name: "r", type: F64, mutable: false },
- { id: "d.0", name: "d", type: F64, mutable: false },
- { id: "name.0", name: "name", type: STRING, mutable: true },
- { id: "raw.0", name: "raw", type: STRING, mutable: true },
- ],
- body,
- loc,
- });
- return name;
-}
-
/** Node's improveStack over the stackless base (`[Error: m]`): when the
* instance's constructor name differs from its `name` and the name ends in
* "Error", the header names both — `[Bailable [Error]: m]`, or
@@ -744,11 +807,13 @@ function inspectSupport(
// for those classes (the runtime's capsule view).
if (eventTargetRootOf(info) !== null) return null;
if (isErrorClass(lowerer, t.className)) {
+ if (userErrorClasses(info).length > 0 && errorHeaderOverridden(lowerer, info))
+ return "error subclasses whose name, message or stack is an accessor or a prototype property have no inspect lowering yet";
if (errorRenderable(info)) return null;
// Subclasses with declared fields render the error base followed
// by their own properties. The static class's own fields must
// render; a subclass whose fields cannot is a runtime refusal on
- // its own instances only (see fieldedErrorInspectHelper).
+ // its own instances only (see errorInspectHelper).
const own = fieldedErrorClasses(info).find((c) => c === info);
const ownWhy = own ? fieldedErrorReason(lowerer, own, visiting, out) : null;
if (ownWhy !== null)
@@ -903,19 +968,10 @@ export function inspectExpr(
}
if (isErrorClass(lowerer, t.className)) {
const info = lowerer.classes.get(t.className);
- if (info && !errorRenderable(info)) {
- return {
- kind: "call",
- callee: fieldedErrorInspectHelper(lowerer, t, info, loc),
- args: [value, recurse, depth],
- type: STRING,
- loc,
- };
- }
if (info && userErrorClasses(info).length > 0) {
return {
kind: "call",
- callee: userErrorInspectHelper(lowerer, t, info, loc),
+ callee: errorInspectHelper(lowerer, t, info, loc),
args: [value, recurse, depth],
type: STRING,
loc,
@@ -1023,8 +1079,17 @@ function typeConstituents(lowerer: Lowerer, u: IrType): IrType[] {
return [u.key, u.value];
case "union":
return lowerer.unions.get(u.unionId)?.arms.slice() ?? [];
- case "object":
- return lowerer.classes.get(u.className)?.def.fields.map((f) => f.type) ?? [];
+ case "object": {
+ const info = lowerer.classes.get(u.className);
+ if (!info) return [];
+ const types = info.def.fields.map((f) => f.type);
+ // An error value can be an instance of a program-declared subclass,
+ // whose own fields its inspection renders (errorInspectHelper).
+ if (isErrorClass(lowerer, u.className))
+ for (const c of userErrorClasses(info))
+ for (const f of ownErrorFields(c)) types.push(f.type);
+ return types;
+ }
default:
return [];
}
@@ -3158,8 +3223,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`,
@@ -3178,9 +3250,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));
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index fa4c98372..11db0ec8a 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. */
@@ -872,6 +872,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.
@@ -2851,6 +2859,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));
}
}
}
diff --git a/packages/compiler/src/frontend/lowering/lower-namespaces.ts b/packages/compiler/src/frontend/lowering/lower-namespaces.ts
index 23b7d0f57..79ac7c278 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
@@ -425,7 +426,7 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id
const sym = lowerer.resolveValueSymbol(root);
if (!sym) return null;
if (lowerer.trapBindings.has(sym)) return root;
- if (lowerer.isStdlibSymbol(sym)) return null;
+ if (lowerer.isStdlibSymbol(sym)) return isTemporalGlobalRoot(lowerer, root, sym) ? root : null;
if (ambientUndefinedFnSymbolOf(lowerer, root) !== null) return root;
const decls = sym.declarations;
if (decls.length === 0) return null;
@@ -440,6 +441,24 @@ export function ambientUndefVarRootOf(lowerer: Lowerer, e: ts.Expression): ts.Id
}
}
+/** True when `ident` reads the ESNext temporal global, which the program
+ * type-checks against TS 7's lib slice (program.ts adds it only to programs
+ * that name it) but Node 24 does not define: Node's read throws the
+ * catchable ReferenceError " 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
diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts
index 7cdb9ef5d..b4e64c9d0 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";
@@ -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
@@ -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 };
}
@@ -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]!,
@@ -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..78d690c6b 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,
@@ -1021,7 +1024,15 @@ 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?.name === owner &&
+ lowerer.isStdlibSymbol(symbol) &&
+ !symbol.declarations.some(
+ (d) => ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS"),
+ )
+ )
+ return symbol;
}
return null;
}
@@ -1464,7 +1475,8 @@ export function lowerStaticFetchCompanionCall(
}
const root = call.expression.expression;
const symbol = lowerer.resolveValueSymbol(root);
- if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
+ if ((!symbol || !lowerer.isStdlibSymbol(symbol)) && !isStreamWebReadableStream(lowerer, root))
+ return null;
const loc = locOf(call);
if (root.text === "AbortSignal") {
noteEventListeners(lowerer, loc);
@@ -1877,6 +1889,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 +1903,11 @@ export function lowerStaticReadableStreamNew(
return null;
}
const symbol = lowerer.resolveValueSymbol(expr.expression);
- if (!symbol || !lowerer.isStdlibSymbol(symbol)) return null;
+ if (
+ (!symbol || !lowerer.isStdlibSymbol(symbol)) &&
+ !isStreamWebReadableStream(lowerer, expr.expression)
+ )
+ return null;
const args = expr.arguments?.nodes ?? [];
if (args.length > 1) return null;
const loc = locOf(expr);
@@ -2436,6 +2458,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",
diff --git a/packages/compiler/src/frontend/lowering/lower-stmts.ts b/packages/compiler/src/frontend/lowering/lower-stmts.ts
index 064cd7a7b..80470448a 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[];
@@ -1490,6 +1497,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);
@@ -2398,20 +2407,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);
@@ -3773,6 +3792,9 @@ function stdlibGlobalTokenDestructure(
const canonical = member.name === "global" ? "globalThis" : member.name;
token = `[builtin ${canonical}]`;
alias = canonical;
+ // Builtin constructors read as their interned function values, the
+ // same values the bare and `globalThis.X` spellings read.
+ value = builtinGlobalValue(canonical, locOf(el), true);
} else if (globalName === "console" && !CONSOLE_CALL_MEMBERS.has(propName)) {
token = `[builtin console.${propName}]`;
} else if (TOKEN_OPAQUE_GLOBALS.has(globalName)) {
@@ -3816,6 +3838,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({
@@ -4725,6 +4751,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
@@ -5145,6 +5175,16 @@ export function lowerVarDecl(
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
@@ -6910,6 +6950,25 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
"require() outside the module's top level (move it to the top of the file)",
);
}
+ // Replacing the export object (or rebinding `exports`) anywhere but a
+ // top-level statement makes the module's exports depend on which path
+ // ran (`cond ? module.exports = a : module.exports = b`, an
+ // environment-dependent branch); the static export table has one shape.
+ if (
+ !topLevelJs &&
+ isJsSourceFile(expr.getSourceFile()) &&
+ ts.isBinaryExpression(expr) &&
+ expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
+ (isCjsModuleExports(lowerer, expr.left) || isCjsExportsIdentifier(lowerer, expr.left))
+ ) {
+ lowerer.unsupported(
+ "SC1090",
+ expr.left,
+ isCjsModuleExports(lowerer, expr.left)
+ ? "replacing module.exports outside a top-level statement (a conditional or environment-dependent export object)"
+ : "rebinding 'exports' outside a top-level statement (a conditional or environment-dependent export object)",
+ );
+ }
if (topLevelJs && ts.isExpressionStatement(stmtNode)) {
if (isFreshExportsReset(lowerer, stmtNode))
return { kind: "block", body: [], loc: locOf(expr) };
@@ -7422,6 +7481,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(expr.getSourceFile()) &&
+ 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 +7562,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(
@@ -8914,8 +9026,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 +9268,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);
@@ -11205,6 +11334,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..44027f65c 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,
@@ -586,6 +588,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 +700,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.
@@ -2044,6 +2078,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
@@ -3296,6 +3351,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 +3829,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 +3861,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 +3876,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 +4001,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 +4061,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 +4329,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/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 1361b346f..a5f376fc0 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;
@@ -1921,6 +2024,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 +2065,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 +2137,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. */
@@ -2496,6 +2617,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 +2710,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 +2793,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 +2886,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 +3093,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 +3223,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);
@@ -3167,6 +3321,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 &&
@@ -8451,6 +8607,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 +8811,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 +8830,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 +9004,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 +9200,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 +9541,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
@@ -9687,6 +9869,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);
}
@@ -10097,6 +10281,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",
@@ -10591,6 +10784,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 +11078,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 +11216,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 +11558,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 +11594,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 +12235,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 +12465,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,8 +12871,20 @@ export class Lowerer {
const callable = this.lowerBuiltinCallableValue(bi, loc);
if (callable) return callable;
}
+ // node:stream/web's exports are the web stream globals themselves: in
+ // JavaScript sources the same identity token as the global spelling
+ // (`require("stream/web").ReadableStream === globalThis.ReadableStream`).
+ if (bi.module === "stream/web" && isJsSourceFile(expr.getSourceFile())) {
+ return { kind: "strLit", value: `[builtin ${bi.member}]`, type: STRING, loc };
+ }
// A bare `fs.constants` read (not one of the baked bits above).
if (bi.member === "constants" && bi.module === "fs") return fsConstantsObject(loc);
+ // `stream.Readable` as a JavaScript value: the runtime's untyped
+ // constructor, the base a constructor function inherits.
+ if (isJsSourceFile(expr.getSourceFile())) {
+ const classKind = builtinMemberClassKind(bi.module, bi.member);
+ if (classKind !== null) return builtinClassValue(classKind, loc);
+ }
if (builtinModuleFnOf(this, bi.module, bi.member)) {
this.unsupported(
"SC1090",
@@ -13096,3 +13433,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/static-function-names.ts b/packages/compiler/src/frontend/lowering/static-function-names.ts
new file mode 100644
index 000000000..d86e55a80
--- /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(info.decl.getSourceFile())) {
+ 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/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index 6fbbe3be4..be2207bcd 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
@@ -3433,6 +3479,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;
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..b52a3e059
--- /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(node.getSourceFile()) &&
+ 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 && !decl.getSourceFile().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(decl.getSourceFile()) &&
+ // 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/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/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..c61f4a439 100644
--- a/packages/compiler/src/frontend/npm-static-bundled-chunks.ts
+++ b/packages/compiler/src/frontend/npm-static-bundled-chunks.ts
@@ -23,6 +23,7 @@
* beside it). Anything else answers a degrade reason. */
import * as ts from "./ts7/syntax.js";
import { helperTokens } from "./helper-tokens.js";
+import { canonicalBuiltinModule } from "./builtin-modules.js";
export interface BundledChunkSplit {
/** The bundle with chunk declarations blanked and calls respelled. */
@@ -31,7 +32,7 @@ export interface BundledChunkSplit {
chunks: { path: string; source: string }[];
}
-interface Edit {
+export interface Edit {
start: number;
end: number;
text: string;
@@ -42,11 +43,11 @@ const CHUNK_SHAPE_DEGRADE =
"recognizer cannot verify";
/** The helper a counter-suffixed bundler name spells (`__commonJS2`). */
-function helperBase(name: string): string {
+export function helperBase(name: string): string {
return /^__[A-Za-z]+\d+$/.test(name) ? name.replace(/\d+$/, "") : name;
}
-function unparen(e: ts.Expression): ts.Expression {
+export function unparen(e: ts.Expression): ts.Expression {
while (ts.isParenthesizedExpression(e)) e = e.expression;
return e;
}
@@ -54,7 +55,7 @@ function unparen(e: ts.Expression): ts.Expression {
/** True for an identifier occurrence that reads or writes a binding (as
* opposed to a property name, a label, or a declaration's own name).
* Shorthand property values count: `{ x }` reads x. */
-function isReference(id: ts.Identifier): boolean {
+export function isReference(id: ts.Identifier): boolean {
const p = id.parent;
if (p === undefined) return false;
if (ts.isPropertyAccessExpression(p)) return p.expression === id;
@@ -83,7 +84,7 @@ function isReference(id: ts.Identifier): boolean {
/** Names a subtree declares anywhere inside it: variables (destructuring
* included), parameters, function and class names, catch bindings. */
-function declaredNames(root: ts.Node): Set {
+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 +273,7 @@ class BundleScopes {
}
/** Every binding reference inside a subtree. */
-function references(root: ts.Node): ts.Identifier[] {
+export function references(root: ts.Node): ts.Identifier[] {
const out: ts.Identifier[] = [];
ts.walkPreorder(root, (n) => {
if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode)
@@ -285,7 +286,7 @@ function references(root: ts.Node): ts.Identifier[] {
/** True when the reference is written (assignment target, update operand,
* loop target, delete operand). */
-function isWritten(id: ts.Identifier): boolean {
+export function isWritten(id: ts.Identifier): boolean {
let child: ts.Node = id;
for (let p = child.parent; p !== undefined; child = p, p = p.parent) {
if (ts.isBinaryExpression(p)) {
@@ -471,7 +472,7 @@ interface TopLevel {
}
/** True when the file opens with a "use strict" directive prologue. */
-function strictPrologue(stmts: readonly ts.Statement[]): boolean {
+export function strictPrologue(stmts: readonly ts.Statement[]): boolean {
for (const s of stmts) {
if (!ts.isExpressionStatement(s) || !ts.isStringLiteral(s.expression)) return false;
if (s.expression.text === "use strict") return true;
@@ -637,10 +638,11 @@ export function splitBundledChunks(
return {
degrade: `its bundled CommonJS module factory '${r.text}' is used other than as a direct call`,
};
+ const forwarded = forwarderSpec(chunk);
callEdits.push({
start: call.getStart(sf),
end: call.getEnd(),
- text: `require(${JSON.stringify(chunk.spec)})`,
+ text: `require(${JSON.stringify(forwarded ?? chunk.spec)})`,
});
}
for (const name of chunks.keys()) {
@@ -652,6 +654,11 @@ export function splitBundledChunks(
const out: BundledChunkSplit["chunks"] = [];
const bundleEdits: Edit[] = [...callEdits];
for (const chunk of chunks.values()) {
+ if (forwarderSpec(chunk) !== null) {
+ // A chunk that only re-exports a builtin module is that module.
+ bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" });
+ continue;
+ }
const result = chunkSource(
sf,
chunk,
@@ -670,7 +677,7 @@ export function splitBundledChunks(
return { text: applyEdits(source, 0, source.length, bundleEdits), chunks: out };
}
-function countDeclarations(sf: ts.SourceFile, name: string): number {
+export function countDeclarations(sf: ts.SourceFile, name: string): number {
let n = 0;
ts.walkPreorder(sf, (node) => {
if (
@@ -694,7 +701,7 @@ function countDeclarations(sf: ts.SourceFile, name: string): number {
/** Applies non-overlapping edits inside [from, to): replaced spans keep
* their newlines (blanked text stays line-aligned); edits nested in a
* blanked span vanish with it. */
-function applyEdits(source: string, from: number, to: number, edits: Edit[]): string {
+export function applyEdits(source: string, from: number, to: number, edits: Edit[]): string {
const inside = edits
.filter((e) => e.start >= from && e.end <= to)
.sort((a, b) => a.start - b.start || b.end - a.end);
@@ -712,6 +719,86 @@ function applyEdits(source: string, from: number, to: number, edits: Edit[]): st
return text + source.slice(at, to);
}
+/** The builtin module a chunk forwards, or null: a body that is exactly
+ * `module.exports = require("")` (readable-stream's
+ * internal/streams/stream.js). Requiring the chunk is then requiring the
+ * builtin itself (same object, and loading a builtin runs no code). */
+function forwarderSpec(chunk: Chunk): string | null {
+ const moduleParam = chunk.params[1];
+ if (moduleParam === undefined) return null;
+ const stmts = chunk.body.statements.nodes.filter(
+ (st) => !(ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)),
+ );
+ const only = stmts[0];
+ if (stmts.length !== 1 || only === undefined || !ts.isExpressionStatement(only)) return null;
+ const e = unparen(only.expression);
+ if (
+ !ts.isBinaryExpression(e) ||
+ e.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
+ !ts.isPropertyAccessExpression(e.left) ||
+ !ts.isIdentifier(e.left.expression) ||
+ e.left.expression.text !== moduleParam ||
+ e.left.name.text !== "exports"
+ )
+ return null;
+ const call = unparen(e.right);
+ if (
+ !ts.isCallExpression(call) ||
+ !ts.isIdentifier(call.expression) ||
+ call.expression.text !== "require" ||
+ call.arguments.nodes.length !== 1 ||
+ !ts.isStringLiteral(call.arguments.nodes[0]!)
+ )
+ return null;
+ const spec = (call.arguments.nodes[0] as ts.StringLiteral).text;
+ return canonicalBuiltinModule(spec) !== null ? spec : null;
+}
+
+/** How code in a factory body (a chunk or a lazy ES-module initializer)
+ * observes the factory's own frame, or null: its receiver, its arguments
+ * object, `super`, a meta property, or (when `returns`) a top-level
+ * return have no module-scope equivalent. */
+export function factoryFrameUse(body: ts.Block, returns: boolean): string | null {
+ let frameUse: string | null = null;
+ /** The innermost frame boundary between a node and the factory body. */
+ const frameOf = (n: ts.Node): "factory" | "arrow" | "function" => {
+ let arrow = false;
+ for (let p = n.parent; p !== undefined && p !== body; p = p.parent) {
+ if (
+ ts.isFunctionDeclaration(p) ||
+ ts.isFunctionExpression(p) ||
+ ts.isMethodDeclaration(p) ||
+ ts.isGetAccessorDeclaration(p) ||
+ ts.isSetAccessorDeclaration(p) ||
+ ts.isConstructorDeclaration(p) ||
+ ts.isClassDeclaration(p) ||
+ ts.isClassExpression(p)
+ )
+ return "function";
+ if (ts.isArrowFunction(p)) arrow = true;
+ }
+ return arrow ? "arrow" : "factory";
+ };
+ ts.walkPreorder(body, (n) => {
+ if (frameUse !== null) return "stop";
+ if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode)
+ return "skip";
+ let use: string | null = null;
+ if (n.kind === ts.SyntaxKind.ThisKeyword) use = "reads its factory's receiver (`this`)";
+ else if (n.kind === ts.SyntaxKind.SuperKeyword) use = "reads `super` at its top level";
+ else if (ts.isIdentifier(n) && n.text === "arguments" && isReference(n))
+ use = "reads its factory's arguments object";
+ else if (ts.isMetaProperty(n)) use = "reads a meta property at its top level";
+ else if (ts.isReturnStatement(n)) {
+ if (returns && frameOf(n) === "factory") frameUse = "returns from its top level";
+ return undefined;
+ }
+ if (use !== null && frameOf(n) !== "function") frameUse = use;
+ return undefined;
+ });
+ return frameUse;
+}
+
/** The virtual module text for one chunk, or a degrade reason. */
function chunkSource(
sf: ts.SourceFile,
@@ -741,45 +828,7 @@ function chunkSource(
);
}
- /* The factory's own frame must stay unobservable: its receiver, its
- * arguments object, and a top-level return have no module equivalent. */
- let frameUse: string | null = null;
- /** The innermost frame boundary between a node and the chunk body. */
- const frameOf = (n: ts.Node): "factory" | "arrow" | "function" => {
- let arrow = false;
- for (let p = n.parent; p !== undefined && p !== body; p = p.parent) {
- if (
- ts.isFunctionDeclaration(p) ||
- ts.isFunctionExpression(p) ||
- ts.isMethodDeclaration(p) ||
- ts.isGetAccessorDeclaration(p) ||
- ts.isSetAccessorDeclaration(p) ||
- ts.isConstructorDeclaration(p) ||
- ts.isClassDeclaration(p) ||
- ts.isClassExpression(p)
- )
- return "function";
- if (ts.isArrowFunction(p)) arrow = true;
- }
- return arrow ? "arrow" : "factory";
- };
- ts.walkPreorder(body, (n) => {
- if (frameUse !== null) return "stop";
- if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode)
- return "skip";
- let use: string | null = null;
- if (n.kind === ts.SyntaxKind.ThisKeyword) use = "reads its factory's receiver (`this`)";
- else if (n.kind === ts.SyntaxKind.SuperKeyword) use = "reads `super` at its top level";
- else if (ts.isIdentifier(n) && n.text === "arguments" && isReference(n))
- use = "reads its factory's arguments object";
- else if (ts.isMetaProperty(n)) use = "reads a meta property at its top level";
- else if (ts.isReturnStatement(n)) {
- if (frameOf(n) === "factory") frameUse = "returns from its top level";
- return undefined;
- }
- if (use !== null && frameOf(n) !== "function") frameUse = use;
- return undefined;
- });
+ const frameUse = factoryFrameUse(body, true);
if (frameUse !== null) return why(frameUse);
const edits: Edit[] = callEdits.filter((e) => e.start >= body.pos && e.end <= body.end);
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..166676b3a
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-bundled-esm.ts
@@ -0,0 +1,825 @@
+/* Lazily initialized ES modules in a CommonJS bundle. esbuild scope-hoists
+ * every ES module it bundles into the bundle's own scope; one that some
+ * code require()s instead of importing runs from a lazy initializer,
+ *
+ * var COUNT, import_fs;
+ * var init_x = __esm({ "src/x.ts"() { import_fs = __toESM(require("fs")); COUNT = 0; } });
+ *
+ * and the requiring site reads its namespace as
+ * `(init_x(), __toCommonJS(x_exports))`. `init_x()` runs the body once (a
+ * call during the body answers immediately). The pass below keeps that
+ * shape in the bundle's scope and respells only what has no static
+ * representation:
+ *
+ * - the initializer becomes a run-once closure over the same body;
+ * - an interop binding the body assigns once from require() (`import_fs`
+ * above) becomes that require at the assignment's position, and its
+ * member reads become member reads of the required module, exactly
+ * like the top-level interop erasure (npm-static-rewrite-syntax.ts);
+ * - a namespace read only through member reads becomes an object
+ * literal of getters over the module's live bindings (what
+ * __toCommonJS copies, minus the non-enumerable __esModule mark no
+ * member read can reach).
+ *
+ * The respelled interop reads resolve the required module even if they
+ * ran before the initializer, where Node would read a member of
+ * undefined and throw; esbuild only reaches them through the module's
+ * own code, which runs after its initializer. Shapes outside the
+ * recognized forms answer a degrade reason. */
+import * as ts from "./ts7/syntax.js";
+import { bareRequireSpecOf } from "./cjs-syntax.js";
+import { helperTokens } from "./helper-tokens.js";
+import {
+ applyEdits,
+ countDeclarations,
+ declaredNames,
+ factoryFrameUse,
+ helperBase,
+ isWritten,
+ references,
+ unparen,
+ type Edit,
+} from "./npm-static-bundled-chunks.js";
+
+export interface EsmInitHost {
+ requireTargetEsModuleStamped(fromFile: string, specifier: string): boolean;
+}
+
+const ESM_SHAPE_DEGRADE =
+ "its shipped JS initializes bundled ES modules through the __esm helper in a shape the " +
+ "recognizer cannot verify";
+
+/* The recognized __esm forms over its parameter names (FN, RES) and the
+ * own-key reader (KEYS): keyed `(FN, RES) => function [name]() { return
+ * ; }` 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 = ret.expression.getText(sf);
+ const keys = /\[\s*([A-Za-z_$][\w$]*(?:\.keys)?)\s*\(/.exec(text)?.[1];
+ if (keys === undefined || (keys !== "Object.keys" && !ownNames(keys))) return null;
+ return helperTokens(text) === template(KEYED_RETURN, f, r, keys) ? "keyed" : null;
+ }
+ if (ts.isArrowFunction(body) && body.parameters.nodes.length === 0 && !ts.isBlock(body.body)) {
+ const actual = helperTokens(body.body.getText(sf));
+ return actual !== null && actual === template(ARROW_BODY, f, r, "") ? "arrow" : null;
+ }
+ return null;
+}
+
+/** Pads the given declarators out of their statements (separating commas
+ * included; a statement whose declarators all go pads whole). */
+function removeDeclarators(sf: ts.SourceFile, dead: ReadonlySet): 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: stmt.getStart(sf), end: stmt.getEnd(), text: "" });
+ continue;
+ }
+ const lastLive = deadAt.lastIndexOf(false);
+ for (let i = 0; i < decls.length; i++) {
+ if (!deadAt[i]) continue;
+ edits.push(
+ i < lastLive
+ ? { start: decls[i]!.getStart(sf), end: decls[i + 1]!.getStart(sf), text: "" }
+ : { start: decls[lastLive]!.getEnd(), end: decls[i]!.getEnd(), text: "" },
+ );
+ }
+ }
+ return edits;
+}
+
+/** The elements of an expression statement's comma chain, in order. */
+function commaElements(e: ts.Expression): ts.Expression[] {
+ const out: ts.Expression[] = [];
+ const visit = (x: ts.Expression): void => {
+ if (ts.isBinaryExpression(x) && x.operatorToken.kind === ts.SyntaxKind.CommaToken) {
+ visit(x.left);
+ visit(x.right);
+ } else out.push(x);
+ };
+ visit(e);
+ return out;
+}
+
+/** True when a call's result is discarded: an expression statement or a
+ * non-final comma operand (parentheses included). */
+function resultUnused(call: ts.Expression): boolean {
+ let child: ts.Node = call;
+ let p = call.parent;
+ while (p !== undefined && ts.isParenthesizedExpression(p)) {
+ child = p;
+ p = p.parent;
+ }
+ if (p === undefined) return false;
+ if (ts.isExpressionStatement(p)) return true;
+ if (ts.isBinaryExpression(p) && p.operatorToken.kind === ts.SyntaxKind.CommaToken) {
+ if (p.left === child) return true;
+ return resultUnused(p);
+ }
+ return false;
+}
+
+/** True when a member access is a plain read (not written, deleted or
+ * updated). */
+function plainRead(access: ts.Expression): boolean {
+ const p = access.parent;
+ if (p === undefined) return true;
+ if (ts.isDeleteExpression(p)) return false;
+ if (
+ ts.isBinaryExpression(p) &&
+ p.left === access &&
+ p.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
+ p.operatorToken.kind <= ts.SyntaxKind.LastAssignment
+ )
+ return false;
+ if (
+ (ts.isPrefixUnaryExpression(p) || ts.isPostfixUnaryExpression(p)) &&
+ (p.operator === ts.SyntaxKind.PlusPlusToken || p.operator === ts.SyntaxKind.MinusMinusToken)
+ )
+ return false;
+ return true;
+}
+
+/** True when a member read is the callee of a call or a tag, which receives
+ * the object it was read from as `this`. */
+function calledWithReceiver(access: ts.Expression): boolean {
+ let callee: ts.Node = access;
+ while (callee.parent !== undefined && ts.isParenthesizedExpression(callee.parent))
+ callee = callee.parent;
+ const p = callee.parent;
+ return (
+ p !== undefined &&
+ ((ts.isCallExpression(p) && p.expression === callee) ||
+ (ts.isTaggedTemplateExpression(p) && p.tag === callee))
+ );
+}
+
+/** True when a function reads its own `this`: in its body or a parameter's
+ * default value, or in an arrow function inside them. */
+function readsThis(fn: ts.FunctionDeclaration): boolean {
+ let found = false;
+ if (fn.body === undefined) return false;
+ const visit = (n: ts.Node): "stop" | "skip" | undefined => {
+ if (found) return "stop";
+ if (
+ ts.isFunctionDeclaration(n) ||
+ ts.isFunctionExpression(n) ||
+ ts.isMethodDeclaration(n) ||
+ ts.isGetAccessorDeclaration(n) ||
+ ts.isSetAccessorDeclaration(n) ||
+ ts.isConstructorDeclaration(n) ||
+ ts.isClassDeclaration(n) ||
+ ts.isClassExpression(n)
+ )
+ return "skip";
+ if (n.kind === ts.SyntaxKind.ThisKeyword) found = true;
+ return undefined;
+ };
+ for (const parameter of fn.parameters.nodes) ts.walkPreorder(parameter, visit);
+ ts.walkPreorder(fn.body, visit);
+ return found;
+}
+
+/** The `__export(T, { name: () => value, … })` getter table of T, or null. */
+function exportTableOf(
+ call: ts.CallExpression,
+): { target: string; entries: [string, ts.Expression][] } | null {
+ if (!ts.isIdentifier(call.expression) || helperBase(call.expression.text) !== "__export")
+ return null;
+ const [target, table] = call.arguments.nodes;
+ if (
+ call.arguments.nodes.length !== 2 ||
+ !target ||
+ !table ||
+ !ts.isIdentifier(target) ||
+ !ts.isObjectLiteralExpression(table)
+ )
+ return null;
+ const entries: [string, ts.Expression][] = [];
+ for (const prop of table.properties.nodes) {
+ if (!ts.isPropertyAssignment(prop)) return null;
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) return null;
+ const getter = unparen(prop.initializer);
+ if (!ts.isArrowFunction(getter) || getter.parameters.nodes.length !== 0) return null;
+ if (ts.isBlock(getter.body)) return null;
+ entries.push([prop.name.text, getter.body]);
+ }
+ return { target: target.text, entries };
+}
+
+/** Lowers a CommonJS bundle's __esm initializers (see the header). Null:
+ * nothing to lower. */
+export function lowerBundledEsmInits(
+ sf: ts.SourceFile,
+ filePath: string,
+ host: EsmInitHost,
+): string | { degrade: string } | null {
+ const source = sf.text;
+ if (!source.includes("__esm")) return null;
+ if (!filePath.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
+
+ interface Top {
+ stmt: ts.Statement;
+ node: ts.VariableDeclaration | ts.FunctionDeclaration;
+ }
+ const top = new Map();
+ 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(init.getText(sf)) === 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 = rhs.getText(sf);
+ const defaultIsModule =
+ interop && (isNodeMode || !host.requireTargetEsModuleStamped(filePath, spec));
+ // Every other reference must be a plain member read.
+ const uses: Edit[] = [];
+ let ok = true;
+ for (const r of refsByName.get(name) ?? []) {
+ if (r === el.left) continue;
+ const access = r.parent;
+ if (
+ access === undefined ||
+ !ts.isPropertyAccessExpression(access) ||
+ access.expression !== r ||
+ !plainRead(access)
+ ) {
+ ok = false;
+ break;
+ }
+ if (defaultIsModule && access.name.text === "default")
+ uses.push({ start: access.getStart(sf), end: access.getEnd(), text: `(${req})` });
+ else uses.push({ start: r.getStart(sf), end: r.getEnd(), text: req });
+ }
+ if (!ok) continue;
+ edits.push({ start: el.getStart(sf), end: el.getEnd(), text: req }, ...uses);
+ deadDeclarators.add(decl);
+ }
+ }
+ }
+
+ /* Namespaces of lazily initialized modules read through members. */
+ let mainTarget: string | null = null;
+ for (const stmt of sf.statements.nodes) {
+ if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue;
+ const e = stmt.expression;
+ const rhs = e.right;
+ if (
+ e.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
+ ts.isPropertyAccessExpression(e.left) &&
+ ts.isIdentifier(e.left.expression) &&
+ e.left.expression.text === "module" &&
+ e.left.name.text === "exports" &&
+ ts.isCallExpression(rhs) &&
+ ts.isIdentifier(rhs.expression) &&
+ helperBase(rhs.expression.text) === "__toCommonJS" &&
+ rhs.arguments.nodes.length === 1 &&
+ ts.isIdentifier(rhs.arguments.nodes[0]!)
+ )
+ mainTarget = (rhs.arguments.nodes[0] as ts.Identifier).text;
+ }
+ const tables = new Map();
+ 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, value.getStart(sf), value.getEnd(), edits);
+ /** A call through the namespace (`ns.fn()`, `L().fn()`) passes the object
+ * the read came from as `this`; the lowered object differs from Node's
+ * module object (`__esModule`, property attributes), so it must stay
+ * unobservable: the callee is a top-level function that never reads
+ * `this`. */
+ const receiverHidden = (values: ReadonlyMap, name: string): boolean => {
+ const value = values.get(name);
+ if (value === undefined || !ts.isIdentifier(value) || !stable(value.text)) return false;
+ const decl = top.get(value.text)?.node;
+ return decl !== undefined && ts.isFunctionDeclaration(decl) && !readsThis(decl);
+ };
+
+ /* Memoized lazy loaders of those namespaces, `L = () => M ??= NS` (a
+ * bundle plugin's lazy re-exports): every `L().name` becomes the
+ * initializer call followed by the export's live read, so the first
+ * access still initializes the module, once, and later accesses read
+ * the binding. */
+ const loaderSpans: { start: number; end: number }[] = [];
+ for (const [name, t] of top) {
+ const d = t.node;
+ if (!ts.isVariableDeclaration(d) || d.initializer === undefined) continue;
+ const fn = unparen(d.initializer);
+ if (!ts.isArrowFunction(fn) || fn.parameters.nodes.length !== 0 || ts.isBlock(fn.body))
+ continue;
+ const memoize = unparen(fn.body);
+ if (
+ !ts.isBinaryExpression(memoize) ||
+ (memoize.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionEqualsToken &&
+ memoize.operatorToken.kind !== ts.SyntaxKind.BarBarEqualsToken) ||
+ !ts.isIdentifier(memoize.left)
+ )
+ continue;
+ let w = unparen(memoize.right);
+ let interop = false;
+ let nodeMode = false;
+ if (
+ ts.isCallExpression(w) &&
+ ts.isIdentifier(w.expression) &&
+ helperBase(w.expression.text) === "__toESM"
+ ) {
+ const [arg, mode] = w.arguments.nodes;
+ if (!arg || w.arguments.nodes.length > 2) continue;
+ if (mode !== undefined) {
+ if (ts.isNumericLiteral(mode)) nodeMode = Number(mode.text) !== 0;
+ else if (mode.kind === ts.SyntaxKind.TrueKeyword) nodeMode = true;
+ else if (mode.kind !== ts.SyntaxKind.FalseKeyword) continue;
+ }
+ interop = true;
+ w = unparen(arg);
+ }
+ let initCall: ts.CallExpression | null = null;
+ if (ts.isBinaryExpression(w) && w.operatorToken.kind === ts.SyntaxKind.CommaToken) {
+ const left = unparen(w.left);
+ if (
+ !ts.isCallExpression(left) ||
+ !ts.isIdentifier(left.expression) ||
+ !inits.has(left.expression.text) ||
+ left.arguments.nodes.length !== 0
+ )
+ continue;
+ initCall = left;
+ w = unparen(w.right);
+ }
+ if (
+ !ts.isCallExpression(w) ||
+ !ts.isIdentifier(w.expression) ||
+ helperBase(w.expression.text) !== "__toCommonJS" ||
+ w.arguments.nodes.length !== 1 ||
+ !ts.isIdentifier(w.arguments.nodes[0]!)
+ )
+ continue;
+ const target = (w.arguments.nodes[0] as ts.Identifier).text;
+ const table = tables.get(target);
+ if (table === undefined) continue;
+ const loaderWhy = {
+ degrade: `its memoized lazy loader '${name}' returns the namespace of a bundled ES module as a value, a module object the static rewrite cannot represent (only member reads of its result compile, and calls only of exported functions that do not read 'this')`,
+ };
+ const memo = memoize.left.text;
+ const memoDecl = top.get(memo)?.node;
+ if (
+ memoDecl === undefined ||
+ !ts.isVariableDeclaration(memoDecl) ||
+ memoDecl.initializer !== undefined ||
+ writes.get(memo) !== 1 ||
+ countDeclarations(sf, memo) !== 1 ||
+ (refsByName.get(memo) ?? []).some((x) => x !== memoize.left) ||
+ writes.has(name) ||
+ countDeclarations(sf, name) !== 1
+ )
+ return loaderWhy;
+ const entries = new Map(table.entries);
+ for (const ref of refsByName.get(name) ?? []) {
+ const call = ref.parent;
+ if (
+ call === undefined ||
+ !ts.isCallExpression(call) ||
+ call.expression !== ref ||
+ call.arguments.nodes.length !== 0
+ )
+ return loaderWhy;
+ let access: ts.Node | undefined = call.parent;
+ if (
+ access === undefined ||
+ !ts.isPropertyAccessExpression(access) ||
+ access.expression !== call ||
+ !plainRead(access)
+ )
+ return loaderWhy;
+ // Node mode binds `default` to the namespace object itself.
+ if (interop && nodeMode && access.name.text === "default") {
+ const outer: ts.Node | undefined = access.parent;
+ if (
+ outer === undefined ||
+ !ts.isPropertyAccessExpression(outer) ||
+ outer.expression !== access ||
+ !plainRead(outer)
+ )
+ return loaderWhy;
+ access = outer;
+ }
+ const member = (access as ts.PropertyAccessExpression).name.text;
+ const value = entries.get(member);
+ if (value === undefined || shadowed(access, table.entries)) return loaderWhy;
+ // The rewrite reads the export without the namespace object, so a
+ // call through it (`L().fn()`) loses its receiver.
+ if (calledWithReceiver(access as ts.Expression) && !receiverHidden(entries, member))
+ return loaderWhy;
+ const init = initCall === null ? "" : `${initCall.getText(sf)}, `;
+ edits.push({
+ start: access.getStart(sf),
+ end: access.getEnd(),
+ text: `(${init}${getterText(value)})`,
+ });
+ }
+ deadDeclarators.add(d);
+ deadDeclarators.add(memoDecl);
+ loaderSpans.push({ start: d.getStart(sf), end: d.getEnd() });
+ lowered.add(target);
+ }
+
+ for (const r of allRefs) {
+ const table = tables.get(r.text);
+ if (table === undefined) continue;
+ if (loaderSpans.some((span) => r.getStart(sf) >= span.start && r.getEnd() <= span.end))
+ continue;
+ const p = r.parent;
+ // The table's own declaration and __export call are handled below.
+ if (p !== undefined && ts.isVariableDeclaration(p) && p.name === r) continue;
+ if (
+ p !== undefined &&
+ ts.isCallExpression(p) &&
+ p.arguments.nodes[0] === r &&
+ ts.isExpressionStatement(p.parent) &&
+ p.parent === table.stmt
+ )
+ continue;
+ const why = `reads the namespace of a bundled ES module ('${r.text}') in a way the static rewrite cannot follow (only member reads of its exports compile, and calls only of exported functions that do not read 'this')`;
+ if (
+ p === undefined ||
+ !ts.isCallExpression(p) ||
+ !ts.isIdentifier(p.expression) ||
+ helperBase(p.expression.text) !== "__toCommonJS" ||
+ p.arguments.nodes.length !== 1
+ )
+ return { degrade: `its shipped JS ${why}` };
+ // The namespace expression: `(init_x(), __toCommonJS(T))` or the call.
+ let ns: ts.Expression = p;
+ for (;;) {
+ const up = ns.parent;
+ if (up !== undefined && ts.isParenthesizedExpression(up)) ns = up;
+ else if (
+ up !== undefined &&
+ ts.isBinaryExpression(up) &&
+ up.operatorToken.kind === ts.SyntaxKind.CommaToken &&
+ up.right === ns
+ )
+ ns = up;
+ else break;
+ }
+ const values = new Map(table.entries);
+ const memberRead = (access: ts.Node | undefined, of: ts.Node): boolean =>
+ access !== undefined &&
+ ts.isPropertyAccessExpression(access) &&
+ access.expression === of &&
+ values.has(access.name.text) &&
+ plainRead(access) &&
+ (!calledWithReceiver(access) || receiverHidden(values, access.name.text));
+ const holder = ns.parent;
+ // `__reExport(main, ns, module.exports)`: a star re-export of the
+ // module into the bundle's own exports (`export * from` in its entry).
+ // The initializer runs at the statement; the names become live getter
+ // entries of the export table, after its explicit ones (first wins)
+ // and without `default`, exactly the names __reExport copies.
+ if (
+ holder !== undefined &&
+ ts.isCallExpression(holder) &&
+ ts.isIdentifier(holder.expression) &&
+ helperBase(holder.expression.text) === "__reExport" &&
+ holder.arguments.nodes[1] === ns &&
+ holder.arguments.nodes.length <= 3 &&
+ ts.isExpressionStatement(holder.parent) &&
+ mainTarget !== null &&
+ ts.isIdentifier(holder.arguments.nodes[0]!) &&
+ (holder.arguments.nodes[0] as ts.Identifier).text === mainTarget
+ ) {
+ const inner = unparen(ns);
+ const init =
+ ts.isBinaryExpression(inner) && inner.operatorToken.kind === ts.SyntaxKind.CommaToken
+ ? `${unparen(inner.left).getText(sf)}; `
+ : "";
+ if (shadowed(holder, table.entries)) return { degrade: `its shipped JS ${why}` };
+ const entries = table.entries
+ .filter(([n]) => n !== "default")
+ .map(([n, value]) => `${JSON.stringify(n)}: () => ${getterText(value)}`)
+ .join(", ");
+ edits.push({
+ start: holder.parent.getStart(sf),
+ end: holder.parent.getEnd(),
+ text: `${init}__export(${mainTarget}, { ${entries} });`,
+ });
+ lowered.add(r.text);
+ continue;
+ }
+ let readable = memberRead(holder, ns);
+ if (
+ !readable &&
+ holder !== undefined &&
+ ts.isVariableDeclaration(holder) &&
+ holder.initializer === ns &&
+ ts.isIdentifier(holder.name)
+ ) {
+ const local = holder.name.text;
+ const localRefs = (refsByName.get(local) ?? []).filter((x) => x !== holder.name);
+ readable =
+ countDeclarations(sf, local) === 1 &&
+ !writes.has(local) &&
+ localRefs.every((x) => memberRead(x.parent, x));
+ }
+ if (!readable) {
+ // A namespace kept for later (`x ??= __toESM(ns)`, a hand-rolled
+ // lazy loader) is a value with no static representation.
+ let outer: ts.Node | undefined = holder;
+ while (
+ outer !== undefined &&
+ (ts.isParenthesizedExpression(outer) ||
+ (ts.isCallExpression(outer) &&
+ ts.isIdentifier(outer.expression) &&
+ helperBase(outer.expression.text) === "__toESM"))
+ )
+ outer = outer.parent;
+ if (
+ outer !== undefined &&
+ ts.isBinaryExpression(outer) &&
+ outer.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
+ outer.operatorToken.kind <= ts.SyntaxKind.LastAssignment
+ )
+ return {
+ degrade: `its shipped JS stores the namespace of a bundled ES module ('${r.text}') in a variable (a memoized lazy loader), a module object the static rewrite cannot represent`,
+ };
+ return { degrade: `its shipped JS ${why}` };
+ }
+ // The getters read top-level bindings: nothing between the use site
+ // and the top level may rebind one of their names.
+ if (shadowed(p, table.entries)) return { degrade: `its shipped JS ${why}` };
+ const getters = table.entries
+ .map(
+ ([n, value]) =>
+ `get ${/^[A-Za-z_$][\w$]*$/.test(n) ? n : JSON.stringify(n)}() { return ${getterText(value)}; }`,
+ )
+ .join(", ");
+ edits.push({ start: p.getStart(sf), end: p.getEnd(), text: `{ ${getters} }` });
+ lowered.add(r.text);
+ }
+ // A table nothing reads (its module is never required) goes too: its
+ // names are no exports of the bundle.
+ for (const [target, table] of tables) {
+ if (lowered.has(target)) continue;
+ const live = (refsByName.get(target) ?? []).some((x) => {
+ const p = x.parent;
+ if (p !== undefined && ts.isVariableDeclaration(p) && p.name === x) return false;
+ return !(x.getStart(sf) >= table.stmt.getStart(sf) && x.getEnd() <= table.stmt.getEnd());
+ });
+ if (!live) lowered.add(target);
+ }
+ for (const target of lowered) {
+ const table = tables.get(target)!;
+ const decl = top.get(target)?.node;
+ if (
+ decl === undefined ||
+ !ts.isVariableDeclaration(decl) ||
+ decl.initializer === undefined ||
+ !ts.isObjectLiteralExpression(unparen(decl.initializer)) ||
+ (unparen(decl.initializer) as ts.ObjectLiteralExpression).properties.nodes.length !== 0 ||
+ writes.has(target)
+ )
+ return { degrade: ESM_SHAPE_DEGRADE };
+ edits.push({ start: table.stmt.getStart(sf), end: table.stmt.getEnd(), text: "" });
+ deadDeclarators.add(decl);
+ }
+ // Every removed declarator in one pass: declarators of one statement
+ // removed separately would leave overlapping edits, and applyEdits keeps
+ // only the first of them.
+ edits.push(...removeDeclarators(sf, deadDeclarators));
+
+ /* The run-once closures. */
+ for (const init of inits.values()) {
+ const { call, body } = init;
+ edits.push(
+ {
+ start: call.getStart(sf),
+ end: body.getStart(sf) + 1,
+ text: "(() => { let __scriptc_done = false; function __scriptc_init() {",
+ },
+ {
+ start: body.getEnd() - 1,
+ end: call.getEnd(),
+ text: "} return function () { if (__scriptc_done) return; __scriptc_done = true; __scriptc_init(); }; })()",
+ },
+ );
+ }
+ return applyEdits(source, 0, source.length, edits);
+}
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..dfe45b862
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-cjs-exports-alias.ts
@@ -0,0 +1,191 @@
+/* CommonJS modules whose export object is one of their own functions:
+ *
+ * exports = module.exports = SemVer; // or module.exports = SemVer;
+ * exports.parse = parse; // or module.exports.parse = …
+ * function SemVer(version) { … }
+ *
+ * After that statement `module.exports` (and, after the rebinding form,
+ * `exports`) IS the function, so every member read or write through them
+ * is a member of the function itself. The pass respells those accesses
+ * through the function's name, the expando form both resolvers model
+ * (members on a function declaration), and drops the `exports =`
+ * rebinding the static export protocol has no storage for.
+ *
+ * Exact only while nothing can observe the module before the statement
+ * runs and nothing replaces the export object again: the statement is a
+ * top-level statement preceded only by inert declarations, it is the
+ * file's only write of `module.exports` and `exports`, the function is a
+ * top-level declaration nothing reassigns and the file's only binding of
+ * its name, and `exports`/`module` are the CommonJS globals (never
+ * declared in the file). Other shapes answer
+ * null and keep their existing treatment. */
+import * as ts from "./ts7/syntax.js";
+import {
+ applyEdits,
+ countDeclarations,
+ declaredNames,
+ isWritten,
+ references,
+ unparen,
+ type Edit,
+} from "./npm-static-bundled-chunks.js";
+
+function isModuleExports(e: ts.Expression): e is ts.PropertyAccessExpression {
+ const x = unparen(e);
+ return (
+ ts.isPropertyAccessExpression(x) &&
+ !x.questionDotToken &&
+ ts.isIdentifier(x.expression) &&
+ x.expression.text === "module" &&
+ x.name.text === "exports"
+ );
+}
+
+/** True for a statement whose evaluation runs no code that could observe
+ * the export object: declarations and member writes (`F.prototype.m =
+ * function …`) without calls, and directives. */
+function inert(stmt: ts.Statement): boolean {
+ if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) return true;
+ if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) return true;
+ if (ts.isExpressionStatement(stmt)) {
+ const e = unparen(stmt.expression);
+ if (
+ !ts.isBinaryExpression(e) ||
+ e.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
+ !(ts.isPropertyAccessExpression(e.left) || ts.isElementAccessExpression(e.left))
+ )
+ return false;
+ let root: ts.Expression = e.left;
+ while (ts.isPropertyAccessExpression(root) || ts.isElementAccessExpression(root))
+ root = root.expression;
+ if (!ts.isIdentifier(root) || root.text === "module" || root.text === "exports") return false;
+ } else if (!ts.isVariableStatement(stmt)) return false;
+ let calls = false;
+ ts.walkPreorder(stmt, (n) => {
+ if (calls) return "stop";
+ if (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) return "skip";
+ if (ts.isCallExpression(n) || ts.isNewExpression(n) || ts.isTaggedTemplateExpression(n))
+ calls = true;
+ return undefined;
+ });
+ return !calls;
+}
+
+export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null {
+ const source = sf.text;
+ if (!source.includes("module.exports")) return null;
+ if (!sf.fileName.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
+ const declared = declaredNames(sf);
+ if (declared.has("exports") || declared.has("module")) return null;
+
+ // The whole-export statement: `[exports =] module.exports = W;`.
+ let whole: { stmt: ts.ExpressionStatement; fn: string; rebinds: boolean } | null = null;
+ const stmts = sf.statements.nodes;
+ for (let i = 0; i < stmts.length; i++) {
+ const stmt = stmts[i]!;
+ if (!ts.isExpressionStatement(stmt)) continue;
+ const top = unparen(stmt.expression);
+ if (!ts.isBinaryExpression(top) || top.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
+ continue;
+ let e: ts.BinaryExpression = top;
+ let rebinds = false;
+ if (ts.isIdentifier(top.left) && top.left.text === "exports") {
+ const inner = unparen(top.right);
+ if (!ts.isBinaryExpression(inner) || inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
+ continue;
+ rebinds = true;
+ e = inner;
+ }
+ if (!isModuleExports(e.left)) continue;
+ const value = unparen(e.right);
+ if (!ts.isIdentifier(value)) return null;
+ if (whole !== null || !stmts.slice(0, i).every(inert)) return null;
+ whole = { stmt, fn: value.text, rebinds };
+ }
+ if (whole === null) return null;
+ const fnName = whole.fn;
+ const fnDecls = stmts.filter(
+ (st) => ts.isFunctionDeclaration(st) && st.name?.text === fnName && st.body !== undefined,
+ );
+ // The respelled reads name the function from wherever they sit, so no
+ // other binding of its name (a parameter, a local, a nested function)
+ // may exist anywhere in the file.
+ if (fnDecls.length !== 1 || countDeclarations(sf, fnName) !== 1) return null;
+
+ const edits: Edit[] = [];
+ let unsafe = false;
+ const refs = references(sf);
+ for (const r of refs) {
+ if (r.text === fnName && isWritten(r)) unsafe = true;
+ if (r.text === "exports") {
+ if (isWritten(r)) {
+ // Only the whole statement's own rebinding.
+ const assign = r.parent;
+ if (
+ !whole.rebinds ||
+ assign === undefined ||
+ !ts.isBinaryExpression(assign) ||
+ assign.parent !== whole.stmt ||
+ assign.left !== r
+ )
+ unsafe = true;
+ continue;
+ }
+ // Without the rebinding, `exports` keeps the replaced object.
+ if (!whole.rebinds || r.getStart(sf) < whole.stmt.getEnd()) {
+ unsafe = true;
+ continue;
+ }
+ // `{ exports }` keeps its key: the shorthand spells out the value.
+ const shorthand =
+ r.parent !== undefined && ts.isShorthandPropertyAssignment(r.parent) && r.parent.name === r;
+ edits.push({
+ start: r.getStart(sf),
+ end: r.getEnd(),
+ text: shorthand ? `exports: ${fnName}` : fnName,
+ });
+ continue;
+ }
+ if (r.text === "module") {
+ const access = r.parent;
+ if (access === undefined || !isModuleExports(access as ts.Expression)) continue;
+ const outer = access.parent;
+ if (outer !== undefined && ts.isBinaryExpression(outer) && outer.left === access) {
+ if (
+ outer.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
+ outer.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
+ !(
+ (outer.parent === whole.stmt && !whole.rebinds) ||
+ (whole.rebinds && outer.parent?.parent === whole.stmt)
+ )
+ )
+ unsafe = true;
+ continue;
+ }
+ // A member access through module.exports: a member of the function.
+ if (
+ outer !== undefined &&
+ (ts.isPropertyAccessExpression(outer) || ts.isElementAccessExpression(outer)) &&
+ outer.expression === access
+ ) {
+ if (access.getStart(sf) < whole.stmt.getEnd() && !insideFunction(access)) unsafe = true;
+ edits.push({ start: access.getStart(sf), end: access.getEnd(), text: fnName });
+ }
+ }
+ }
+ if (unsafe) return null;
+ if (whole.rebinds) {
+ // `exports = module.exports = W` → `module.exports = W`.
+ const e = unparen(whole.stmt.expression) as ts.BinaryExpression;
+ edits.push({ start: e.getStart(sf), end: unparen(e.right).getStart(sf), text: "" });
+ }
+ if (edits.length === 0) return null;
+ return applyEdits(source, 0, source.length, edits);
+}
+
+function insideFunction(node: ts.Node): boolean {
+ for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
+ if (ts.isFunctionLike(p)) return true;
+ }
+ return false;
+}
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..1af4ea1ed
--- /dev/null
+++ b/packages/compiler/src/frontend/npm-static-cjs-exports-var.ts
@@ -0,0 +1,238 @@
+/* CommonJS modules that name their export object through a variable:
+ *
+ * var ip = exports;
+ * ip.isPrivate = function (addr) { … };
+ * ip.isPublic = function (addr) { return !ip.isPrivate(addr); };
+ *
+ * `ip` IS the export object, so every member read or write through it is
+ * an export. The pass respells those accesses as `exports.`, the
+ * form the static export protocol models, and drops the alias declaration
+ * (the export object has no storage of its own to bind).
+ *
+ * Exact only while the variable is nothing but that alias: one top-level
+ * declaration initialized with the CommonJS `exports` global, never
+ * reassigned, never declared again anywhere in the file (so no nested
+ * binding shadows it), and used only as the object of member accesses.
+ * `exports` itself must be the global (never declared in the file) and is
+ * never reassigned. Other shapes answer null and keep their existing
+ * treatment. */
+import * as ts from "./ts7/syntax.js";
+
+function unparen(e: ts.Expression): ts.Expression {
+ let x = e;
+ while (ts.isParenthesizedExpression(x)) x = x.expression;
+ return x;
+}
+
+function isDeclarationName(id: ts.Identifier): boolean {
+ const p = id.parent;
+ return (
+ ((ts.isVariableDeclaration(p) ||
+ ts.isParameter(p) ||
+ ts.isBindingElement(p) ||
+ ts.isFunctionDeclaration(p) ||
+ ts.isFunctionExpression(p) ||
+ ts.isClassDeclaration(p) ||
+ ts.isClassExpression(p)) &&
+ p.name === id) ||
+ (ts.isPropertyAccessExpression(p) && p.name === id) ||
+ ((ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p) || ts.isPropertyDeclaration(p)) &&
+ p.name === id)
+ );
+}
+
+function isFunctionScope(n: ts.Node): boolean {
+ return (
+ ts.isFunctionDeclaration(n) ||
+ ts.isFunctionExpression(n) ||
+ ts.isArrowFunction(n) ||
+ ts.isMethodDeclaration(n) ||
+ ts.isGetAccessorDeclaration(n) ||
+ ts.isSetAccessorDeclaration(n) ||
+ ts.isConstructorDeclaration(n)
+ );
+}
+
+function bindsName(name: ts.BindingName | undefined, text: string): boolean {
+ if (name === undefined) return false;
+ if (ts.isIdentifier(name)) return name.text === text;
+ for (const element of name.elements.nodes) {
+ if (!ts.isOmittedExpression(element) && bindsName(element.name, text)) return true;
+ }
+ return false;
+}
+
+/** True when one of `statements` declares `text` lexically: a let/const,
+ * or a class or function declaration. */
+function declaresLexically(statements: readonly ts.Statement[], text: string): boolean {
+ for (const stmt of statements) {
+ if (
+ ts.isVariableStatement(stmt) &&
+ (stmt.declarationList.flags & ts.NodeFlags.BlockScoped) !== 0 &&
+ stmt.declarationList.declarations.nodes.some((d) => bindsName(d.name, text))
+ )
+ return true;
+ if ((ts.isClassDeclaration(stmt) || ts.isFunctionDeclaration(stmt)) && stmt.name?.text === text)
+ return true;
+ }
+ return false;
+}
+
+/** True when a function body declares `text` with `var` (or a function
+ * declaration in a nested block) anywhere outside nested functions. */
+function declaresVar(body: ts.Node, text: string): boolean {
+ let found = false;
+ ts.walkPreorder(body, (n) => {
+ if (found) return "stop";
+ if (n === body) return undefined;
+ if (isFunctionScope(n)) {
+ if (ts.isFunctionDeclaration(n) && n.name?.text === text) found = true;
+ return "skip";
+ }
+ if (
+ ts.isVariableDeclarationList(n) &&
+ (n.flags & ts.NodeFlags.BlockScoped) === 0 &&
+ n.declarations.nodes.some((d) => bindsName(d.name, text))
+ )
+ found = true;
+ return undefined;
+ });
+ return found;
+}
+
+/** True when `scope` declares `text` itself, so references inside it name
+ * that binding instead of the module-level alias: a function's parameters,
+ * own name, `var`s and the let/const, class and function declarations of
+ * its body; a block's or case block's own let/const, class and function
+ * declarations; a catch binding; a for loop's let/const. A let/const in a
+ * nested block shadows only that block, which the walk reaches as its own
+ * scope. */
+function shadows(scope: ts.Node, text: string): boolean {
+ if (isFunctionScope(scope)) {
+ const fn = scope as ts.SignatureDeclaration;
+ if (fn.parameters.nodes.some((p) => bindsName(p.name, text))) return true;
+ if (ts.isFunctionExpression(scope) && scope.name?.text === text) return true;
+ const body = (scope as { body?: ts.Node }).body;
+ if (body === undefined || !ts.isBlock(body)) return false;
+ return declaresVar(body, text) || declaresLexically(body.statements.nodes, text);
+ }
+ if (ts.isBlock(scope)) return declaresLexically(scope.statements.nodes, text);
+ if (ts.isCaseBlock(scope))
+ return declaresLexically(
+ scope.clauses.nodes.flatMap((clause) => clause.statements.nodes),
+ text,
+ );
+ if (ts.isCatchClause(scope)) return bindsName(scope.variableDeclaration?.name, text);
+ if (ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) {
+ const init = scope.initializer;
+ return (
+ init !== undefined &&
+ ts.isVariableDeclarationList(init) &&
+ (init.flags & ts.NodeFlags.BlockScoped) !== 0 &&
+ init.declarations.nodes.some((d) => bindsName(d.name, text))
+ );
+ }
+ if (ts.isClassExpression(scope)) return scope.name?.text === text;
+ return false;
+}
+
+export function normalizeCjsExportsVar(sf: ts.SourceFile): string | null {
+ const source = sf.text;
+ if (!/\b(?:var|let|const)\s+[A-Za-z_$][\w$]*\s*=\s*exports\s*[;,\n]/.test(source)) return null;
+ if (!sf.fileName.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
+ let alias: { name: string; stmt: ts.VariableStatement } | null = null;
+ for (const stmt of sf.statements.nodes) {
+ if (!ts.isVariableStatement(stmt)) continue;
+ const decls = stmt.declarationList.declarations.nodes;
+ if (decls.length !== 1) continue;
+ const decl = decls[0]!;
+ const init = decl.initializer !== undefined ? unparen(decl.initializer) : undefined;
+ if (
+ ts.isIdentifier(decl.name) &&
+ init !== undefined &&
+ ts.isIdentifier(init) &&
+ init.text === "exports"
+ ) {
+ if (alias !== null) return null; /* two aliases: keep the plain treatment */
+ alias = { name: decl.name.text, stmt };
+ }
+ }
+ if (alias === null) return null;
+ const name = alias.name;
+ const aliasDecl = alias.stmt.declarationList.declarations.nodes[0]!;
+ const edits: { start: number; end: number; text: string }[] = [];
+ let ok = true;
+ ts.walkPreorder(sf, (n) => {
+ if (!ok) return "stop";
+ if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode)
+ return "skip";
+ if (
+ ts.isBinaryExpression(n) &&
+ n.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
+ ts.isPropertyAccessExpression(unparen(n.left)) &&
+ (unparen(n.left) as ts.PropertyAccessExpression).name.text === "exports" &&
+ ts.isIdentifier((unparen(n.left) as ts.PropertyAccessExpression).expression) &&
+ ((unparen(n.left) as ts.PropertyAccessExpression).expression as ts.Identifier).text ===
+ "module"
+ ) {
+ ok = false; /* module.exports replaced: the alias no longer names the exports */
+ return "stop";
+ }
+ // A scope that declares the alias's name again: its references name
+ // that binding, not the export object.
+ if (
+ n !== sf &&
+ (isFunctionScope(n) ||
+ ts.isBlock(n) ||
+ ts.isCaseBlock(n) ||
+ ts.isCatchClause(n) ||
+ ts.isForStatement(n) ||
+ ts.isForInStatement(n) ||
+ ts.isForOfStatement(n) ||
+ ts.isClassExpression(n)) &&
+ shadows(n, name)
+ )
+ return "skip";
+ if (!ts.isIdentifier(n)) return undefined;
+ if (n.text === "exports" && n !== aliasDecl.initializer) {
+ // `exports` declared in the file, or the export object replaced
+ if (isDeclarationName(n) && !ts.isPropertyAccessExpression(n.parent)) ok = false;
+ const p = n.parent;
+ if (
+ ts.isBinaryExpression(p) &&
+ p.left === n &&
+ p.operatorToken.kind === ts.SyntaxKind.EqualsToken
+ )
+ ok = false;
+ return undefined;
+ }
+ if (n.text !== name) return undefined;
+ if (n === aliasDecl.name) return undefined;
+ const p = n.parent;
+ if (ts.isPropertyAccessExpression(p) && p.name === n) return undefined;
+ if ((ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p)) && p.name === n) return undefined;
+ if (isDeclarationName(n)) {
+ ok = false; /* declared again (a shadowing binding): not a plain alias */
+ return undefined;
+ }
+ if (
+ (ts.isPropertyAccessExpression(p) || ts.isElementAccessExpression(p)) &&
+ p.expression === n
+ ) {
+ edits.push({ start: n.getStart(sf), end: n.getEnd(), text: "exports" });
+ return undefined;
+ }
+ ok = false; /* used as a value, assigned, or passed along */
+ return undefined;
+ });
+ if (!ok || edits.length === 0) return null;
+ edits.push({ start: alias.stmt.getStart(sf), end: alias.stmt.getEnd(), text: "" });
+ edits.sort((a, b) => a.start - b.start);
+ let out = "";
+ let at = 0;
+ for (const e of edits) {
+ out += source.slice(at, e.start) + e.text;
+ at = e.end;
+ }
+ return out + source.slice(at);
+}
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..348b04817 100644
--- a/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts
+++ b/packages/compiler/src/frontend/npm-static-rewrite-syntax.ts
@@ -22,6 +22,9 @@ export interface CjsRewriteHost {
* exists, so Node's lexer-visible name set bounds nothing and every
* name the export plumbing defines is part of its surface. */
requireOnly?: ((filePath: string) => boolean) | undefined;
+ /** Told the names Node's lexer detects in the original file that the
+ * rewritten text spells only as accessors (which the lexer skips). */
+ lexerVisibleNames?: ((filePath: string, names: readonly string[]) => void) | undefined;
}
/** True when esbuild's export object (`module.exports = __toCommonJS(T)`
@@ -119,8 +122,12 @@ interface EntrySpec {
name: string;
/** "ident": VALUE is a plain identifier spelled directly in the table.
* "hoist": VALUE is an expression text hoisted to a tail const first.
+ * "getter": VALUE is a getter body's returned expression the table
+ * keeps as a live accessor (read on every access, like the original
+ * getter, so a lazily initialized module still initializes at the
+ * first read).
* "any": the checked-dynamic fallback (`__scriptc_any`). */
- kind: "ident" | "hoist" | "any";
+ kind: "ident" | "hoist" | "getter" | "any";
value?: string;
}
@@ -158,6 +165,26 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string }
return null;
}
+/** An object literal of side-effect-free member values (`exports.default
+ * = { context: context_api_1.context, metrics: metrics_api_1.metrics }`,
+ * tsc's default-export object): the tail const can build it once, like a
+ * chased value. Null for any other shape (spreads, methods, accessors,
+ * computed keys, or a value chaseValue rejects). */
+function chaseObjectLiteral(e: ts.Expression): { kind: "hoist"; text: string } | null {
+ let cur: ts.Expression = e;
+ while (ts.isParenthesizedExpression(cur)) cur = cur.expression;
+ if (!ts.isObjectLiteralExpression(cur)) return null;
+ for (const prop of cur.properties.nodes) {
+ if (
+ !ts.isPropertyAssignment(prop) ||
+ !(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) ||
+ (chaseValue(prop.initializer) === null && chaseObjectLiteral(prop.initializer) === null)
+ )
+ return null;
+ }
+ return { kind: "hoist", text: cur.getText() };
+}
+
/** The getter body's returned expression for the two descriptor getter
* spellings (`get() { return X }` / `get: function () { return X }`) and
* the arrow forms the esbuild table uses (`() => X`, `() => { return X }`). */
@@ -760,7 +787,14 @@ export function rewriteBundlerCjsSyntax(
...exportTables.filter((t) => t.target !== esbuildMainTarget),
];
for (const t of tablesOrdered) {
- for (const [name, value] of t.entries) claim(name, classify(name, value));
+ for (const [name, value] of t.entries) {
+ const spec = classify(name, value);
+ // The table feeding module.exports keeps a getter it cannot chase
+ // as a live accessor rather than a snapshot.
+ if (spec.kind === "any" && value !== null && t.target === esbuildMainTarget)
+ claim(name, { name, kind: "getter", value: value.getText() });
+ else claim(name, spec);
+ }
}
for (const d of definePropEntries) claim(d.name, classify(d.name, d.value));
@@ -792,7 +826,7 @@ export function rewriteBundlerCjsSyntax(
// anything else keeps the file untouched (the offender path owns
// it: neutralizing would drop an evaluation, keeping it meets the
// lowering's discard fence at load).
- const chased = chaseValue(cur);
+ const chased = chaseValue(cur) ?? chaseObjectLiteral(cur);
if (chased === null) {
return toEsm.pads.length > 0 ? { degrade: TO_ESM_MIXED_DEGRADE } : null;
}
@@ -893,6 +927,7 @@ export function rewriteBundlerCjsSyntax(
"__toESM",
"__toCommonJS",
"__commonJS",
+ "__esm",
"__exportStar",
"__createBinding",
"__setModuleDefault",
@@ -1038,6 +1073,8 @@ export function rewriteBundlerCjsSyntax(
const v = `__scriptc_e${hoistN++}`;
hoists.push(`const ${v} = ${dropInteropDefault(spec.value!)};`);
plainParts.push(`${keyOf(spec.name)}: ${v},`);
+ } else if (spec.kind === "getter") {
+ plainParts.push(`get ${keyOf(spec.name)}() { return ${dropInteropDefault(spec.value!)}; },`);
} else {
plainParts.push(`${keyOf(spec.name)}: __scriptc_any,`);
}
@@ -1063,5 +1100,12 @@ export function rewriteBundlerCjsSyntax(
lines.push(
`module.exports = {${spreadParts.join("")}${plainParts.join("")}${esModulePart}${memberParts.join("")}};`,
);
+ /* Node's lexer skips accessors (and stops reading an object literal at
+ * the first one), so a canonical table with accessor entries hides
+ * names the original showed it (esbuild's dead annotation): the host
+ * keeps the original's lexer answer, so ES-module importers link
+ * exactly as they do against the original. */
+ if ([...entries.values()].some((spec) => spec.kind === "getter"))
+ host.lexerVisibleNames?.(filePath, [...lexed.exports]);
return text + lines.join("\n") + "\n";
}
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..c29f31c8a 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);
@@ -5207,6 +5260,9 @@ function preflight7(load: LoadResult): {
continue;
}
}
+ // An npm package served as a builtin (graceful-fs as fs) is never
+ // a program-module edge, even when --npm-static names it.
+ if (npmBuiltinAliasOf(req.spec) !== null) continue;
const isRelative = isRelativeSpecifier(req.spec);
let dep = resolveImport7(program, sf, req.spec, "require");
if (dep === null && !isRelative) {
@@ -5270,6 +5326,7 @@ function preflight7(load: LoadResult): {
if (load.externalTypes.has(spec)) {
continue;
}
+ if (npmBuiltinAliasOf(spec) !== null) continue;
const projectDep = resolveImport7(program, sf, spec, "require");
if (projectDep !== null) {
// A bare require of a JSON document only parses it: nothing to
@@ -5520,12 +5577,18 @@ function cjsNamedImportLinkCheck(
npmStaticDepSf7(program, from, spec, "require");
return dep !== null && isCjsJsFile7(dep, program) ? dep : null;
};
+ // A bundler rewrite spells some lexer-visible names as accessors.
+ const lexWithAccessorNames = (sf: ts.SourceFile): ReturnType => {
+ const lexed = cjsLexedExportsOfFile(sf);
+ for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name);
+ return lexed;
+ };
const visible = (dep: ts.SourceFile, name: string): boolean =>
// `default` is the module.exports binding itself — always provided.
// The program already owns native source ASTs; reuse them without
// starting another syntax session or reparsing their source text.
name === "default" ||
- cjsVisibleNames(dep, cjsLexedExportsOfFile, resolveCjsDep, lexMemo).has(name);
+ cjsVisibleNames(dep, lexWithAccessorNames, resolveCjsDep, lexMemo).has(name);
/** The statement's resolved LOCAL CommonJS dependency, when it is an
* import/re-export from one. */
@@ -6039,6 +6102,7 @@ export { ambientDtsPath, fallbackDtsPath, isNodeTypesPath, overridesDtsPath } fr
export {
builtinDefaultImportModule,
canonicalBuiltinModule,
+ npmBuiltinAliasOf,
SUPPORTED_BUILTIN_MODULES,
} from "./builtin-modules.js";
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.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/ts7/ast-node.ts b/packages/compiler/src/frontend/ts7/ast-node.ts
index 92ab5e4b8..3760fff8d 100644
--- a/packages/compiler/src/frontend/ts7/ast-node.ts
+++ b/packages/compiler/src/frontend/ts7/ast-node.ts
@@ -19,13 +19,22 @@ import { SyntaxKind, NodeFlags } from "./enums.js";
/** A list of child nodes with the list's own position, the shape of tsc-ts's
* NodeList. The file keeps the nodes array, not this object: each read of a
* list field returns a new AstNodeList over the same array, so `nodes` keeps
- * its identity and a decoded file holds no list objects. */
+ * its identity and a decoded file holds no list objects. The position is
+ * decoded from the wire when it is read; most readers only use `nodes`. */
export class AstNodeList {
constructor(
- readonly pos: number,
- readonly end: number,
+ private readonly wire: AstWireFile,
+ private readonly index: number,
readonly nodes: readonly T[],
) {}
+
+ get pos(): number {
+ return this.wire.pos(this.index) >>> 0;
+ }
+
+ get end(): number {
+ return this.wire.end(this.index) >>> 0;
+ }
}
/** One immutable AST response owns one object per node and one array per
@@ -83,7 +92,7 @@ export class AstFile {
this.lists[index] = nodes;
this.materialized?.();
}
- return new AstNodeList(this.wire.pos(index) >>> 0, this.wire.end(index) >>> 0, nodes);
+ return new AstNodeList(this.wire, index, nodes);
}
namedChild(node: AstNode, name: string): number {
diff --git a/packages/compiler/src/frontend/ts7/checker-backend.test.ts b/packages/compiler/src/frontend/ts7/checker-backend.test.ts
new file mode 100644
index 000000000..e37357afa
--- /dev/null
+++ b/packages/compiler/src/frontend/ts7/checker-backend.test.ts
@@ -0,0 +1,79 @@
+import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { afterEach, expect, test } from "vitest";
+import { checkerBackend, tscTsExecutable, tscTsPlatformPackage } from "./checker-backend.js";
+
+const roots: string[] = [];
+
+afterEach(() => {
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
+});
+
+function tempRoot(): string {
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "scriptc-checker-")));
+ roots.push(root);
+ return root;
+}
+
+function writePackage(directory: string, name: string, executable?: string): void {
+ mkdirSync(directory, { recursive: true });
+ writeFileSync(join(directory, "package.json"), JSON.stringify({ name }));
+ if (executable === undefined) return;
+ mkdirSync(join(directory, "lib"), { recursive: true });
+ writeFileSync(join(directory, "lib", executable), "");
+}
+
+test("tsc-ts is the default checker except on Windows, and SCRIPTC_CHECKER selects one", () => {
+ expect(checkerBackend({}, "linux")).toBe("tsc-ts");
+ expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "linux")).toBe("tsc-ts");
+ expect(checkerBackend({}, "darwin")).toBe("tsc-ts");
+ expect(checkerBackend({}, "win32")).toBe("tsgo");
+ expect(checkerBackend({ SCRIPTC_CHECKER: "" }, "win32")).toBe("tsgo");
+ expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "linux")).toBe("tsc-ts");
+ expect(checkerBackend({ SCRIPTC_CHECKER: "tsc-ts" }, "win32")).toBe("tsc-ts");
+ expect(checkerBackend({ SCRIPTC_CHECKER: "tsgo" }, "linux")).toBe("tsgo");
+ expect(() => checkerBackend({ SCRIPTC_CHECKER: "tsc" })).toThrow(
+ 'SCRIPTC_CHECKER must be tsgo or tsc-ts, not "tsc"',
+ );
+});
+
+test("tsc-ts platform packages are named for the host", () => {
+ expect(tscTsPlatformPackage("linux", "x64", false)).toBe("@tsc-ts/linux-x64-gnu");
+ expect(tscTsPlatformPackage("linux", "arm64", true)).toBe("@tsc-ts/linux-arm64-musl");
+ expect(tscTsPlatformPackage("darwin", "arm64", false)).toBe("@tsc-ts/darwin-arm64");
+ expect(tscTsPlatformPackage("win32", "x64", false)).toBe("@tsc-ts/win32-x64-msvc");
+});
+
+test("the tsc-ts executable is found next to an npm install of tsc-ts", () => {
+ const root = tempRoot();
+ writePackage(join(root, "node_modules/tsc-ts"), "tsc-ts");
+ writePackage(join(root, "node_modules/@tsc-ts/linux-x64-gnu"), "@tsc-ts/linux-x64-gnu", "tsc-ts");
+ mkdirSync(join(root, "src/deep"), { recursive: true });
+ expect(tscTsExecutable([join(root, "src/deep")], "@tsc-ts/linux-x64-gnu", "linux")).toBe(
+ join(root, "node_modules/@tsc-ts/linux-x64-gnu/lib/tsc-ts"),
+ );
+});
+
+test("the tsc-ts executable is found in a pnpm install of tsc-ts", () => {
+ const root = tempRoot();
+ const store = join(root, "node_modules/.pnpm/tsc-ts@0.0.3/node_modules");
+ writePackage(join(store, "tsc-ts"), "tsc-ts");
+ writePackage(join(store, "@tsc-ts/win32-x64-msvc"), "@tsc-ts/win32-x64-msvc", "tsc-ts.exe");
+ mkdirSync(join(root, "app/node_modules"), { recursive: true });
+ symlinkSync(join(store, "tsc-ts"), join(root, "app/node_modules/tsc-ts"));
+ expect(tscTsExecutable([join(root, "app")], "@tsc-ts/win32-x64-msvc", "win32")).toBe(
+ join(store, "@tsc-ts/win32-x64-msvc/lib/tsc-ts.exe"),
+ );
+});
+
+test("a missing tsc-ts install or platform package is an error", () => {
+ const root = tempRoot();
+ expect(() => tscTsExecutable([root], "@tsc-ts/linux-x64-gnu", "linux")).toThrow(
+ "the tsc-ts package is not installed",
+ );
+ writePackage(join(root, "node_modules/tsc-ts"), "tsc-ts");
+ expect(() => tscTsExecutable([root], "@tsc-ts/linux-x64-gnu", "linux")).toThrow(
+ "has no @tsc-ts/linux-x64-gnu executable",
+ );
+});
diff --git a/packages/compiler/src/frontend/ts7/checker-backend.ts b/packages/compiler/src/frontend/ts7/checker-backend.ts
new file mode 100644
index 000000000..14418cc94
--- /dev/null
+++ b/packages/compiler/src/frontend/ts7/checker-backend.ts
@@ -0,0 +1,111 @@
+import { existsSync, realpathSync, statSync } from "node:fs";
+import { dirname, join, resolve } from "node:path";
+import { fileURLToPath } from "node:url";
+
+/** The TypeScript 7 API servers a compile can check with: tsc-ts, the default,
+ * and tsgo from the `typescript` package, selected with `SCRIPTC_CHECKER=tsgo`.
+ * Both serve the same API and give the same results. On Windows the default is
+ * tsgo: the compiler connects to the server over a named pipe there, and tsc-ts's
+ * API server has no named-pipe transport yet. */
+export type CheckerBackend = "tsgo" | "tsc-ts";
+
+export function checkerBackend(
+ env: NodeJS.ProcessEnv = process.env,
+ platform: string = process.platform,
+): CheckerBackend {
+ const value = env["SCRIPTC_CHECKER"];
+ if (value === undefined || value === "") return platform === "win32" ? "tsgo" : "tsc-ts";
+ if (value === "tsc-ts") return "tsc-ts";
+ if (value === "tsgo") return "tsgo";
+ throw new Error(`SCRIPTC_CHECKER must be tsgo or tsc-ts, not ${JSON.stringify(value)}`);
+}
+
+/** tsc-ts's platform package for a host: `@tsc-ts/linux-x64-gnu`,
+ * `@tsc-ts/darwin-arm64`, `@tsc-ts/win32-x64-msvc`, and so on. */
+export function tscTsPlatformPackage(
+ platform: string,
+ architecture: string,
+ musl: boolean,
+): string {
+ const abi =
+ platform === "linux" ? (musl ? "-musl" : "-gnu") : platform === "win32" ? "-msvc" : "";
+ return `@tsc-ts/${platform}-${architecture}${abi}`;
+}
+
+let hostMusl: boolean | undefined;
+
+/** Whether this process runs on musl, the libc of tsc-ts's `-musl` Linux packages: Node reports no glibc
+ * runtime version there. The libc cannot change within a process, and a report is costly to generate. */
+export function hostIsMusl(): boolean {
+ if (process.platform !== "linux") return false;
+ if (hostMusl === undefined) {
+ const report = process.report?.getReport() as
+ | { header?: { glibcVersionRuntime?: unknown } }
+ | undefined;
+ hostMusl = typeof report?.header?.glibcVersionRuntime !== "string";
+ }
+ return hostMusl;
+}
+
+/** The checker's part of an executable cache key: the identity of the tsc-ts
+ * executable, or undefined for tsgo, whose keys predate the choice. */
+export function checkerCacheIdentity(env: NodeJS.ProcessEnv = process.env): string | undefined {
+ if (checkerBackend(env) === "tsgo") return undefined;
+ const path = hostTscTsExecutable();
+ const info = statSync(path);
+ return JSON.stringify({
+ path,
+ canonical: realpathSync(path),
+ dev: info.dev,
+ ino: info.ino,
+ size: info.size,
+ mtime: info.mtimeMs,
+ ctime: info.ctimeMs,
+ });
+}
+
+/** The Node host's tsc-ts executable: that of the `tsc-ts` package installed
+ * with this compiler, or else for the working directory. */
+export function hostTscTsExecutable(): string {
+ return tscTsExecutable(
+ [dirname(fileURLToPath(import.meta.url)), process.cwd()],
+ tscTsPlatformPackage(process.platform, process.arch, hostIsMusl()),
+ process.platform,
+ );
+}
+
+/** The tsc-ts executable of the `tsc-ts` package in the nearest
+ * `node_modules` above each of `starts`, in order. The platform package is
+ * installed next to `tsc-ts` (npm) or in the directory that holds it (pnpm). */
+export function tscTsExecutable(
+ starts: readonly string[],
+ platformPackage: string,
+ platform: string,
+): string {
+ const name = platform === "win32" ? "tsc-ts.exe" : "tsc-ts";
+ for (const start of starts) {
+ const packageRoot = installedPackage(start, "tsc-ts");
+ if (packageRoot === undefined) continue;
+ const root = realpathSync(packageRoot);
+ for (const candidate of [
+ join(root, "node_modules", platformPackage),
+ join(dirname(root), platformPackage),
+ ]) {
+ const executable = join(candidate, "lib", name);
+ if (existsSync(executable)) return executable;
+ }
+ throw new Error(`tsc-ts checker: ${root} has no ${platformPackage} executable`);
+ }
+ throw new Error("tsc-ts checker: the tsc-ts package is not installed");
+}
+
+function installedPackage(start: string, name: string): string | undefined {
+ let directory = resolve(start);
+ for (;;) {
+ const candidate = join(directory, "node_modules", name);
+ if (existsSync(join(candidate, "package.json"))) return candidate;
+ const parent = dirname(directory);
+ if (parent === directory) return undefined;
+ directory = parent;
+ }
+}
diff --git a/packages/compiler/src/frontend/ts7/rpc-api.ts b/packages/compiler/src/frontend/ts7/rpc-api.ts
index a6ae609d4..a21ba6279 100644
--- a/packages/compiler/src/frontend/ts7/rpc-api.ts
+++ b/packages/compiler/src/frontend/ts7/rpc-api.ts
@@ -1,6 +1,7 @@
import { createRequire } from "node:module";
import { dirname, join } from "node:path";
import { Ts7Api as ConnectedTs7Api } from "./api.js";
+import { checkerBackend, hostTscTsExecutable } from "./checker-backend.js";
import { Ts7RpcClient } from "./rpc-client.js";
import { TS7_FILE_SYSTEM_CALLBACKS, type Ts7FileSystem } from "./rpc-filesystem.js";
import { spawnTs7Wire } from "./rpc-process.js";
@@ -12,8 +13,15 @@ const { default: getExePath } = require(join(packageRoot, "lib/getExePath.js"))
};
/** Platform-package discovery and process creation remain at the host
- * boundary. The session itself uses no SDK implementation or Node process. */
+ * boundary. The session itself uses no SDK implementation or Node process.
+ * The executable is tsc-ts's (`hostTscTsExecutable`), or tsgo's with
+ * `SCRIPTC_CHECKER=tsgo`. */
export function ts7Executable(): string {
+ return checkerBackend() === "tsgo" ? getExePath() : hostTscTsExecutable();
+}
+
+/** tsgo's executable, from the `typescript` package, whichever checker compiles. */
+export function tsgoExecutable(): string {
return getExePath();
}
diff --git a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts
index cb05db50c..5c5f2e2fc 100644
--- a/packages/compiler/src/frontend/ts7/semantic-checker.test.ts
+++ b/packages/compiler/src/frontend/ts7/semantic-checker.test.ts
@@ -6,7 +6,7 @@ import { API } from "typescript/unstable/sync";
import type { Node as SdkNode } from "typescript/unstable/ast";
import { AstNode } from "./ast-node.js";
import { Ts7RpcClient } from "./rpc-client.js";
-import { ts7Executable } from "./rpc-api.js";
+import { tsgoExecutable } from "./rpc-api.js";
import { spawnTs7Wire } from "./rpc-process.js";
import { Ts7Session } from "./session.js";
import { SyntaxKind } from "./enums.js";
@@ -45,8 +45,9 @@ beforeAll(() => {
files: [file],
}),
);
+ // Compared with the pinned SDK, which runs tsgo.
session = new Ts7Session(
- new Ts7RpcClient(spawnTs7Wire(ts7Executable(), ["--api", "--cwd", directory])),
+ new Ts7RpcClient(spawnTs7Wire(tsgoExecutable(), ["--api", "--cwd", directory])),
);
oracle = new API({ cwd: directory });
});
diff --git a/packages/compiler/src/frontend/ts7/session.test.ts b/packages/compiler/src/frontend/ts7/session.test.ts
index ceaceb113..522b72fe6 100644
--- a/packages/compiler/src/frontend/ts7/session.test.ts
+++ b/packages/compiler/src/frontend/ts7/session.test.ts
@@ -11,7 +11,7 @@ import {
type Ts7FileSystem,
} from "./rpc-filesystem.js";
import { spawnTs7Wire } from "./rpc-process.js";
-import { ts7Executable } from "./rpc-api.js";
+import { tsgoExecutable } from "./rpc-api.js";
import { Ts7Session, Ts7SessionProject, Ts7SessionSnapshot } from "./session.js";
import { AstNode } from "./ast-node.js";
@@ -25,9 +25,10 @@ const fallback: Ts7FileSystem = {
getAccessibleEntries: () => undefined,
};
+// These tests compare the session with the pinned SDK, which runs tsgo, so the session runs tsgo too.
function transport(cwd: string, fs: Ts7FileSystem = fallback, timing = false) {
const rpc = new Ts7RpcClient(
- spawnTs7Wire(ts7Executable(), [
+ spawnTs7Wire(tsgoExecutable(), [
"--api",
"--cwd",
cwd,
diff --git a/packages/compiler/src/frontend/ts7/source-parser.test.ts b/packages/compiler/src/frontend/ts7/source-parser.test.ts
index 1fae05ff6..d5b54f080 100644
--- a/packages/compiler/src/frontend/ts7/source-parser.test.ts
+++ b/packages/compiler/src/frontend/ts7/source-parser.test.ts
@@ -208,3 +208,30 @@ test("JSDoc callable types cannot replace method body views", () => {
expect(member.body?.parent).toBe(member);
}
});
+
+test("a parser starts a new server once it has parsed its source budget, and earlier results stay usable", () => {
+ const directory = mkdtempSync(
+ join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-source-parser-"),
+ );
+ directories.push(directory);
+ const servers: Ts7Api[] = [];
+ const parser = new Ts7SourceParser(
+ (options) => {
+ const api = new Ts7Api(options);
+ servers.push(api);
+ return api;
+ },
+ directory,
+ 64,
+ );
+ parsers.push(parser);
+ const first = parser.parse("a.ts", `export const a = ${"1 + ".repeat(20)}1;`, "ts");
+ expect(servers).toHaveLength(1);
+ const second = parser.parse("b.ts", "export const b = 2;", "ts");
+ expect(servers).toHaveLength(2);
+ const third = parser.parse("b.ts", "export const b = 3;", "ts");
+ expect(servers).toHaveLength(2);
+ expect(first.statements.nodes[0]!.getText()).toBe(`export const a = ${"1 + ".repeat(20)}1;`);
+ expect(second.statements.nodes[0]!.getText()).toBe("export const b = 2;");
+ expect(third.statements.nodes[0]!.getText()).toBe("export const b = 3;");
+});
diff --git a/packages/compiler/src/frontend/ts7/source-parser.ts b/packages/compiler/src/frontend/ts7/source-parser.ts
index 38605cd64..f9029774c 100644
--- a/packages/compiler/src/frontend/ts7/source-parser.ts
+++ b/packages/compiler/src/frontend/ts7/source-parser.ts
@@ -7,12 +7,21 @@ import { tsgoPath } from "./session-path.js";
export type Ts7SourceKind = "js" | "jsx" | "ts" | "tsx";
export type Ts7SourceApiFactory = (options: { cwd: string; fs: Ts7FileSystem }) => Ts7Api;
+/** The source text, in UTF-16 code units, one parser server parses before a
+ * new server replaces it. tsc-ts keeps the syntax trees it builds for the
+ * life of its process, so a server that parses many files grows with them; a
+ * new one starts empty. Results are detached ASTs and outlive the server that
+ * made them. */
+const SERVER_SOURCE_BUDGET = 256 * 1024;
+
/** A syntax-only session with a closed, in-memory filesystem. Parsing a
* source string never reads imports, project configuration, or libraries
* from disk. The returned AST owns its wire data and outlives the session.
* Only one project and one input are retained between requests. */
export class Ts7SourceParser {
- private readonly api: Ts7Api;
+ private api: Ts7Api;
+ private readonly createApi: Ts7SourceApiFactory;
+ private parsedSource = 0;
private readonly files = new Map();
private readonly configPath: string;
private previousPath: string | undefined;
@@ -21,10 +30,19 @@ export class Ts7SourceParser {
private closed = false;
private readonly cwd: string;
- constructor(createApi: Ts7SourceApiFactory, cwd = process.cwd()) {
+ constructor(
+ createApi: Ts7SourceApiFactory,
+ cwd = process.cwd(),
+ private readonly sourceBudget = SERVER_SOURCE_BUDGET,
+ ) {
this.cwd = resolve(cwd);
this.configPath = tsgoPath(resolve(this.cwd, "__scriptc-source-parser.tsconfig.json"));
- this.api = createApi({
+ this.createApi = createApi;
+ this.api = this.connect();
+ }
+
+ private connect(): Ts7Api {
+ return this.createApi({
cwd: this.cwd,
fs: {
readFile: (path) => this.files.get(tsgoPath(path)) ?? null,
@@ -61,7 +79,14 @@ export class Ts7SourceParser {
this.previousFile !== undefined
)
return this.previousFile;
- const previousPath = this.previousPath;
+ let previousPath = this.previousPath;
+ if (this.parsedSource >= this.sourceBudget) {
+ this.api.close();
+ this.api = this.connect();
+ this.parsedSource = 0;
+ previousPath = undefined;
+ }
+ this.parsedSource += source.length;
this.files.clear();
this.files.set(path, source);
this.files.set(
diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts
index 1986ef5a0..4c3dd92bc 100644
--- a/packages/compiler/src/frontend/type-mapper.ts
+++ b/packages/compiler/src/frontend/type-mapper.ts
@@ -980,7 +980,7 @@ function isDeclaredInAmbientModule(d: ts.Declaration, name: string): boolean {
/** True when the declaration's nearest enclosing namespace is `name`
* (`declare global { namespace NodeJS { ... } }` — the NodeJS-global
* interfaces @types/node declares outside any ambient module). */
-function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean {
+export function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean {
let node: ts.Node | undefined = d.parent;
while (node) {
if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) {
@@ -991,6 +991,31 @@ function isDeclaredInAmbientNamespace(d: ts.Declaration, name: string): boolean
return false;
}
+/** Whether `type` is one of the stream spellings the general stream
+ * representation (TypeMapperCtx.generalNodeStreams) maps differently:
+ * NodeJS.ReadableStream / NodeJS.WritableStream, @types/node's
+ * stream.Readable / stream.Writable, or the fallback declarations'
+ * ChildStdin. */
+export function isGeneralNodeStreamInterface(
+ type: ts.Type,
+ ctx: Pick,
+): boolean {
+ const psym = type.getSymbol() ?? type.getAliasSymbol();
+ if (psym === undefined) return false;
+ return psym.declarations.some((d) => {
+ if (!(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d))) return false;
+ if (!ctx.isStdlibFile(d.getSourceFile())) return false;
+ if (psym.name === "ReadableStream" || psym.name === "WritableStream")
+ return ts.isInterfaceDeclaration(d) && isDeclaredInAmbientNamespace(d, "NodeJS");
+ if (psym.name === "ChildStdin") return isDeclaredInAmbientModule(d, "child_process");
+ return (
+ (psym.name === "Readable" || psym.name === "Writable") &&
+ isNodeTypesPath(d.getSourceFile().fileName) &&
+ isDeclaredInAmbientModule(d, "stream")
+ );
+ });
+}
+
/** A type declared in the shipped WebAssembly namespace (scriptc.d.ts):
* its modules, instances, memories, tables, globals, error classes and the
* Exports/Imports records. */
@@ -1146,6 +1171,19 @@ export interface TypeMapperCtx {
* type descriptions for project-owned values, but their imported runtime
* bindings are fenced separately by the Lowerer. */
isExternalTypeFile: (sf: ts.SourceFile) => boolean;
+ /** The program's general stream representation (lowerToIr decides it
+ * per program): NodeJS.ReadableStream / NodeJS.WritableStream and
+ * @types/node's stream.Readable / stream.Writable map to the runtime
+ * stream classes instead of the child/process stdio handles, so values
+ * from every stream producer share their slots. Off (the default), the
+ * stdio handles keep those spellings. */
+ generalNodeStreams?: boolean;
+ /** With generalNodeStreams, the program's checked-dynamic stream slots
+ * (lowerToIr decides it per program, dyn-streams.ts): NodeJS.ReadableStream
+ * and NodeJS.WritableStream map to the checked-dynamic representation, so
+ * a slot holds a native stream (a boxed runtime stream, identity kept) or
+ * a JavaScript stream object. Members dispatch per value at run time. */
+ dynNodeStreams?: boolean;
/** Successful context-free mappings, keyed by the
* TypeScript server's stable type id. A generated facade can reference the
* same large record/function type thousands of times; remapping its full
@@ -1355,6 +1393,136 @@ function isWorkerHandleSymbol(
/** The `{ message: any }` object type (or its union with undefined) that
* worker_threads declares as receiveMessageOnPort's result. */
+/** A union of dns lookup option bags with numbers and nullish values that
+ * includes LookupAllOptions (whose `all: true` arm has no record form). */
+function isLookupOptionsUnion(type: ts.Type, ctx: Pick): boolean {
+ if (!type.isUnionType()) return false;
+ let all = false;
+ for (const member of ts.constituentTypes(type)) {
+ if (member.flags & (ts.TypeFlags.NumberLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined))
+ continue;
+ const symbol = member.getSymbol();
+ if (
+ symbol === undefined ||
+ !/^Lookup(One|All)?Options$/.test(symbol.name) ||
+ !symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
+ )
+ return false;
+ if (symbol.name === "LookupAllOptions") all = true;
+ }
+ return all;
+}
+
+/** The binary and stream members of fetch's BodyInit (undici-types):
+ * NodeJS.ArrayBufferView expands to the typed arrays and DataView. */
+const FETCH_BODY_MEMBERS: ReadonlySet = new Set([
+ "ArrayBuffer",
+ "SharedArrayBuffer",
+ "AsyncIterable",
+ "Iterable",
+ "Blob",
+ "FormData",
+ "URLSearchParams",
+ "ReadableStream",
+ "DataView",
+ "Buffer",
+ "Int8Array",
+ "Uint8Array",
+ "Uint8ClampedArray",
+ "Int16Array",
+ "Uint16Array",
+ "Int32Array",
+ "Uint32Array",
+ "Float16Array",
+ "Float32Array",
+ "Float64Array",
+ "BigInt64Array",
+ "BigUint64Array",
+]);
+
+/** fetch's request body type, BodyInit, however TypeScript spells it
+ * (`RequestInit["body"]`, `NonNullable<...>`, an optional member): a
+ * union of strings, the stdlib binary and stream types and nullish
+ * values that includes both FormData and URLSearchParams. Native fetch
+ * reads such a body at run time, so it rides checked-dynamic storage. */
+function isFetchBodyUnion(type: ts.Type, ctx: Pick): boolean {
+ if (!type.isUnionType()) return false;
+ let formData = false;
+ let searchParams = false;
+ for (const member of ts.constituentTypes(type)) {
+ if (member.flags & (ts.TypeFlags.StringLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined))
+ continue;
+ const symbol = member.getSymbol();
+ if (
+ symbol === undefined ||
+ !FETCH_BODY_MEMBERS.has(symbol.name) ||
+ !symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
+ )
+ return false;
+ if (symbol.name === "FormData") formData = true;
+ if (symbol.name === "URLSearchParams") searchParams = true;
+ }
+ return formData && searchParams;
+}
+
+/** A mapped object type (Omit, Pick, Partial) whose members all come from
+ * one stdlib interface that maps to the checked dynamic. */
+function isDynInterfaceSubset(type: ts.Type, ctx: TypeMapperCtx): boolean {
+ if (!(type.flags & ts.TypeFlags.Object) || type.isClassOrInterface()) return false;
+ const alias = type.getAliasSymbol();
+ if (alias === undefined) return false;
+ const props = ctx.checker.getPropertiesOfType(type);
+ if (props.length === 0) return false;
+ let owner: ts.InterfaceDeclaration | undefined;
+ for (const prop of props) {
+ const decl = prop.declarations[0];
+ const parent = decl?.parent;
+ if (
+ parent === undefined ||
+ !ts.isInterfaceDeclaration(parent) ||
+ !ctx.isStdlibFile(parent.getSourceFile()) ||
+ (owner !== undefined && owner !== parent)
+ )
+ return false;
+ owner = parent;
+ }
+ return (
+ owner !== undefined && mapType(ctx.checker.getTypeAtLocation(owner.name), ctx)?.kind === "dyn"
+ );
+}
+
+/** fetch's HeadersInit (undici-types): a union of the stdlib Headers
+ * class with header-pair arrays and/or a header record, plus nullish
+ * values. Native fetch reads it at run time like the init around it. */
+function isFetchHeadersUnion(type: ts.Type, ctx: Pick): boolean {
+ if (!type.isUnionType()) return false;
+ let headers = false;
+ let other = false;
+ for (const member of ts.constituentTypes(type)) {
+ if (member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) continue;
+ const symbol = member.getSymbol();
+ if (
+ symbol?.name === "Headers" &&
+ symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
+ ) {
+ headers = true;
+ continue;
+ }
+ const alias = member.getAliasSymbol()?.name;
+ if (
+ symbol?.name === "Array" ||
+ symbol?.name === "ReadonlyArray" ||
+ alias === "Record" ||
+ alias === "HeaderRecord"
+ ) {
+ other = true;
+ continue;
+ }
+ return false;
+ }
+ return headers && other;
+}
+
function isReceiveMessageResult(
type: ts.Type,
ctx: Pick,
@@ -1522,6 +1690,25 @@ function classExprNeedsContext(decl: ts.ClassLikeDeclaration): boolean {
return false;
}
+/** True for the type of a TypeScript object literal with no contextual
+ * type (`const options = { ...location, path }`) that spreads a value
+ * mapping to the checked dynamic (a stdlib options type such as
+ * http.RequestOptions). A literal with a contextual type is built in that
+ * type's representation instead, and JavaScript literals keep their own
+ * rules. */
+function spreadsCheckedDynamic(symbol: ts.Symbol | undefined, ctx: TypeMapperCtx): boolean {
+ if (symbol === undefined || (symbol.flags & ts.SymbolFlags.ObjectLiteral) === 0) return false;
+ const decl = symbol.declarations[0];
+ if (decl === undefined || !ts.isObjectLiteralExpression(decl)) return false;
+ if (isJsSourceFile(decl.getSourceFile())) return false;
+ if (ctx.checker.getContextualType(decl)) return false;
+ return decl.properties.nodes.some(
+ (prop) =>
+ ts.isSpreadAssignment(prop) &&
+ mapType(ctx.checker.getTypeAtLocation(prop.expression), ctx)?.kind === "dyn",
+ );
+}
+
function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const { checker, unions, classNamer, resolveTypeParam } = ctx;
// TypeScript's intrinsic globalThis symbol has no declaration. A stored
@@ -1542,9 +1729,24 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const moduleId = ctx.moduleNamespaceId?.(type) ?? null;
if (moduleId !== null) return { kind: "moduleNs", moduleId };
}
+ // A TypeScript object literal that spreads a checked-dynamic value
+ // (`{ ...requestOptions, path, timeout }`): its keys are the source's
+ // runtime keys, which no record shape holds, so the literal rides
+ // checked-dynamic storage like its source.
+ if (spreadsCheckedDynamic(intrinsic, ctx)) return DYN;
// worker_threads.receiveMessageOnPort's `{ message: any } | undefined`:
// the runtime's { message } object, a checked-dynamic value.
if (isReceiveMessageResult(type, ctx)) return DYN;
+ if (isFetchBodyUnion(type, ctx) || isFetchHeadersUnion(type, ctx)) return DYN;
+ // Omit/Pick/Partial of a stdlib interface that rides checked-dynamic
+ // storage (`Omit`): every member comes
+ // from that interface, so the value is a subset of the same runtime-read
+ // options object and rides the same storage.
+ if (isDynInterfaceSubset(type, ctx)) return DYN;
+ // The lookup-function option union (`LookupOneOptions | LookupAllOptions
+ // | number | undefined`): with a LookupAllOptions arm it is an option bag
+ // dns.lookup reads at run time, so it rides checked-dynamic storage.
+ if (isLookupOptionsUnion(type, ctx)) return DYN;
// @types/node spells child_process.ExecFileException as an intersection
// type alias over two Error-derived metadata interfaces. The value is
// still one ordinary Error; recognize the owned alias before generic
@@ -1580,6 +1782,18 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// option) holds that dynamic value too, so the whole union is dynamic.
if (type.isUnionType() && ts.constituentTypes(type).some((t) => isWebAssemblyType(t, ctx)))
return DYN;
+ // JavaScript inference merges WebAssembly.instantiate's overloads into
+ // `Promise & Promise`: a promise
+ // of a WebAssembly value either way.
+ if (
+ type.isIntersectionType() &&
+ ts.constituentTypes(type).every((t) => {
+ if (t.getSymbol()?.name !== "Promise") return false;
+ const arg = ctx.checker.getTypeArguments(t as ts.TypeReference)[0];
+ return arg !== undefined && isWebAssemblyType(arg, ctx);
+ })
+ )
+ return { kind: "promise", inner: DYN };
// Immutable (@scriptc/types) is a deep-readonly view: the value is
// still the T it maps.
const immutableSource = immutableSourceType(type, ctx);
@@ -1641,6 +1855,16 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
contextResolutions++;
return viaIndexed;
}
+ // `Parameters` / `ReturnType` over a bound function-typed
+ // parameter (variadic higher-order wrappers): the checker keeps both
+ // conditionals symbolic inside the body, and their `infer` positions
+ // match a call signature, which matchInferReference cannot decide.
+ const viaSignature =
+ mapBoundSignatureAlias(type, ctx) ?? mapBoundKeyofNeverConditional(type, ctx);
+ if (viaSignature !== null) {
+ contextResolutions++;
+ return viaSignature;
+ }
// A conditional type over a bound parameter (`Infer`, a
// mapped type's member): evaluate it against the bindings.
if (type.isConditionalType()) {
@@ -2511,6 +2735,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// spawn consumes a presence-sensitive option bag, including optional
// stdio and env values. Keep it checked-dynamic for native normalization.
if (isStdlibInterface("SpawnOptions")) return DYN;
+ // dns.LookupAllOptions (`all: true`, the overload whose callback takes
+ // an address list): a lookup option bag dns.lookup reads at run time.
+ // It rides checked-dynamic storage, so the lookup function type that
+ // takes `LookupOneOptions | LookupAllOptions | number` compiles.
+ if (isStdlibInterface("LookupAllOptions")) return DYN;
// http.RequestOptions / https.RequestOptions as a declared type: an
// option record whose members include functions, agents and signals.
// Values ride checked-dynamic storage, like SpawnOptions.
@@ -2998,6 +3227,41 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// global-namespace interface real code writes in its own signatures —
// prefixStream's `output` param). The representation is the raw fd
// scalar; the lowered surface is write(string).
+ // Under the program's general stream representation (generalNodeStreams)
+ // NodeJS.WritableStream is the runtime Writable instead: the process
+ // streams convert into it where they flow (process.stdioWritable).
+ // Checked-dynamic stream slots: an intersection over a NodeJS stream
+ // interface (`NodeJS.ReadableStream & { destroy?(): void }`) holds the
+ // same values as the interface.
+ if (
+ ctx.dynNodeStreams === true &&
+ (type.flags & ts.TypeFlags.Intersection) !== 0 &&
+ ts.constituentTypes(type).some((part) => {
+ const sym = part.getSymbol();
+ return (
+ (sym?.name === "ReadableStream" || sym?.name === "WritableStream") &&
+ sym.declarations.some(
+ (d) =>
+ ts.isInterfaceDeclaration(d) &&
+ ctx.isStdlibFile(d.getSourceFile()) &&
+ isDeclaredInAmbientNamespace(d, "NodeJS"),
+ )
+ );
+ })
+ ) {
+ return DYN;
+ }
+ const nodeWritableInterface =
+ psym?.name === "WritableStream" &&
+ psym.declarations.some(
+ (d) =>
+ ts.isInterfaceDeclaration(d) &&
+ ctx.isStdlibFile(d.getSourceFile()) &&
+ isDeclaredInAmbientNamespace(d, "NodeJS"),
+ );
+ if (nodeWritableInterface && ctx.generalNodeStreams === true) {
+ return ctx.dynNodeStreams === true ? DYN : { kind: "object", className: "%Writable" };
+ }
if (
(psym?.name === "WriteStream" &&
psym.declarations.some(
@@ -3007,13 +3271,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
(isDeclaredInAmbientModule(d, "tty") ||
(isDeclaredInAmbientModule(d, "process") && isDeclaredInAmbientNamespace(d, "NodeJS"))),
)) ||
- (psym?.name === "WritableStream" &&
- psym.declarations.some(
- (d) =>
- ts.isInterfaceDeclaration(d) &&
- ctx.isStdlibFile(d.getSourceFile()) &&
- isDeclaredInAmbientNamespace(d, "NodeJS"),
- ))
+ nodeWritableInterface
) {
return PROCSTREAM_T;
}
@@ -3023,6 +3281,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// of the broad @types class fence where they are constructed.
if (
(psym?.name === "ChildStdin" &&
+ ctx.generalNodeStreams !== true &&
psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
@@ -3030,6 +3289,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
isDeclaredInAmbientModule(d, "child_process"),
)) ||
(psym?.name === "Writable" &&
+ ctx.generalNodeStreams !== true &&
psym.declarations.some(
(d) =>
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
@@ -3040,6 +3300,20 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
) {
return { kind: "childWriter" };
}
+ // Under the general stream representation the fallback declarations'
+ // ChildStdin is the runtime Writable too (child.stdioStream bridges it).
+ if (
+ ctx.generalNodeStreams === true &&
+ psym?.name === "ChildStdin" &&
+ psym.declarations.some(
+ (d) =>
+ ts.isInterfaceDeclaration(d) &&
+ ctx.isStdlibFile(d.getSourceFile()) &&
+ isDeclaredInAmbientModule(d, "child_process"),
+ )
+ ) {
+ return { kind: "object", className: "%Writable" };
+ }
// The static node:stream classes (the shipped fallback declarations —
// NOT @types/node, whose stream.Readable also types child stdio; under
// @types/node the childStream mapping below keeps priority and the
@@ -3047,6 +3321,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// branch: the fallback's Readable must map to the runtime class, and
// under the fallback child.stdout is NodeJS.ReadableStream (its own
// branch below), so the two never collide.
+ // @types/node's Duplex/Transform/PassThrough classes have no child-stdio
+ // reading, so they map to the runtime classes too; its Readable and
+ // Writable join them under the general representation.
for (const [irName, rec] of RUNTIME_STREAM_CLASSES) {
if (
psym?.name === rec.lib &&
@@ -3054,38 +3331,39 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
(d) =>
ts.isClassDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
- !isNodeTypesPath(d.getSourceFile().fileName) &&
+ (!isNodeTypesPath(d.getSourceFile().fileName) ||
+ ctx.generalNodeStreams === true ||
+ (rec.lib !== "Readable" && rec.lib !== "Writable")) &&
isDeclaredInAmbientModule(d, "stream"),
)
) {
return { kind: "object", className: irName };
}
}
- // node:zlib's stream classes (the shipped fallback declarations): their
- // instances are runtime Transform streams.
+ // node:zlib's stream classes (the shipped fallback declarations and
+ // @types/node's): their instances are runtime Transform streams.
if (
psym !== undefined &&
ZLIB_STREAM_MODES.has(psym.name) &&
psym.declarations.some(
(d) =>
- ts.isClassDeclaration(d) &&
+ (ts.isClassDeclaration(d) ||
+ (ts.isInterfaceDeclaration(d) && isNodeTypesPath(d.getSourceFile().fileName))) &&
ctx.isStdlibFile(d.getSourceFile()) &&
- !isNodeTypesPath(d.getSourceFile().fileName) &&
isDeclaredInAmbientModule(d, "zlib"),
)
) {
return { kind: "object", className: "%Transform" };
}
- // fs.ReadStream / fs.WriteStream (the shipped fallback declarations):
- // their instances are runtime Readable / Writable streams
- // (scr_fs_stream.c).
+ // fs.ReadStream / fs.WriteStream (the shipped fallback declarations and
+ // @types/node's): their instances are runtime Readable / Writable
+ // streams (scr_fs_stream.c).
if (
(psym?.name === "ReadStream" || psym?.name === "WriteStream") &&
psym.declarations.some(
(d) =>
ts.isClassDeclaration(d) &&
ctx.isStdlibFile(d.getSourceFile()) &&
- !isNodeTypesPath(d.getSourceFile().fileName) &&
isDeclaredInAmbientModule(d, "fs"),
)
) {
@@ -3107,6 +3385,14 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
isDeclaredInAmbientNamespace(d, "NodeJS"),
))
) {
+ // The general representation: the runtime Readable, which child
+ // stdio reads produce through the child stream bridge. With
+ // checked-dynamic stream slots, NodeJS.ReadableStream holds any
+ // stream value (dyn-streams.ts); stream.Readable keeps the class.
+ if (ctx.generalNodeStreams === true) {
+ if (ctx.dynNodeStreams === true && psym.name === "ReadableStream") return DYN;
+ return { kind: "object", className: "%Readable" };
+ }
return { kind: "childStream" };
}
// dgram.Socket: the same ambiguous "Socket" name disambiguated by its
@@ -3817,6 +4103,27 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
return { kind: "record", shapeId: perfEntryShape(ctx.shapes, ctx.unions) };
}
// readline's getCursorPos() record, in Node's key order ({ cols, rows }).
+ // perf_hooks' EventLoopUtilization (performance.eventLoopUtilization's
+ // result): a plain data record in Node's key order.
+ if (isStdlibInterface("EventLoopUtilization")) {
+ return {
+ kind: "record",
+ shapeId: ctx.shapes.intern(
+ [
+ { name: "active", type: F64 },
+ { name: "idle", type: F64 },
+ { name: "utilization", type: F64 },
+ ],
+ false,
+ undefined,
+ ["idle", "active", "utilization"],
+ ),
+ };
+ }
+ // monitorEventLoopDelay's IntervalHistogram: a numeric handle into the
+ // runtime's histogram table, like a timer handle. Its members lower over
+ // the handle (builtins/performance.ts).
+ if (isStdlibInterface("IntervalHistogram")) return F64;
if (isStdlibInterface("CursorPos")) {
return {
kind: "record",
@@ -3844,6 +4151,34 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
),
};
}
+ // v8.getHeapStatistics()'s record, in Node's key order (builtins/v8.ts).
+ if (isStdlibInterface("HeapInfo")) {
+ const names = [
+ "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",
+ ];
+ return {
+ kind: "record",
+ shapeId: ctx.shapes.intern(
+ [...names].sort().map((name) => ({ name, type: F64 })),
+ false,
+ undefined,
+ names,
+ ),
+ };
+ }
// process.cpuUsage()/threadCpuUsage()'s {user, system} microsecond
// record and resourceUsage()'s getrusage record — stdlib data
// interfaces interned explicitly (the RemoteInfo pattern; declared
@@ -4125,10 +4460,23 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const mapped = mapType(checker.getTypeOfSymbol(p), ctx);
if (!mapped) return false;
const arms = mapped.kind === "union" ? ctx.unions.get(mapped.unionId)?.arms : [mapped];
+ // Under the general stream representation the stdio members are the
+ // runtime stream classes.
+ const isExpected = (arm: IrType): boolean =>
+ arm.kind === expected ||
+ (ctx.generalNodeStreams === true &&
+ arm.kind === "object" &&
+ ((expected === "childStream" && arm.className === "%Readable") ||
+ (expected === "childWriter" && arm.className === "%Writable"))) ||
+ // Checked-dynamic stream slots: `NodeJS.ReadableStream | null`
+ // collapses to dyn (the reads still produce the bridged stream).
+ (ctx.dynNodeStreams === true &&
+ arm.kind === "dyn" &&
+ (expected === "childStream" || expected === "childWriter"));
return (
arms !== undefined &&
- arms.some((arm) => arm.kind === expected) &&
- arms.every((arm) => arm.kind === expected || arm.kind === unit)
+ arms.some(isExpected) &&
+ arms.every((arm) => isExpected(arm) || arm.kind === unit)
);
};
const childShaped =
@@ -4351,11 +4699,44 @@ function mapCallSignature(sig: ts.Signature, ctx: TypeMapperCtx): IrType | null
// declaration as a typed packed-array closure would bypass its boxed
// spread/error semantics and change coverage classification.
if (rest && decl !== undefined && isJsSourceFile(decl.getSourceFile())) return null;
+ // A fixed-tuple rest (`...args: [A, B]`, `...args: Parameters` over
+ // a bound F) flattens to its positional parameters. Pattern rests keep
+ // their packed-record ABI and stay unmapped as values.
+ if (rest && decl !== undefined && ts.isParameter(decl) && ts.isIdentifier(decl.name)) {
+ const elems = restTupleParamTypes(checker.getTypeOfSymbol(p), ctx);
+ if (elems) {
+ params.push(...elems);
+ continue;
+ }
+ }
const optional =
decl !== undefined &&
ts.isParameter(decl) &&
(decl.questionToken !== undefined || decl.initializer !== undefined);
let pt = mapType(checker.getTypeOfSymbol(p), ctx);
+ // A parameter a TypeScript program types `any` takes whatever the
+ // caller passes: the checked-dynamic slot, as an `any`-typed binding
+ // already is (dynFallbackType). Declaration files keep their paths.
+ // Only function declarations, spelled function types and lambda
+ // parameters whose default is the only source of their type (`(n =
+ // lookup() ?? 5) => ...` with an `any` lookup): other lambdas and
+ // methods in object literals keep their existing checked-dynamic story.
+ if (
+ !pt &&
+ !rest &&
+ decl !== undefined &&
+ ts.isParameter(decl) &&
+ decl.parent !== undefined &&
+ (ts.isFunctionDeclaration(decl.parent) ||
+ ts.isFunctionTypeNode(decl.parent) ||
+ ((ts.isArrowFunction(decl.parent) || ts.isFunctionExpression(decl.parent)) &&
+ decl.initializer !== undefined &&
+ decl.type === undefined &&
+ !isJsSourceFile(decl.getSourceFile()))) &&
+ !decl.getSourceFile().isDeclarationFile &&
+ (checker.getTypeOfSymbol(p).flags & ts.TypeFlags.Any) !== 0
+ )
+ pt = DYN;
if (
decl &&
ts.isParameter(decl) &&
@@ -4503,6 +4884,9 @@ function unionDiscriminant(
): IrUnionDiscriminant | undefined {
if (arms.filter((arm) => arm.kind === "record").length < 2 || parts.length < 2) return undefined;
const { checker } = ctx;
+ // Tuple shapes keep positions only: their checker `length` literal has
+ // no slot to compare (discriminantField refuses tuple shapes too).
+ if (parts.some((part) => ctx.shapes.get(part.mapped.shapeId)?.tuple)) return undefined;
for (const candidate of checker.getPropertiesOfType(parts[0]!.source)) {
if (candidate.name.startsWith("__@")) continue;
const byTag = new Map();
@@ -4840,6 +5224,167 @@ function mapBoundIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | null
return mapType(checker.getTypeOfSymbol(prop), ctx);
}
+/** The standard library's `Parameters` / `ReturnType` over a type
+ * parameter the current instantiation binds to a callable checker type:
+ * that type's one call signature. Concrete arguments never reach here (the
+ * checker evaluates those conditionals itself); generic bodies keep them
+ * symbolic because their `infer` sits in a call signature. */
+function boundSignatureAlias(
+ type: ts.Type,
+ ctx: TypeMapperCtx,
+): { name: "Parameters" | "ReturnType"; sig: ts.Signature } | null {
+ const { checker, resolveTypeParamTs } = ctx;
+ if (!resolveTypeParamTs) return null;
+ const alias = type.getAliasSymbol();
+ if (!alias || (alias.name !== "Parameters" && alias.name !== "ReturnType")) return null;
+ const aliasArgs = type.getAliasTypeArguments();
+ const arg = aliasArgs[0];
+ if (aliasArgs.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return null;
+ const isLibAlias = alias.declarations.some(
+ (d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()),
+ );
+ if (!isLibAlias) return null;
+ const bound = resolveTypeParamTs(arg);
+ if (!bound || bound.flags & ts.TypeFlags.TypeParameter) return null;
+ const sigs = checker.getCallSignatures(bound);
+ const sig = sigs[0];
+ if (sigs.length !== 1 || !sig || sig.getTypeParameters().length > 0) return null;
+ return { name: alias.name, sig };
+}
+
+/** One position of a fixed-arity parameter tuple. An optional position
+ * still has a native slot (the function type's `T | undefined` slot). */
+interface TupleElement {
+ type: ts.Type;
+ optional: boolean;
+}
+
+/** The positional parameters of a signature as a fixed tuple: a trailing
+ * rest flattened when its own type is a fixed tuple. Null for a variadic
+ * position, which has no fixed native arity. */
+function signatureParameters(sig: ts.Signature, ctx: TypeMapperCtx): TupleElement[] | null {
+ const { checker } = ctx;
+ const out: TupleElement[] = [];
+ for (const p of sig.getParameters()) {
+ const decl = p.valueDeclaration;
+ const type = checker.getTypeOfSymbol(p);
+ if (decl !== undefined && ts.isParameter(decl)) {
+ if (decl.dotDotDotToken !== undefined) {
+ const inner = restTupleElements(type, ctx);
+ if (!inner) return null;
+ out.push(...inner);
+ continue;
+ }
+ out.push({
+ type,
+ optional: decl.questionToken !== undefined || decl.initializer !== undefined,
+ });
+ continue;
+ }
+ out.push({ type, optional: (p.flags & ts.SymbolFlags.Optional) !== 0 });
+ }
+ return out;
+}
+
+/** The required-only positional parameter types of a signature (the
+ * value-level `Parameters` tuple), null when a position is optional
+ * or variadic: such a tuple's length varies per call. */
+function signatureParameterTypes(sig: ts.Signature, ctx: TypeMapperCtx): ts.Type[] | null {
+ const elems = signatureParameters(sig, ctx);
+ if (!elems || elems.some((e) => e.optional)) return null;
+ return elems.map((e) => e.type);
+}
+
+/** The element types of a REST parameter's type when it is a fixed tuple:
+ * a tuple of required elements (`[number, string]`, the empty `[]`), a type
+ * parameter bound to one, or `Parameters` over a bound function type.
+ * Such a rest parameter has a compile-time arity, so its native ABI is the
+ * flattened positional list (exactly the equivalent fixed signature —
+ * TypeScript treats `(...a: [A, B]) => R` and `(a: A, b: B) => R` alike). */
+export function restTupleElementTypes(type: ts.Type, ctx: TypeMapperCtx): ts.Type[] | null {
+ return restTupleElements(type, ctx)?.map((e) => e.type) ?? null;
+}
+
+/** restTupleElementTypes with each position's optionality: a rest tuple
+ * may end in optional positions (`[A, B?]`, `Parameters<(a: A, b?: B) =>
+ * R>`), which flatten to `T | undefined` slots exactly as the function
+ * type's optional parameters do. */
+function restTupleElements(type: ts.Type, ctx: TypeMapperCtx): TupleElement[] | null {
+ const { checker } = ctx;
+ if (type.flags & ts.TypeFlags.TypeParameter) {
+ const bound = ctx.resolveTypeParamTs?.(type);
+ return bound && bound !== type ? restTupleElements(bound, ctx) : null;
+ }
+ const viaAlias = boundSignatureAlias(type, ctx);
+ if (viaAlias) {
+ return viaAlias.name === "Parameters" ? signatureParameters(viaAlias.sig, ctx) : null;
+ }
+ if (!checker.isTupleType(type)) return null;
+ const ref = type as ts.TupleTypeReference;
+ const tupleShape =
+ (ref.elementFlags as ts.ElementFlags[] | undefined) !== undefined
+ ? ref
+ : (ref.getTarget() as ts.TupleType | undefined);
+ const args = checker.getTypeArguments(ref);
+ if (args.length === 0) return [];
+ const flags = tupleShape?.elementFlags;
+ if (flags === undefined) return null;
+ if (flags.some((f) => !(f & (ts.ElementFlags.Required | ts.ElementFlags.Optional)))) return null;
+ return args.map((t, i) => ({
+ type: t,
+ optional: (flags[i]! & ts.ElementFlags.Optional) !== 0,
+ }));
+}
+
+/** One tuple element's IR type under the tuple mapping's rules. */
+function mapTupleElement(t: ts.Type, ctx: TypeMapperCtx): IrType | null {
+ let et = (t.flags & ts.TypeFlags.Any) !== 0 ? DYN : mapType(t, ctx);
+ if (et?.kind === "void" && isUnitOnlyTsType(t, ctx.resolveTypeParam))
+ et = unitOnlyUnion(ctx.unions);
+ return !et || et.kind === "void" ? null : et;
+}
+
+/** The mapped element types of a fixed-tuple rest parameter type
+ * (restTupleElementTypes), null when it is not one or an element does not
+ * map. */
+export function restTupleParamTypes(type: ts.Type, ctx: TypeMapperCtx): IrType[] | null {
+ const elems = restTupleElements(type, ctx);
+ if (!elems) return null;
+ const out: IrType[] = [];
+ for (const e of elems) {
+ let et = mapTupleElement(e.type, ctx);
+ // An optional position's slot holds undefined when the caller omits
+ // it, as an optional parameter's does (mapCallSignature).
+ if (et && e.optional && et.kind !== "dyn") et = withUndefinedArm(et, ctx.unions);
+ if (!et) return null;
+ out.push(et);
+ }
+ return out;
+}
+
+/** `Parameters` (the fixed tuple of F's parameters, interned exactly as
+ * the spelled tuple maps) and `ReturnType` (F's mapped result) over a
+ * bound function-typed type parameter. */
+function mapBoundSignatureAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
+ const target = boundSignatureAlias(type, ctx);
+ if (!target) return null;
+ if (target.name === "ReturnType") {
+ const ret = ctx.checker.getReturnTypeOfSignature(target.sig);
+ return ret.flags & ts.TypeFlags.Any ? DYN : mapType(ret, ctx);
+ }
+ const elems = signatureParameterTypes(target.sig, ctx);
+ if (!elems) return null;
+ if (elems.length === 0) return arrayOf(unitOnlyUnion(ctx.unions));
+ const fields: { name: string; type: IrType }[] = [];
+ for (let i = 0; i < elems.length; i++) {
+ const et = mapTupleElement(elems[i]!, ctx);
+ if (!et) return null;
+ fields.push({ name: String(i), type: et });
+ }
+ fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
+ return { kind: "record", shapeId: ctx.shapes.intern(fields, true) };
+}
+
/** `Partial` / `Readonly` where T is a STILL-GENERIC type parameter —
* the one mapped-type spelling a monomorphized body can contain that the
* checker cannot resolve for us (`keyof T` is unknown inside the body, so
@@ -4911,13 +5456,17 @@ function mapGenericAwaitedAlias(type: ts.Type, ctx: TypeMapperCtx): IrType | nul
const aliasArgs = type.getAliasTypeArguments();
const arg = aliasArgs[0];
if (aliasArgs.length !== 1 || !arg) return null;
- if (!(arg.flags & ts.TypeFlags.TypeParameter)) return null;
+ // `Awaited>` (an async wrapper's settled result) resolves
+ // through F's bound signature like a bare type parameter.
+ const viaSignature =
+ arg.flags & ts.TypeFlags.TypeParameter ? null : boundSignatureAlias(arg, ctx);
+ if (!(arg.flags & ts.TypeFlags.TypeParameter) && viaSignature?.name !== "ReturnType") return null;
// Provenance: the STANDARD LIBRARY's alias, not a user's shadowing one.
const isLibAlias = alias.declarations.some(
(d) => ts.isTypeAliasDeclaration(d) && ctx.isStdlibFile(d.getSourceFile()),
);
if (!isLibAlias) return null;
- const bound = resolveTypeParam(arg);
+ const bound = viaSignature ? mapBoundSignatureAlias(arg, ctx) : resolveTypeParam(arg);
if (!bound) return null;
let r = bound;
while (r.kind === "promise") r = r.inner;
@@ -4943,11 +5492,15 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
const obj = type.getObjectType();
if (obj === undefined || !(obj.flags & ts.TypeFlags.TypeParameter)) return null;
const bound = resolveTypeParam(obj);
+ const idx = type.getIndexType();
+ if (idx === undefined) return null;
+ // `T[keyof T]` over a T bound to an unknown-valued record view
+ // (`Record` maps to the checked-dynamic value, see
+ // mapRecordType): every member reads as unknown.
+ if (bound?.kind === "dyn" && idx.isIndexType() && idx.getTarget() === obj) return DYN;
if (!bound || bound.kind !== "record") return null;
const shape = shapes.get(bound.shapeId);
if (!shape || shape.tuple) return null;
- const idx = type.getIndexType();
- if (idx === undefined) return null;
let covered: IrType[] | null = null;
if (idx.isStringLiteralType()) {
const f = shape.fields.find((x) => x.name === idx.value);
@@ -4964,7 +5517,62 @@ function mapGenericIndexedAccess(type: ts.Type, ctx: TypeMapperCtx): IrType | nu
}
if (!covered || covered.length === 0) return null;
const first = covered[0]!;
- return covered.every((t) => typeEquals(t, first)) ? first : null;
+ if (covered.every((t) => typeEquals(t, first))) return first;
+ // Only the `keyof T` index itself covers every field at once; a key
+ // parameter bound to string keeps its literal-key resolution below.
+ if (!(idx.isIndexType() && idx.getTarget() === obj)) return null;
+ // Fields of different scalar types (`Object.entries` over a bound record
+ // reads `T[keyof T]`): any one of them, the union of their types.
+ const arms: IrType[] = [];
+ for (const t of covered) {
+ const parts = t.kind === "union" ? (ctx.unions.get(t.unionId)?.arms ?? null) : [t];
+ if (!parts) return null;
+ for (const part of parts) {
+ if (
+ part.kind !== "f64" &&
+ part.kind !== "string" &&
+ part.kind !== "bool" &&
+ part.kind !== "undefinedT" &&
+ part.kind !== "nullT"
+ )
+ return null;
+ if (!arms.some((a) => typeEquals(a, part))) arms.push(part);
+ }
+ }
+ if (arms.length === 1) return arms[0]!;
+ arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
+ return { kind: "union", unionId: ctx.unions.intern(arms) };
+}
+
+/** `keyof T extends never ? A : B` over a bound record T (the lib's
+ * Object.entries element spelling): decided by whether the binding has any
+ * key. */
+function mapBoundKeyofNeverConditional(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
+ const { resolveTypeParam, shapes } = ctx;
+ if (!resolveTypeParam || !type.isConditionalType()) return null;
+ const check = type.getCheckType();
+ const ext = type.getExtendsType();
+ if (!check || !ext || !(ext.flags & ts.TypeFlags.Never) || !check.isIndexType()) return null;
+ const target = check.getTarget();
+ if (!target || !(target.flags & ts.TypeFlags.TypeParameter)) return null;
+ const bound = resolveTypeParam(target);
+ // A T bound to the checked-dynamic value (`Record`, an
+ // unknown-valued record view) has no static key set to decide the
+ // branch, but both branches of the lib's `keyof T extends never ?
+ // unknown : T[keyof T]` read as unknown there: one answer either way.
+ if (bound?.kind === "dyn") {
+ const whenEmpty = type.getTrueType();
+ const otherwise = type.getFalseType();
+ const a = whenEmpty ? mapType(whenEmpty, ctx) : null;
+ const b = otherwise ? mapType(otherwise, ctx) : null;
+ return a && b && typeEquals(a, b) ? a : null;
+ }
+ if (bound?.kind !== "record") return null;
+ const shape = shapes.get(bound.shapeId);
+ if (!shape || shape.tuple) return null;
+ const hasKeys = shape.fields.length > 0 || shape.indexValue !== undefined;
+ const branch = hasKeys ? type.getFalseType() : type.getTrueType();
+ return branch ? mapType(branch, ctx) : null;
}
/** A mapped type over a type parameter of the instance being lowered (`{
@@ -5107,10 +5715,13 @@ function matchInferReference(
const target = (t: ts.Type): ts.Type | undefined =>
t.isTypeReference?.() ? (t as ts.TypeReference).getTarget?.() : undefined;
const wantTarget = target(extendsType) ?? extendsType.getAliasSymbol();
- if (!wantTarget) return undefined;
+ if (!wantTarget) return matchInferSignature(checked, extendsType, ctx);
const gotTarget = target(checked) ?? checked.getAliasSymbol();
- if (!gotTarget) return checked.flags & ts.TypeFlags.Object ? undefined : false;
- if (gotTarget !== wantTarget) return undefined;
+ if (!gotTarget)
+ return checked.flags & ts.TypeFlags.Object
+ ? matchInferThenable(checked, extendsType, ctx)
+ : false;
+ if (gotTarget !== wantTarget) return matchInferThenable(checked, extendsType, ctx);
const wantArgs = target(extendsType)
? checker.getTypeArguments(extendsType as ts.TypeReference)
: extendsType.getAliasTypeArguments();
@@ -5131,6 +5742,154 @@ function matchInferReference(
return inferred;
}
+/** The type parameter an `infer X` declares, when `t` is one. */
+function inferSymbolOf(t: ts.Type, _ctx: TypeMapperCtx): ts.Symbol | undefined {
+ if (!(t.flags & ts.TypeFlags.TypeParameter)) return undefined;
+ const symbol = t.getSymbol();
+ return symbol &&
+ symbol.declarations.some((d) => d.parent !== undefined && ts.isInferTypeNode(d.parent))
+ ? symbol
+ : undefined;
+}
+
+/** `checked extends (...args: any) => infer R` — the shape of the standard
+ * library's ReturnType, over a concrete function type: R binds to its
+ * result. Pattern parameters must be `any` or `any[]`, and a pattern rest
+ * `any` or `any[]`; anything else stays undecided. The match follows
+ * TypeScript's assignability: a primitive never matches a function type,
+ * a function that needs more arguments than a pattern without a rest
+ * passes never matches, and a positional `any[]` parameter needs the
+ * function's parameter there to accept an array. */
+function matchInferSignature(
+ checked: ts.Type,
+ extendsType: ts.Type,
+ ctx: TypeMapperCtx,
+): Map | false | undefined {
+ const { checker } = ctx;
+ const wantSigs = checker.getCallSignatures(extendsType);
+ const want = wantSigs[0];
+ if (wantSigs.length !== 1 || !want || want.getTypeParameters().length > 0) return undefined;
+ const wantShape = signatureShape(want, ctx);
+ if (wantShape === undefined) return undefined;
+ const isAny = (t: ts.Type): boolean => (t.flags & ts.TypeFlags.Any) !== 0;
+ const isAnyArray = (t: ts.Type): boolean =>
+ checker.isArrayType(t) && isAny(checker.getTypeArguments(t as ts.TypeReference)[0]!);
+ if (!wantShape.params.every((p) => isAny(p.type) || isAnyArray(p.type))) return undefined;
+ if (wantShape.rest !== null && !isAny(wantShape.rest)) return undefined;
+ if (!(checked.flags & ts.TypeFlags.Object)) return false;
+ const gotSigs = checker.getCallSignatures(checked);
+ const got = gotSigs[0];
+ if (gotSigs.length !== 1 || !got || got.getTypeParameters().length > 0) return undefined;
+ const gotShape = signatureShape(got, ctx);
+ if (gotShape !== undefined) {
+ // `(a: any) => infer R` rejects `(a: string, b: string) => R` and
+ // `(...args: [string, number]) => R`: they need more arguments.
+ if (wantShape.rest === null && requiredArguments(gotShape) > wantShape.params.length)
+ return false;
+ for (let i = 0; i < wantShape.params.length; i++) {
+ if (!isAnyArray(wantShape.params[i]!.type)) continue;
+ const param = gotShape.params[i]?.type ?? gotShape.rest;
+ if (param === undefined || param === null) continue;
+ const accepts = acceptsArray(param, ctx);
+ if (accepts !== true) return accepts;
+ }
+ } else if (wantShape.params.some((p) => isAnyArray(p.type))) {
+ return undefined;
+ }
+ const wantRet = checker.getReturnTypeOfSignature(want);
+ const infer = inferSymbolOf(wantRet, ctx);
+ if (infer) return new Map([[infer, checker.getReturnTypeOfSignature(got)]]);
+ return wantRet.flags & ts.TypeFlags.Any ? new Map() : undefined;
+}
+
+/** A signature's parameters as a call sees them: the positional ones, with
+ * a fixed-tuple rest (`...args: [A, B?]`) flattened into positions, and the
+ * element type of an array rest (`any` for `...args: any`). Undefined when
+ * a parameter has no declaration, or the rest is a variadic tuple or
+ * another type. */
+function signatureShape(
+ sig: ts.Signature,
+ ctx: TypeMapperCtx,
+): { params: { type: ts.Type; optional: boolean }[]; rest: ts.Type | null } | undefined {
+ const { checker } = ctx;
+ const params: { type: ts.Type; optional: boolean }[] = [];
+ for (const p of sig.getParameters()) {
+ const decl = p.valueDeclaration;
+ if (decl === undefined || !ts.isParameter(decl)) return undefined;
+ const type = checker.getTypeOfSymbol(p);
+ if (decl.dotDotDotToken === undefined) {
+ params.push({
+ type,
+ optional: decl.questionToken !== undefined || decl.initializer !== undefined,
+ });
+ continue;
+ }
+ if (type.flags & ts.TypeFlags.Any) return { params, rest: type };
+ if (checker.isArrayType(type))
+ return { params, rest: checker.getTypeArguments(type as ts.TypeReference)[0]! };
+ const elems = restTupleElements(type, ctx);
+ if (!elems) return undefined;
+ for (const e of elems) params.push({ type: e.type, optional: e.optional });
+ return { params, rest: null };
+ }
+ return { params, rest: null };
+}
+
+/** The fewest arguments a call must pass: up to the last required position. */
+function requiredArguments(shape: { params: { optional: boolean }[] }): number {
+ let required = 0;
+ shape.params.forEach((p, i) => {
+ if (!p.optional) required = i + 1;
+ });
+ return required;
+}
+
+/** Whether a parameter of this type accepts an `any[]` argument: true for
+ * `any`, `unknown` and array types, false for primitives, undefined
+ * otherwise. */
+function acceptsArray(t: ts.Type, ctx: TypeMapperCtx): boolean | undefined {
+ if (t.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) return true;
+ if (ctx.checker.isArrayType(t)) return true;
+ const primitive =
+ ts.TypeFlags.StringLike |
+ ts.TypeFlags.NumberLike |
+ ts.TypeFlags.BooleanLike |
+ ts.TypeFlags.BigIntLike |
+ ts.TypeFlags.ESSymbolLike |
+ ts.TypeFlags.Undefined |
+ ts.TypeFlags.Null |
+ ts.TypeFlags.Void;
+ return t.flags & primitive ? false : undefined;
+}
+
+/** `checked extends PromiseLike` (or `Promise`) over the
+ * standard library's Promise: U binds to the settled type. An object with
+ * no `then` member is no thenable. Everything else stays undecided. */
+function matchInferThenable(
+ checked: ts.Type,
+ extendsType: ts.Type,
+ ctx: TypeMapperCtx,
+): Map | false | undefined {
+ const { checker } = ctx;
+ const stdlibNamed = (t: ts.Type, names: readonly string[]): boolean => {
+ const symbol = t.getSymbol();
+ return (
+ symbol !== undefined &&
+ names.includes(symbol.name) &&
+ symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
+ );
+ };
+ if (!stdlibNamed(extendsType, ["PromiseLike", "Promise"])) return undefined;
+ const wantArgs = checker.getTypeArguments(extendsType as ts.TypeReference);
+ const infer = wantArgs.length === 1 ? inferSymbolOf(wantArgs[0]!, ctx) : undefined;
+ if (!infer) return undefined;
+ if (stdlibNamed(checked, ["Promise", "PromiseLike"])) {
+ const gotArgs = checker.getTypeArguments(checked as ts.TypeReference);
+ return gotArgs.length === 1 ? new Map([[infer, gotArgs[0]!]]) : undefined;
+ }
+ return checker.getPropertyOfType(checked, "then") === undefined ? false : undefined;
+}
+
/** The type mentions a mapped key or bound alias parameter (a template
* shape evalMappedTemplate does not reduce). */
function mentionsMappedParams(
@@ -5294,10 +6053,22 @@ export function isUnitOnlyTsType(t: ts.Type, resolveTypeParam?: TypeParamResolve
return parts.every(
(p) =>
(p.flags & UNIT) !== 0 ||
- ((p.flags & ts.TypeFlags.TypeParameter) !== 0 && resolveTypeParam?.(p)?.kind === "void"),
+ ((p.flags & ts.TypeFlags.TypeParameter) !== 0 && resolveTypeParam?.(p)?.kind === "void") ||
+ boundVoidReturnType(p, resolveTypeParam),
);
}
+/** `ReturnType` over a type parameter bound to a void-returning
+ * function (a variadic wrapper's result slot): a unit-only value. */
+function boundVoidReturnType(p: ts.Type, resolveTypeParam?: TypeParamResolver): boolean {
+ if (!resolveTypeParam || p.getAliasSymbol()?.name !== "ReturnType") return false;
+ const args = p.getAliasTypeArguments();
+ const arg = args[0];
+ if (args.length !== 1 || !arg || !(arg.flags & ts.TypeFlags.TypeParameter)) return false;
+ const bound = resolveTypeParam(arg);
+ return bound?.kind === "func" && bound.ret.kind === "void";
+}
+
/** IR-level `t | undefined`, canonicalized and fenced exactly like the
* ts-union branch of mapType (typeKey-sorted arms, deduplicated;
* Date/dyn/void arms unrepresentable; a func arm IS representable — the
@@ -5430,6 +6201,7 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType
});
if (!hasError) return null;
let error: IrType | null = null;
+ const extra: ts.Symbol[] = [];
for (const part of parts) {
const mapped = mapType(part, ctx);
if (mapped?.kind === "object" && RUNTIME_ERROR_CLASSES.has(mapped.className)) {
@@ -5450,14 +6222,45 @@ function mapErrorMetadataIntersection(type: ts.Type, ctx: TypeMapperCtx): IrType
: ["code", "syscall", "path", "dest"].includes(property.name)
? "string"
: null;
- if (!expected || !(property.flags & ts.SymbolFlags.Optional)) return null;
+ if (!expected) {
+ extra.push(property);
+ continue;
+ }
const field = mapType(ctx.checker.getTypeOfSymbol(property), ctx);
const arms =
field?.kind === "union" ? ctx.unions.get(field.unionId)?.arms : field ? [field] : [];
if (!arms?.length || !arms.every((arm) => arm.kind === expected || arm.kind === "undefinedT"))
return null;
+ // A required member must still admit the absent value the slot reads.
+ if (
+ !(property.flags & ts.SymbolFlags.Optional) &&
+ !arms.some((arm) => arm.kind === "undefinedT")
+ )
+ return null;
}
}
+ if (!error) return null;
+ // Members beyond Error's own and the system-error metadata (`Error & {
+ // status: number }`, Object.assign's result) are own properties of the
+ // error's checked-dynamic node: each must map to a type a checked read
+ // can produce. Error's own string members keep their slots.
+ for (const property of extra) {
+ const field = mapType(ctx.checker.getTypeOfSymbol(property), ctx);
+ if (!field || field.kind === "void") return null;
+ if (property.name === "message" || property.name === "name") {
+ if (field.kind !== "string") return null;
+ continue;
+ }
+ if (property.name === "stack" || property.name === "cause") return null;
+ if (
+ !canDynCheckTo(
+ field,
+ (id) => ctx.shapes.get(id),
+ (id) => ctx.unions.get(id),
+ )
+ )
+ return null;
+ }
return error;
}
@@ -5908,6 +6711,7 @@ interface RecordShapeParts {
fields: { name: string; type: IrType }[];
indexValue?: IrType;
declaredOrder?: string[];
+ accessorOrder?: string[];
}
function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
@@ -5944,15 +6748,14 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
// (generic type parameters, mixin instantiations) cannot intern by
// checker-type identity — fence recursive generic-open shapes.
if (contextResolutions !== sensitivityAtEntry) return null;
- return {
- kind: "record",
- shapeId: shapes.finalizeRecursive(
- widened,
- inner.fields,
- inner.indexValue,
- inner.declaredOrder,
- ),
- };
+ const recursive = shapes.finalizeRecursive(
+ widened,
+ inner.fields,
+ inner.indexValue,
+ inner.declaredOrder,
+ );
+ adoptAccessorOrder(shapes.get(recursive), inner.accessorOrder);
+ return { kind: "record", shapeId: recursive };
}
// A record with no typed payload contract is a view of an existing
// object, including across parameters and returns. Building a native
@@ -5965,17 +6768,296 @@ function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
(inner.indexValue === undefined || inner.indexValue.kind === "dyn")
)
return DYN;
+ const covered = coveredOptionalFieldsShape(widened, inner, ctx);
+ if (covered !== null) return { kind: "record", shapeId: covered };
+ const merged = dictSpreadLiteralShape(widened, inner, ctx);
+ if (merged === "dyn") return DYN;
+ if (merged !== null) return { kind: "record", shapeId: merged };
const refined = refinedConstituentShape(widened, inner, ctx);
if (refined !== null) return { kind: "record", shapeId: refined };
- return {
- kind: "record",
- shapeId: shapes.intern(inner.fields, false, inner.indexValue, inner.declaredOrder),
- };
+ const shapeId = shapes.intern(inner.fields, false, inner.indexValue, inner.declaredOrder);
+ adoptAccessorOrder(shapes.get(shapeId), inner.accessorOrder);
+ return { kind: "record", shapeId };
} finally {
shapes.inProgress.delete(widened);
}
}
+/** An index signature already describes every OPTIONAL declared member
+ * whose type is its slot type: @types/node's `ProcessEnv extends
+ * Dict { TZ?: string }` and every spread of it, or a declared
+ * environment `{ CI?: string } & Dictionary`. At runtime such a
+ * member is just another key, so the shape interns as the pure
+ * index-signature record. Its slot admits undefined, as the optional
+ * members do (`env.CI = undefined` keeps the key). A hybrid layout would
+ * give each member a struct slot, which enumerates before the overflow
+ * keys (Node keeps one insertion order) and makes `delete` and spread
+ * answer differently for declared and undeclared keys. Required members,
+ * and members typed otherwise than the slot (whose reads the slot type
+ * could not give back), keep the hybrid shape. */
+function coveredOptionalFieldsShape(
+ widened: ts.Type,
+ parts: RecordShapeParts,
+ ctx: TypeMapperCtx,
+): string | null {
+ const slot = parts.indexValue;
+ if (!slot || parts.fields.length === 0) return null;
+ const { checker, shapes, unions } = ctx;
+ // The members' common type must be the slot plus undefined. Read it off
+ // the members (already interned) rather than building it: a hybrid that
+ // does not qualify must not intern anything new.
+ const optionalSlot = parts.fields[0]!.type;
+ if (!parts.fields.every((field) => typeEquals(field.type, optionalSlot))) return null;
+ if (optionalSlot.kind !== "union" || unions.armTag(optionalSlot.unionId, UNDEFINED_T) < 0)
+ return null;
+ const memberArms = unions.get(optionalSlot.unionId)?.arms ?? [];
+ const slotArms =
+ slot.kind === "union" ? (unions.get(slot.unionId)?.arms ?? []) : isUnitType(slot) ? [] : [slot];
+ const definedArms = memberArms.filter((arm): boolean => arm.kind !== "undefinedT");
+ const slotDefined = slotArms.filter((arm): boolean => arm.kind !== "undefinedT");
+ if (
+ definedArms.length === 0 ||
+ definedArms.length !== slotDefined.length ||
+ !slotDefined.every((arm) => definedArms.some((other) => typeEquals(arm, other)))
+ )
+ return null;
+ if (!isSupportedIndexValue(optionalSlot)) return null;
+ const props = checker.getPropertiesOfType(widened);
+ if (props.length !== parts.fields.length) return null;
+ if (!props.every((p) => (p.flags & ts.SymbolFlags.Optional) !== 0)) return null;
+ return shapes.intern([], false, optionalSlot, []);
+}
+
+/** The type of an object literal that spreads an index-signature record
+ * (`{ ...tags, bucket: b, status }`, the metric-tags idiom). The checker
+ * drops the signature and types the literal by its explicit members only,
+ * but at run time the object holds every key the spread copied, in one
+ * insertion order. Such a literal maps to the pure index-signature record
+ * whose slot joins the spread slots and the member types, so the merge,
+ * every enumeration and keyed writes see the same keys Node does. A
+ * member read still answers its member type: members written after the
+ * spreads always win, and a member written before a spread must have that
+ * spread's slot type, so an override cannot change its type. The join
+ * covers scalar and unit types only; other literals keep their shape and
+ * the spread fence. */
+function dictSpreadLiteralShape(
+ widened: ts.Type,
+ parts: RecordShapeParts,
+ ctx: TypeMapperCtx,
+): string | "dyn" | null {
+ if (parts.indexValue) return null;
+ const symbol = widened.getSymbol();
+ if (!symbol || (symbol.flags & ts.SymbolFlags.ObjectLiteral) === 0) return null;
+ const literal = symbol.declarations.find((d): d is ts.ObjectLiteralExpression =>
+ ts.isObjectLiteralExpression(d),
+ );
+ if (!literal) return null;
+ const { checker, shapes, unions } = ctx;
+ const props = literal.properties.nodes;
+ if (!props.some((p) => ts.isSpreadAssignment(p))) return null;
+ // Decide on checker types alone before mapping anything: object literals
+ // with ordinary spreads are everywhere, and mapping their sources here
+ // would intern types out of their usual order.
+ // A literal written into a typed index-signature slot already lowers into
+ // it (the merge into the contextual record); its own type keeps its
+ // mapping.
+ const contextualRaw = checker.getContextualType(literal);
+ const contextual = contextualRaw ? checker.getNonNullableType(contextualRaw) : undefined;
+ // A literal written into a declared slot lowers into that slot (a typed
+ // index-signature record merges into it, a declared record converts),
+ // and its own type keeps its mapping. Only a literal with no declared
+ // destination (an unannotated binding, a type parameter, `any` or
+ // `unknown`, an unknown-valued record) is built by its own type.
+ // A generic argument's context is the instantiated parameter: the
+ // literal's own type when the type argument was inferred from it.
+ const ownContext =
+ contextual !== undefined &&
+ (contextual === widened || contextual.getSymbol()?.declarations.includes(literal) === true);
+ if (
+ contextual &&
+ !ownContext &&
+ !(contextual.flags & (ts.TypeFlags.TypeParameter | ts.TypeFlags.Any | ts.TypeFlags.Unknown))
+ ) {
+ const infos = checker.getIndexInfosOfType(contextual);
+ const unknownValued =
+ infos.length > 0 &&
+ infos.every(
+ (info) => (info.valueType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0,
+ );
+ if (!unknownValued) return null;
+ }
+ const kinds = props.map((p) =>
+ ts.isSpreadAssignment(p) ? spreadSourceKindTs(checker, p.expression) : "member",
+ );
+ if (kinds.some((k) => k === null) || !kinds.includes("dict")) return null;
+ const dictSlotOf = (e: ts.Expression): IrType | null => {
+ const t = checker.getNonNullableType(checker.getTypeAtLocation(e));
+ if (checker.getPropertiesOfType(t).length === 0) {
+ const infos = checker.getIndexInfosOfType(t);
+ if (infos.length !== 1 || !(infos[0]!.keyType.flags & ts.TypeFlags.String)) return null;
+ return mapType(infos[0]!.valueType, ctx);
+ }
+ // Another such literal's type (`{ ...tags, repo }` over a binding
+ // initialized by `{ ...base, plan }`) is stored as the same kind of
+ // pure index-signature record.
+ if (t === widened) return null;
+ const mapped = mapType(t, ctx);
+ if (mapped?.kind === "dyn") return DYN;
+ const shape = mapped?.kind === "record" ? shapes.get(mapped.shapeId) : undefined;
+ return shape?.indexValue && shape.fields.length === 0 && !shape.tuple ? shape.indexValue : null;
+ };
+ const arms = new Map();
+ const addArms = (t: IrType): boolean => {
+ const list = t.kind === "union" ? (unions.get(t.unionId)?.arms ?? []) : [t];
+ for (const arm of list) {
+ if (!OVERLOAD_JOIN_SCALARS.has(arm.kind)) return false;
+ arms.set(typeKey(arm), arm);
+ }
+ return true;
+ };
+ const before: string[] = [];
+ // An unknown-valued spread (`Record`, or another such
+ // literal) makes the object the checked-dynamic value: it is built like
+ // a JavaScript object literal (copy each spread's own keys in order), and
+ // member reads check their declared types.
+ let unknownValued = false;
+ for (const [i, p] of props.entries()) {
+ // A fixed-shape or conditional spread contributes its own members,
+ // which the checker already types exactly (they are in `parts`).
+ if (kinds[i] === "record") {
+ // Its members may be overwritten by a later dict spread, like members
+ // written before one.
+ before.push(...spreadMemberNamesTs(checker, (p as ts.SpreadAssignment).expression));
+ continue;
+ }
+ if (ts.isSpreadAssignment(p)) {
+ const slot = dictSlotOf(p.expression);
+ if (slot?.kind === "dyn") {
+ unknownValued = true;
+ continue;
+ }
+ if (!slot) return null;
+ if (!addArms(slot)) {
+ // A slot outside the scalar join still has the checked-dynamic
+ // representation when it converts to it.
+ if (
+ !canConvertToDyn(
+ slot,
+ (id) => shapes.get(id),
+ (id) => unions.get(id),
+ )
+ )
+ return null;
+ unknownValued = true;
+ }
+ // A member written before this spread may be overwritten by it: the
+ // typed join keeps member types only where the slot is that type;
+ // otherwise the checked-dynamic object (whose member reads check
+ // their types) holds whatever the spread wrote.
+ for (const name of before) {
+ const field = parts.fields.find((f) => f.name === name);
+ if (!field) return null;
+ if (!typeEquals(field.type, slot)) unknownValued = true;
+ }
+ continue;
+ }
+ if (!(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p))) return null;
+ const name = p.name;
+ if (!(ts.isIdentifier(name) || ts.isStringLiteral(name))) return null;
+ before.push(name.text);
+ }
+ if (parts.fields.some((f) => f.name.startsWith("%"))) return null;
+ // Members outside the scalar join (an array of labels beside the tags)
+ // make the object the checked-dynamic value too.
+ if (!unknownValued && !parts.fields.every((f) => addArms(f.type))) unknownValued = true;
+ if (unknownValued) {
+ const getRecord = (id: string) => shapes.get(id);
+ const getUnion = (id: string) => unions.get(id);
+ return parts.fields.every(
+ (f) => f.type.kind === "dyn" || canConvertToDyn(f.type, getRecord, getUnion),
+ )
+ ? "dyn"
+ : null;
+ }
+ for (const field of parts.fields) if (!addArms(field.type)) return null;
+ if (![...arms.values()].some((arm) => !isUnitType(arm))) return null;
+ const sorted = [...arms.values()].sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
+ const slot: IrType =
+ sorted.length === 1 ? sorted[0]! : { kind: "union", unionId: unions.intern(sorted) };
+ return shapes.intern([], false, slot, []);
+}
+
+/** How a spread source contributes to dictSpreadLiteralShape, judged on
+ * checker types only: "dict" for a pure string index signature or another
+ * literal that spreads one, "record" for a fixed object shape or a
+ * conditional spread (`...(c && { k: v })`), null otherwise. */
+function spreadSourceKindTs(checker: ts.TypeChecker, e: ts.Expression): "dict" | "record" | null {
+ if (dictSpreadSourceTs(checker, e, 0)) return "dict";
+ let inner = e;
+ while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
+ if (
+ (ts.isBinaryExpression(inner) &&
+ inner.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) ||
+ ts.isConditionalExpression(inner)
+ )
+ return "record";
+ const t = checker.getNonNullableType(checker.getTypeAtLocation(e));
+ if ((t.flags & ts.TypeFlags.Object) === 0 || t.isUnionType()) return null;
+ if (checker.getIndexInfosOfType(t).length > 0) return null;
+ if (checker.getCallSignatures(t).length > 0 || checker.getConstructSignatures(t).length > 0)
+ return null;
+ const symbol = t.getSymbol();
+ if (symbol && (symbol.flags & ts.SymbolFlags.Class) !== 0) return null;
+ return "record";
+}
+
+/** The member names a fixed-shape or conditional spread contributes. */
+function spreadMemberNamesTs(checker: ts.TypeChecker, e: ts.Expression): string[] {
+ let inner = e;
+ while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
+ const literals: ts.Expression[] = [];
+ if (ts.isBinaryExpression(inner)) literals.push(inner.right);
+ else if (ts.isConditionalExpression(inner)) literals.push(inner.whenTrue, inner.whenFalse);
+ else literals.push(inner);
+ const names: string[] = [];
+ for (const l of literals) {
+ const t = checker.getNonNullableType(checker.getTypeAtLocation(l));
+ for (const property of checker.getPropertiesOfType(t)) names.push(property.name);
+ }
+ return names;
+}
+
+/** A spread source the checker types as a pure string index signature,
+ * or as another object literal that spreads one (dictSpreadLiteralShape),
+ * judged on checker types only. */
+function dictSpreadSourceTs(checker: ts.TypeChecker, e: ts.Expression, depth: number): boolean {
+ const t = checker.getNonNullableType(checker.getTypeAtLocation(e));
+ if (checker.getPropertiesOfType(t).length === 0) {
+ const infos = checker.getIndexInfosOfType(t);
+ return infos.length === 1 && (infos[0]!.keyType.flags & ts.TypeFlags.String) !== 0;
+ }
+ if (depth > 4) return false;
+ const symbol = t.getSymbol();
+ if (!symbol || (symbol.flags & ts.SymbolFlags.ObjectLiteral) === 0) return false;
+ const literal = symbol.declarations.find((d): d is ts.ObjectLiteralExpression =>
+ ts.isObjectLiteralExpression(d),
+ );
+ if (!literal) return false;
+ const spreads = literal.properties.nodes.filter((p) => ts.isSpreadAssignment(p));
+ return (
+ spreads.length > 0 &&
+ spreads.every((p) =>
+ dictSpreadSourceTs(checker, (p as ts.SpreadAssignment).expression, depth + 1),
+ )
+ );
+}
+
+/** The first-seen accessor key order of a shape with accessor slots. */
+function adoptAccessorOrder(shape: IrRecordShape | undefined, order: string[] | undefined): void {
+ if (shape !== undefined && order !== undefined && shape.accessorOrder === undefined)
+ shape.accessorOrder = order;
+}
+
/** A refinement intersection (`Expr & { kind: "mul" }`, which the checker
* reduces to `Mul & { kind: "mul" }`) keeps the shape of the constituent it
* refines when the refinement changes no field's representation. Recursive
@@ -6335,8 +7417,18 @@ function mapRecordTypeInner(
const declaredOrder = esOwnKeyOrder(
fields.filter((f) => accessorSlotProp(f.name) === null).map((f) => f.name),
);
+ // With accessors: every own key in that order, an accessor property
+ // named once for its get/set pair (the checked-dynamic copy lists it).
+ const accessorOrder = fields.some((f) => accessorSlotProp(f.name) !== null)
+ ? esOwnKeyOrder([...new Set(fields.map((f) => accessorSlotProp(f.name)?.prop ?? f.name))])
+ : undefined;
fields.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
- return { fields, ...(indexValue ? { indexValue } : {}), declaredOrder };
+ return {
+ fields,
+ ...(indexValue ? { indexValue } : {}),
+ declaredOrder,
+ ...(accessorOrder ? { accessorOrder } : {}),
+ };
}
}
diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts
index 7d8d88889..9c02cdd8b 100644
--- a/packages/compiler/src/index.ts
+++ b/packages/compiler/src/index.ts
@@ -8,6 +8,7 @@ import {
libraryWasmRefusal,
} from "./library/prepare.js";
import { analyzeWithFrontend } from "./frontend/analysis.js";
+import { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js";
import { analyzeWithVerdictCache, fileVerdictStore } from "./coverage/verdict-cache.js";
import { coverageCompilerIdentity } from "./coverage/verdict-startup.js";
import { commentOnlyReplay } from "./coverage/verdict-semantic.js";
@@ -379,6 +380,12 @@ export async function analyzeCached(
const nodeFrontend: FrontendFactory = (entry, npmStatic, externalTypes, libraryNpmStatic) =>
runFrontend(entry, loadProgram, npmStatic, externalTypes, libraryNpmStatic);
+/** The checker's part of the executable cache key (`checkerCacheIdentity`), as the CLI's startup route computes it. */
+function checkerField(): { checker?: string } {
+ const checker = checkerCacheIdentity();
+ return checker === undefined ? {} : { checker };
+}
+
/** The whole pipeline: load → preflight → lower → validate → emit LLVM → link. */
function clearCompileSessionCaches(): void {
clearResolveCaches();
@@ -528,6 +535,7 @@ async function compileExecutableNative(
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,
@@ -537,18 +545,21 @@ async function compileExecutableNative(
dc: features.dc,
dynAsync: features.dynAsync,
...(features.dynTier ? { dynTier: true } : {}),
+ ...(features.streamDyn ? { streamDyn: true } : {}),
events: features.events,
emitter: features.emitter,
symbol: features.symbol,
bigint: features.bigint,
searchParams: features.searchParams,
qs: features.qs,
+ dynBuiltinClass: features.dynBuiltinClass,
parseArgs: features.parseArgs,
stream: features.stream,
net: features.net,
http: features.http,
http2: features.http2,
dgram: features.dgram,
+ dnsPromises: features.dnsPromises,
watch: features.watch,
foreignFfi: features.foreignFfi,
...(features.workers ? { workers: true } : {}),
@@ -558,6 +569,7 @@ async function compileExecutableNative(
tlsCa: features.tlsCa,
cipher: features.cipher,
webcrypto: features.webcrypto,
+ perfLoop: features.perfLoop,
...(onArtifactReady === undefined ? {} : { onArtifactReady }),
...(ffi === null
? {}
@@ -722,6 +734,7 @@ async function prepareExecutableInput(
wasi: buildPlatform === "wasi",
runtimeAbiMarker: outputKind === "obj",
objectAudit: opts.sanitize === true,
+ ...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
...inlineRcOption(opts.optimization, opts.sanitize === true),
});
} catch (err) {
@@ -787,6 +800,7 @@ async function prepareExecutableInput(
wasi: buildPlatform === "wasi",
runtimeAbiMarker: useRuntimePack,
objectAudit: opts.sanitize === true,
+ ...(opts.sanitize === true ? { wasmBoundsChecks: true } : {}),
...inlineRcOption(opts.optimization, opts.sanitize === true),
...(opts.detachedExit === true ? { detachedExit: true } : {}),
});
@@ -1052,6 +1066,7 @@ async function compileTracked(
...(opts.strip ? { strip: true as const } : {}),
...(opts.windowsSubsystem === "gui" ? { windowsSubsystem: "gui" as const } : {}),
...(opts.detachedExit === true ? { detachedExit: true as const } : {}),
+ ...checkerField(),
...(youngNurseryRequested() ? { youngNursery: true as const } : {}),
npmStatic: opts.npmStatic ?? null,
ffiProfile:
diff --git a/packages/compiler/src/ir/build.ts b/packages/compiler/src/ir/build.ts
index adb7366cc..77259450c 100644
--- a/packages/compiler/src/ir/build.ts
+++ b/packages/compiler/src/ir/build.ts
@@ -58,10 +58,10 @@ export function dynUndefinedExpr(loc: SrcLoc): IrExpr {
}
/** An always-throwing Node error with the replaced expression's type.
- * Kinds: 0 Error, 1 TypeError, 2 RangeError, 5 ReferenceError. An empty
+ * Kinds: 0 Error, 1 TypeError, 2 RangeError, 5 ReferenceError, 6 EvalError. An empty
* code omits the error code property. */
export function nodeThrowExpr(
- kind: 0 | 1 | 2 | 5,
+ kind: 0 | 1 | 2 | 5 | 6,
code: string,
message: string,
type: IrType,
diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts
index 6757851d9..d4e96a35a 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -75,6 +75,10 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"process.stdoutWriteBytes",
"process.stderrWriteBytes",
"procStream.write",
+ "child.stdioStream",
+ "stream.unboxDyn",
+ "stream.pipeDyn",
+ "stream.unpipeDyn",
"weakMap.new",
"weakSet.new",
"dyn.fromEntries",
@@ -133,6 +137,20 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"blob.new",
"blob.fileNew",
"blob.slice",
+ "wasm.compile",
+ "wasm.validate",
+ "wasm.instantiate",
+ "wasm.moduleNew",
+ "wasm.instanceNew",
+ "wasm.errorNew",
+ "wasm.memoryNew",
+ "wasm.tableNew",
+ "wasm.globalNew",
+ "wasm.withoutNew",
+ "wasm.moduleImports",
+ "wasm.moduleExports",
+ "wasm.moduleCustomSections",
+ "wasm.valueOf",
"events.addAbortListener",
"fetch.streamNew",
"fetch.streamFrom",
@@ -263,6 +281,14 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
// ("Invalid URL") or a non-http scheme (ERR_INVALID_PROTOCOL).
"http.requestUrl",
"http.requestUrlCb",
+ // The runtime options walk throws Node's validation errors and the
+ // fence for documented options it does not implement.
+ "http.requestDyn",
+ "http.requestDynCb",
+ "https.requestDyn",
+ "https.requestDynCb",
+ // address() of a Unix-socket server read as the AddressInfo record.
+ "net.serverAddressCheck",
// The h2 client's throwing entries: connect on a bad/non-http
// authority (ERR_INVALID_URL / ERR_INVALID_PROTOCOL), request on a
// closed/destroyed session (ERR_HTTP2_INVALID_SESSION), setEncoding on
@@ -324,6 +350,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"stream.onDyn",
"readable.resume",
"readable.pipe",
+ "readable.pipeGeneral",
"readable.unpipe",
"writable.write",
"writable.writeStr",
@@ -367,6 +394,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"perf.entries",
"perf.clearMarks",
"perf.clearMeasures",
+ // monitorEventLoopDelay's resolution: Node's ERR_OUT_OF_RANGE.
+ "perf.eldNew",
// Non-TTY stdin: Node's stdin has no setRawMode member, so the call is
// the exact catchable TypeError.
"process.stdinSetRawMode",
@@ -403,6 +432,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"error.stack",
"error.cause",
"error.setCause",
+ "error.assign",
"error.deleteCause",
"dyn.objectTag",
// Numeric coercion runs user valueOf/toString — throws propagate.
@@ -491,6 +521,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"json.stringifyReplacer",
"json.stringifyValue",
"date.nativeNew",
+ "date.nativeNewFields",
+ "date.nativeNewValue",
"date.checkedValue",
"util.parseArgs",
"util.parseEnv",
@@ -557,6 +589,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"dyn.objCreateWithProperties",
"dyn.getPrototype",
"dyn.setPrototype",
+ // a proxy in the prototype chain can throw
+ "dyn.builtinInstanceOf",
+ "dyn.functionInstanceOf",
"dyn.getOwnPropertyNames",
"dyn.ownKeys",
"dyn.getOwnPropertySymbols",
@@ -792,6 +827,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
// Node's argument checks (ERR_INVALID_ARG_TYPE/VALUE, ERR_SOCKET_BAD_PORT).
"dns.lookupDyn",
"dns.lookupService",
+ "dns.promisesLookup",
+ "dns.promisesLookupService",
"dns.setDefaultResultOrder",
"net.connect",
"net.connectCb",
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index 2cbb35185..30f0f7365 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -108,6 +108,45 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"blob.bytes": { argTypes: [DYN], result: { kind: "promise", inner: BYTES_U8 } },
"blob.arrayBuffer": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
"blob.is": { argTypes: [DYN, F64], result: BOOL },
+ /** WebAssembly (scr_wasm.c): modules translated at build time
+ * (wasm/translate.ts), found at run time by the SHA-256 of their bytes.
+ * compile and instantiate over bytes settle on a later loop turn;
+ * instantiate over a Module settles now, as in Node. `wasm.is` answers
+ * instanceof for Module, Instance, Memory, Table and Global (kinds 0-4)
+ * and CompileError, LinkError and RuntimeError (5-7); `wasm.errorNew`
+ * constructs one of the error classes (kind 5-7, message, options). */
+ "wasm.compile": { argTypes: [DYN], result: { kind: "promise", inner: DYN } },
+ "wasm.validate": { argTypes: [DYN], result: BOOL },
+ "wasm.instantiate": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
+ "wasm.moduleNew": { argTypes: [DYN], result: DYN },
+ "wasm.instanceNew": { argTypes: [DYN, DYN], result: DYN },
+ "wasm.is": { argTypes: [DYN, F64], result: BOOL },
+ "wasm.errorNew": { argTypes: [F64, DYN, DYN], result: DYN },
+ /** new WebAssembly.Memory(descriptor), Table(descriptor, value) and
+ * Global(descriptor, value) (an undefined value is the type's default);
+ * `wasm.withoutNew` throws the TypeError for Module, Instance, Memory,
+ * Table or Global (kinds 0-4) called without new, after its arguments
+ * evaluate. */
+ "wasm.memoryNew": { argTypes: [DYN], result: DYN },
+ "wasm.tableNew": { argTypes: [DYN, DYN], result: DYN },
+ "wasm.globalNew": { argTypes: [DYN, DYN], result: DYN },
+ "wasm.withoutNew": { argTypes: [F64, DYN, DYN], result: DYN },
+ /** WebAssembly.Module.imports(module), exports(module) and
+ * customSections(module, name): checked-dynamic arrays of descriptor
+ * objects and ArrayBuffers. */
+ "wasm.moduleImports": { argTypes: [DYN], result: DYN },
+ "wasm.moduleExports": { argTypes: [DYN], result: DYN },
+ "wasm.moduleCustomSections": { argTypes: [DYN, DYN], result: DYN },
+ /** `v.valueOf()` on a WebAssembly-typed receiver: a Global's value,
+ * dyn.valueOf's answer for anything else. */
+ "wasm.valueOf": { argTypes: [DYN], result: DYN },
+ /** WebAssembly.compile (0), instantiate (1) and validate (2) as function
+ * values, and the namespace object holding them. */
+ "wasm.function": { argTypes: [F64], result: DYN },
+ "wasm.namespace": { argTypes: [], result: DYN },
+ /** The global object viewed through a cast to a type with a WebAssembly
+ * member: an object holding the namespace. */
+ "wasm.globalView": { argTypes: [], result: DYN },
"abort.timeout": { argTypes: [DYN], result: DYN },
"abort.now": { argTypes: [DYN], result: DYN },
"abort.any": { argTypes: [DYN], result: DYN },
@@ -228,7 +267,25 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.nativeMapIs": { argTypes: [DYN], result: BOOL },
"dyn.nativeUrlIs": { argTypes: [DYN], result: BOOL },
"dyn.nativeDateIs": { argTypes: [DYN], result: BOOL },
+ /** A builtin constructor (`Object`, `Date`, `RegExp`, `Map`, `Set`,
+ * `Promise`, `WeakMap`, `WeakSet`) read as a JavaScript value: one
+ * interned function per name, the same one dynamic values answer for
+ * `.constructor`. The name is a compiler-chosen literal. */
+ "dyn.globalConstructor": { argTypes: [STRING], result: DYN },
+ /** A builtin global function (setTimeout, setInterval, setImmediate,
+ * the clear functions, queueMicrotask, process.nextTick) read as a
+ * JavaScript value: one interned function per name whose calls run the
+ * timer APIs (it pulls in the event loop). The name is a compiler-chosen
+ * literal. */
+ "dyn.builtinFunction": { argTypes: [STRING], result: DYN },
"date.nativeNew": { argTypes: [DYN], result: DYN },
+ /** `new Date(year, monthIndex, ...)` in JavaScript: the argument array's
+ * first seven values pass through ToNumber in order (later ones are
+ * evaluated, never converted), then the local-time field constructor. */
+ "date.nativeNewFields": { argTypes: [DYN], result: DYN },
+ /** `new Date(value)` in JavaScript: a string argument parses with V8's
+ * full date parser; every other value constructs as date.nativeNew. */
+ "date.nativeNewValue": { argTypes: [DYN], result: DYN },
"date.checkedValue": { argTypes: [DYN], result: DATE_T },
"dyn.nativeRegexIs": { argTypes: [DYN], result: BOOL },
/** toString() on a checked-dynamic receiver: runtime kind dispatch
@@ -256,6 +313,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.promiseAll": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
"dyn.promiseAllSettled": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
"dyn.promiseAny": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
+ /** PromiseResolve over one checked-dynamic value: the promise it holds,
+ * a promise that adopts a thenable's settlement, or a promise fulfilled
+ * with the value. The second argument reads `then` as above. */
+ "dyn.promiseResolve": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
/** Bare `typeof v` on a dyn value AS A STRING (arg: the dyn value,
* borrowed; result: an owned string) — the dyn kind's JS answer:
* undefined→"undefined", null/object/array/bytes→"object" (JS's oldest
@@ -379,6 +440,20 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"perf.entryDetail": { argTypes: [F64], result: DYN },
"perf.entryKeep": { argTypes: [DYN], result: VOID },
"perf.entryRef": { argTypes: [F64], result: DYN },
+ /** perf_hooks event-loop metrics (scr_perf_loop.c): loopStart is the
+ * loop's start in performance.now() ms (0 before it), idleTime the idle
+ * ms since. eldNew(resolution ms) creates a monitorEventLoopDelay
+ * histogram handle (MAY THROW Node's resolution RangeError); enable/disable answer whether the state changed;
+ * eldPercentile expects Node's validated range; eldStat(which) reads
+ * 0 min, 1 max, 2 mean, 3 stddev, 4 count, 5 exceeds. */
+ "perf.loopStart": { argTypes: [], result: F64 },
+ "perf.idleTime": { argTypes: [], result: F64 },
+ "perf.eldNew": { argTypes: [F64], result: F64 },
+ "perf.eldEnable": { argTypes: [F64], result: BOOL },
+ "perf.eldDisable": { argTypes: [F64], result: BOOL },
+ "perf.eldReset": { argTypes: [F64], result: VOID },
+ "perf.eldPercentile": { argTypes: [F64, F64], result: F64 },
+ "perf.eldStat": { argTypes: [F64, F64], result: F64 },
/** process.memoryUsage(): memoryUsageTake samples the resident set and
* the runtime heap once (MAY THROW Node's uv_resident_set_memory error)
* and memoryUsageField(k) reads that sample (0 rss, 1 heapTotal, 2
@@ -1258,6 +1333,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"net.createServer": { argTypes: [], result: NETSERVER_T },
"net.createServerCb": { argTypes: [null], result: NETSERVER_T },
"net.listen": { argTypes: [NETSERVER_T, F64], result: VOID },
+ /** listen(path[, cb]): a Unix-socket server; the socket file is removed
+ * when the server closes. Failures are the async 'error'. */
+ "net.listenPath": { argTypes: [NETSERVER_T, STRING], result: VOID },
+ "net.listenPathCb": {
+ argTypes: [NETSERVER_T, STRING, { kind: "func", params: [], ret: VOID }],
+ result: VOID,
+ },
"net.listenCb": {
argTypes: [NETSERVER_T, F64, { kind: "func", params: [], ret: VOID }],
result: VOID,
@@ -1300,6 +1382,15 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* answers null (the serverPort stance). */
"net.serverAddress": { argTypes: [NETSERVER_T], result: VOID },
"net.serverClose": { argTypes: [NETSERVER_T], result: VOID },
+ /** server.close((err) => ...): the callback (a dyn function) gets
+ * undefined once the server closes, or Node's ERR_SERVER_NOT_RUNNING
+ * when it was not listening. */
+ "net.serverCloseDyn": { argTypes: [NETSERVER_T, DYN], result: VOID },
+ /** server.address() in programs that may listen on a Unix socket: the
+ * path a listening server answers ("" for a TCP server), and the check
+ * a record-typed address() makes first (throws for a Unix server). */
+ "net.serverUnixPath": { argTypes: [NETSERVER_T], result: STRING },
+ "net.serverAddressCheck": { argTypes: [NETSERVER_T], result: VOID },
"net.serverCloseCb": {
argTypes: [NETSERVER_T, { kind: "func", params: [], ret: VOID }],
result: VOID,
@@ -1463,6 +1554,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* callback) and lookupService(address, port, callback). */
"dns.lookupDyn": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID },
"dns.lookupService": { argTypes: [DYN, DYN, DYN, BOOL], result: VOID },
+ /** node:dns/promises: lookup(hostname, options) and lookupService(address,
+ * port) over the callback forms; the promise fulfills with the dyn
+ * { address, family } (array for all: true) / { hostname, service }.
+ * Argument errors throw synchronously (may-throw), unlike the
+ * util.promisify forms below. */
+ "dns.promisesLookup": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
+ "dns.promisesLookupService": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
"dns.getServers": { argTypes: [], result: DYN },
"dns.getDefaultResultOrder": { argTypes: [], result: STRING },
"dns.setDefaultResultOrder": { argTypes: [DYN], result: VOID },
@@ -1882,6 +1980,17 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* non-http scheme. */
"http.requestUrl": { argTypes: [STRING, STRING, BOOL], result: HTTPCLIENTREQ_T },
"http.requestUrlCb": { argTypes: [STRING, STRING, BOOL, null], result: HTTPCLIENTREQ_T },
+ /** request/get with a runtime options record: a RequestOptions
+ * variable, or a literal that spreads one or sets socketPath. Args
+ * (options, autoEnd[, cb]). The runtime reads the record in the
+ * ClientRequest constructor's order (host/port or socketPath, path,
+ * method, headers, timeout, agent, signal, maxHeaderSize) with Node's
+ * errors, and documented options it does not implement throw the
+ * runtime fence. The "https." pair adds the TLS options. MAY THROW. */
+ "http.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T },
+ "http.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T },
+ "https.requestDyn": { argTypes: [DYN, BOOL], result: HTTPCLIENTREQ_T },
+ "https.requestDynCb": { argTypes: [DYN, BOOL, null], result: HTTPCLIENTREQ_T },
/** The paused-mode demux surface (portless's first-byte TLS peek:
* once('readable') + read(1) + unshift + emit('connection')).
* sockOnReadable registers a zero-param listener (a consumer: arrived
@@ -1927,6 +2036,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** Listeners with a parameter, as dyn functions: close(hadError),
* write's cb(err) and end's cb. */
"net.sockOnCloseDyn": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID },
+ /** socket.on/once('lookup', listener): the listener (a dyn function)
+ * gets (err, address, family, host) once per resolved address, or once
+ * with the lookup error, before the connect. */
+ "net.sockOnLookup": { argTypes: [NETSOCKET_T, DYN, BOOL], result: VOID },
"net.sockOnWriteFlush": {
argTypes: [NETSOCKET_T, { kind: "func", params: [], ret: VOID }],
result: VOID,
@@ -2534,8 +2647,13 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* queue, end/destroy settle it, writable is a pure state read, and
* listener callbacks move into the writer registry. */
"child.stdin": { argTypes: [CHILD_T], result: VOID },
+ /** Under the general stream representation (general-streams.ts), a
+ * stdio read (slot 0, 1 or 2) answers the child's bridged runtime stream
+ * — `Writable | null` for stdin, `Readable | null` otherwise; the bridge
+ * throws when the program lacks the stream unit's boxing helpers. */
+ "child.stdioStream": { argTypes: [CHILD_T, F64], result: VOID },
// Listener registrations: callback func shapes are program-dependent
- // (zero-param / Buffer / Buffer-armed union) — the slots pin arity,
+ // (zero-param / Buffer / string / unknown / Buffer-armed union) — the slots pin arity,
// the stream receiver, and the once-flag.
"stream.onData": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID },
"stream.onDataStr": { argTypes: [CHILDSTREAM_T, null, BOOL], result: VOID },
@@ -2555,6 +2673,35 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* stderrWrite paths so buffering and ordering stay identical. Data
* borrowed; the bool is Node's always-true backpressure signal. */
"procStream.write": { argTypes: [PROCSTREAM_T, STRING], result: BOOL },
+ /** process.stdout/stderr flowing into a runtime Writable slot under the
+ * general stream representation: an fd-backed Writable (+1) whose
+ * writes take process.stdout.write's synchronous path. */
+ /** A pipeline() stage that is a checked-dynamic value: a boxed native
+ * stream unwraps by identity (+1); anything else throws the given
+ * not-supported message. Typed as a Duplex so every stage position
+ * accepts it; the runtime pipeline checks the stream's real sides. */
+ "stream.unboxDyn": {
+ argTypes: [DYN, STRING],
+ result: { kind: "object", className: "%Duplex" },
+ },
+ /** Checked-dynamic stream slots (dyn-streams.ts): the native stream a
+ * checked-dynamic value boxes (+1) — a runtime stream with the readable
+ * (1) or writable (2) side — or null for any other value. Never throws;
+ * the result union is `%Readable | null` or `%Writable | null`. */
+ "stream.dynNative": { argTypes: [DYN, F64], result: VOID },
+ /** readable.pipe(destination, end) into a checked-dynamic destination
+ * that is not a native stream: [source, the source boxed, destination,
+ * end]. Node's pipe listener protocol over the destination's members
+ * (scr_stream_dyn.c); throws what those members throw. */
+ "stream.pipeDyn": { argTypes: [null, DYN, DYN, BOOL], result: VOID },
+ /** readable.unpipe(destination) for a checked-dynamic destination that
+ * is not a native stream: [source, destination]. Unpipes the pipe
+ * stream.pipeDyn registered into it, if any (scr_stream_dyn.c). */
+ "stream.unpipeDyn": { argTypes: [null, DYN], result: VOID },
+ "stream.stdioWritable": {
+ argTypes: [PROCSTREAM_T],
+ result: { kind: "object", className: "%Writable" },
+ },
"child.killed": { argTypes: [CHILD_T], result: BOOL },
"child.kill": { argTypes: [CHILD_T, STRING], result: BOOL },
"child.killNum": { argTypes: [CHILD_T, F64], result: BOOL },
@@ -3143,6 +3290,23 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"error.hasCause": { argTypes: [null], result: BOOL },
/** Assignment borrows both operands and retains the new cause. */
"error.setCause": { argTypes: [null, DYN], result: VOID },
+ /** `err.code = s` on an error-hierarchy receiver: the code slot that
+ * error.code reads and coded throws stamp. Borrows both; never throws. */
+ "error.setCode": { argTypes: [null, STRING], result: VOID },
+ /** `err.code = v` with a value that is not statically a string: strings
+ * take the code slot, anything else clears it and sets the dynamic own
+ * property. Borrows both; never throws. */
+ "error.setCodeDyn": { argTypes: [null, DYN], result: VOID },
+ /** `Object.assign(err, ...sources)` onto an error-hierarchy receiver
+ * (args: the error, a dyn array of the sources): the own-member copy
+ * onto its checked-dynamic node, then the typed slots (name, message,
+ * stack, cause, code) re-read from it. Throws what the copy throws. */
+ "error.assign": { argTypes: [null, DYN], result: VOID },
+ /** `key in err` for a key no compiled class of the receiver declares
+ * (args: the error, the key): a runtime-set own property (Object.assign,
+ * throw-site fields, dynamic writes) or an Object.prototype member. */
+ "error.hasKey": { argTypes: [null, STRING], result: BOOL },
+ "error.hasCode": { argTypes: [null], result: BOOL },
"error.defineCause": { argTypes: [null, DYN], result: VOID },
"error.deleteCause": { argTypes: [null], result: VOID },
/** The compiler-resolved Node-parity throw for always-throwing lowered
@@ -3435,6 +3599,16 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.classPrototype": { argTypes: [DYN, DYN], result: DYN },
"dyn.classBasePrototype": { argTypes: [DYN], result: DYN },
"dyn.classInherit": { argTypes: [DYN, DYN], result: DYN },
+ /** EventEmitter (0), Stream (1), Readable (2), Writable (3), Duplex (4),
+ * Transform (5) and PassThrough (6) as untyped constructor values, one
+ * per thread (scr_dyn_builtin_class.c). */
+ "dyn.builtinClass": { argTypes: [F64], result: DYN },
+ /** `value instanceof C` for those constructors, over the value's
+ * prototype chain. */
+ "dyn.builtinInstanceOf": { argTypes: [DYN, F64], result: BOOL },
+ /** `value instanceof f` for an untyped constructor function: the
+ * prototype chain against f.prototype. */
+ "dyn.functionInstanceOf": { argTypes: [DYN, DYN], result: BOOL },
"dyn.classSuper": { argTypes: [DYN, DYN, DYN], result: VOID },
"dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN },
"dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN },
@@ -3592,7 +3766,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* returns the below-hwm answer. readable.read: (recv, size — -1 for
* absent) → Buffer|null union. pause/resume return recv +1 (`this`);
* isPaused answers the flag. readable.pipe: (recv, dst, end) → dst +1,
- * fires 'pipe' on dst; readable.unpipe (recv[, dst]) → recv +1.
+ * fires 'pipe' on dst; readable.pipeGeneral: the same, with `end` off
+ * when dst is the converted process.stdout / process.stderr;
+ * readable.unpipe (recv[, dst]) → recv +1.
* writable.write/writeStr: (recv, chunk[, cb]) → below-hwm bool (cb
* MOVES when present, called after the user write completes).
* writable.end: (recv, flags[, chunk][, cb]) → recv +1. cork/uncork are
@@ -3790,6 +3966,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"readable.resume": { argTypes: [null], result: VOID },
"readable.isPaused": { argTypes: [null], result: BOOL },
"readable.pipe": { argTypes: [null, null, BOOL], result: VOID },
+ "readable.pipeGeneral": { argTypes: [null, null, BOOL], result: VOID },
"readable.unpipe": { argTypes: [null], result: VOID },
"readable.flowing": { argTypes: [null], result: VOID },
"writable.write": { argTypes: [null, BYTES_U8], result: BOOL },
@@ -3968,6 +4145,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
/** A numeric typed array that is not a Buffer (any element kind). */
"insp.typedArray": { argTypes: [null, F64, F64], result: STRING },
"insp.error": { argTypes: [null, F64, F64], result: STRING },
+ "insp.errorHasCause": { argTypes: [null], result: BOOL },
+ "insp.errorBagHasProperties": { argTypes: [DYN], result: BOOL },
+ "insp.errorCause": { argTypes: [null, F64, F64], result: STRING },
"insp.dyn": { argTypes: [DYN, F64, F64], result: STRING },
"insp.dynS": { argTypes: [DYN, F64], result: STRING },
"insp.begin": { argTypes: [F64], result: VOID },
@@ -4339,13 +4519,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"date.toDateString": { argTypes: [DATE_T], result: STRING },
"date.toTimeString": { argTypes: [DATE_T], result: STRING },
/** The composed `new Date(dateString).getTime()` read: one borrowed
- * string, f64 milliseconds since epoch. The parsed grammar is BOUNDED
- * (documented divergence — V8's parser accepts far more): the ASN.1
- * validity shape X509Certificate.validFrom/validTo answer ("Jul 1
- * 00:00:00 2026 GMT" — the portless cert-expiry read), and ECMA's own
- * date-time string format (YYYY-MM-DD[THH:mm[:ss[.sss]]][Z|±HH:MM] —
- * date-only forms are UTC, exactly the spec). Anything else is NaN,
- * Node's invalid-date getTime. Never throws. */
+ * string, f64 milliseconds since epoch, with V8's date-string grammar
+ * (the ES5 format, then the legacy forms: RFC 1123/2822, Date#toString,
+ * "Jan 2, 2024", ...). Zone-less dates are local time; anything V8
+ * rejects is NaN, Node's invalid-date getTime. Never throws. */
"date.parseGetTime": { argTypes: [STRING], result: F64 },
/** `Date.UTC(...)` — seven f64 arguments (the frontend completes the
* spec's defaults for omitted trailing parts: month 0, date 1, time
@@ -4353,6 +4530,12 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* map to 1900+year, out-of-range months/dates roll over, non-finite
* parts and out-of-range results answer NaN. Never throws. */
"date.utc": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: F64 },
+ /** `new Date(year, monthIndex[, date, hours, minutes, seconds, ms])` —
+ * the same seven-f64 completion as date.utc, but the fields are LOCAL
+ * time: V8's MakeDay/MakeTime, then UTC(t) in the host zone (DST gaps
+ * read with the offset before the transition, overlaps answer the
+ * earlier instant), then TimeClip. Never throws. */
+ "date.newLocal": { argTypes: [F64, F64, F64, F64, F64, F64, F64], result: DATE_T },
/** WHATWG TextDecoder.decode over u8 bytes (scr_bytes.c): utf-8 with
* default options — the same maximal-subpart replacement decode as
* Buffer.toString("utf8"), with the leading BOM stripped (the one
diff --git a/packages/compiler/src/ir/capture-values.ts b/packages/compiler/src/ir/capture-values.ts
index d7a1516bb..779ca1b76 100644
--- a/packages/compiler/src/ir/capture-values.ts
+++ b/packages/compiler/src/ir/capture-values.ts
@@ -238,7 +238,7 @@ interface FunctionFacts {
hoisted: Map;
/** Bindings called through `callValue` without a receiver. */
callees: Set;
- /** Temporal-dead-zone bindings: the statement list holding both the
+ /** TDZ (temporal dead zone) bindings: the statement list holding both the
* empty `varDecl` and the initializing `assign`, with their indexes. */
tdzInits: Map;
classCaptured: Set;
diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts
index c2d461e09..4ecc60a6f 100644
--- a/packages/compiler/src/ir/function-references.test.ts
+++ b/packages/compiler/src/ir/function-references.test.ts
@@ -154,6 +154,7 @@ const MODULE_FIELDS: Record>, Role> = {
"classes[].jsName": "other",
"classes[].genericOf": "other",
"classes[].baseValueGlobal": "other",
+ "classes[].errorNameAfter": "other",
"classes[].prototypeDataHelper": "reference",
"classes[].instancePrototypeHelper": "reference",
"classes[].localCaptures[].name": "other",
@@ -170,6 +171,7 @@ const MODULE_FIELDS: Record>, Role> = {
"records[].id": "other",
"records[].fields[].name": "other",
"records[].declaredOrder[]": "other",
+ "records[].accessorOrder[]": "other",
"unions[].id": "other",
"unions[].discriminant.field": "other",
"unions[].discriminant.cases[].values[]": "other",
@@ -210,6 +212,8 @@ const MODULE_FIELDS: Record>, Role> = {
"contracts.acyclic[]": "other",
"contracts.immortalGlobals[]": "other",
"contracts.programLifetime[]": "other",
+ "wasmModules[].sha256": "other",
+ "wasmModules[].bytes": "other",
"threadSlotKeys[]": "other",
};
diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts
index fe0467fac..a08693013 100644
--- a/packages/compiler/src/ir/int32-slots.ts
+++ b/packages/compiler/src/ir/int32-slots.ts
@@ -347,6 +347,8 @@ const DYN_READ_ONLY = new Set([
"arrayBuffer.viewU8",
"arrayBuffer.viewU8C",
"date.nativeNew",
+ "date.nativeNewFields",
+ "date.nativeNewValue",
// The dynamic operators run the ToPrimitive and coercion helpers of
// dyn.compare, dyn.numberConstructor and dyn.toStringCoerce, then build a
// fresh number, string or bigint. dyn.increment steps dyn.toNumeric's result.
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index 6589f530e..9b36e6791 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -1047,6 +1047,24 @@ export interface IrModule {
* write in the program (ir/contract-check.ts). Backends may rely on them
* unconditionally. Absent when the program writes no contract. */
contracts?: IrContracts;
+ /** The WebAssembly modules translated into this program (wasm/
+ * translate.ts), present exactly when the program uses the WebAssembly
+ * API (possibly empty). The backend appends each module's translation
+ * and emits the registry the runtime looks modules up in. */
+ wasmModules?: IrWasmModule[];
+ /** The program calls WebAssembly.Module.customSections, so translated
+ * modules keep their custom sections (otherwise they carry none, and
+ * debug sections cost nothing). */
+ wasmCustomSections?: boolean;
+}
+
+/** A WebAssembly module whose bytes the build saw, validated within the
+ * translatable feature level. */
+export interface IrWasmModule {
+ /** Hex SHA-256 of the bytes. */
+ sha256: string;
+ /** The bytes, base64. */
+ bytes: string;
}
/** Verified contract facts, keyed by IR names. */
@@ -1409,6 +1427,11 @@ export interface IrClassDef {
* bag. Its descriptors preserve presence and insertion order independently
* of native layout order; field values remain in their native slots. */
tracksOwnFields?: boolean;
+ /** Error subclasses: the field an instance's own `name` follows among its
+ * own properties, or null when it precedes every field. Absent when no
+ * class of the chain sets `name` during construction, and then an own
+ * `name` follows every other own property. */
+ errorNameAfter?: string | null;
/** Stable module symbol identities for public instance layout fields. */
symbolFields?: { field: string; globalId: string }[];
/** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC
@@ -1624,6 +1647,10 @@ export interface IrRecordShape {
* fields carry that declaredOrder OMITS are internal '%'-fields (Dirent's
* %dtype) — hidden from every key-order surface, JSON included. */
declaredOrder?: string[];
+ /** Shapes with accessor slots: every own key in declaration order, with
+ * an accessor property named once (see accessorSlotProp). The
+ * checked-dynamic copy lists its keys in this order. */
+ accessorOrder?: string[];
}
/** fs.Dirent rows: the hidden %dtype entry kind (libuv's UV_DIRENT
@@ -3349,9 +3376,10 @@ export type IrExpr =
/** Keyed read on a dyn value — `pkg.name` / `pkg["k"]` / the
* `pkg?.scripts` chain step on a JSON.parse result. `key` is
* string-typed (a strLit for the dot form); `type` is always dyn. An
- * OBJ receiver answers the member (+1) or the undefined singleton (the
- * own-property answer — prototype members like `toString` answer
- * undefined, SEMANTICS.md); ARR answers `length` and canonical
+ * OBJ receiver answers the member (+1), an inherited Object.prototype
+ * member (the Object constructor and the interned valueOf/toString/
+ * hasOwnProperty/propertyIsEnumerable values), or the undefined singleton;
+ * ARR answers `length` and canonical
* in-range indices, STR answers `length` (UTF-16-exact), both
* undefined otherwise; NUM/BOOL/BYTES answer undefined. An
* undefined/null receiver THROWS the catchable Node-shaped TypeError
@@ -4270,9 +4298,6 @@ function canBoxDynCompositeAt(
const shape = getRecord(t.shapeId);
if (!shape) return false;
if (recordTextCodecClass(shape) !== null) return false;
- // Accessor slots hold the getter and setter closures, not property
- // values (see accessorSlotProp).
- if (shapeHasAccessorSlots(shape)) return false;
// Recursive shapes answer coinductively, like isJsonSafeType.
if (visiting.has(t.shapeId)) return true;
visiting.add(t.shapeId);
diff --git a/packages/compiler/src/ir/runtime-features.ts b/packages/compiler/src/ir/runtime-features.ts
index 4d59a6241..0d680d7af 100644
--- a/packages/compiler/src/ir/runtime-features.ts
+++ b/packages/compiler/src/ir/runtime-features.ts
@@ -21,6 +21,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet = new Set([
"dyn.promiseAll",
"dyn.promiseAllSettled",
"dyn.promiseAny",
+ "dyn.promiseResolve",
"process.onUncaughtException",
"process.offUncaughtException",
"process.onUnhandledRejection",
@@ -130,6 +131,10 @@ const LEGACY_TEXT_DECODER_LIB_FNS: ReadonlySet = new Set([
/** Signal-taking readline, stream and timers/promises entry points, which register abort listeners. */
const ABORT_SIGNAL_LIB_FNS: ReadonlySet = new Set([
"http.clientSignal",
+ "http.requestDyn",
+ "http.requestDynCb",
+ "https.requestDyn",
+ "https.requestDynCb",
"rl.createSignal",
"rl.questionAsyncSignal",
"sp.finishedOptsSignal",
@@ -144,6 +149,7 @@ const ABORT_SIGNAL_LIB_FNS: ReadonlySet = new Set([
/** Entry points that link the stream unit (with STREAM_LIB_PREFIXES). */
const STREAM_LIB_FNS: ReadonlySet = new Set([
+ "child.stdioStream",
"console.bind",
"crypto.hashStream",
"crypto.hashStreamPiped",
@@ -162,6 +168,11 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([
"stream.destroyErr",
"stream.errored",
"stream.prop",
+ "stream.dynNative",
+ "stream.pipeDyn",
+ "stream.unpipeDyn",
+ "stream.stdioWritable",
+ "stream.unboxDyn",
"stream.workerStdio",
]);
@@ -196,14 +207,29 @@ const READABLE_DYN_ASYNC_PREFIXES: readonly string[] = ["readable.fromGen", "rea
/** Entry points that link the dyn-invoke unit. */
const DYN_INVOKE_LIB_FNS: ReadonlySet