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/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..73b48c88d 100644
--- a/docs/content/limitations/builtin-apis/streams-and-readline.mdx
+++ b/docs/content/limitations/builtin-apis/streams-and-readline.mdx
@@ -1,4 +1,7 @@
- **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.
+ A `pipeline` stage that arrives as an untyped value must be a native stream at run time; other values throw a not-supported error.
`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 +14,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 +30,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/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..8269207fa 100644
--- a/docs/content/limitations/webassembly-modules.mdx
+++ b/docs/content/limitations/webassembly-modules.mdx
@@ -1,7 +1,30 @@
-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, ...)`), 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`, 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`).
+Imports can be functions, numbers or BigInts for immutable globals, and memories, tables and globals exported by other instances.
+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 `Memory`, `Table`, `Global`, `Tag` and `Exception` constructors, `WebAssembly.Module.imports`, `exports` and `customSections`, `compileStreaming` and `instantiateStreaming`, and the classes used as values other than with `new` and `instanceof`.
+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:
+
+- `Memory.prototype.grow` does not detach the previous `ArrayBuffer`.
+ The memory never moves, so views created before the growth stay valid but keep their old length.
+- Instances are never freed, together with their memories, imports and exports.
+- A function reference (`funcref`) can reach JavaScript only as a function the module exports; other function references throw a `TypeError`.
+- 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 4b320f3d2..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,
@@ -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"
}
},
{
@@ -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 55483f3b3..7260fff78 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": 1185,
- "verify-gap": 2140,
- "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,21 @@
"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"
+ ]
}
}
},
@@ -77831,6 +77873,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",
@@ -81983,12 +82048,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 903547576..280c642f2 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,
@@ -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,14 @@
"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-spec-control/main.mjs",
+ "tests/corpus/webassembly-spec-float/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/fixtures/node-types/navigator.ts",
"tests/harness/fetch-conformance.test.ts",
"tests/harness/project-config.test.ts"
@@ -53600,8 +53657,18 @@
"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"
+ ]
},
"anchorSource": "exact"
},
@@ -104503,6 +104570,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"
]
},
@@ -105625,8 +105694,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"
},
@@ -111238,6 +111311,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"
]
},
@@ -111506,8 +111581,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 3fb7803b1..d6dc342ae 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"],
"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"],
@@ -385,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 d733aef19..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;
}
}
@@ -1907,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
@@ -3088,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 {
@@ -4716,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. */
@@ -4727,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;
@@ -4737,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). */
@@ -4755,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. */
@@ -5078,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;
@@ -5095,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
@@ -5370,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;
}
@@ -6187,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. */
@@ -6346,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,
@@ -6596,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;
@@ -6980,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 31a17cb92..38f44e58e 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,
@@ -31,12 +32,14 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
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,
diff --git a/packages/compiler/src/backend/library-pack.ts b/packages/compiler/src/backend/library-pack.ts
index a7cd5478c..42275c5af 100644
--- a/packages/compiler/src/backend/library-pack.ts
+++ b/packages/compiler/src/backend/library-pack.ts
@@ -42,12 +42,14 @@ 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,
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 6be6681a1..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()`);
@@ -729,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
@@ -838,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()`] : []),
diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts
index 9d8bd1f9e..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;
@@ -5578,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 3c277cc8b..788cbf570 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -323,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",
@@ -739,6 +740,8 @@ 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",
"process.kill": "scr_process_kill_named",
"process.killNum": "scr_process_kill",
// fs.promises (scr_bytes_io.c / scr_lib.c): the promise forms settle
@@ -871,6 +874,16 @@ 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.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",
@@ -939,8 +952,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",
@@ -954,7 +968,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",
@@ -1117,6 +1135,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",
@@ -1164,6 +1183,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",
@@ -1427,6 +1450,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",
@@ -1599,6 +1625,7 @@ export const USES_TIMERS_LIB_FNS = new Set([
"readable.opReduce",
"readable.resume",
"readable.pipe",
+ "readable.pipeGeneral",
"readable.unpipe",
"writable.write",
"writable.writeStr",
@@ -1639,6 +1666,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",
@@ -1657,6 +1686,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",
@@ -1678,4 +1711,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..0ded59a1a 100644
--- a/packages/compiler/src/backend/llvm/lib-stream.ts
+++ b/packages/compiler/src/backend/llvm/lib-stream.ts
@@ -56,6 +56,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 +80,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 +90,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..6a79ff5fe
--- /dev/null
+++ b/packages/compiler/src/backend/llvm/wasm-modules.ts
@@ -0,0 +1,74 @@
+/* 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 });
+ } 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 ae2abdae4..031f99ddd 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;
@@ -46,12 +49,15 @@ export interface NativeLinkFeatures {
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;
diff --git a/packages/compiler/src/backend/native/contracts.ts b/packages/compiler/src/backend/native/contracts.ts
index 428f07056..8abdc6c38 100644
--- a/packages/compiler/src/backend/native/contracts.ts
+++ b/packages/compiler/src/backend/native/contracts.ts
@@ -118,6 +118,8 @@ export interface CcOptions {
* 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 +152,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 +184,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. */
diff --git a/packages/compiler/src/backend/native/executable.ts b/packages/compiler/src/backend/native/executable.ts
index f8db31d29..33c126de8 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 ||
@@ -237,6 +238,15 @@ 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.
@@ -254,6 +264,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..be94b33b9 100644
--- a/packages/compiler/src/backend/runtime-pack-core.ts
+++ b/packages/compiler/src/backend/runtime-pack-core.ts
@@ -82,6 +82,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
workers: boolean;
youngNursery: boolean;
eventTarget: boolean;
+ wasm: boolean;
dynTier: boolean;
warningReplace: boolean;
stdioExtended: boolean;
@@ -272,6 +273,7 @@ export function effectiveRuntimeFeatures(
workers: features.workers === true,
youngNursery: features.youngNursery === true,
eventTarget: features.eventTarget === true,
+ wasm: features.wasm === true,
dynTier: features.dynTier === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
diff --git a/packages/compiler/src/backend/runtime-pack-targets.test.ts b/packages/compiler/src/backend/runtime-pack-targets.test.ts
index f384f6844..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,
diff --git a/packages/compiler/src/backend/runtime-pack.test.ts b/packages/compiler/src/backend/runtime-pack.test.ts
index 7040a8f68..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,
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 146f2a414..9a505d517 100644
--- a/packages/compiler/src/backend/warm-cache.ts
+++ b/packages/compiler/src/backend/warm-cache.ts
@@ -50,12 +50,14 @@ 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,
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..824790bd9 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, Module, Instance, instanceof, the error classes, exports, memories, tables, globals 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..05c8ae672 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,19 @@ 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 Memory, Table and Global constructors,
+ * Module.imports/exports/customSections, the streaming functions, the
+ * classes as values. compile, instantiate, validate, Module, Instance and
+ * the error classes lower (builtins/webassembly.ts). `surface` names what
+ * was used ('new WebAssembly.Memory', 'WebAssembly.Module.imports'). */
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, Module, Instance 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 Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions 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 cce2889c3..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,
@@ -200,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));
@@ -217,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 4dca7f2ed..fd4a34ec2 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;
@@ -71,12 +72,14 @@ export interface EarlyExecutableNativeFeatures {
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;
@@ -117,6 +120,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;
@@ -186,12 +192,14 @@ const BOOLEAN_NATIVE_KEYS = [
"bigint",
"searchParams",
"qs",
+ "dynBuiltinClass",
"parseArgs",
"stream",
"net",
"http",
"http2",
"dgram",
+ "dnsPromises",
"watch",
"foreignFfi",
"nodeTest",
@@ -232,6 +240,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
? ""
@@ -272,6 +281,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 f83958b32..d2e4b53d5 100644
--- a/packages/compiler/src/frontend/lowering/builtin-calls.ts
+++ b/packages/compiler/src/frontend/lowering/builtin-calls.ts
@@ -16,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
@@ -55,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(
@@ -130,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/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/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..e60a14c0c 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,21 @@ 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;
+ 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..2179816c1 100644
--- a/packages/compiler/src/frontend/lowering/builtins/webassembly.ts
+++ b/packages/compiler/src/frontend/lowering/builtins/webassembly.ts
@@ -2,17 +2,41 @@ 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";
/* 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 and new WebAssembly.Instance lower to wasm.*
+ * libCalls; members of the values they produce (exports, memory
+ * buffers, globals, tables) dispatch through the native handles;
+ * - `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 bare namespace, the
+ * Memory, Table and Global constructors, Module.imports and friends, the
+ * streaming functions) 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 +154,158 @@ 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);
+}
+
+/** The arguments, padded with undefined to `count`; null for spreads or
+ * extra arguments (they stay refused). */
+function lowerArgs(
+ lowerer: Lowerer,
+ args: readonly ts.Expression[],
+ count: number,
+ loc: IrExpr["loc"],
+): IrExpr[] | null {
+ if (args.length > count || args.some(ts.isSpreadElement)) return null;
+ 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,
+ };
+}
+
+/** `WebAssembly.compile(bytes)`, `WebAssembly.instantiate(source,
+ * imports?)`, `WebAssembly.validate(bytes)`, and the error classes called
+ * without `new`. 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 null;
+ const loc = locOf(call);
+ 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?)` 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 (member !== "Module" && member !== "Instance" && !isError) return null;
+ const loc = locOf(expr);
+ const used = member === "Module" ? 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
+ 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 }]);
+}
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-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/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/error-name-order.ts b/packages/compiler/src/frontend/lowering/error-name-order.ts
new file mode 100644
index 000000000..7453d123e
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/error-name-order.ts
@@ -0,0 +1,98 @@
+import * as ts from "../ts7/adapter.js";
+import { isClassOwnEnumerableFieldName } from "../../ir/ir.js";
+import type { ClassInfo } from "./lower-classes.js";
+
+type ErrorClassLink = Pick & {
+ base: ErrorClassLink | null;
+};
+
+/** The property key a member or element-access name spells statically:
+ * an identifier, a quoted string, a number in its canonical spelling, or a
+ * computed string literal. Null for other keys. */
+function staticKeyText(name: ts.Node): string | null {
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
+ if (ts.isNumericLiteral(name)) return String(Number(name.text));
+ if (ts.isComputedPropertyName(name)) return staticKeyText(name.expression);
+ return null;
+}
+
+/** Whether `node` is an assignment to `this.` or `this[""]`,
+ * and the name. */
+function thisAssignmentName(node: ts.Node): string | null {
+ if (!ts.isBinaryExpression(node) || node.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
+ return null;
+ const target = node.left;
+ if (ts.isPropertyAccessExpression(target) && target.expression.kind === ts.SyntaxKind.ThisKeyword)
+ return ts.isIdentifier(target.name) ? target.name.text : null;
+ if (ts.isElementAccessExpression(target) && target.expression.kind === ts.SyntaxKind.ThisKeyword)
+ return staticKeyText(target.argumentExpression);
+ return null;
+}
+
+/** Where an Error subclass instance's own `name` sits among its fields when
+ * a class of its chain gives it one during construction. A property's
+ * position is fixed when it is created: a `name` field takes its place
+ * among the fields (parameter properties first, then the declared ones),
+ * and `this.name = ...` in a constructor follows the fields created so far,
+ * including the ones that constructor created before it. The answer is the
+ * field `name` follows, null when it precedes every field, and undefined
+ * when no class of the chain sets it during construction. `fields` are the
+ * instance's own enumerable fields. */
+export function errorNameAfterField(
+ info: ErrorClassLink,
+ fields: ReadonlySet,
+): string | null | undefined {
+ const chain: ErrorClassLink[] = [];
+ for (let current: ErrorClassLink | null | undefined = info; current; current = current.base)
+ if (current.decl) chain.unshift(current);
+ const created = new Set();
+ let last: string | null = null;
+ const create = (name: string): void => {
+ if (!fields.has(name) || created.has(name)) return;
+ created.add(name);
+ last = name;
+ };
+ for (const link of chain) {
+ for (const property of link.paramProps ?? []) create(property.name);
+ for (const member of link.decl!.members.nodes) {
+ if (!ts.isPropertyDeclaration(member)) continue;
+ const key = staticKeyText(member.name);
+ if (key === null) continue;
+ if (
+ member.modifiers?.nodes.some(
+ (m) => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.DeclareKeyword,
+ )
+ )
+ continue;
+ if (key === "name") return last;
+ create(key);
+ }
+ const body = link.ctor?.body;
+ if (!body) continue;
+ let found = false;
+ ts.walkPreorder(body, (node) => {
+ if (ts.isTypeNode(node)) return "skip";
+ if (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return "skip";
+ const name = thisAssignmentName(node);
+ if (name === "name") {
+ found = true;
+ return "stop";
+ }
+ if (name !== null) create(name);
+ return undefined;
+ });
+ if (found) return last;
+ }
+ return undefined;
+}
+
+/** The fields an Error subclass's own-property view lists. */
+export function errorViewFields(fields: readonly { name: string }[]): Set {
+ return new Set(
+ fields
+ .map((field) => field.name)
+ .filter(
+ (name) => isClassOwnEnumerableFieldName(name) && name !== "name" && name !== "message",
+ ),
+ );
+}
diff --git a/packages/compiler/src/frontend/lowering/expressions/array-literals.ts b/packages/compiler/src/frontend/lowering/expressions/array-literals.ts
index ef2aa9778..d311d6d79 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/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 27ce7327e..c2e5ddcbb 100644
--- a/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/identifiers.ts
@@ -1,5 +1,6 @@
/* 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";
@@ -23,21 +24,30 @@ import { lowerNodeModuleIdentifier } from "../lower-node-module.js";
import { lowerCjsExportsObjectValue } from "../cjs-exports-object.js";
import { captureContextArguments } from "../function-context.js";
import { argumentsOwner, typedArgumentsCandidate } from "../typed-arguments.js";
-import { isCjsExportTableLiteral, isJsSourceFile, isNodeEsmFile, locOf } from "../../program.js";
+import {
+ isCjsExportTableLiteral,
+ isCjsJsFile,
+ isJsSourceFile,
+ isNodeEsmFile,
+ locOf,
+} from "../../program.js";
import { builtinClassAliasOwnsNoSlot, runtimeClassValueOf } from "../builtins/class-values.js";
import {
cjsAccessorCall,
+ cjsAccessorSnapshotOf,
primitiveCtorClosure,
runtimeOptionalElementKey,
runtimeOptionalUseOf,
} from "../lower-exprs.js";
import { runtimeOptionalReceiverRead } from "./narrowing.js";
+import { builtinGlobalValueAt } from "./property-access.js";
import { varRef } from "../../../ir/build.js";
import {
ambientUndefReadType,
ambientUndefinedFnSymbolOf,
contextualUndefReadType,
fenceEarlyAliasUse,
+ isTemporalGlobalRoot,
lowerNsIdentifierValue,
nsUndefRead,
} from "../lower-namespaces.js";
@@ -543,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,
};
@@ -728,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
@@ -943,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 fc78e3a38..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(
@@ -1325,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).
@@ -1378,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);
@@ -1452,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/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
index 49f7e3448..22bcb6609 100644
--- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
@@ -100,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,
@@ -158,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).
@@ -765,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.
@@ -799,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")
@@ -811,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
@@ -890,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
@@ -1296,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,
@@ -1303,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;
}
@@ -1346,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 {
@@ -2625,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,
@@ -3371,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,
@@ -3598,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
@@ -3622,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 146766a46..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",
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/general-streams.ts b/packages/compiler/src/frontend/lowering/general-streams.ts
new file mode 100644
index 000000000..0113a9958
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/general-streams.ts
@@ -0,0 +1,114 @@
+/* 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" };
+ const accepts =
+ (expected.kind === "object" && expected.className === "%Writable") ||
+ (expected.kind === "union" && lowerer.armTag(expected.unionId, writable) >= 0);
+ 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;
+}
diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts
index edd36b9cc..f3864aa41 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -1,5 +1,6 @@
import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js";
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.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";
@@ -250,6 +251,8 @@ 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 {
@@ -265,10 +268,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
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index 241c6f580..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";
@@ -71,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,
@@ -145,6 +148,7 @@ import {
ambientUndefReadType,
ambientUndefVarRootOf,
ambientUndefinedFnSymbolOf,
+ contextualUndefReadType,
fenceEarlyAliasUse,
fenceEarlyNsMemberRef,
nsMemberIdentOf,
@@ -2220,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,
@@ -4356,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).
@@ -5279,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,
@@ -8931,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. */
@@ -9055,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)
@@ -9119,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) {
@@ -9663,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",
@@ -10320,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 3629c7c24..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)) {
@@ -2066,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,
@@ -2615,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
@@ -2646,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-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 3be155402..1310469fb 100644
--- a/packages/compiler/src/frontend/lowering/lower-inspect.ts
+++ b/packages/compiler/src/frontend/lowering/lower-inspect.ts
@@ -1,4 +1,5 @@
import { dynUndefinedExpr, boolLit, helperLoc, numLit, strLit, varRef } from "../../ir/build.js";
+import { builtinClassDynValueOf, builtinClassKindOf } from "./builtins/builtin-class-dyn.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -2609,8 +2610,15 @@ function lowerUtilCallableCall(
const arity = member === "deprecate" ? 4 : member === "callbackify" ? 1 : 2;
// Program classes keep static prototypes: there is no function object
// whose prototype chain the runtime could relink or wrap.
+ // EventEmitter and the stream classes are the exception as superCtor:
+ // the runtime has untyped constructors for them (builtin-class-dyn.ts).
const classArgs = member === "inherits" ? 2 : member === "deprecate" ? 1 : 0;
- for (const arg of expr.arguments.nodes.slice(0, classArgs)) {
+ const builtinBase =
+ member === "inherits" && expr.arguments.nodes[1] !== undefined
+ ? builtinClassKindOf(lowerer, expr.arguments.nodes[1])
+ : null;
+ for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) {
+ if (index === 1 && builtinBase !== null) continue;
if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval")
lowerer.noLowering(
`util.${member} over a class constructor`,
@@ -2629,9 +2637,12 @@ function lowerUtilCallableCall(
const stmts: IrStmt[] = [];
const args: IrExpr[] = [];
for (let i = 0; i < arity; i++) {
- const input: IrExpr = expr.arguments.nodes[i]
- ? lowerer.lowerExprExpecting(expr.arguments.nodes[i]!, DYN)
- : dynUndefinedExpr(loc);
+ const node = expr.arguments.nodes[i];
+ const input: IrExpr = !node
+ ? dynUndefinedExpr(loc)
+ : i === 1 && builtinBase !== null
+ ? (builtinClassDynValueOf(lowerer, node) ?? lowerer.lowerExprExpecting(node, DYN))
+ : lowerer.lowerExprExpecting(node, DYN);
const saved = lowerer.declareHiddenLocal("%utilInput", DYN);
stmts.push({ kind: "varDecl", localId: saved.id, init: input, loc });
args.push(varRef(saved.id, DYN, loc));
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 9831ad0be..29cd9c437 100644
--- a/packages/compiler/src/frontend/lowering/lower-stmts.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stmts.ts
@@ -1,5 +1,7 @@
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 {
matchAllRowsReadByElements,
storedMatchAllRowsReadByElements,
@@ -33,6 +35,7 @@ import {
import {
cjsExportMemberWriteGlobalOf,
isCjsExportsIdentifier,
+ isCjsModuleExports,
isFreshExportsReset,
} from "./cjs-exports-object.js";
import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js";
@@ -196,6 +199,7 @@ import {
import { globalSymbolKey } from "./expressions/global-symbols.js";
import { lowerShortCircuitAssignment } from "./expressions/nullish-assignment.js";
import {
+ builtinGlobalValue,
jsHoldsDynObject,
lowerEnvironmentKey,
lowerNativeFunctionAssignment,
@@ -224,6 +228,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[];
@@ -2401,20 +2406,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);
@@ -3776,6 +3791,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)) {
@@ -3819,6 +3837,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({
@@ -6927,6 +6949,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) };
@@ -7520,6 +7561,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(
@@ -8961,6 +9027,8 @@ export function lowerForOf(lowerer: Lowerer, stmt: ts.ForOfStatement): IrStmt {
}
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,
@@ -9194,6 +9262,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);
diff --git a/packages/compiler/src/frontend/lowering/lower-stream.ts b/packages/compiler/src/frontend/lowering/lower-stream.ts
index dd0ddcc0e..eb919909b 100644
--- a/packages/compiler/src/frontend/lowering/lower-stream.ts
+++ b/packages/compiler/src/frontend/lowering/lower-stream.ts
@@ -49,6 +49,7 @@ 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 { requestGeneralStreamSurface } from "./general-streams.js";
import { extractIteratorValue, genStep, genValue, stepDrivable } from "./lower-generators.js";
import {
BOOL,
@@ -586,6 +587,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 +699,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 +2077,24 @@ 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") {
+ return {
+ kind: "libCall",
+ fn: "stream.unboxDyn",
+ args: [
+ v,
+ strLit(
+ "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 +3347,124 @@ 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,
+ };
+}
+
+/** 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);
+ 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" };
+ 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 +3714,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,
@@ -3674,8 +3847,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 +3907,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 +4175,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 6c9ccd0b7..26d331967 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -1,6 +1,8 @@
import { onlyForwarded } from "./typed-arguments.js";
+import { needsGeneralNodeStreams, 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,
@@ -31,8 +33,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";
@@ -186,6 +193,7 @@ import type {
IrGlobal,
IrLocal,
IrModule,
+ IrWasmModule,
IrParam,
IrRecordShape,
IrStmt,
@@ -878,6 +886,10 @@ 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;
/** LoadResult.inlineRequireCycleCalls. */
inlineRequireCycleCalls?: ReadonlySet | undefined;
/** LowerOptions.libraryCallbacks (see there). */
@@ -1034,10 +1046,40 @@ 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;
+ if (generalNodeStreams) {
+ 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,
+ });
+ 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("general-streams-relower", { reachable: reachable.size });
+ }
// 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
@@ -1045,10 +1087,11 @@ 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 } : {}),
reachable,
targetPlatform,
startupCrash,
@@ -1104,25 +1147,69 @@ 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;
+ 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): Lowerer =>
+ new Lowerer(program, entry, moduleOrder, {
+ frontendServices: options.frontendServices,
+ projectCheckJs: options.projectCheckJs ?? false,
+ ...(general ? { generalNodeStreams: 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);
+ rem = remainder.run();
+ }
timing("coverage-remainder");
// Tiers (tier-analysis.ts): reached statements in dynamic-tier functions,
// and with tierReport the report-only demotion analysis.
@@ -1157,21 +1244,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;
@@ -1963,6 +2036,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). */
@@ -2031,7 +2108,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. */
@@ -2509,6 +2588,12 @@ 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;
/** 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. */
@@ -2587,6 +2672,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. */
@@ -2666,6 +2755,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();
@@ -2755,6 +2848,9 @@ 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 constructs a LifetimeRegion (lifetime-regions.ts). */
usesLifetimeRegions = false;
/** Per-directory answers of experimental-import.ts's declaration-file test. */
@@ -2956,6 +3052,7 @@ 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;
/** Functions finishModule placed in the dynamic tier (ir/tiers.ts). */
dynamicFunctions: DynamicFunction[] = [];
/** Their IR, for coverage's dynamic statement count. */
@@ -3084,6 +3181,7 @@ 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.workerTargets = mode.workerTargets ?? [];
this.workerTargets.forEach((sf, id) => {
this.workerTargetIdByPath.set(tsgoPath(resolve(sf.fileName)), id);
@@ -3180,6 +3278,7 @@ export class Lowerer {
isExternalTypeFile: (sf) =>
this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))),
typeMemo: this.typeMemo,
+ ...(this.generalNodeStreams ? { generalNodeStreams: true } : {}),
canMemoizeType: () =>
this.typeParamBindings === null &&
this.typeParamTsBindings === null &&
@@ -8464,6 +8563,8 @@ 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;
// 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.
@@ -8662,6 +8763,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) {
@@ -8670,6 +8782,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)) {
@@ -9036,6 +9151,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;
@@ -9373,6 +9492,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
@@ -10112,6 +10232,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",
@@ -12057,6 +12186,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) ??
@@ -12285,6 +12416,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) ??
@@ -12689,8 +12822,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",
@@ -13264,3 +13409,180 @@ function readsLength(fn: ts.Node, isRead: (id: ts.Identifier) => boolean): boole
});
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/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts
index ba1ea3c08..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",
@@ -2824,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
@@ -3454,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/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 bd6723dfc..a1292ad6a 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,13 @@ 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;
/** 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 +1387,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 +1684,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 +1723,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 +1776,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);
@@ -2521,6 +2729,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.
@@ -3008,6 +3221,20 @@ 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).
+ 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 { kind: "object", className: "%Writable" };
+ }
if (
(psym?.name === "WriteStream" &&
psym.declarations.some(
@@ -3017,13 +3244,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;
}
@@ -3033,6 +3254,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) &&
@@ -3040,6 +3262,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)) &&
@@ -3050,6 +3273,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
@@ -3057,6 +3294,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 &&
@@ -3064,38 +3304,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"),
)
) {
@@ -3117,6 +3358,9 @@ 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.
+ if (ctx.generalNodeStreams === true) return { kind: "object", className: "%Readable" };
return { kind: "childStream" };
}
// dgram.Socket: the same ambiguous "Socket" name disambiguated by its
@@ -3875,6 +4119,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
@@ -4156,10 +4428,18 @@ 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")));
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 =
@@ -4557,6 +4837,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();
@@ -5424,9 +5707,12 @@ function inferSymbolOf(t: ts.Type, _ctx: TypeMapperCtx): ts.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. Parameters must be `any` (they constrain nothing); anything else
- * stays undecided. A primitive never matches a function type, and neither
- * does a function that needs more arguments than fixed parameters pass. */
+ * 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,
@@ -5436,52 +5722,97 @@ function matchInferSignature(
const wantSigs = checker.getCallSignatures(extendsType);
const want = wantSigs[0];
if (wantSigs.length !== 1 || !want || want.getTypeParameters().length > 0) return undefined;
- const anyParams = want.getParameters().every((p) => {
- const t = checker.getTypeOfSymbol(p);
- return (
- (t.flags & ts.TypeFlags.Any) !== 0 ||
- (checker.isArrayType(t) &&
- (checker.getTypeArguments(t as ts.TypeReference)[0]!.flags & ts.TypeFlags.Any) !== 0)
- );
- });
- if (!anyParams) 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;
- // A pattern with fixed parameters admits only functions that need no
- // more arguments than it passes: `(a: any) => infer R` rejects
- // `(a: string, b: string) => R`, as TypeScript's assignability does.
- const wantArity = signatureArity(want);
- const gotArity = signatureArity(got);
- if (
- wantArity !== undefined &&
- gotArity !== undefined &&
- !wantArity.rest &&
- gotArity.required > want.getParameters().length
- )
- return false;
+ 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;
}
-/** How many arguments a call of `sig` must pass (up to its last required
- * parameter), and whether it takes a rest parameter. Undefined when a
- * parameter has no declaration to read this from; the arity check is then
- * skipped. */
-function signatureArity(sig: ts.Signature): { required: number; rest: boolean } | undefined {
- let required = 0;
- let rest = false;
- const params = sig.getParameters();
- for (let i = 0; i < params.length; i++) {
- const decl = params[i]!.valueDeclaration;
+/** 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;
- if (decl.dotDotDotToken !== undefined) rest = true;
- else if (decl.questionToken === undefined && decl.initializer === undefined) required = i + 1;
+ 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 { required, rest };
+ 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
@@ -6333,6 +6664,7 @@ interface RecordShapeParts {
fields: { name: string; type: IrType }[];
indexValue?: IrType;
declaredOrder?: string[];
+ accessorOrder?: string[];
}
function mapRecordType(widened: ts.Type, ctx: TypeMapperCtx): IrType | null {
@@ -6369,15 +6701,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
@@ -6390,17 +6721,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
@@ -6760,8 +7370,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 d2af829b5..7b1ef7109 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,
@@ -543,12 +551,14 @@ async function compileExecutableNative(
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 } : {}),
@@ -723,6 +733,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) {
@@ -788,6 +799,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 } : {}),
});
@@ -1053,6 +1065,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 03fa5c6fd..f42885043 100644
--- a/packages/compiler/src/ir/builtin-effects.ts
+++ b/packages/compiler/src/ir/builtin-effects.ts
@@ -75,6 +75,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"process.stdoutWriteBytes",
"process.stderrWriteBytes",
"procStream.write",
+ "child.stdioStream",
+ "stream.unboxDyn",
"weakMap.new",
"weakSet.new",
"dyn.fromEntries",
@@ -133,6 +135,12 @@ 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",
"events.addAbortListener",
"fetch.streamNew",
"fetch.streamFrom",
@@ -263,6 +271,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 +340,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([
"stream.onDyn",
"readable.resume",
"readable.pipe",
+ "readable.pipeGeneral",
"readable.unpipe",
"writable.write",
"writable.writeStr",
@@ -494,6 +511,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",
@@ -560,6 +579,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",
@@ -795,6 +817,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 b807c7b42..714903788 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -108,6 +108,27 @@ 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 },
+ /** 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 +249,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 +295,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
@@ -1272,6 +1315,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,
@@ -1314,6 +1364,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,
@@ -1477,6 +1536,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 },
@@ -1896,6 +1962,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
@@ -1941,6 +2018,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,
@@ -2548,8 +2629,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 },
@@ -2569,6 +2655,21 @@ 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" },
+ },
+ "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 },
@@ -3466,6 +3567,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 },
@@ -3623,7 +3734,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
@@ -3821,6 +3934,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 },
@@ -4370,13 +4484,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
@@ -4384,6 +4495,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 d6ff0fe72..ba86a4eb4 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -1044,6 +1044,20 @@ 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[];
+}
+
+/** 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. */
@@ -1406,6 +1420,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
@@ -1621,6 +1640,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
@@ -3346,9 +3369,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
@@ -4267,9 +4291,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 4ba157d74..b52afafcb 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,8 @@ const STREAM_LIB_FNS: ReadonlySet = new Set([
"stream.destroyErr",
"stream.errored",
"stream.prop",
+ "stream.stdioWritable",
+ "stream.unboxDyn",
"stream.workerStdio",
]);
@@ -198,12 +206,24 @@ const READABLE_DYN_ASYNC_PREFIXES: readonly string[] = ["readable.fromGen", "rea
const DYN_INVOKE_LIB_FNS: ReadonlySet = new Set([
"dyn.arrayProtoCall",
"dyn.arrayPrototype",
+ "dyn.builtinFunction",
"dyn.builtinMethod",
"dyn.defineProperty",
"dyn.defineProps",
"dyn.definePrototypeProps",
"dyn.functionApply",
+ "dyn.globalConstructor",
"dyn.objCreateWithProperties",
+ "dyn.objectTag",
+]);
+
+/** Member names whose constant-key dynamic reads answer the inherited Object.prototype member through the dyn-invoke unit. Mirrors expr-dynamic.ts INHERITED_OBJECT_MEMBERS (the IR layer does not import the backend). */
+const INHERITED_MEMBER_NAMES: ReadonlySet = new Set([
+ "constructor",
+ "hasOwnProperty",
+ "propertyIsEnumerable",
+ "toString",
+ "valueOf",
]);
/** Entry points that link the child-process unit (with CHILD_PROCESS_LIB_PREFIXES): the fork IPC channel on the process object. */
@@ -221,8 +241,21 @@ const CHILD_PROCESS_LIB_FNS: ReadonlySet = new Set([
/** libCall name prefixes that link the child-process unit. */
const CHILD_PROCESS_LIB_PREFIXES: readonly string[] = ["child.", "cp.", "spawnRes.", "writer."];
+/** The entry points scr_dyn_builtin_class.c defines. The unit calls
+ * listeners through the dyn-invoke unit, renders unhandled 'error'
+ * payloads through the inspect unit, creates EventEmitter's private
+ * symbols through the symbol unit and shares defaultMaxListeners with the
+ * emitter unit, so each of them links all five. */
+const BUILTIN_CLASS_LIB_FNS: readonly IrLibFn[] = [
+ "dyn.builtinClass",
+ "dyn.builtinInstanceOf",
+ "dyn.functionInstanceOf",
+];
+
/** Entry points that link the symbol unit, besides the sym. and disposable. families. */
const SYMBOL_LIB_FNS: ReadonlySet = new Set([
+ // EventEmitter's kShapeMode and kCapture properties
+ ...BUILTIN_CLASS_LIB_FNS,
"dyn.usingDispose",
"dyn.usingMethod",
"util.isDeepStrictEqual",
@@ -450,7 +483,12 @@ const RUNTIME_FEATURE_RULES = {
* queueMicrotask scheduling goes through the runtime's *_als entry
* points, which carry the active stores into each callback. */
asyncContext: rule({ fns: [...ASYNC_CONTEXT_LIB_FNS] }),
- bigint: rule({ fns: ["process.hrtimeValue"], prefixes: ["bigint."], types: ["bigint"] }),
+ /** scr_wasm.c converts i64 values through BigInt. */
+ bigint: rule({
+ fns: ["process.hrtimeValue"],
+ prefixes: ["bigint.", "wasm."],
+ types: ["bigint"],
+ }),
childProcess: rule({
fns: [...CHILD_PROCESS_LIB_FNS],
prefixes: CHILD_PROCESS_LIB_PREFIXES,
@@ -468,6 +506,8 @@ const RUNTIME_FEATURE_RULES = {
dc: rule({ prefixes: ["dc."] }),
/** dns.lookup lives in the dgram unit. */
dgram: rule({ prefixes: ["dgram.", "dns."], types: ["dgramSocket"] }),
+ /** node:dns/promises: scr_dns_promises.c over the dgram unit. */
+ dnsPromises: rule({ fns: ["dns.promisesLookup", "dns.promisesLookupService"] }),
dynAsync: rule({
initial: (mod) =>
mod.workers === true ||
@@ -475,14 +515,15 @@ const RUNTIME_FEATURE_RULES = {
(fn) => fn.async === true && fn.generator === undefined && fn.returnType.kind === "dyn",
),
// The legacy url functions and crypto.createCipheriv emit their
- // deprecation warnings through the dyn-async unit.
+ // deprecation warnings through the dyn-async unit; scr_wasm.c boxes its
+ // handles and promises through it.
fns: [
...DYN_ASYNC_LIB_FNS,
...READABLE_DYN_ASYNC_LIB_FNS,
...LEGACY_URL_LIB_FNS,
"crypto.cipherNew",
],
- prefixes: READABLE_DYN_ASYNC_PREFIXES,
+ prefixes: [...READABLE_DYN_ASYNC_PREFIXES, "wasm."],
expr: (node, tables) =>
(node.kind === "awaitExpr" && node.type.kind === "dyn") ||
// Rejection reasons cross as dyn values (allSettled's records, any's
@@ -491,10 +532,23 @@ const RUNTIME_FEATURE_RULES = {
(node.name === "promise.allSettled" || node.name === "promise.any")) ||
(node.kind === "dynFrom" && boxesPromise(node.value.type, tables(), new Set())),
}),
+ /** EventEmitter and the stream classes as untyped constructor values
+ * (scr_dyn_builtin_class.c), which call listeners through the dyn-invoke
+ * unit, render unhandled 'error' payloads through the inspect unit and
+ * create EventEmitter's private symbols through the symbol unit. */
+ dynBuiltinClass: rule({
+ fns: [...BUILTIN_CLASS_LIB_FNS],
+ }),
dynInvoke: rule({
initial: (mod) => mod.workers === true,
- fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS],
- expr: (node, _tables) => node.kind === "dynInvoke",
+ fns: [...DYN_INVOKE_LIB_FNS, ...UTIL_CALLABLE_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
+ // Constant-key reads of Object.prototype member names answer the
+ // inherited member through scr_dyn_invoke.c.
+ expr: (node, _tables) =>
+ node.kind === "dynInvoke" ||
+ (node.kind === "dynKeyGet" &&
+ node.key.kind === "strLit" &&
+ INHERITED_MEMBER_NAMES.has(node.key.value)),
}),
/** The program has dynamic-tier jsval operations (js.* builtins, the
* jsval pass): links the scr_js_* units (two-tier design I3). */
@@ -504,7 +558,7 @@ const RUNTIME_FEATURE_RULES = {
emitter: rule({
initial: (mod) =>
mod.workers === true || (mod.classes ?? []).some((c) => c.name === RUNTIME_EMITTER_CLASS),
- fns: [...STREAM_LIB_FNS],
+ fns: [...STREAM_LIB_FNS, ...BUILTIN_CLASS_LIB_FNS],
prefixes: ["emitter.", ...STREAM_LIB_PREFIXES],
}),
/** Subclasses of the runtime EventTarget/Event bases tear down and box
@@ -520,6 +574,9 @@ const RUNTIME_FEATURE_RULES = {
fetch: rule({ prefixes: ["fetch."] }),
fileHandle: rule({ types: ["fileHandle"] }),
fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }),
+ /** A socket 'lookup' listener: the C main installs the net unit's
+ * lookup records (scr_net_lookup_install). */
+ netLookup: rule({ fns: ["net.sockOnLookup"] }),
http: rule({
prefixes: ["http.", "tls.", "https.", "http2."],
types: ["httpReq", "httpRes", "httpClientReq", "secureCtx"],
@@ -535,7 +592,13 @@ const RUNTIME_FEATURE_RULES = {
* terminal unit prints completion errors through it when there is a
* completer. */
inspect: rule({
- fns: ["global.native", "util.styleText", "crypto.cipherNew", ...UTIL_CALLABLE_LIB_FNS],
+ fns: [
+ "global.native",
+ "util.styleText",
+ "crypto.cipherNew",
+ ...BUILTIN_CLASS_LIB_FNS,
+ ...UTIL_CALLABLE_LIB_FNS,
+ ],
prefixes: ["insp.", "console.", "test."],
libCall: (node) => node.fn === "rl.terminal" && !isDynUndefined(node.args[3]!),
}),
@@ -615,6 +678,8 @@ const RUNTIME_FEATURE_RULES = {
tlsCa: rule({ prefixes: ["tlsca."] }),
/** process.emitWarning is assigned (process.setEmitWarning). */
warningReplace: rule({ fns: ["process.setEmitWarning"] }),
+ /** The WebAssembly API (scr_wasm.c). */
+ wasm: rule({ prefixes: ["wasm."] }),
/** crypto.subtle importKey/sign/verify/digest and CryptoKey:
* scr_webcrypto.c. */
webcrypto: rule({ prefixes: ["webcrypto."] }),
@@ -640,7 +705,9 @@ function noRuntimeFeatures(): RuntimeFeatures {
copying: false,
dc: false,
dgram: false,
+ dnsPromises: false,
dynAsync: false,
+ dynBuiltinClass: false,
dynInvoke: false,
dynTier: false,
emitter: false,
@@ -648,6 +715,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
fetch: false,
fileHandle: false,
fsWatch: false,
+ netLookup: false,
http: false,
http2: false,
inspect: false,
@@ -667,6 +735,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
tls: false,
tlsCa: false,
warningReplace: false,
+ wasm: false,
webcrypto: false,
zlib: false,
};
diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts
index ec37ddcf8..835dfa8ba 100644
--- a/packages/compiler/src/ir/validate.ts
+++ b/packages/compiler/src/ir/validate.ts
@@ -4432,7 +4432,9 @@ function validateFunction(
e.fn === "http.clientOnResponse" ||
e.fn === "http.requestAgentCb" ||
e.fn === "https.requestAgentCb" ||
- e.fn === "https.requestUrlCb"
+ e.fn === "https.requestUrlCb" ||
+ e.fn === "http.requestDynCb" ||
+ e.fn === "https.requestDynCb"
) {
// The response listener: void, no params or exactly (res: httpReq).
const cbT =
@@ -4443,11 +4445,13 @@ function validateFunction(
? 9
: e.fn === "http.requestUrlCb" || e.fn === "https.requestUrlCb"
? 3
- : e.fn === "http.requestAgentCb"
- ? 8
- : e.fn === "https.requestAgentCb"
- ? 10
- : 1
+ : e.fn === "http.requestDynCb" || e.fn === "https.requestDynCb"
+ ? 2
+ : e.fn === "http.requestAgentCb"
+ ? 8
+ : e.fn === "https.requestAgentCb"
+ ? 10
+ : 1
]?.type;
let ok = cbT?.kind === "func" && cbT.ret.kind === "void" && cbT.params.length <= 1;
if (ok && cbT?.kind === "func" && cbT.params.length === 1) {
@@ -5229,6 +5233,20 @@ function validateFunction(
}
return true;
}
+ if (e.fn === "child.stdioStream") {
+ const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
+ const slot = e.args[1];
+ const className =
+ slot?.kind === "numLit" ? (slot.value === 0 ? "%Writable" : "%Readable") : null;
+ const ok =
+ def &&
+ className !== null &&
+ def.arms.length === 2 &&
+ def.arms.some((a) => a.kind === "object" && a.className === className) &&
+ def.arms.some((a) => a.kind === "nullT");
+ if (!ok) err(`libCall ${e.fn} must return the stream class of its stdio slot or null`, e.loc);
+ return true;
+ }
if (e.fn === "child.stdin" || e.fn === "child.stdout" || e.fn === "child.stderr") {
const def = e.type.kind === "union" ? unions.get(e.type.unionId) : undefined;
const handleKind = e.fn === "child.stdin" ? "childWriter" : "childStream";
@@ -5720,9 +5738,9 @@ function validateFunction(
}
return true;
}
- if (e.fn === "readable.pipe") {
+ if (e.fn === "readable.pipe" || e.fn === "readable.pipeGeneral") {
if (!isStreamObject(e.args[1]?.type) || !typeEquals(e.type, e.args[1]!.type)) {
- err(`libCall readable.pipe must return its destination's type`, e.loc);
+ err(`libCall ${e.fn} must return its destination's type`, e.loc);
}
return true;
}
@@ -6205,7 +6223,10 @@ function validateFunction(
if (expectsArg && (rp.params.length !== 1 || !typeEquals(rp.params[0]!, inner))) {
err(`newPromise resolve param must be (${inner.kind}) => void`, e.loc);
}
- if (!expectsArg && rp.params.length !== 0) {
+ // A Promise executor types resolve as `(value: never) =>
+ // void` (one parameter over a void inner): the void resolve never
+ // reads an argument, and a never value cannot be produced.
+ if (!expectsArg && rp.params.length > 1) {
err("newPromise resolve takes no argument", e.loc);
}
}
diff --git a/packages/compiler/src/native/cli.ts b/packages/compiler/src/native/cli.ts
index 1a4edb4dd..3fa934da1 100644
--- a/packages/compiler/src/native/cli.ts
+++ b/packages/compiler/src/native/cli.ts
@@ -89,12 +89,14 @@ async function main(): Promise {
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,
diff --git a/packages/compiler/src/native/prepare.ts b/packages/compiler/src/native/prepare.ts
index f24119fab..a2a15179f 100644
--- a/packages/compiler/src/native/prepare.ts
+++ b/packages/compiler/src/native/prepare.ts
@@ -143,12 +143,14 @@ function validFeatures(value: unknown): value is NativeLinkFeatures {
"bigint",
"searchParams",
"qs",
+ "dynBuiltinClass",
"parseArgs",
"stream",
"net",
"http",
"http2",
"dgram",
+ "dnsPromises",
"watch",
"foreignFfi",
"nodeTest",
diff --git a/packages/compiler/src/native/sanitizer.test.ts b/packages/compiler/src/native/sanitizer.test.ts
index c56a035b7..ea9690ce7 100644
--- a/packages/compiler/src/native/sanitizer.test.ts
+++ b/packages/compiler/src/native/sanitizer.test.ts
@@ -75,6 +75,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
workers: false,
youngNursery: false,
eventTarget: false,
+ wasm: false,
dynTier: false,
warningReplace: false,
stdioExtended: false,
@@ -95,12 +96,14 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
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,
diff --git a/packages/compiler/src/native/toolchain.test.ts b/packages/compiler/src/native/toolchain.test.ts
index 9afccdc18..0ee434300 100644
--- a/packages/compiler/src/native/toolchain.test.ts
+++ b/packages/compiler/src/native/toolchain.test.ts
@@ -14,6 +14,7 @@ test("native toolchain installation paths relocate with the manifest", () => {
compiler_version: "1.2.3",
target: "macos-arm64",
ts7: "typescript/tsc",
+ tsc_ts: "tsc-ts/lib/tsc-ts",
llvm_package: "llvm",
runtime_pack: "runtime",
linker: "tools/linker",
@@ -23,7 +24,10 @@ test("native toolchain installation paths relocate with the manifest", () => {
try {
writeFileSync(path, JSON.stringify(manifest));
const toolchain = loadNativeToolchain(path);
- expect(toolchain.ts7Executable).toBe(join(root, "typescript/tsc"));
+ expect(toolchain.ts7Executable).toBe(join(root, "tsc-ts/lib/tsc-ts"));
+ expect(loadNativeToolchain(path, { SCRIPTC_CHECKER: "tsgo" }).ts7Executable).toBe(
+ join(root, "typescript/tsc"),
+ );
expect(toolchain.helperExecutable).toBe(join(root, "llvm/bin/scriptc-llvm-codegen"));
expect(toolchain.runtimePackRoot).toBe(join(root, "runtime"));
expect(toolchain.linker).toBe(join(root, "tools/linker"));
@@ -39,6 +43,7 @@ test("native toolchain installation paths relocate with the manifest", () => {
{ ...manifest, schema: "other" },
{ ...manifest, target: "other" },
{ ...manifest, ts7: "" },
+ { ...manifest, tsc_ts: "" },
{ ...manifest, linker_args: [1] },
]) {
writeFileSync(path, JSON.stringify(invalid));
@@ -71,6 +76,7 @@ test("native toolchain assets resolve from the physical manifest through package
compiler_version: "1.2.3",
target: "windows-x64-msvc",
ts7: asset("typescript/lib/tsc.exe"),
+ tsc_ts: asset("tsc-ts/lib/tsc-ts.exe"),
llvm_package: asset("llvm"),
runtime_pack: asset("runtime"),
runtime_packs: [{ target: "windows-x64-msvc", path: asset("runtime") }],
@@ -94,6 +100,10 @@ test("native toolchain assets resolve from the physical manifest through package
const viaLink = loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"));
expect(viaLink).toEqual(direct);
expect(viaLink.ts7Executable).toBe(join(assets, "typescript/lib/tsc.exe"));
+ expect(
+ loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), { SCRIPTC_CHECKER: "tsc-ts" })
+ .ts7Executable,
+ ).toBe(join(assets, "tsc-ts/lib/tsc-ts.exe"));
expect(viaLink.helperExecutable).toBe(join(assets, "llvm/bin/scriptc-llvm-codegen.exe"));
expect(
loadNativeToolchain(join(linked, "bin", "scriptc.exe.json"), {
@@ -126,6 +136,7 @@ test("cross-target packs can be added to a project after the compiler is install
compiler_version: "1.2.3",
target: "macos-arm64",
ts7: "../lib/typescript/tsc",
+ tsc_ts: "../lib/tsc-ts/lib/tsc-ts",
llvm_package: "../lib/llvm",
runtime_pack: "../lib/runtime-darwin-arm64",
linker: "clang",
@@ -174,6 +185,7 @@ test("versioned GNU targets select Zig while ordinary host builds keep the insta
compiler_version: "1.2.3",
target: "linux-x64-gnu",
ts7: "tsc",
+ tsc_ts: "tsc-ts",
llvm_package: "llvm",
runtime_pack: "runtime",
linker: "clang",
@@ -192,3 +204,41 @@ test("versioned GNU targets select Zig while ordinary host builds keep the insta
rmSync(root, { recursive: true, force: true });
}
});
+
+test("a manifest without tsc_ts takes the host's tsc-ts executable from the installed package", () => {
+ const root = realpathSync(
+ mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-checker-tools-")),
+ );
+ const path = join(root, "toolchain.json");
+ const manifest: NativeToolchainManifest = {
+ schema: "scriptc.native-toolchain.v1",
+ compiler_version: "1.2.3",
+ target: "linux-x64-musl",
+ ts7: "typescript/tsc",
+ llvm_package: "llvm",
+ runtime_pack: "runtime",
+ linker: "tools/linker",
+ linker_args: ["cc"],
+ dsymutil: "dsymutil",
+ };
+ try {
+ writeFileSync(path, JSON.stringify(manifest));
+ const project = join(root, "project");
+ const platformPackage = join(project, "node_modules/@tsc-ts/linux-x64-musl");
+ mkdirSync(join(project, "node_modules/tsc-ts"), { recursive: true });
+ writeFileSync(join(project, "node_modules/tsc-ts/package.json"), '{"name":"tsc-ts"}');
+ mkdirSync(join(platformPackage, "lib"), { recursive: true });
+ writeFileSync(join(platformPackage, "lib/tsc-ts"), "");
+ expect(loadNativeToolchain(path, {}, project).ts7Executable).toBe(
+ join(platformPackage, "lib/tsc-ts"),
+ );
+ expect(loadNativeToolchain(path, { SCRIPTC_CHECKER: "tsgo" }, project).ts7Executable).toBe(
+ join(root, "typescript/tsc"),
+ );
+ expect(() => loadNativeToolchain(path, {}, root)).toThrow(
+ "the tsc-ts package is not installed",
+ );
+ } finally {
+ rmSync(root, { recursive: true, force: true });
+ }
+});
diff --git a/packages/compiler/src/native/toolchain.ts b/packages/compiler/src/native/toolchain.ts
index 651d87027..fbcd94251 100644
--- a/packages/compiler/src/native/toolchain.ts
+++ b/packages/compiler/src/native/toolchain.ts
@@ -6,6 +6,11 @@ import {
type NativeTargetSpec,
type NativeHelperSpec,
} from "../backend/targets.js";
+import {
+ checkerBackend,
+ tscTsExecutable,
+ tscTsPlatformPackage,
+} from "../frontend/ts7/checker-backend.js";
export interface NativeToolchain {
compilerVersion: string;
@@ -34,6 +39,8 @@ export interface NativeToolchainManifest {
compiler_version: string;
target: string;
ts7: string;
+ /** The tsc-ts executable, the default checker; without it, an installed `tsc-ts` package's. */
+ tsc_ts?: string;
llvm_package: string;
runtime_pack: string;
runtime_sources?: string;
@@ -106,6 +113,7 @@ export function loadNativeToolchain(
}
const manifest = raw as NativeToolchainManifest;
for (const name of [
+ "tsc_ts",
"runtime_sources",
"declarations",
"archiver",
@@ -169,7 +177,19 @@ export function loadNativeToolchain(
: [...target.defaultLinkerArgs];
return {
compilerVersion: manifest.compiler_version,
- ts7Executable: pathFrom(root, manifest.ts7),
+ // tsc-ts, the default checker except on Windows, comes with the distribution;
+ // a development build takes it from the tsc-ts package installed with the
+ // compiler or for the working directory. SCRIPTC_CHECKER selects a checker.
+ ts7Executable:
+ checkerBackend(env, host.platform) === "tsgo"
+ ? pathFrom(root, manifest.ts7)
+ : manifest.tsc_ts !== undefined
+ ? pathFrom(root, manifest.tsc_ts)
+ : tscTsExecutable(
+ [root, cwd],
+ tscTsPlatformPackage(host.platform, host.architecture, host.name.endsWith("-musl")),
+ host.platform,
+ ),
target,
helper: host.helper,
helperPackageRoot,
diff --git a/packages/compiler/src/startup-cache.ts b/packages/compiler/src/startup-cache.ts
index fedc36fd5..c2f82fdad 100644
--- a/packages/compiler/src/startup-cache.ts
+++ b/packages/compiler/src/startup-cache.ts
@@ -22,5 +22,6 @@ export function precompiledRuntimePackTarget(
): NativeTargetSpec | null {
return nativeCodegenTarget(env);
}
+export { checkerCacheIdentity } from "./frontend/ts7/checker-backend.js";
export { readCoverageVerdict } from "./coverage/verdict-startup.js";
export { coverageEnvelope, coveragePasses, renderCoverage } from "./coverage/report.js";
diff --git a/packages/compiler/src/wasm/base64.ts b/packages/compiler/src/wasm/base64.ts
new file mode 100644
index 000000000..122bcd4ef
--- /dev/null
+++ b/packages/compiler/src/wasm/base64.ts
@@ -0,0 +1,61 @@
+/* Base64 for WebAssembly module bytes: decoding inline modules found in
+ * source text (discover.ts) and carrying translated modules on the IR
+ * (IrModule.wasmModules). Plain TypeScript, so the native compiler
+ * compiles it too. */
+
+const BASE64_VALUES = (() => {
+ const table = new Int16Array(128).fill(-1);
+ const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ for (let i = 0; i < alphabet.length; i++) table[alphabet.charCodeAt(i)] = i;
+ // base64url spellings
+ table["-".charCodeAt(0)] = 62;
+ table["_".charCodeAt(0)] = 63;
+ return table;
+})();
+
+/** Decodes standard or URL-safe base64 (padding optional); null when the
+ * text is not base64. */
+export function decodeBase64(text: string): Uint8Array | null {
+ let end = text.length;
+ while (end > 0 && text.charCodeAt(end - 1) === 61) end--;
+ if (text.length - end > 2 || end % 4 === 1) return null;
+ const out = new Uint8Array(Math.floor((end * 3) / 4));
+ let acc = 0;
+ let bits = 0;
+ let j = 0;
+ for (let i = 0; i < end; i++) {
+ const c = text.charCodeAt(i);
+ const v = c < 128 ? BASE64_VALUES[c]! : -1;
+ if (v < 0) return null;
+ acc = ((acc << 6) | v) & 0xffffff;
+ bits += 6;
+ if (bits >= 8) {
+ bits -= 8;
+ out[j++] = (acc >> bits) & 0xff;
+ }
+ }
+ return j === out.length ? out : out.subarray(0, j);
+}
+
+const BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+/** Standard base64 with padding. */
+export function encodeBase64(bytes: Uint8Array): string {
+ const parts: string[] = [];
+ let chunk = "";
+ for (let i = 0; i < bytes.length; i += 3) {
+ const a = bytes[i]!;
+ const b = i + 1 < bytes.length ? bytes[i + 1]! : 0;
+ const c = i + 2 < bytes.length ? bytes[i + 2]! : 0;
+ const n = (a << 16) | (b << 8) | c;
+ chunk += BASE64_ALPHABET[(n >> 18) & 63]! + BASE64_ALPHABET[(n >> 12) & 63]!;
+ chunk += i + 1 < bytes.length ? BASE64_ALPHABET[(n >> 6) & 63]! : "=";
+ chunk += i + 2 < bytes.length ? BASE64_ALPHABET[n & 63]! : "=";
+ if (chunk.length >= 65536) {
+ parts.push(chunk);
+ chunk = "";
+ }
+ }
+ parts.push(chunk);
+ return parts.join("");
+}
diff --git a/packages/compiler/src/wasm/binary.ts b/packages/compiler/src/wasm/binary.ts
index 5ddc28cc9..eaedf25af 100644
--- a/packages/compiler/src/wasm/binary.ts
+++ b/packages/compiler/src/wasm/binary.ts
@@ -20,6 +20,7 @@
* The decoder never evaluates anything and keeps no module contents beyond
* the counts and the feature set it reports. It is plain TypeScript that
* the native compiler also compiles. */
+import { readMemargFlags } from "./module.js";
/** Proposals the summary can name, in report order. The first group is the
* translatable set; the rest are reported as unsupported. */
@@ -1244,19 +1245,9 @@ class FunctionValidator {
/** A memarg. A memory other than 0 stops typing (multi-memory). */
memarg(naturalAlign: number): void {
const r = this.r;
- let align = r.u32();
- let multi = false;
- if (align >= 64 && align < 128) {
- align -= 64;
- multi = true;
- } else if (align >= 64) {
- r.malformed("malformed memop flags");
- }
- let memory = 0;
- if (multi) {
- memory = r.u32();
- if (memory !== 0) this.otherMemory(memory);
- }
+ const { align, memory } = readMemargFlags(() => r.u32());
+ if (align < 0) r.malformed("malformed memop flags");
+ if (memory !== 0) this.otherMemory(memory);
if (this.m.memory64) r.u64();
else r.u32();
if (!this.typed) return;
@@ -1840,9 +1831,9 @@ class FunctionValidator {
scanMemarg(): void {
const r = this.r;
- const align = r.u32();
- if (align >= 128) r.malformed("malformed memop flags");
- if (align >= 64 && r.u32() !== 0) this.m.feature("multi-memory");
+ const { align, memory } = readMemargFlags(() => r.u32());
+ if (align < 0) r.malformed("malformed memop flags");
+ if (memory !== 0) this.m.feature("multi-memory");
if (this.m.memory64) r.u64();
else r.u32();
}
diff --git a/packages/compiler/src/wasm/discover.ts b/packages/compiler/src/wasm/discover.ts
index 95fc28647..020c2330b 100644
--- a/packages/compiler/src/wasm/discover.ts
+++ b/packages/compiler/src/wasm/discover.ts
@@ -28,6 +28,9 @@ import {
trackedReadFile,
} from "../frontend/input-tracker.js";
import { analyzeWasm, isWasmHeader, type WasmModuleSummary } from "./binary.js";
+import { decodeBase64 } from "./base64.js";
+
+export { decodeBase64 };
export type WasmSourceKind = "path" | "inline" | "package" | "config";
@@ -56,6 +59,9 @@ export interface WasmDiscoveryInput {
configured: readonly string[];
/** require.resolve from `fromFile`, or null when it does not resolve. */
resolveRequest: (fromFile: string, specifier: string) => string | null;
+ /** Receives each distinct module's bytes once, when it is first found
+ * (builds translate them). */
+ onModule?: (sha256: string, bytes: Uint8Array) => void;
}
const SCAN_KINDS: ReadonlySet = new Set([
@@ -110,6 +116,7 @@ class Discovery {
};
this.byHash.set(sha256, report);
this.order.push(sha256);
+ this.input.onModule?.(sha256, bytes);
}
if (report.path === undefined && path !== undefined) report.path = path;
addSource(report, source);
@@ -351,40 +358,6 @@ function byteArrayLiteral(node: ts.ArrayLiteralExpression): Uint8Array | null {
return out;
}
-const BASE64_VALUES = (() => {
- const table = new Int16Array(128).fill(-1);
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
- for (let i = 0; i < alphabet.length; i++) table[alphabet.charCodeAt(i)] = i;
- // base64url spellings
- table["-".charCodeAt(0)] = 62;
- table["_".charCodeAt(0)] = 63;
- return table;
-})();
-
-/** Decodes standard or URL-safe base64 (padding optional); null when the
- * text is not base64. */
-export function decodeBase64(text: string): Uint8Array | null {
- let end = text.length;
- while (end > 0 && text.charCodeAt(end - 1) === 61) end--;
- if (text.length - end > 2 || end % 4 === 1) return null;
- const out = new Uint8Array(Math.floor((end * 3) / 4));
- let acc = 0;
- let bits = 0;
- let j = 0;
- for (let i = 0; i < end; i++) {
- const c = text.charCodeAt(i);
- const v = c < 128 ? BASE64_VALUES[c]! : -1;
- if (v < 0) return null;
- acc = ((acc << 6) | v) & 0xffffff;
- bits += 6;
- if (bits >= 8) {
- bits -= 8;
- out[j++] = (acc >> bits) & 0xff;
- }
- }
- return j === out.length ? out : out.subarray(0, j);
-}
-
/** The root directory of the npm package that contains `file` (the
* directory below its innermost node_modules), or null outside one. */
export function packageRootOf(file: string): string | null {
diff --git a/packages/compiler/src/wasm/module.ts b/packages/compiler/src/wasm/module.ts
new file mode 100644
index 000000000..587a20d81
--- /dev/null
+++ b/packages/compiler/src/wasm/module.ts
@@ -0,0 +1,676 @@
+/* The structure of a WebAssembly module, for the translator.
+ *
+ * binary.ts validates a module and keeps nothing but counts. This parser
+ * reads the parts translation needs (types, imports, functions, tables,
+ * memories, globals, exports, the start function, element and data
+ * segments, function bodies and custom sections) from bytes that binary.ts
+ * already accepted, so it checks nothing beyond what it must to stay in
+ * bounds. Constant expressions and function bodies are kept as byte ranges
+ * that the translator reads itself.
+ *
+ * i64 values never become JavaScript numbers: the translator reads their
+ * LEB128 encodings into two 32-bit halves. Plain TypeScript, so the native
+ * compiler compiles it too. */
+
+export const VAL_I32 = 0x7f;
+export const VAL_I64 = 0x7e;
+export const VAL_F32 = 0x7d;
+export const VAL_F64 = 0x7c;
+export const VAL_FUNCREF = 0x70;
+export const VAL_EXTERNREF = 0x6f;
+
+export const KIND_FUNC = 0;
+export const KIND_TABLE = 1;
+export const KIND_MEMORY = 2;
+export const KIND_GLOBAL = 3;
+
+export interface WasmFuncType {
+ params: number[];
+ results: number[];
+}
+
+/** A byte range of the module: a constant expression (through its `end`
+ * opcode) or a payload. */
+export interface WasmRange {
+ start: number;
+ end: number;
+}
+
+export interface WasmImport {
+ module: string;
+ name: string;
+ kind: number;
+ /** Functions: the type index. Tables and globals: the value type. */
+ type: number;
+ min: number;
+ max: number | null;
+ mutable: boolean;
+}
+
+export interface WasmTable {
+ elemType: number;
+ min: number;
+ max: number | null;
+ imported: boolean;
+ /** A defined table's initializer (reference-types table with an init
+ * expression), or null for the null reference. */
+ init: WasmRange | null;
+}
+
+export interface WasmMemory {
+ min: number;
+ max: number | null;
+ imported: boolean;
+}
+
+export interface WasmGlobal {
+ type: number;
+ mutable: boolean;
+ imported: boolean;
+ init: WasmRange | null;
+}
+
+export interface WasmExport {
+ name: string;
+ kind: number;
+ index: number;
+}
+
+export interface WasmElem {
+ mode: "active" | "passive" | "declarative";
+ table: number;
+ offset: WasmRange | null;
+ elemType: number;
+ /** One initializer per element: a function index (>= 0), or -1 when the
+ * element is a constant expression in `exprs`. */
+ funcs: number[];
+ exprs: (WasmRange | null)[];
+}
+
+export interface WasmData {
+ mode: "active" | "passive";
+ memory: number;
+ offset: WasmRange | null;
+ payload: WasmRange;
+}
+
+export interface WasmCode {
+ /** Declared locals, expanded (not including parameters). */
+ locals: number[];
+ body: WasmRange;
+}
+
+export interface WasmCustom {
+ name: string;
+ payload: WasmRange;
+}
+
+export interface WasmModule {
+ bytes: Uint8Array;
+ types: WasmFuncType[];
+ imports: WasmImport[];
+ /** The type index of every function, imported functions first. */
+ funcTypes: number[];
+ importedFuncs: number;
+ tables: WasmTable[];
+ memories: WasmMemory[];
+ globals: WasmGlobal[];
+ exports: WasmExport[];
+ start: number | null;
+ elems: WasmElem[];
+ datas: WasmData[];
+ codes: WasmCode[];
+ customs: WasmCustom[];
+ /** Functions referenced outside a body (exports, element segments,
+ * ref.func in constant expressions): only these need a function
+ * reference record. Bodies add their own ref.func targets, which a valid
+ * module also declares in one of these places. */
+ declaredRefs: Set;
+}
+
+/** A byte cursor over a module that binary.ts validated. */
+export class WasmCursor {
+ bytes: Uint8Array;
+ pos: number;
+ constructor(bytes: Uint8Array, pos: number) {
+ this.bytes = bytes;
+ this.pos = pos;
+ }
+
+ u8(): number {
+ const b = this.bytes[this.pos];
+ if (b === undefined) throw new Error("wasm: unexpected end of module");
+ this.pos++;
+ return b;
+ }
+
+ u32(): number {
+ let result = 0;
+ let scale = 1;
+ for (;;) {
+ const b = this.u8();
+ result += (b & 0x7f) * scale;
+ if ((b & 0x80) === 0) return result;
+ scale *= 128;
+ }
+ }
+
+ /** A signed LEB128 of at most 33 bits. */
+ s33(): number {
+ let result = 0;
+ let scale = 1;
+ for (;;) {
+ const b = this.u8();
+ result += (b & 0x7f) * scale;
+ scale *= 128;
+ if ((b & 0x80) === 0) {
+ if (b & 0x40) result -= scale;
+ return result;
+ }
+ }
+ }
+
+ /** A signed 32-bit LEB128 as a signed 32-bit number. */
+ s32(): number {
+ return this.s33() | 0;
+ }
+
+ /** A signed 64-bit LEB128 as its two's-complement halves [lo, hi], each
+ * an unsigned 32-bit number. */
+ s64(): [number, number] {
+ let lo = 0;
+ let hi = 0;
+ let shift = 0;
+ let b = 0;
+ do {
+ b = this.u8();
+ const bits = b & 0x7f;
+ if (shift < 32) {
+ lo = (lo | (bits << shift)) >>> 0;
+ if (shift > 25) hi = (hi | (bits >>> (32 - shift))) >>> 0;
+ } else if (shift < 64) {
+ hi = (hi | (bits << (shift - 32))) >>> 0;
+ }
+ shift += 7;
+ } while (b & 0x80);
+ if (shift < 64 && b & 0x40) {
+ // Sign-extend from bit `shift`.
+ if (shift < 32) {
+ lo = (lo | (~0 << shift)) >>> 0;
+ hi = 0xffffffff;
+ } else {
+ hi = (hi | (~0 << (shift - 32))) >>> 0;
+ }
+ }
+ return [lo, hi];
+ }
+
+ u32le(): number {
+ const b = this.bytes;
+ const p = this.pos;
+ this.pos += 4;
+ return (b[p]! | (b[p + 1]! << 8) | (b[p + 2]! << 16) | (b[p + 3]! << 24)) >>> 0;
+ }
+
+ name(): string {
+ const length = this.u32();
+ const start = this.pos;
+ this.pos += length;
+ return decodeUtf8(this.bytes, start, this.pos);
+ }
+
+ /** Skips a constant expression through its `end` and answers its range. */
+ constExpr(): WasmRange {
+ const start = this.pos;
+ for (;;) {
+ const op = this.u8();
+ if (op === 0x0b) break;
+ skipImmediates(this, op);
+ }
+ return { start, end: this.pos };
+ }
+
+ valType(): number {
+ return this.u8();
+ }
+
+ /** A block type: -1 for the empty type, a value type, or a type index. */
+ blockType():
+ | { kind: "empty" }
+ | { kind: "value"; type: number }
+ | { kind: "index"; index: number } {
+ const b = this.bytes[this.pos]!;
+ if (b === 0x40) {
+ this.pos++;
+ return { kind: "empty" };
+ }
+ if (
+ b === VAL_I32 ||
+ b === VAL_I64 ||
+ b === VAL_F32 ||
+ b === VAL_F64 ||
+ b === VAL_FUNCREF ||
+ b === VAL_EXTERNREF
+ ) {
+ this.pos++;
+ return { kind: "value", type: b };
+ }
+ return { kind: "index", index: this.s33() };
+ }
+}
+
+function decodeUtf8(bytes: Uint8Array, start: number, end: number): string {
+ let out = "";
+ let i = start;
+ while (i < end) {
+ const b = bytes[i]!;
+ let cp = b;
+ let need = 0;
+ if (b >= 0xf0) {
+ cp = b & 0x07;
+ need = 3;
+ } else if (b >= 0xe0) {
+ cp = b & 0x0f;
+ need = 2;
+ } else if (b >= 0xc0) {
+ cp = b & 0x1f;
+ need = 1;
+ }
+ for (let k = 1; k <= need; k++) cp = cp * 64 + (bytes[i + k]! & 0x3f);
+ out += String.fromCodePoint(cp);
+ i += need + 1;
+ }
+ return out;
+}
+
+/** The flags of a memarg (the immediates of loads, stores and atomics):
+ * the alignment exponent and the memory index. Flags with bit 6 set
+ * (multi-memory) are followed by an explicit memory index, which may be
+ * 0; flags of 128 or more are malformed and answer align -1 without
+ * reading further. The offset follows. Validation (binary.ts), translation
+ * and skipImmediates all read memargs through this function, so they agree
+ * on where each instruction ends; `u32` reads the next u32 immediate of
+ * their own reader. */
+export function readMemargFlags(u32: () => number): { align: number; memory: number } {
+ const flags = u32();
+ if (flags >= 128) return { align: -1, memory: 0 };
+ if (flags >= 64) return { align: flags - 64, memory: u32() };
+ return { align: flags, memory: 0 };
+}
+
+/** Reads the immediates of opcode `op` (already consumed). Covers the
+ * translatable feature level; anything else was refused before
+ * translation. */
+export function skipImmediates(c: WasmCursor, op: number): void {
+ switch (op) {
+ case 0x02:
+ case 0x03:
+ case 0x04:
+ c.blockType();
+ return;
+ case 0x0c:
+ case 0x0d:
+ case 0x10:
+ case 0x20:
+ case 0x21:
+ case 0x22:
+ case 0x23:
+ case 0x24:
+ case 0x25:
+ case 0x26:
+ case 0xd2:
+ c.u32();
+ return;
+ case 0x0e: {
+ const n = c.u32();
+ for (let i = 0; i <= n; i++) c.u32();
+ return;
+ }
+ case 0x11:
+ c.u32();
+ c.u32();
+ return;
+ case 0x1c: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) c.valType();
+ return;
+ }
+ case 0x3f:
+ case 0x40:
+ // The memory index (a LEB128 zero; V8 accepts long encodings).
+ c.u32();
+ return;
+ case 0x41:
+ c.s32();
+ return;
+ case 0x42:
+ c.s64();
+ return;
+ case 0x43:
+ c.pos += 4;
+ return;
+ case 0x44:
+ c.pos += 8;
+ return;
+ case 0xd0:
+ c.valType();
+ return;
+ case 0xfc: {
+ const sub = c.u32();
+ switch (sub) {
+ case 8:
+ c.u32();
+ c.u32();
+ return;
+ case 9:
+ case 13:
+ case 15:
+ case 16:
+ case 17:
+ c.u32();
+ return;
+ case 10:
+ c.u32();
+ c.u32();
+ return;
+ case 11:
+ c.u32();
+ return;
+ case 12:
+ case 14:
+ c.u32();
+ c.u32();
+ return;
+ default:
+ return;
+ }
+ }
+ default:
+ if (op >= 0x28 && op <= 0x3e) {
+ const flags = readMemargFlags(() => c.u32());
+ if (flags.align < 0 || flags.memory !== 0)
+ throw new Error(`wasm: memarg of memory ${flags.memory} is not supported`);
+ c.u32();
+ }
+ return;
+ }
+}
+
+function limits(c: WasmCursor): { min: number; max: number | null } {
+ const flags = c.u8();
+ const min = c.u32();
+ const max = flags & 0x01 ? c.u32() : null;
+ return { min, max };
+}
+
+/** Parses a module binary.ts reported as valid within the translatable
+ * feature level. */
+export function parseWasmModule(bytes: Uint8Array): WasmModule {
+ const m: WasmModule = {
+ bytes,
+ types: [],
+ imports: [],
+ funcTypes: [],
+ importedFuncs: 0,
+ tables: [],
+ memories: [],
+ globals: [],
+ exports: [],
+ start: null,
+ elems: [],
+ datas: [],
+ codes: [],
+ customs: [],
+ declaredRefs: new Set(),
+ };
+ const c = new WasmCursor(bytes, 8);
+ while (c.pos < bytes.length) {
+ const id = c.u8();
+ const size = c.u32();
+ const end = c.pos + size;
+ switch (id) {
+ case 0: {
+ const name = c.name();
+ m.customs.push({ name, payload: { start: c.pos, end } });
+ break;
+ }
+ case 1: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ c.u8(); // 0x60
+ const params: number[] = [];
+ const np = c.u32();
+ for (let k = 0; k < np; k++) params.push(c.valType());
+ const results: number[] = [];
+ const nr = c.u32();
+ for (let k = 0; k < nr; k++) results.push(c.valType());
+ m.types.push({ params, results });
+ }
+ break;
+ }
+ case 2: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const module = c.name();
+ const name = c.name();
+ const kind = c.u8();
+ const imp: WasmImport = {
+ module,
+ name,
+ kind,
+ type: 0,
+ min: 0,
+ max: null,
+ mutable: false,
+ };
+ if (kind === KIND_FUNC) {
+ imp.type = c.u32();
+ m.funcTypes.push(imp.type);
+ m.importedFuncs++;
+ } else if (kind === KIND_TABLE) {
+ imp.type = c.valType();
+ const l = limits(c);
+ imp.min = l.min;
+ imp.max = l.max;
+ m.tables.push({
+ elemType: imp.type,
+ min: l.min,
+ max: l.max,
+ imported: true,
+ init: null,
+ });
+ } else if (kind === KIND_MEMORY) {
+ const l = limits(c);
+ imp.min = l.min;
+ imp.max = l.max;
+ m.memories.push({ min: l.min, max: l.max, imported: true });
+ } else {
+ imp.type = c.valType();
+ imp.mutable = c.u8() === 1;
+ m.globals.push({ type: imp.type, mutable: imp.mutable, imported: true, init: null });
+ }
+ m.imports.push(imp);
+ }
+ break;
+ }
+ case 3: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) m.funcTypes.push(c.u32());
+ break;
+ }
+ case 4: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ let init: WasmRange | null = null;
+ if (c.bytes[c.pos] === 0x40) {
+ // table with an initializer: 0x40 0x00 tabletype expr
+ c.pos += 2;
+ const elemType = c.valType();
+ const l = limits(c);
+ init = c.constExpr();
+ m.tables.push({ elemType, min: l.min, max: l.max, imported: false, init });
+ continue;
+ }
+ const elemType = c.valType();
+ const l = limits(c);
+ m.tables.push({ elemType, min: l.min, max: l.max, imported: false, init });
+ }
+ break;
+ }
+ case 5: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const l = limits(c);
+ m.memories.push({ min: l.min, max: l.max, imported: false });
+ }
+ break;
+ }
+ case 6: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const type = c.valType();
+ const mutable = c.u8() === 1;
+ const init = c.constExpr();
+ noteConstRefs(m, init);
+ m.globals.push({ type, mutable, imported: false, init });
+ }
+ break;
+ }
+ case 7: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const name = c.name();
+ const kind = c.u8();
+ const index = c.u32();
+ m.exports.push({ name, kind, index });
+ if (kind === KIND_FUNC) m.declaredRefs.add(index);
+ }
+ break;
+ }
+ case 8:
+ m.start = c.u32();
+ break;
+ case 9: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) m.elems.push(elemSegment(m, c));
+ break;
+ }
+ case 10: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const size = c.u32();
+ const bodyEnd = c.pos + size;
+ const locals: number[] = [];
+ const groups = c.u32();
+ for (let g = 0; g < groups; g++) {
+ const count = c.u32();
+ const type = c.valType();
+ for (let k = 0; k < count; k++) locals.push(type);
+ }
+ m.codes.push({ locals, body: { start: c.pos, end: bodyEnd } });
+ c.pos = bodyEnd;
+ }
+ break;
+ }
+ case 11: {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) {
+ const flags = c.u32();
+ if (flags === 1) {
+ const size = c.u32();
+ m.datas.push({
+ mode: "passive",
+ memory: 0,
+ offset: null,
+ payload: { start: c.pos, end: c.pos + size },
+ });
+ c.pos += size;
+ continue;
+ }
+ const memory = flags === 2 ? c.u32() : 0;
+ const offset = c.constExpr();
+ const size = c.u32();
+ m.datas.push({
+ mode: "active",
+ memory,
+ offset,
+ payload: { start: c.pos, end: c.pos + size },
+ });
+ c.pos += size;
+ }
+ break;
+ }
+ default:
+ // 12 (data count) and anything binary.ts accepted without content
+ // translation needs.
+ break;
+ }
+ c.pos = end;
+ }
+ return m;
+}
+
+function elemSegment(m: WasmModule, c: WasmCursor): WasmElem {
+ const flags = c.u32();
+ const passiveOrDeclarative = (flags & 1) !== 0;
+ const explicitTable = (flags & 2) !== 0;
+ const usesExprs = (flags & 4) !== 0;
+ let mode: WasmElem["mode"] = "active";
+ if (passiveOrDeclarative) mode = explicitTable ? "declarative" : "passive";
+ let table = 0;
+ let offset: WasmRange | null = null;
+ if (!passiveOrDeclarative) {
+ if (explicitTable) table = c.u32();
+ offset = c.constExpr();
+ }
+ let elemType = VAL_FUNCREF;
+ if (passiveOrDeclarative || explicitTable) {
+ // elemkind (0x00 = funcref) or a reference type
+ const b = c.u8();
+ elemType = usesExprs ? b : VAL_FUNCREF;
+ }
+ const n = c.u32();
+ const funcs: number[] = [];
+ const exprs: (WasmRange | null)[] = [];
+ for (let i = 0; i < n; i++) {
+ if (usesExprs) {
+ const expr = c.constExpr();
+ const direct = refFuncOf(m.bytes, expr);
+ if (direct !== null) {
+ funcs.push(direct);
+ exprs.push(null);
+ m.declaredRefs.add(direct);
+ } else {
+ noteConstRefs(m, expr);
+ funcs.push(-1);
+ exprs.push(expr);
+ }
+ } else {
+ const f = c.u32();
+ funcs.push(f);
+ exprs.push(null);
+ m.declaredRefs.add(f);
+ }
+ }
+ return { mode, table, offset, elemType, funcs, exprs };
+}
+
+/** The function of a `ref.func f; end` expression, or null. */
+function refFuncOf(bytes: Uint8Array, expr: WasmRange): number | null {
+ if (bytes[expr.start] !== 0xd2) return null;
+ const c = new WasmCursor(bytes, expr.start + 1);
+ const f = c.u32();
+ return c.pos === expr.end - 1 && bytes[c.pos] === 0x0b ? f : null;
+}
+
+function noteConstRefs(m: WasmModule, expr: WasmRange): void {
+ const c = new WasmCursor(m.bytes, expr.start);
+ while (c.pos < expr.end) {
+ const op = c.u8();
+ if (op === 0xd2) {
+ m.declaredRefs.add(c.u32());
+ continue;
+ }
+ skipImmediates(c, op);
+ }
+}
diff --git a/packages/compiler/src/wasm/translate.test.ts b/packages/compiler/src/wasm/translate.test.ts
new file mode 100644
index 000000000..12519b85e
--- /dev/null
+++ b/packages/compiler/src/wasm/translate.test.ts
@@ -0,0 +1,196 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+import { describe, expect, test } from "vitest";
+import { analyzeWasm } from "./binary.js";
+import { decodeBase64, encodeBase64 } from "./base64.js";
+import { WasmCursor, parseWasmModule } from "./module.js";
+import {
+ signatureHash,
+ stackCheckedFunctions,
+ translateWasmModule,
+ wasmSymbolPrefix,
+} from "./translate.js";
+
+const SHA = new Uint8Array(32).fill(0xab);
+
+/** (module (func (export "add") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add)
+ * (memory (export "mem") 1) (data (i32.const 8) "hi")) */
+const ADD = new Uint8Array([
+ 0, 97, 115, 109, 1, 0, 0, 0, 1, 7, 1, 96, 2, 127, 127, 1, 127, 3, 2, 1, 0, 5, 3, 1, 0, 1, 7, 13,
+ 2, 3, 97, 100, 100, 0, 0, 3, 109, 101, 109, 2, 0, 10, 9, 1, 7, 0, 32, 0, 32, 1, 106, 11, 11, 8, 1,
+ 0, 65, 8, 11, 2, 104, 105,
+]);
+
+/** Every top-level line must be one the native helper's text partitioner
+ * accepts (native/llvm-codegen/src/partition.cpp), so translated modules
+ * keep the program on its concurrent parsing path. */
+function topLevelLines(text: string[]): string[] {
+ const lines: string[] = [];
+ for (const part of text) {
+ let inFunction = false;
+ for (const line of part.split("\n")) {
+ if (inFunction) {
+ if (line === "}") inFunction = false;
+ continue;
+ }
+ if (line.startsWith("define ")) inFunction = true;
+ lines.push(line);
+ }
+ }
+ return lines;
+}
+
+describe("translateWasmModule", () => {
+ test("emits functions, thunks, data and the descriptor in the helper's grammar", () => {
+ const out = translateWasmModule(ADD, SHA, { boundsChecks: false });
+ const prefix = wasmSymbolPrefix(SHA);
+ expect(prefix).toBe("scr_wasm_abababababababab");
+ expect(out.descriptor).toBe(`@${prefix}_desc`);
+ const text = out.text.join("\n");
+ expect(text).toContain(`define internal i32 @${prefix}_f0(ptr %inst, i32 %p0, i32 %p1)`);
+ expect(text).toContain(`define internal void @${prefix}_x0(ptr %inst, ptr %a, ptr %r)`);
+ expect(text).toContain(`c"hi"`);
+ expect(text).toContain(`"disable-tail-calls"="true"`);
+ for (const line of topLevelLines(out.text)) {
+ expect(
+ line === "" || /^(define internal |@[\w.$-]+ = (private|internal) )/.test(line),
+ line,
+ ).toBe(true);
+ }
+ for (const decl of out.decls) expect(decl).toMatch(/^declare /);
+ });
+
+ test("checks memory accesses explicitly only when asked", () => {
+ const guarded = translateWasmModule(ADD, SHA, { boundsChecks: false }).text.join("\n");
+ const checked = translateWasmModule(ADD, SHA, { boundsChecks: true }).text.join("\n");
+ expect(guarded).not.toContain("%msize");
+ expect(checked).toContain("%msize");
+ });
+
+ test("translates every valid module of the WebAssembly spec test suite", () => {
+ const cases = readFileSync(join(import.meta.dirname, "../../test/wasm-spec-cases.txt"), "utf8")
+ .split("\n")
+ .filter((line) => line !== "" && !line.startsWith("#"));
+ let translated = 0;
+ for (const line of cases) {
+ const [, , where, b64] = line.split(" ");
+ const bytes = decodeBase64(b64!)!;
+ if (analyzeWasm(bytes).status !== "valid") continue;
+ try {
+ translateWasmModule(bytes, SHA, { boundsChecks: false });
+ } catch (error) {
+ throw new Error(`${where}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ translated++;
+ }
+ expect(translated).toBeGreaterThan(800);
+ });
+});
+
+/** A module from its sections: `[id, ...payload]`, each payload under 128
+ * bytes. */
+function wasmModule(...sections: number[][]): Uint8Array {
+ const out = [0, 97, 115, 109, 1, 0, 0, 0];
+ for (const [id, ...payload] of sections) out.push(id!, payload.length, ...payload);
+ return new Uint8Array(out);
+}
+
+/** (module (memory 1) (func (export "f") (param i32) (result i32)
+ * local.get 0 i32.load )) with the memarg's bytes as given. */
+function loadModule(memarg: number[]): Uint8Array {
+ const body = [0, 0x20, 0, 0x28, ...memarg, 0x0b];
+ return wasmModule(
+ [1, 1, 0x60, 1, 0x7f, 1, 0x7f],
+ [3, 1, 0],
+ [5, 1, 0, 1],
+ [7, 1, 1, 0x66, 0, 0],
+ [10, 1, body.length, ...body],
+ );
+}
+
+describe("memargs and operand types", () => {
+ test("a memarg naming memory 0 explicitly translates as the plain form", () => {
+ // Flags 0x42: alignment 2 with bit 6 set, then memory index 0 and
+ // offset 4. Validation and translation read the same three immediates.
+ // Only the descriptor (which records the module's byte length) differs.
+ const code = (memarg: number[]): string[] =>
+ translateWasmModule(loadModule(memarg), SHA, { boundsChecks: true })
+ .text.join("\n")
+ .split("\n")
+ .filter((line) => !line.startsWith(`@${wasmSymbolPrefix(SHA)}_desc =`));
+ expect(code([0x42, 0, 4])).toEqual(code([0x02, 4]));
+ });
+
+ test("a memarg naming another memory is refused", () => {
+ expect(() =>
+ translateWasmModule(loadModule([0x42, 1, 4]), SHA, { boundsChecks: true }),
+ ).toThrow(/memory 1/);
+ });
+
+ test("local.tee of an i32 into an externref local is refused", () => {
+ // (func (local externref) i32.const 1 local.tee 0 drop): the operand
+ // never reaches a reference slot.
+ const body = [1, 1, 0x6f, 0x41, 1, 0x22, 0, 0x1a, 0x0b];
+ const bytes = wasmModule([1, 1, 0x60, 0, 0], [3, 1, 0], [10, 1, body.length, ...body]);
+ expect(() => translateWasmModule(bytes, SHA, { boundsChecks: false })).toThrow(
+ /local\.tee operand type does not match/,
+ );
+ });
+});
+
+describe("module parsing", () => {
+ test("reads the parts translation needs", () => {
+ const m = parseWasmModule(ADD);
+ expect(m.types).toEqual([{ params: [0x7f, 0x7f], results: [0x7f] }]);
+ expect(m.exports.map((e) => e.name)).toEqual(["add", "mem"]);
+ expect(m.memories).toEqual([{ min: 1, max: null, imported: false }]);
+ expect(m.datas[0]!.mode).toBe("active");
+ expect([...m.declaredRefs]).toEqual([0]);
+ });
+
+ test("decodes signed 64-bit LEB128 into two's-complement halves", () => {
+ const halves = (bytes: number[]): [number, number] =>
+ new WasmCursor(new Uint8Array(bytes), 0).s64();
+ expect(halves([0x7f])).toEqual([0xffffffff, 0xffffffff]);
+ expect(halves([0x80, 0x80, 0x80, 0x80, 0x10])).toEqual([0, 1]);
+ expect(halves([0xff, 0xff, 0xff, 0xff, 0x0f])).toEqual([0xffffffff, 0]);
+ expect(halves([0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x7f])).toEqual([
+ 0, 0x80000000,
+ ]);
+ expect(halves([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00])).toEqual([
+ 0xffffffff, 0x7fffffff,
+ ]);
+ });
+});
+
+describe("stack checks", () => {
+ test("only functions through which the stack can grow without bound check it", () => {
+ // f0 <-> f1 recurse mutually, f2 recurses on itself, f3 calls f0 once.
+ const calls = new Uint8Array([
+ 0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 5, 4, 0, 0, 0, 0, 10, 21, 4, 4, 0, 16, 1,
+ 11, 4, 0, 16, 0, 11, 4, 0, 16, 2, 11, 4, 0, 16, 0, 11,
+ ]);
+ expect([...stackCheckedFunctions(parseWasmModule(calls))].sort()).toEqual([0, 1, 2]);
+ expect(stackCheckedFunctions(parseWasmModule(ADD)).size).toBe(0);
+ // Only code without the fault handler checks explicitly.
+ const checked = translateWasmModule(calls, SHA, { boundsChecks: true }).text.join("\n");
+ expect(checked.split("@scr_wasm_stack_limit").length - 1).toBe(3);
+ const guarded = translateWasmModule(calls, SHA, { boundsChecks: false }).text.join("\n");
+ expect(guarded).not.toContain("@scr_wasm_stack_limit");
+ });
+});
+
+describe("helpers", () => {
+ test("equal signatures share their identity across modules", () => {
+ expect(signatureHash("ii:i")).toBe(signatureHash("ii:i"));
+ expect(signatureHash("ii:i")).not.toBe(signatureHash("i:i"));
+ expect(signatureHash(":")).toMatch(/^u0x[0-9a-f]{16}$/);
+ });
+
+ test("base64 round-trips module bytes", () => {
+ expect(encodeBase64(ADD)).toBe(Buffer.from(ADD).toString("base64"));
+ expect(decodeBase64(encodeBase64(ADD))).toEqual(ADD);
+ expect(encodeBase64(new Uint8Array([1]))).toBe("AQ==");
+ expect(encodeBase64(new Uint8Array([1, 2]))).toBe("AQI=");
+ });
+});
diff --git a/packages/compiler/src/wasm/translate.ts b/packages/compiler/src/wasm/translate.ts
new file mode 100644
index 000000000..f17c5dae3
--- /dev/null
+++ b/packages/compiler/src/wasm/translate.ts
@@ -0,0 +1,2212 @@
+/* WebAssembly to LLVM IR.
+ *
+ * Translates one validated module (binary.ts accepted it within the
+ * translatable feature level) to LLVM assembly that the program's emitter
+ * appends to its own module, so the native helper optimizes, partitions and
+ * caches it with the program. The runtime (scr_wasm.c) implements the
+ * JavaScript API over the descriptor this emits; scr_wasm_abi.h is the
+ * contract both sides follow.
+ *
+ * Shape of the output:
+ * - one internal function per wasm function, taking the instance as its
+ * first parameter (`ptr %inst`) and the wasm values directly (i32, i64,
+ * float, double; references are pointers). Several results return as a
+ * literal struct. An imported function is a stub that packs its
+ * arguments and calls scr_wasm_import_call.
+ * - one thunk per exported function, `void (ptr inst, ptr args, ptr
+ * results)` over 64-bit slots, which the runtime calls under its trap
+ * guard;
+ * - an init function (globals, table initializers, active element and
+ * data segments); the descriptor names the start function;
+ * - constant data (segments, names, signatures) and the descriptor.
+ *
+ * The instance layout (scr_wasm_abi.h): host at 0, memory base at 8,
+ * memory object at 16, function reference records at 24, then one pointer
+ * per table and one cell pointer per global.
+ *
+ * Memory accesses compute `base + zext(address) + offset` in 64 bits. With
+ * guard pages (64-bit hosts) the runtime reserves enough address space that
+ * every such address past the memory faults, so accesses are unchecked and
+ * the base is loaded once per function (the memory never moves). With
+ * explicit bounds checks (sanitizer builds and hosts without the fault
+ * handler) the base and byte length live in function-local slots that are
+ * reloaded after every call and memory.grow.
+ *
+ * Locals, block results and loop parameters are allocas; LLVM promotes them
+ * to SSA values. Traps call scr_wasm_trap, which never returns. Plain
+ * TypeScript without bigint, so the native compiler compiles it too. */
+import {
+ KIND_FUNC,
+ KIND_GLOBAL,
+ KIND_MEMORY,
+ KIND_TABLE,
+ VAL_EXTERNREF,
+ VAL_F32,
+ VAL_F64,
+ VAL_FUNCREF,
+ VAL_I32,
+ VAL_I64,
+ WasmCursor,
+ parseWasmModule,
+ readMemargFlags,
+ skipImmediates,
+ type WasmFuncType,
+ type WasmModule,
+ type WasmRange,
+} from "./module.js";
+
+/** Trap codes (scr_wasm_abi.h SCR_WASM_TRAP_*). */
+export const TRAP_UNREACHABLE = 1;
+export const TRAP_MEMORY_OOB = 2;
+export const TRAP_DIV_BY_ZERO = 3;
+export const TRAP_DIV_UNREPRESENTABLE = 4;
+export const TRAP_REM_BY_ZERO = 5;
+export const TRAP_FLOAT_UNREPRESENTABLE = 6;
+export const TRAP_TABLE_OOB = 7;
+export const TRAP_SIGNATURE = 8;
+export const TRAP_NULL = 9;
+/** scr_wasm_abi.h SCR_WASM_TRAP_EXHAUSTION. */
+export const TRAP_EXHAUSTION = 100;
+
+/** The descriptor ABI version both sides agree on. */
+export const WASM_ABI_VERSION = 1;
+
+/** Instance slot offsets (bytes). */
+const OFF_MEMBASE = 8;
+const OFF_MEMORY = 16;
+const OFF_FUNCREFS = 24;
+const OFF_TABLES = 32;
+/** sizeof(ScrWasmFuncRef). */
+const FUNCREF_SIZE = 24;
+
+export interface WasmTranslateOptions {
+ /** Explicit bounds checks instead of guard pages. */
+ boundsChecks: boolean;
+}
+
+export interface WasmTranslation {
+ /** External declarations, one line each. */
+ decls: string[];
+ /** Definitions in the emitter's top-level grammar. */
+ text: string[];
+ /** The descriptor's symbol (internal). */
+ descriptor: string;
+}
+
+/** A construct the translator does not support in an otherwise valid module. */
+export class WasmTranslateError extends Error {}
+
+/** Attributes of translated functions. Recursion must exhaust the stack
+ * as it does in V8, so LLVM may not turn self-recursion into loops (tail
+ * recursion elimination, including its accumulator form, which would make
+ * `f(x) + 1` recursion loop forever) or calls into jumps. */
+const FUNC_ATTRS = `nounwind "disable-tail-calls"="true"`;
+
+const RUNTIME_DECLS = [
+ "declare void @scr_wasm_trap(i32) noreturn nounwind cold",
+ "declare void @scr_wasm_import_call(ptr, i32, ptr, ptr)",
+ "declare i32 @scr_wasm_memory_grow(ptr, i32)",
+ "declare void @scr_wasm_memory_fill(ptr, i32, i32, i32)",
+ "declare void @scr_wasm_memory_copy(ptr, i32, i32, i32)",
+ "declare void @scr_wasm_memory_init(ptr, i32, i32, i32, i32)",
+ "declare void @scr_wasm_data_drop(ptr, i32)",
+ "declare ptr @scr_wasm_table_get(ptr, i32, i32)",
+ "declare void @scr_wasm_table_set(ptr, i32, i32, ptr)",
+ "declare i32 @scr_wasm_table_grow(ptr, i32, ptr, i32)",
+ "declare void @scr_wasm_table_fill(ptr, i32, i32, ptr, i32)",
+ "declare void @scr_wasm_table_copy(ptr, i32, i32, i32, i32, i32)",
+ "declare void @scr_wasm_table_init(ptr, i32, i32, i32, i32, i32)",
+ "declare void @scr_wasm_elem_drop(ptr, i32)",
+ "declare void @scr_wasm_global_set_ref(ptr, ptr)",
+];
+
+function llType(t: number): string {
+ switch (t) {
+ case VAL_I32:
+ return "i32";
+ case VAL_I64:
+ return "i64";
+ case VAL_F32:
+ return "float";
+ case VAL_F64:
+ return "double";
+ case VAL_FUNCREF:
+ case VAL_EXTERNREF:
+ return "ptr";
+ default:
+ throw new WasmTranslateError(`value type 0x${t.toString(16)} is not supported`);
+ }
+}
+
+function zeroOf(t: number): string {
+ switch (t) {
+ case VAL_I32:
+ case VAL_I64:
+ return "0";
+ case VAL_F32:
+ case VAL_F64:
+ return "0.0";
+ default:
+ return "null";
+ }
+}
+
+/** The runtime's signature letter of a value type. */
+function sigChar(t: number): string {
+ switch (t) {
+ case VAL_I32:
+ return "i";
+ case VAL_I64:
+ return "I";
+ case VAL_F32:
+ return "f";
+ case VAL_F64:
+ return "F";
+ case VAL_FUNCREF:
+ return "r";
+ case VAL_EXTERNREF:
+ return "x";
+ default:
+ throw new WasmTranslateError(`value type 0x${t.toString(16)} is not supported`);
+ }
+}
+
+function sigString(ft: WasmFuncType): string {
+ let s = "";
+ for (const p of ft.params) s += sigChar(p);
+ s += ":";
+ for (const r of ft.results) s += sigChar(r);
+ return s;
+}
+
+function retType(results: readonly number[]): string {
+ if (results.length === 0) return "void";
+ if (results.length === 1) return llType(results[0]!);
+ return `{ ${results.map(llType).join(", ")} }`;
+}
+
+function hex8(n: number): string {
+ return (n >>> 0).toString(16).padStart(8, "0");
+}
+
+/** An i64 constant from its halves, in LLVM's unsigned hexadecimal form. */
+function i64Lit(lo: number, hi: number): string {
+ return `u0x${hex8(hi)}${hex8(lo)}`;
+}
+
+/** The canonical signature identity call_indirect compares: two
+ * independent 32-bit FNV-1a hashes of the signature text. Equal signatures
+ * in different modules get the same identity. */
+export function signatureHash(sig: string): string {
+ let a = 0x811c9dc5;
+ let b = 0x01000193 ^ 0x5bd1e995;
+ for (let i = 0; i < sig.length; i++) {
+ const c = sig.charCodeAt(i);
+ a = Math.imul(a ^ c, 0x01000193);
+ b = Math.imul(b ^ c, 0x01000193) ^ (b >>> 13);
+ }
+ return i64Lit(a >>> 0, b >>> 0);
+}
+
+/** UTF-8 bytes of a string. */
+function utf8Bytes(text: string): number[] {
+ const out: number[] = [];
+ for (let i = 0; i < text.length; i++) {
+ const cp = text.codePointAt(i)!;
+ if (cp > 0xffff) i++;
+ if (cp < 0x80) out.push(cp);
+ else if (cp < 0x800) out.push(0xc0 | (cp >> 6), 0x80 | (cp & 0x3f));
+ else if (cp < 0x10000)
+ out.push(0xe0 | (cp >> 12), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f));
+ else {
+ out.push(
+ 0xf0 | (cp >> 18),
+ 0x80 | ((cp >> 12) & 0x3f),
+ 0x80 | ((cp >> 6) & 0x3f),
+ 0x80 | (cp & 0x3f),
+ );
+ }
+ }
+ return out;
+}
+
+/** An LLVM c"..." string body for bytes. */
+function cBytes(bytes: Uint8Array, start: number, end: number): string {
+ let s = "";
+ for (let i = start; i < end; i++) {
+ const b = bytes[i]!;
+ if (b >= 0x20 && b < 0x7f && b !== 0x22 && b !== 0x5c) s += String.fromCharCode(b);
+ else s += "\\" + (b < 16 ? "0" : "") + b.toString(16).toUpperCase();
+ }
+ return s;
+}
+
+interface StackValue {
+ v: string;
+ t: number;
+ /** The line of the load that produced the value (this.lines). */
+ load?: number;
+}
+
+interface Frame {
+ kind: "block" | "loop" | "if" | "func";
+ params: number[];
+ results: number[];
+ /** Branch target: the loop header, or the end label. */
+ target: string;
+ end: string;
+ /** Allocas the values of a branch to this frame are stored in: the loop
+ * parameters for a loop, the results otherwise. */
+ branchSlots: string[];
+ /** Allocas of the results at the frame's end (loops: their own). */
+ resultSlots: string[];
+ height: number;
+ /** An if whose else branch has not started. */
+ elseLabel: string | null;
+ /** The parameter values of an if, for its implicit else. */
+ ifParams: StackValue[];
+ /** The start of the frame was reachable. */
+ live: boolean;
+}
+
+class Lines {
+ lines: string[] = [];
+ add(line: string): void {
+ this.lines.push(line);
+ }
+}
+
+/** One module's translation. */
+class ModuleTranslator {
+ m: WasmModule;
+ p: string;
+ opts: WasmTranslateOptions;
+ text: string[] = [];
+ /** Functions that need a reference record. */
+ refs: Set;
+ /** Functions whose entry checks the stack (stackCheckedFunctions). */
+ stackChecked: Set;
+ typeHashes: string[];
+ memoryCount: number;
+
+ constructor(m: WasmModule, prefix: string, opts: WasmTranslateOptions) {
+ this.m = m;
+ this.p = prefix;
+ this.opts = opts;
+ this.refs = new Set(m.declaredRefs);
+ this.typeHashes = m.types.map((t) => signatureHash(sigString(t)));
+ this.memoryCount = m.memories.length;
+ if (m.memories.length > 1) throw new WasmTranslateError("multiple memories are not supported");
+ this.stackChecked = stackCheckedFunctions(m);
+ }
+
+ fn(index: number): string {
+ return `@${this.p}_f${index}`;
+ }
+
+ funcType(index: number): WasmFuncType {
+ return this.m.types[this.m.funcTypes[index]!]!;
+ }
+
+ globalSlot(index: number): number {
+ return OFF_TABLES + 8 * (this.m.tables.length + index);
+ }
+
+ tableSlot(index: number): number {
+ return OFF_TABLES + 8 * index;
+ }
+
+ signature(ft: WasmFuncType, name: string): string {
+ const params = ["ptr %inst"];
+ for (let i = 0; i < ft.params.length; i++) params.push(`${llType(ft.params[i]!)} %p${i}`);
+ return `${retType(ft.results)} ${name}(${params.join(", ")})`;
+ }
+
+ translate(): void {
+ const m = this.m;
+ for (let i = 0; i < m.importedFuncs; i++) this.importStub(i);
+ for (let i = 0; i < m.codes.length; i++) {
+ const index = m.importedFuncs + i;
+ const f = new FunctionTranslator(this, index);
+ this.text.push(...f.translate());
+ }
+ }
+
+ importStub(index: number): void {
+ const ft = this.funcType(index);
+ const out: string[] = [];
+ out.push(`define internal ${this.signature(ft, this.fn(index))} nounwind {`);
+ out.push(`entry:`);
+ const np = Math.max(1, ft.params.length);
+ const nr = Math.max(1, ft.results.length);
+ out.push(` %args = alloca [${np} x i64], align 8`);
+ out.push(` %res = alloca [${nr} x i64], align 8`);
+ for (let i = 0; i < ft.params.length; i++) {
+ const slot = toSlot(out, ft.params[i]!, `%p${i}`, `a${i}`);
+ out.push(` %ap${i} = getelementptr i64, ptr %args, i64 ${i}`);
+ out.push(` store i64 ${slot}, ptr %ap${i}`);
+ }
+ out.push(` %host = load ptr, ptr %inst`);
+ out.push(` call void @scr_wasm_import_call(ptr %host, i32 ${index}, ptr %args, ptr %res)`);
+ emitSlotReturn(out, ft.results, "%res");
+ out.push(`}`);
+ this.text.push(out.join("\n"));
+ }
+
+ /** The thunk the runtime calls for exported function `index`. */
+ exportThunk(index: number): string {
+ const name = `@${this.p}_x${index}`;
+ const ft = this.funcType(index);
+ const out: string[] = [];
+ out.push(`define internal void ${name}(ptr %inst, ptr %a, ptr %r) nounwind {`);
+ out.push(`entry:`);
+ const args: string[] = ["ptr %inst"];
+ for (let i = 0; i < ft.params.length; i++) {
+ out.push(` %ap${i} = getelementptr i64, ptr %a, i64 ${i}`);
+ out.push(` %as${i} = load i64, ptr %ap${i}`);
+ args.push(`${llType(ft.params[i]!)} ${fromSlot(out, ft.params[i]!, `%as${i}`, `a${i}`)}`);
+ }
+ const ret = retType(ft.results);
+ if (ft.results.length === 0) {
+ out.push(` call void ${this.fn(index)}(${args.join(", ")})`);
+ } else {
+ out.push(` %res = call ${ret} ${this.fn(index)}(${args.join(", ")})`);
+ for (let i = 0; i < ft.results.length; i++) {
+ let v = "%res";
+ if (ft.results.length > 1) {
+ out.push(` %rv${i} = extractvalue ${ret} %res, ${i}`);
+ v = `%rv${i}`;
+ }
+ const slot = toSlot(out, ft.results[i]!, v, `r${i}`);
+ out.push(` %rp${i} = getelementptr i64, ptr %r, i64 ${i}`);
+ out.push(` store i64 ${slot}, ptr %rp${i}`);
+ }
+ }
+ out.push(` ret void`);
+ out.push(`}`);
+ this.text.push(out.join("\n"));
+ return name;
+ }
+
+ /** Emits the init function: globals, table initializers and segments,
+ * in the specification's order. The runtime calls the start function
+ * (which takes and returns nothing) separately. */
+ initFunction(): string {
+ const m = this.m;
+ const name = `@${this.p}_init`;
+ const f = new FunctionTranslator(this, -1);
+ const body = f.lines;
+ // Globals.
+ for (let g = 0; g < m.globals.length; g++) {
+ const global = m.globals[g]!;
+ if (global.imported || global.init === null) continue;
+ const v = f.constExpr(global.init);
+ const cell = f.loadPtrSlot(this.globalSlot(g));
+ if (global.type === VAL_EXTERNREF) {
+ // A defined externref global starts as null or another global's
+ // value; the runtime's setter retains it.
+ body.add(` call void @scr_wasm_global_set_ref(ptr ${cell}, ptr ${v})`);
+ } else {
+ body.add(` store ${llType(global.type)} ${v}, ptr ${cell}`);
+ }
+ }
+ // Defined tables with an initializer.
+ for (let t = 0; t < m.tables.length; t++) {
+ const table = m.tables[t]!;
+ if (table.imported || table.init === null) continue;
+ const v = f.constExpr(table.init);
+ body.add(
+ ` call void @scr_wasm_table_fill(ptr %inst, i32 ${t}, i32 0, ptr ${v}, i32 ${table.min})`,
+ );
+ }
+ // Element segments: active ones initialize their table and drop;
+ // declarative ones only drop.
+ for (let s = 0; s < m.elems.length; s++) {
+ const seg = m.elems[s]!;
+ if (seg.mode === "active") {
+ const offset = f.constExpr(seg.offset!);
+ body.add(
+ ` call void @scr_wasm_table_init(ptr %inst, i32 ${seg.table}, i32 ${s}, i32 ${offset}, i32 0, i32 ${seg.funcs.length})`,
+ );
+ }
+ if (seg.mode !== "passive") body.add(` call void @scr_wasm_elem_drop(ptr %inst, i32 ${s})`);
+ }
+ for (let s = 0; s < m.datas.length; s++) {
+ const seg = m.datas[s]!;
+ if (seg.mode !== "active") continue;
+ const offset = f.constExpr(seg.offset!);
+ const length = seg.payload.end - seg.payload.start;
+ body.add(
+ ` call void @scr_wasm_memory_init(ptr %inst, i32 ${s}, i32 ${offset}, i32 0, i32 ${length})`,
+ );
+ body.add(` call void @scr_wasm_data_drop(ptr %inst, i32 ${s})`);
+ }
+ body.add(` ret void`);
+ const out = [`define internal void ${name}(ptr %inst) nounwind {`, `entry:`];
+ out.push(...f.entry.lines);
+ out.push(...body.lines);
+ out.push(`}`);
+ this.text.push(out.join("\n"));
+ return name;
+ }
+
+ /** Constant data and the descriptor. */
+ descriptor(sha256: Uint8Array): string {
+ const m = this.m;
+ const p = this.p;
+ const lines: string[] = [];
+ // First, so that every function its constant expressions reference has
+ // a record in the tables below.
+ const init = this.initFunction();
+ let strings = 0;
+ const cstr = (text: readonly number[]): string => {
+ const sym = `@${p}_s${strings++}`;
+ const bytes = new Uint8Array(text);
+ lines.push(
+ `${sym} = private unnamed_addr constant [${bytes.length + 1} x i8] c"${cBytes(bytes, 0, bytes.length)}\\00"`,
+ );
+ return sym;
+ };
+ const textStr = (text: string): { sym: string; len: number } => {
+ const bytes = utf8Bytes(text);
+ return { sym: cstr(bytes), len: bytes.length };
+ };
+ // Imports.
+ const importRows: string[] = [];
+ let funcImport = 0;
+ let tableImport = 0;
+ let memoryImport = 0;
+ let globalImport = 0;
+ for (const imp of m.imports) {
+ const mod = textStr(imp.module);
+ const name = textStr(imp.name);
+ let index = 0;
+ let type = "";
+ if (imp.kind === KIND_FUNC) {
+ index = funcImport++;
+ type = sigString(m.types[imp.type]!);
+ } else if (imp.kind === KIND_TABLE) {
+ index = tableImport++;
+ type = sigChar(imp.type);
+ } else if (imp.kind === KIND_MEMORY) {
+ index = memoryImport++;
+ } else {
+ index = globalImport++;
+ type = sigChar(imp.type);
+ }
+ const typeSym = cstr(utf8Bytes(type));
+ importRows.push(
+ `{ ptr, i64, ptr, i64, i64, i64, ptr } { ptr ${mod.sym}, i64 ${mod.len}, ptr ${name.sym}, i64 ${name.len}, i64 ${imp.kind}, i64 ${index}, ptr ${typeSym} }`,
+ );
+ }
+ const importType = "{ ptr, i64, ptr, i64, i64, i64, ptr }";
+ const importsSym = `@${p}_imports`;
+ lines.push(
+ `${importsSym} = private constant [${importRows.length} x ${importType}] [${importRows.join(", ")}]`,
+ );
+ // Exports.
+ const thunks = new Map();
+ const exportRows: string[] = [];
+ for (const ex of m.exports) {
+ const name = textStr(ex.name);
+ let type = "";
+ let call = "null";
+ if (ex.kind === KIND_FUNC) {
+ type = sigString(this.funcType(ex.index));
+ let thunk = thunks.get(ex.index);
+ if (thunk === undefined) {
+ thunk = this.exportThunk(ex.index);
+ thunks.set(ex.index, thunk);
+ }
+ call = thunk;
+ } else if (ex.kind === KIND_TABLE) {
+ type = sigChar(m.tables[ex.index]!.elemType);
+ } else if (ex.kind === KIND_GLOBAL) {
+ type = sigChar(m.globals[ex.index]!.type);
+ }
+ const typeSym = cstr(utf8Bytes(type));
+ exportRows.push(
+ `{ ptr, i64, i64, i64, ptr, ptr } { ptr ${name.sym}, i64 ${name.len}, i64 ${ex.kind}, i64 ${ex.index}, ptr ${typeSym}, ptr ${call} }`,
+ );
+ }
+ const exportType = "{ ptr, i64, i64, i64, ptr, ptr }";
+ const exportsSym = `@${p}_exports`;
+ lines.push(
+ `${exportsSym} = private constant [${exportRows.length} x ${exportType}] [${exportRows.join(", ")}]`,
+ );
+ // Function references: code, signature identity and signature text for
+ // every function a reference can name.
+ const nf = m.funcTypes.length;
+ const codes: string[] = [];
+ const types: string[] = [];
+ const sigs: string[] = [];
+ const sigSyms = new Map();
+ for (let f = 0; f < nf; f++) {
+ const referenced = this.refs.has(f);
+ codes.push(referenced ? `ptr ${this.fn(f)}` : "ptr null");
+ types.push(`i64 ${this.typeHashes[m.funcTypes[f]!]!}`);
+ const sig = sigString(this.funcType(f));
+ let sym = sigSyms.get(sig);
+ if (sym === undefined) {
+ sym = cstr(utf8Bytes(sig));
+ sigSyms.set(sig, sym);
+ }
+ sigs.push(`ptr ${sym}`);
+ }
+ const codesSym = `@${p}_codes`;
+ const typesSym = `@${p}_types`;
+ const sigsSym = `@${p}_sigs`;
+ lines.push(`${codesSym} = private constant [${nf} x ptr] [${codes.join(", ")}]`);
+ lines.push(`${typesSym} = private constant [${nf} x i64] [${types.join(", ")}]`);
+ lines.push(`${sigsSym} = private constant [${nf} x ptr] [${sigs.join(", ")}]`);
+ // Tables.
+ const tableRows = m.tables.map(
+ (t) =>
+ `{ i64, i64, i64, i64 } { i64 ${sigChar(t.elemType).charCodeAt(0)}, i64 ${t.min}, i64 ${t.max === null ? -1 : t.max}, i64 ${t.imported ? 1 : 0} }`,
+ );
+ const tablesSym = `@${p}_tables`;
+ lines.push(
+ `${tablesSym} = private constant [${tableRows.length} x { i64, i64, i64, i64 }] [${tableRows.join(", ")}]`,
+ );
+ // Globals.
+ const globalRows = m.globals.map(
+ (g) =>
+ `{ i64, i64, i64 } { i64 ${sigChar(g.type).charCodeAt(0)}, i64 ${g.mutable ? 1 : 0}, i64 ${g.imported ? 1 : 0} }`,
+ );
+ const globalsSym = `@${p}_globals`;
+ lines.push(
+ `${globalsSym} = private constant [${globalRows.length} x { i64, i64, i64 }] [${globalRows.join(", ")}]`,
+ );
+ // Data segments.
+ const dataRows: string[] = [];
+ for (let s = 0; s < m.datas.length; s++) {
+ const seg = m.datas[s]!;
+ const len = seg.payload.end - seg.payload.start;
+ if (len === 0) {
+ dataRows.push(`{ ptr, i64 } { ptr null, i64 0 }`);
+ continue;
+ }
+ const sym = `@${p}_d${s}`;
+ lines.push(
+ `${sym} = private unnamed_addr constant [${len} x i8] c"${cBytes(m.bytes, seg.payload.start, seg.payload.end)}", align 1`,
+ );
+ dataRows.push(`{ ptr, i64 } { ptr ${sym}, i64 ${len} }`);
+ }
+ const datasSym = `@${p}_datas`;
+ lines.push(
+ `${datasSym} = private constant [${dataRows.length} x { ptr, i64 }] [${dataRows.join(", ")}]`,
+ );
+ // Element segments: a function index, -1 for null, or -(2 + g) for the
+ // value of global g.
+ const elemRows: string[] = [];
+ for (let s = 0; s < m.elems.length; s++) {
+ const seg = m.elems[s]!;
+ const items: string[] = [];
+ for (let k = 0; k < seg.funcs.length; k++) {
+ const f = seg.funcs[k]!;
+ if (f >= 0) {
+ items.push(`i64 ${f}`);
+ continue;
+ }
+ items.push(`i64 ${this.elemExprCode(seg.exprs[k]!)}`);
+ }
+ let itemsRef = "ptr null";
+ if (items.length > 0) {
+ const sym = `@${p}_e${s}`;
+ lines.push(`${sym} = private constant [${items.length} x i64] [${items.join(", ")}]`);
+ itemsRef = `ptr ${sym}`;
+ }
+ elemRows.push(
+ `{ i64, i64, ptr } { i64 ${sigChar(seg.elemType).charCodeAt(0)}, i64 ${items.length}, ${itemsRef} }`,
+ );
+ }
+ const elemsSym = `@${p}_elems`;
+ lines.push(
+ `${elemsSym} = private constant [${elemRows.length} x { i64, i64, ptr }] [${elemRows.join(", ")}]`,
+ );
+ const memory = m.memories[0];
+ const sha: string[] = [];
+ for (let i = 0; i < 32; i++) sha.push(`i8 ${sha256[i]!}`);
+ const desc = `@${p}_desc`;
+ const fields = [
+ `[32 x i8] [${sha.join(", ")}]`,
+ `i64 ${m.bytes.length}`,
+ `i64 ${WASM_ABI_VERSION}`,
+ `i64 ${this.opts.boundsChecks ? 1 : 0}`,
+ `i64 ${m.imports.length}`,
+ `ptr ${importsSym}`,
+ `i64 ${m.exports.length}`,
+ `ptr ${exportsSym}`,
+ `i64 ${nf}`,
+ `i64 ${m.importedFuncs}`,
+ `ptr ${codesSym}`,
+ `ptr ${typesSym}`,
+ `ptr ${sigsSym}`,
+ `i64 ${m.tables.length}`,
+ `ptr ${tablesSym}`,
+ `i64 ${this.memoryCount}`,
+ `i64 ${memory === undefined ? 0 : memory.min}`,
+ `i64 ${memory === undefined || memory.max === null ? -1 : memory.max}`,
+ `i64 ${m.globals.length}`,
+ `ptr ${globalsSym}`,
+ `i64 ${m.datas.length}`,
+ `ptr ${datasSym}`,
+ `i64 ${m.elems.length}`,
+ `ptr ${elemsSym}`,
+ `ptr ${init}`,
+ `ptr ${m.start === null ? "null" : this.fn(m.start)}`,
+ ];
+ const descType =
+ "{ [32 x i8], i64, i64, i64, i64, ptr, i64, ptr, i64, i64, ptr, ptr, ptr, i64, ptr, i64, i64, i64, i64, ptr, i64, ptr, i64, ptr, ptr, ptr }";
+ lines.push(`${desc} = internal constant ${descType} { ${fields.join(", ")} }`);
+ this.text.push(lines.join("\n"));
+ return desc;
+ }
+
+ /** The element-segment code of a non-ref.func constant expression. */
+ elemExprCode(expr: WasmRange): number {
+ const c = new WasmCursor(this.m.bytes, expr.start);
+ const op = c.u8();
+ if (op === 0xd0) {
+ c.valType();
+ if (c.u8() === 0x0b) return -1;
+ } else if (op === 0x23) {
+ const g = c.u32();
+ if (c.u8() === 0x0b) return -(2 + g);
+ }
+ throw new WasmTranslateError(
+ "element segment initializers other than ref.null, ref.func and global.get are not supported",
+ );
+ }
+}
+
+/** Store a value as a 64-bit slot; answers the slot operand. */
+function toSlot(out: string[], t: number, v: string, tag: string): string {
+ switch (t) {
+ case VAL_I32:
+ out.push(` %${tag}z = zext i32 ${v} to i64`);
+ return `%${tag}z`;
+ case VAL_I64:
+ return v;
+ case VAL_F32:
+ out.push(` %${tag}b = bitcast float ${v} to i32`);
+ out.push(` %${tag}z = zext i32 %${tag}b to i64`);
+ return `%${tag}z`;
+ case VAL_F64:
+ out.push(` %${tag}z = bitcast double ${v} to i64`);
+ return `%${tag}z`;
+ default:
+ out.push(` %${tag}z = ptrtoint ptr ${v} to i64`);
+ return `%${tag}z`;
+ }
+}
+
+function fromSlot(out: string[], t: number, slot: string, tag: string): string {
+ switch (t) {
+ case VAL_I32:
+ out.push(` %${tag}v = trunc i64 ${slot} to i32`);
+ return `%${tag}v`;
+ case VAL_I64:
+ return slot;
+ case VAL_F32:
+ out.push(` %${tag}t = trunc i64 ${slot} to i32`);
+ out.push(` %${tag}v = bitcast i32 %${tag}t to float`);
+ return `%${tag}v`;
+ case VAL_F64:
+ out.push(` %${tag}v = bitcast i64 ${slot} to double`);
+ return `%${tag}v`;
+ default:
+ out.push(` %${tag}v = inttoptr i64 ${slot} to ptr`);
+ return `%${tag}v`;
+ }
+}
+
+function emitSlotReturn(out: string[], results: readonly number[], buffer: string): void {
+ if (results.length === 0) {
+ out.push(` ret void`);
+ return;
+ }
+ const vals: string[] = [];
+ for (let i = 0; i < results.length; i++) {
+ out.push(` %rp${i} = getelementptr i64, ptr ${buffer}, i64 ${i}`);
+ out.push(` %rs${i} = load i64, ptr %rp${i}`);
+ vals.push(fromSlot(out, results[i]!, `%rs${i}`, `r${i}`));
+ }
+ if (results.length === 1) {
+ out.push(` ret ${llType(results[0]!)} ${vals[0]}`);
+ return;
+ }
+ const ret = retType(results);
+ let agg = "undef";
+ for (let i = 0; i < results.length; i++) {
+ out.push(` %agg${i} = insertvalue ${ret} ${agg}, ${llType(results[i]!)} ${vals[i]}, ${i}`);
+ agg = `%agg${i}`;
+ }
+ out.push(` ret ${ret} ${agg}`);
+}
+
+const LOAD_INFO: Record = {
+ // opcode: [result type, memory type, bytes, extension]
+ 0x28: [VAL_I32, "i32", 4, ""],
+ 0x29: [VAL_I64, "i64", 8, ""],
+ 0x2a: [VAL_F32, "float", 4, ""],
+ 0x2b: [VAL_F64, "double", 8, ""],
+ 0x2c: [VAL_I32, "i8", 1, "sext"],
+ 0x2d: [VAL_I32, "i8", 1, "zext"],
+ 0x2e: [VAL_I32, "i16", 2, "sext"],
+ 0x2f: [VAL_I32, "i16", 2, "zext"],
+ 0x30: [VAL_I64, "i8", 1, "sext"],
+ 0x31: [VAL_I64, "i8", 1, "zext"],
+ 0x32: [VAL_I64, "i16", 2, "sext"],
+ 0x33: [VAL_I64, "i16", 2, "zext"],
+ 0x34: [VAL_I64, "i32", 4, "sext"],
+ 0x35: [VAL_I64, "i32", 4, "zext"],
+};
+
+const STORE_INFO: Record = {
+ 0x36: [VAL_I32, "i32", 4],
+ 0x37: [VAL_I64, "i64", 8],
+ 0x38: [VAL_F32, "float", 4],
+ 0x39: [VAL_F64, "double", 8],
+ 0x3a: [VAL_I32, "i8", 1],
+ 0x3b: [VAL_I32, "i16", 2],
+ 0x3c: [VAL_I64, "i8", 1],
+ 0x3d: [VAL_I64, "i16", 2],
+ 0x3e: [VAL_I64, "i32", 4],
+};
+
+/** Translates one function body (index >= 0) or hosts the init function's
+ * constant expressions (index -1). */
+class FunctionTranslator {
+ mt: ModuleTranslator;
+ m: WasmModule;
+ index: number;
+ ft: WasmFuncType;
+ entry = new Lines();
+ lines = new Lines();
+ stack: StackValue[] = [];
+ frames: Frame[] = [];
+ localTypes: number[] = [];
+ next = 0;
+ labels = 0;
+ live = true;
+ trapLabels = new Map();
+ /** Guard mode: the memory base, loaded once in the entry block. */
+ baseValue: string | null = null;
+ checked: boolean;
+
+ constructor(mt: ModuleTranslator, index: number) {
+ this.mt = mt;
+ this.m = mt.m;
+ this.index = index;
+ this.ft = index >= 0 ? mt.funcType(index) : { params: [], results: [] };
+ this.checked = mt.opts.boundsChecks;
+ }
+
+ tmp(): string {
+ return `%v${this.next++}`;
+ }
+
+ label(hint: string): string {
+ return `${hint}${this.labels++}`;
+ }
+
+ add(line: string): void {
+ this.lines.add(line);
+ }
+
+ emit(expr: string): string {
+ const v = this.tmp();
+ this.add(` ${v} = ${expr}`);
+ return v;
+ }
+
+ push(v: string, t: number): void {
+ this.stack.push({ v, t });
+ }
+
+ pop(): StackValue {
+ const v = this.stack.pop();
+ if (v === undefined) throw new Error("wasm translator: operand stack underflow");
+ return v;
+ }
+
+ /** Pops a value that must have type `t`: the type a local, global,
+ * table or memory store writes. Validation guarantees it; a mismatch
+ * means the translator and the validator disagree about the code, so
+ * the module is not translated. */
+ popType(t: number, what: string): StackValue {
+ const v = this.pop();
+ if (v.t !== t) throw new WasmTranslateError(`${what} operand type does not match`);
+ return v;
+ }
+
+ /** The declared type of local `i` (parameters first). */
+ localType(i: number): number {
+ const t = this.localTypes[i];
+ if (t === undefined) throw new WasmTranslateError(`unknown local ${i}`);
+ return t;
+ }
+
+ /** The offset of a memarg over memory 0 (the only memory a translatable
+ * module has). */
+ memargOffset(c: WasmCursor): number {
+ const flags = readMemargFlags(() => c.u32());
+ if (flags.align < 0 || flags.memory !== 0)
+ throw new WasmTranslateError(`memarg of memory ${flags.memory} is not supported`);
+ return c.u32();
+ }
+
+ /** A memory index immediate (memory.size, memory.grow and the bulk
+ * memory instructions), which must be 0. */
+ memoryIndex(c: WasmCursor): void {
+ const index = c.u32();
+ if (index !== 0) throw new WasmTranslateError(`memory ${index} is not supported`);
+ }
+
+ popN(n: number): StackValue[] {
+ const out = this.stack.splice(this.stack.length - n, n);
+ if (out.length !== n) throw new Error("wasm translator: operand stack underflow");
+ return out;
+ }
+
+ /** Starts a new basic block. */
+ startBlock(label: string): void {
+ this.add(`${label}:`);
+ }
+
+ alloca(type: string): string {
+ const v = `%s${this.next++}`;
+ this.entry.add(` ${v} = alloca ${type}`);
+ return v;
+ }
+
+ slotPtr(offset: number): string {
+ if (offset === 0) return "%inst";
+ return this.emit(`getelementptr inbounds i8, ptr %inst, i64 ${offset}`);
+ }
+
+ loadPtrSlot(offset: number): string {
+ return this.emit(`load ptr, ptr ${this.slotPtr(offset)}, align 8`);
+ }
+
+ trap(code: number): string {
+ let label = this.trapLabels.get(code);
+ if (label === undefined) {
+ label = `trap${code}`;
+ this.trapLabels.set(code, label);
+ }
+ return label;
+ }
+
+ // ── memory ──────────────────────────────────────────────────────────
+
+ /** Checked mode: the slots holding the base and byte length. */
+ memSlots: { base: string; size: string } | null = null;
+
+ refreshMemory(): void {
+ if (!this.checked || this.memSlots === null) return;
+ // From the memory object, not the instance: a memory another instance
+ // (or JavaScript) grew may have moved.
+ const mem = this.loadPtrSlot(OFF_MEMORY);
+ const base = this.emit(`load ptr, ptr ${mem}, align 8`);
+ this.add(` store ptr ${base}, ptr ${this.memSlots.base}`);
+ const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${mem}, i64 8`);
+ const size = this.emit(`load i64, ptr ${sizePtr}, align 8`);
+ this.add(` store i64 ${size}, ptr ${this.memSlots.size}`);
+ }
+
+ base(): string {
+ if (this.checked) {
+ return this.emit(`load ptr, ptr ${this.memSlots!.base}`);
+ }
+ if (this.baseValue === null) {
+ this.baseValue = "%membase";
+ }
+ return this.baseValue;
+ }
+
+ /** The address of an access of `bytes` bytes at `addr + offset`. */
+ address(addr: string, offset: number, bytes: number): string {
+ const wide = this.emit(`zext i32 ${addr} to i64`);
+ const ea = offset === 0 ? wide : this.emit(`add i64 ${wide}, ${offset}`);
+ if (this.checked) {
+ const size = this.emit(`load i64, ptr ${this.memSlots!.size}`);
+ const endAddr = this.emit(`add i64 ${ea}, ${bytes}`);
+ const oob = this.emit(`icmp ugt i64 ${endAddr}, ${size}`);
+ const ok = this.label("ok");
+ this.add(` br i1 ${oob}, label %${this.trap(TRAP_MEMORY_OOB)}, label %${ok}`);
+ this.startBlock(ok);
+ }
+ return this.emit(`getelementptr i8, ptr ${this.base()}, i64 ${ea}`);
+ }
+
+ // ── constant expressions ────────────────────────────────────────────
+
+ constExpr(range: WasmRange): string {
+ const c = new WasmCursor(this.m.bytes, range.start);
+ const stack: string[] = [];
+ for (;;) {
+ const op = c.u8();
+ if (op === 0x0b) break;
+ switch (op) {
+ case 0x41:
+ stack.push(String(c.s32()));
+ break;
+ case 0x42: {
+ const [lo, hi] = c.s64();
+ stack.push(i64Lit(lo, hi));
+ break;
+ }
+ case 0x43:
+ stack.push(this.emit(`bitcast i32 ${c.u32le() | 0} to float`));
+ break;
+ case 0x44: {
+ const lo = c.u32le();
+ const hi = c.u32le();
+ stack.push(this.emit(`bitcast i64 ${i64Lit(lo, hi)} to double`));
+ break;
+ }
+ case 0x23: {
+ const g = c.u32();
+ const cell = this.loadPtrSlot(this.mt.globalSlot(g));
+ stack.push(this.emit(`load ${llType(this.m.globals[g]!.type)}, ptr ${cell}`));
+ break;
+ }
+ case 0xd0:
+ c.valType();
+ stack.push("null");
+ break;
+ case 0xd2:
+ stack.push(this.funcRef(c.u32()));
+ break;
+ case 0x6a:
+ case 0x6b:
+ case 0x6c:
+ case 0x7c:
+ case 0x7d:
+ case 0x7e: {
+ const b = stack.pop()!;
+ const a = stack.pop()!;
+ const t = op < 0x70 ? "i32" : "i64";
+ const name =
+ op === 0x6a || op === 0x7c ? "add" : op === 0x6b || op === 0x7d ? "sub" : "mul";
+ stack.push(this.emit(`${name} ${t} ${a}, ${b}`));
+ break;
+ }
+ default:
+ throw new WasmTranslateError(
+ `constant expression opcode 0x${op.toString(16)} is not supported`,
+ );
+ }
+ }
+ return stack[stack.length - 1]!;
+ }
+
+ funcRef(f: number): string {
+ this.mt.refs.add(f);
+ const records = this.loadPtrSlot(OFF_FUNCREFS);
+ return this.emit(`getelementptr inbounds i8, ptr ${records}, i64 ${f * FUNCREF_SIZE}`);
+ }
+
+ // ── control ─────────────────────────────────────────────────────────
+
+ blockSig(c: WasmCursor): { params: number[]; results: number[] } {
+ const bt = c.blockType();
+ if (bt.kind === "empty") return { params: [], results: [] };
+ if (bt.kind === "value") return { params: [], results: [bt.type] };
+ const t = this.m.types[bt.index]!;
+ return { params: t.params.slice(), results: t.results.slice() };
+ }
+
+ slotsFor(types: readonly number[]): string[] {
+ return types.map((t) => this.alloca(llType(t)));
+ }
+
+ storeTo(slots: readonly string[], values: readonly StackValue[]): void {
+ for (let i = 0; i < slots.length; i++) {
+ const v = values[i]!;
+ this.add(` store ${llType(v.t)} ${v.v}, ptr ${slots[i]}`);
+ }
+ }
+
+ loadFrom(slots: readonly string[], types: readonly number[]): void {
+ for (let i = 0; i < slots.length; i++) {
+ const t = types[i]!;
+ this.push(this.emit(`load ${llType(t)}, ptr ${slots[i]}`), t);
+ }
+ }
+
+ /** Branch to the frame `depth` levels out (the stack keeps its values). */
+ branch(depth: number): void {
+ const f = this.frames[this.frames.length - 1 - depth]!;
+ const n = f.branchSlots.length;
+ if (n > 0) this.storeTo(f.branchSlots, this.stack.slice(this.stack.length - n));
+ this.add(` br label %${f.target}`);
+ }
+
+ /** The label of a block that branches to `depth`, for conditional
+ * branches and tables: the target itself when no values travel. */
+ branchEdge(depth: number, pending: string[]): string {
+ const f = this.frames[this.frames.length - 1 - depth]!;
+ if (f.branchSlots.length === 0) return f.target;
+ const edge = this.label("edge");
+ const saved = this.lines;
+ const edgeLines = new Lines();
+ this.lines = edgeLines;
+ this.startBlock(edge);
+ this.branch(depth);
+ this.lines = saved;
+ pending.push(...edgeLines.lines);
+ return edge;
+ }
+
+ goDead(): void {
+ this.live = false;
+ }
+
+ pushFrame(kind: Frame["kind"], sig: { params: number[]; results: number[] }): Frame {
+ const end = this.label("end");
+ const frame: Frame = {
+ kind,
+ params: sig.params,
+ results: sig.results,
+ target: end,
+ end,
+ branchSlots: [],
+ resultSlots: this.slotsFor(sig.results),
+ height: this.stack.length - sig.params.length,
+ elseLabel: null,
+ ifParams: [],
+ live: true,
+ };
+ frame.branchSlots = frame.resultSlots;
+ this.frames.push(frame);
+ return frame;
+ }
+
+ /** Skips dead code from just after a terminator to the `else` or `end`
+ * of the current frame; answers the opcode it stopped at (consumed). */
+ skipDead(c: WasmCursor): number {
+ let depth = 0;
+ for (;;) {
+ const op = c.u8();
+ if (op === 0x02 || op === 0x03 || op === 0x04) {
+ c.blockType();
+ depth++;
+ continue;
+ }
+ if (op === 0x0b) {
+ if (depth === 0) return op;
+ depth--;
+ continue;
+ }
+ if (op === 0x05 && depth === 0) return op;
+ skipImmediates(c, op);
+ }
+ }
+
+ translate(): string[] {
+ const ft = this.ft;
+ const code = this.m.codes[this.index - this.m.importedFuncs]!;
+ // Locals.
+ const localSlots: string[] = [];
+ this.localTypes = ft.params.concat(code.locals);
+ for (let i = 0; i < this.localTypes.length; i++) {
+ const t = this.localTypes[i]!;
+ const slot = `%l${i}`;
+ localSlots.push(slot);
+ this.entry.add(` ${slot} = alloca ${llType(t)}`);
+ }
+ for (let i = 0; i < ft.params.length; i++) {
+ this.entry.add(` store ${llType(ft.params[i]!)} %p${i}, ptr %l${i}`);
+ }
+ for (let i = ft.params.length; i < this.localTypes.length; i++) {
+ const t = this.localTypes[i]!;
+ this.entry.add(` store ${llType(t)} ${zeroOf(t)}, ptr %l${i}`);
+ }
+ if (this.checked && this.mt.memoryCount > 0) {
+ this.memSlots = { base: "%mbase", size: "%msize" };
+ this.entry.add(` %mbase = alloca ptr`);
+ this.entry.add(` %msize = alloca i64`);
+ }
+ // The function frame: branches to it return.
+ const ret = this.label("ret");
+ const fnFrame: Frame = {
+ kind: "func",
+ params: [],
+ results: ft.results,
+ target: ret,
+ end: ret,
+ branchSlots: [],
+ resultSlots: this.slotsFor(ft.results),
+ height: 0,
+ elseLabel: null,
+ ifParams: [],
+ live: true,
+ };
+ fnFrame.branchSlots = fnFrame.resultSlots;
+ this.frames.push(fnFrame);
+ if (this.memSlots !== null) this.refreshMemory();
+ const c = new WasmCursor(this.m.bytes, code.body.start);
+ this.body(c, code.body.end);
+ // Epilogue.
+ const out: string[] = [];
+ out.push(`define internal ${this.mt.signature(ft, this.mt.fn(this.index))} ${FUNC_ATTRS} {`);
+ out.push(`entry:`);
+ out.push(...this.entry.lines);
+ if (!this.checked && this.baseValue !== null) {
+ out.push(` %membase.p = getelementptr inbounds i8, ptr %inst, i64 ${OFF_MEMBASE}`);
+ out.push(` %membase = load ptr, ptr %membase.p, align 8`);
+ }
+ if (this.checked && this.mt.stackChecked.has(this.index)) {
+ // Without the fault handler, stack exhaustion is an explicit check of
+ // the stack pointer against the runtime's guard for the running stack
+ // (thread or async fiber), in the functions through which the stack
+ // can grow without bound (stackCheckedFunctions).
+ // The address of a frame slot stands for the stack pointer; unlike
+ // llvm.stacksave it constrains no other code.
+ out.push(` %sp = alloca i8, align 1`);
+ out.push(` %sp.i = ptrtoint ptr %sp to i64`);
+ out.push(` %limit.p = call ptr @llvm.threadlocal.address.p0(ptr @scr_wasm_stack_limit)`);
+ out.push(` %limit = load i64, ptr %limit.p, align 8`);
+ out.push(` %exhausted = icmp ult i64 %sp.i, %limit`);
+ out.push(` br i1 %exhausted, label %exhausted.trap, label %body`);
+ out.push(`exhausted.trap:`);
+ out.push(` call void @scr_wasm_trap(i32 ${TRAP_EXHAUSTION})`);
+ out.push(` unreachable`);
+ } else {
+ out.push(` br label %body`);
+ }
+ out.push(`body:`);
+ out.push(...this.lines.lines);
+ for (const [code2, label] of this.trapLabels) {
+ out.push(`${label}:`);
+ out.push(` call void @scr_wasm_trap(i32 ${code2})`);
+ out.push(` unreachable`);
+ }
+ out.push(`}`);
+ return [out.join("\n")];
+ }
+
+ /** The function body; returns at the function's final `end`. */
+ body(c: WasmCursor, end: number): void {
+ while (c.pos < end) {
+ if (!this.live) {
+ const op = this.skipDead(c);
+ if (op === 0x05) this.opElse();
+ else if (this.opEnd()) return;
+ continue;
+ }
+ const op = c.u8();
+ if (op === 0x0b) {
+ if (this.opEnd()) return;
+ continue;
+ }
+ this.instruction(c, op);
+ }
+ }
+
+ /** `end`: closes the innermost frame; true when that was the function. */
+ opEnd(): boolean {
+ const f = this.frames[this.frames.length - 1]!;
+ if (this.live) {
+ const n = f.results.length;
+ this.storeTo(f.resultSlots, this.stack.slice(this.stack.length - n));
+ this.add(` br label %${f.end}`);
+ }
+ if (f.elseLabel !== null) {
+ // An if without else: the implicit else passes its parameters on.
+ this.startBlock(f.elseLabel);
+ if (f.live) {
+ this.storeTo(f.resultSlots, f.ifParams);
+ }
+ this.add(` br label %${f.end}`);
+ f.elseLabel = null;
+ }
+ this.frames.pop();
+ this.stack.length = f.height;
+ this.startBlock(f.end);
+ if (f.kind === "func") {
+ if (f.results.length === 0) this.add(` ret void`);
+ else {
+ this.loadFrom(f.resultSlots, f.results);
+ const vals = this.popN(f.results.length);
+ if (vals.length === 1) this.add(` ret ${llType(vals[0]!.t)} ${vals[0]!.v}`);
+ else {
+ const rt = retType(f.results);
+ let agg = "undef";
+ for (let i = 0; i < vals.length; i++) {
+ agg = this.emit(`insertvalue ${rt} ${agg}, ${llType(vals[i]!.t)} ${vals[i]!.v}, ${i}`);
+ }
+ this.add(` ret ${rt} ${agg}`);
+ }
+ }
+ return true;
+ }
+ this.loadFrom(f.resultSlots, f.results);
+ this.live = true;
+ return false;
+ }
+
+ opElse(): void {
+ const f = this.frames[this.frames.length - 1]!;
+ if (this.live) {
+ const n = f.results.length;
+ this.storeTo(f.resultSlots, this.stack.slice(this.stack.length - n));
+ this.add(` br label %${f.end}`);
+ }
+ this.stack.length = f.height;
+ this.startBlock(f.elseLabel!);
+ f.elseLabel = null;
+ for (const v of f.ifParams) this.stack.push(v);
+ this.live = f.live;
+ if (!f.live) {
+ // The if itself was dead: its else branch is too.
+ this.add(` unreachable`);
+ }
+ }
+
+ call(index: number): void {
+ const ft = this.mt.funcType(index);
+ const args = this.popN(ft.params.length);
+ const argText = ["ptr %inst", ...args.map((a) => `${llType(a.t)} ${a.v}`)].join(", ");
+ this.callWith(this.mt.fn(index), ft, argText);
+ }
+
+ callWith(callee: string, ft: WasmFuncType, argText: string): void {
+ const rt = retType(ft.results);
+ if (ft.results.length === 0) {
+ this.add(` call void ${callee}(${argText})`);
+ } else {
+ const r = this.emit(`call ${rt} ${callee}(${argText})`);
+ if (ft.results.length === 1) this.push(r, ft.results[0]!);
+ else
+ for (let i = 0; i < ft.results.length; i++)
+ this.push(this.emit(`extractvalue ${rt} ${r}, ${i}`), ft.results[i]!);
+ }
+ this.refreshMemory();
+ }
+
+ callIndirect(typeIndex: number, table: number): void {
+ const ft = this.m.types[typeIndex]!;
+ const idx = this.pop();
+ const args = this.popN(ft.params.length);
+ const t = this.loadPtrSlot(this.mt.tableSlot(table));
+ const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${t}, i64 8`);
+ const size = this.emit(`load i32, ptr ${sizePtr}, align 8`);
+ const inRange = this.emit(`icmp ult i32 ${idx.v}, ${size}`);
+ const ok = this.label("ci");
+ this.add(` br i1 ${inRange}, label %${ok}, label %${this.trap(TRAP_TABLE_OOB)}`);
+ this.startBlock(ok);
+ const elems = this.emit(`load ptr, ptr ${t}, align 8`);
+ const wide = this.emit(`zext i32 ${idx.v} to i64`);
+ const slot = this.emit(`getelementptr inbounds ptr, ptr ${elems}, i64 ${wide}`);
+ const rec = this.emit(`load ptr, ptr ${slot}, align 8`);
+ const isNull = this.emit(`icmp eq ptr ${rec}, null`);
+ const nn = this.label("cn");
+ this.add(` br i1 ${isNull}, label %${this.trap(TRAP_SIGNATURE)}, label %${nn}`);
+ this.startBlock(nn);
+ const typePtr = this.emit(`getelementptr inbounds i8, ptr ${rec}, i64 8`);
+ const type = this.emit(`load i64, ptr ${typePtr}, align 8`);
+ const same = this.emit(`icmp eq i64 ${type}, ${this.mt.typeHashes[typeIndex]}`);
+ const go = this.label("cg");
+ this.add(` br i1 ${same}, label %${go}, label %${this.trap(TRAP_SIGNATURE)}`);
+ this.startBlock(go);
+ const codePtr = this.emit(`load ptr, ptr ${rec}, align 8`);
+ const instPtr = this.emit(`getelementptr inbounds i8, ptr ${rec}, i64 16`);
+ const inst = this.emit(`load ptr, ptr ${instPtr}, align 8`);
+ const argText = [`ptr ${inst}`, ...args.map((a) => `${llType(a.t)} ${a.v}`)].join(", ");
+ this.callWith(codePtr, ft, argText);
+ }
+
+ /** Integer division and remainder with wasm's traps. */
+ divRem(op: number, t: string): void {
+ const b = this.pop();
+ const a = this.pop();
+ const ty = b.t;
+ const zero = this.emit(`icmp eq ${t} ${b.v}, 0`);
+ const signed = op === 0x6d || op === 0x6f || op === 0x7f || op === 0x81;
+ const rem = op === 0x6f || op === 0x70 || op === 0x81 || op === 0x82;
+ const nz = this.label("nz");
+ this.add(
+ ` br i1 ${zero}, label %${this.trap(rem ? TRAP_REM_BY_ZERO : TRAP_DIV_BY_ZERO)}, label %${nz}`,
+ );
+ this.startBlock(nz);
+ const min = t === "i32" ? "-2147483648" : "-9223372036854775808";
+ if (signed && !rem) {
+ const isMin = this.emit(`icmp eq ${t} ${a.v}, ${min}`);
+ const isNeg1 = this.emit(`icmp eq ${t} ${b.v}, -1`);
+ const ovf = this.emit(`and i1 ${isMin}, ${isNeg1}`);
+ const okl = this.label("dv");
+ this.add(` br i1 ${ovf}, label %${this.trap(TRAP_DIV_UNREPRESENTABLE)}, label %${okl}`);
+ this.startBlock(okl);
+ this.push(this.emit(`sdiv ${t} ${a.v}, ${b.v}`), ty);
+ return;
+ }
+ if (signed && rem) {
+ // INT_MIN % -1 is 0 in wasm (LLVM's srem would be undefined).
+ const isNeg1 = this.emit(`icmp eq ${t} ${b.v}, -1`);
+ const divisor = this.emit(`select i1 ${isNeg1}, ${t} 1, ${t} ${b.v}`);
+ this.push(this.emit(`srem ${t} ${a.v}, ${divisor}`), ty);
+ return;
+ }
+ this.push(this.emit(`${rem ? "urem" : "udiv"} ${t} ${a.v}, ${b.v}`), ty);
+ }
+
+ /** Trapping float-to-int conversion. */
+ truncTrap(op: number): void {
+ const x = this.pop();
+ const from = x.t === VAL_F32 ? "float" : "double";
+ // [lower bound exclusive?, lower, upper (exclusive)] as exact doubles.
+ let lo = "";
+ let loInclusive = false;
+ let hi = "";
+ let to = "i32";
+ let signed = true;
+ switch (op) {
+ case 0xa8: // i32.trunc_f32_s
+ case 0xaa: // i32.trunc_f64_s
+ lo = op === 0xa8 ? "-2147483648.0" : "-2147483649.0";
+ loInclusive = op === 0xa8;
+ hi = "2147483648.0";
+ break;
+ case 0xa9:
+ case 0xab:
+ lo = "-1.0";
+ hi = "4294967296.0";
+ signed = false;
+ break;
+ case 0xae:
+ case 0xb0:
+ lo = "-9223372036854775808.0";
+ loInclusive = true;
+ hi = "9223372036854775808.0";
+ to = "i64";
+ break;
+ default:
+ lo = "-1.0";
+ hi = "18446744073709551616.0";
+ to = "i64";
+ signed = false;
+ break;
+ }
+ const d = from === "double" ? x.v : this.emit(`fpext float ${x.v} to double`);
+ const above = this.emit(`fcmp ${loInclusive ? "oge" : "ogt"} double ${d}, ${hexDouble(lo)}`);
+ const below = this.emit(`fcmp olt double ${d}, ${hexDouble(hi)}`);
+ const ok = this.emit(`and i1 ${above}, ${below}`);
+ const okl = this.label("cv");
+ this.add(` br i1 ${ok}, label %${okl}, label %${this.trap(TRAP_FLOAT_UNREPRESENTABLE)}`);
+ this.startBlock(okl);
+ this.push(
+ this.emit(`${signed ? "fptosi" : "fptoui"} ${from} ${x.v} to ${to}`),
+ to === "i32" ? VAL_I32 : VAL_I64,
+ );
+ }
+
+ unaryIntrinsic(name: string, t: string, vt: number, extra = ""): void {
+ const a = this.pop();
+ this.push(this.emit(`call ${t} @llvm.${name}(${t} ${a.v}${extra})`), vt);
+ }
+
+ binaryIntrinsic(name: string, t: string, vt: number): void {
+ const b = this.pop();
+ const a = this.pop();
+ this.push(this.emit(`call ${t} @llvm.${name}(${t} ${a.v}, ${t} ${b.v})`), vt);
+ }
+
+ binary(instr: string, t: string, vt: number): void {
+ const b = this.pop();
+ const a = this.pop();
+ this.push(this.emit(`${instr} ${t} ${a.v}, ${b.v}`), vt);
+ }
+
+ compare(pred: string, t: string, float: boolean): void {
+ const b = this.pop();
+ const a = this.pop();
+ const r = this.emit(`${float ? "fcmp" : "icmp"} ${pred} ${t} ${a.v}, ${b.v}`);
+ this.push(this.emit(`zext i1 ${r} to i32`), VAL_I32);
+ }
+
+ eqz(t: string): void {
+ const a = this.pop();
+ const r = this.emit(`icmp eq ${t} ${a.v}, 0`);
+ this.push(this.emit(`zext i1 ${r} to i32`), VAL_I32);
+ }
+
+ shift(instr: string, t: string, vt: number): void {
+ const b = this.pop();
+ const a = this.pop();
+ const masked = this.emit(`and ${t} ${b.v}, ${t === "i32" ? 31 : 63}`);
+ this.push(this.emit(`${instr} ${t} ${a.v}, ${masked}`), vt);
+ }
+
+ rotate(name: string, t: string, vt: number): void {
+ const b = this.pop();
+ const a = this.pop();
+ this.push(this.emit(`call ${t} @llvm.${name}.${t}(${t} ${a.v}, ${t} ${a.v}, ${t} ${b.v})`), vt);
+ }
+
+ convert(instr: string, from: string, to: string, vt: number): void {
+ const a = this.pop();
+ this.push(this.emit(`${instr} ${from} ${a.v} to ${to}`), vt);
+ }
+
+ instruction(c: WasmCursor, op: number): void {
+ switch (op) {
+ case 0x00: // unreachable
+ this.add(` br label %${this.trap(TRAP_UNREACHABLE)}`);
+ this.goDead();
+ return;
+ case 0x01:
+ return;
+ case 0x02: {
+ // block
+ const sig = this.blockSig(c);
+ this.pushFrame("block", sig);
+ return;
+ }
+ case 0x03: {
+ // loop
+ const sig = this.blockSig(c);
+ const f = this.pushFrame("loop", sig);
+ f.target = this.label("loop");
+ f.branchSlots = this.slotsFor(sig.params);
+ const params = this.popN(sig.params.length);
+ this.storeTo(f.branchSlots, params);
+ this.add(` br label %${f.target}`);
+ this.startBlock(f.target);
+ this.loadFrom(f.branchSlots, sig.params);
+ return;
+ }
+ case 0x04: {
+ // if
+ const sig = this.blockSig(c);
+ const cond = this.pop();
+ const f = this.pushFrame("if", sig);
+ f.ifParams = this.stack.slice(this.stack.length - sig.params.length);
+ const then = this.label("then");
+ f.elseLabel = this.label("else");
+ const test = this.emit(`icmp ne i32 ${cond.v}, 0`);
+ this.add(` br i1 ${test}, label %${then}, label %${f.elseLabel}`);
+ this.startBlock(then);
+ return;
+ }
+ case 0x05:
+ this.opElse();
+ return;
+ case 0x0c: {
+ const depth = c.u32();
+ this.branch(depth);
+ this.goDead();
+ return;
+ }
+ case 0x0d: {
+ const depth = c.u32();
+ const cond = this.pop();
+ const test = this.emit(`icmp ne i32 ${cond.v}, 0`);
+ const pending: string[] = [];
+ const target = this.branchEdge(depth, pending);
+ const cont = this.label("br");
+ this.add(` br i1 ${test}, label %${target}, label %${cont}`);
+ for (const line of pending) this.add(line);
+ this.startBlock(cont);
+ return;
+ }
+ case 0x0e: {
+ const n = c.u32();
+ const depths: number[] = [];
+ for (let i = 0; i < n; i++) depths.push(c.u32());
+ const def = c.u32();
+ const idx = this.pop();
+ const pending: string[] = [];
+ const edges = new Map();
+ const edgeOf = (d: number): string => {
+ let e = edges.get(d);
+ if (e === undefined) {
+ e = this.branchEdge(d, pending);
+ edges.set(d, e);
+ }
+ return e;
+ };
+ const cases = depths.map((d, i) => `i32 ${i}, label %${edgeOf(d)}`);
+ this.add(` switch i32 ${idx.v}, label %${edgeOf(def)} [${cases.join(" ")}]`);
+ for (const line of pending) this.add(line);
+ this.goDead();
+ return;
+ }
+ case 0x0f: // return
+ this.branch(this.frames.length - 1);
+ this.goDead();
+ return;
+ case 0x10:
+ this.call(c.u32());
+ return;
+ case 0x11: {
+ const type = c.u32();
+ const table = c.u32();
+ this.callIndirect(type, table);
+ return;
+ }
+ case 0x1a: {
+ // A dropped load must still fault when it is out of bounds: LLVM
+ // would delete an unused ordinary load.
+ const v = this.pop();
+ if (v.load !== undefined) {
+ const line = this.lines.lines[v.load]!;
+ this.lines.lines[v.load] = line.replace(" = load ", " = load volatile ");
+ }
+ return;
+ }
+ case 0x1b:
+ case 0x1c: {
+ if (op === 0x1c) {
+ const n = c.u32();
+ for (let i = 0; i < n; i++) c.valType();
+ }
+ const cond = this.pop();
+ const b = this.pop();
+ const a = this.pop();
+ const test = this.emit(`icmp ne i32 ${cond.v}, 0`);
+ const t = llType(a.t);
+ this.push(this.emit(`select i1 ${test}, ${t} ${a.v}, ${t} ${b.v}`), a.t);
+ return;
+ }
+ case 0x20: {
+ const i = c.u32();
+ const t = this.localType(i);
+ this.push(this.emit(`load ${llType(t)}, ptr %l${i}`), t);
+ return;
+ }
+ case 0x21: {
+ const i = c.u32();
+ const t = this.localType(i);
+ const v = this.popType(t, "local.set");
+ this.add(` store ${llType(t)} ${v.v}, ptr %l${i}`);
+ return;
+ }
+ case 0x22: {
+ const i = c.u32();
+ const t = this.localType(i);
+ const v = this.popType(t, "local.tee");
+ this.stack.push(v);
+ this.add(` store ${llType(t)} ${v.v}, ptr %l${i}`);
+ return;
+ }
+ case 0x23: {
+ const g = c.u32();
+ const t = this.m.globals[g]!.type;
+ const cell = this.loadPtrSlot(this.mt.globalSlot(g));
+ this.push(this.emit(`load ${llType(t)}, ptr ${cell}, align 8`), t);
+ return;
+ }
+ case 0x24: {
+ const g = c.u32();
+ const global = this.m.globals[g];
+ if (global === undefined) throw new WasmTranslateError(`unknown global ${g}`);
+ const v = this.popType(global.type, "global.set");
+ const cell = this.loadPtrSlot(this.mt.globalSlot(g));
+ if (v.t === VAL_EXTERNREF)
+ this.add(` call void @scr_wasm_global_set_ref(ptr ${cell}, ptr ${v.v})`);
+ else this.add(` store ${llType(v.t)} ${v.v}, ptr ${cell}, align 8`);
+ return;
+ }
+ case 0x25: {
+ const t = c.u32();
+ const i = this.pop();
+ this.push(
+ this.emit(`call ptr @scr_wasm_table_get(ptr %inst, i32 ${t}, i32 ${i.v})`),
+ this.m.tables[t]!.elemType,
+ );
+ return;
+ }
+ case 0x26: {
+ const t = c.u32();
+ const table = this.m.tables[t];
+ if (table === undefined) throw new WasmTranslateError(`unknown table ${t}`);
+ const v = this.popType(table.elemType, "table.set");
+ const i = this.popType(VAL_I32, "table.set");
+ this.add(` call void @scr_wasm_table_set(ptr %inst, i32 ${t}, i32 ${i.v}, ptr ${v.v})`);
+ return;
+ }
+ case 0x3f: {
+ this.memoryIndex(c);
+ const mem = this.loadPtrSlot(OFF_MEMORY);
+ const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${mem}, i64 8`);
+ const size = this.emit(`load i64, ptr ${sizePtr}, align 8`);
+ const pages = this.emit(`lshr i64 ${size}, 16`);
+ this.push(this.emit(`trunc i64 ${pages} to i32`), VAL_I32);
+ return;
+ }
+ case 0x40: {
+ this.memoryIndex(c);
+ const delta = this.pop();
+ this.push(this.emit(`call i32 @scr_wasm_memory_grow(ptr %inst, i32 ${delta.v})`), VAL_I32);
+ this.refreshMemory();
+ return;
+ }
+ case 0x41:
+ this.push(String(c.s32()), VAL_I32);
+ return;
+ case 0x42: {
+ const [lo, hi] = c.s64();
+ this.push(i64Lit(lo, hi), VAL_I64);
+ return;
+ }
+ case 0x43:
+ this.push(this.emit(`bitcast i32 ${c.u32le() | 0} to float`), VAL_F32);
+ return;
+ case 0x44: {
+ const lo = c.u32le();
+ const hi = c.u32le();
+ this.push(this.emit(`bitcast i64 ${i64Lit(lo, hi)} to double`), VAL_F64);
+ return;
+ }
+ case 0xd0:
+ c.valType();
+ this.push("null", c.bytes[c.pos - 1]!);
+ return;
+ case 0xd1: {
+ const a = this.pop();
+ const r = this.emit(`icmp eq ptr ${a.v}, null`);
+ this.push(this.emit(`zext i1 ${r} to i32`), VAL_I32);
+ return;
+ }
+ case 0xd2:
+ this.push(this.funcRef(c.u32()), VAL_FUNCREF);
+ return;
+ case 0xfc:
+ this.prefixed(c);
+ return;
+ default:
+ break;
+ }
+ if (op >= 0x28 && op <= 0x35) {
+ const offset = this.memargOffset(c);
+ const [vt, mt, bytes, ext] = LOAD_INFO[op]!;
+ const addr = this.popType(VAL_I32, "load address");
+ const p = this.address(addr.v, offset, bytes);
+ const line = this.lines.lines.length;
+ const loaded = this.emit(`load ${mt}, ptr ${p}, align 1`);
+ if (ext === "") this.stack.push({ v: loaded, t: vt, load: line });
+ else {
+ const extended = this.emit(`${ext} ${mt} ${loaded} to ${llType(vt)}`);
+ this.stack.push({ v: extended, t: vt, load: line });
+ }
+ return;
+ }
+ if (op >= 0x36 && op <= 0x3e) {
+ const offset = this.memargOffset(c);
+ const [vt, mt, bytes] = STORE_INFO[op]!;
+ const v = this.popType(vt, "store");
+ const addr = this.popType(VAL_I32, "store address");
+ const p = this.address(addr.v, offset, bytes);
+ let value = v.v;
+ if (mt === "i8" || mt === "i16" || (mt === "i32" && v.t === VAL_I64)) {
+ value = this.emit(`trunc ${llType(v.t)} ${v.v} to ${mt}`);
+ }
+ this.add(` store ${mt} ${value}, ptr ${p}, align 1`);
+ return;
+ }
+ this.numeric(op);
+ }
+
+ numeric(op: number): void {
+ const I32 = VAL_I32;
+ const I64 = VAL_I64;
+ const F32 = VAL_F32;
+ const F64 = VAL_F64;
+ switch (op) {
+ case 0x45:
+ return this.eqz("i32");
+ case 0x46:
+ return this.compare("eq", "i32", false);
+ case 0x47:
+ return this.compare("ne", "i32", false);
+ case 0x48:
+ return this.compare("slt", "i32", false);
+ case 0x49:
+ return this.compare("ult", "i32", false);
+ case 0x4a:
+ return this.compare("sgt", "i32", false);
+ case 0x4b:
+ return this.compare("ugt", "i32", false);
+ case 0x4c:
+ return this.compare("sle", "i32", false);
+ case 0x4d:
+ return this.compare("ule", "i32", false);
+ case 0x4e:
+ return this.compare("sge", "i32", false);
+ case 0x4f:
+ return this.compare("uge", "i32", false);
+ case 0x50:
+ return this.eqz("i64");
+ case 0x51:
+ return this.compare("eq", "i64", false);
+ case 0x52:
+ return this.compare("ne", "i64", false);
+ case 0x53:
+ return this.compare("slt", "i64", false);
+ case 0x54:
+ return this.compare("ult", "i64", false);
+ case 0x55:
+ return this.compare("sgt", "i64", false);
+ case 0x56:
+ return this.compare("ugt", "i64", false);
+ case 0x57:
+ return this.compare("sle", "i64", false);
+ case 0x58:
+ return this.compare("ule", "i64", false);
+ case 0x59:
+ return this.compare("sge", "i64", false);
+ case 0x5a:
+ return this.compare("uge", "i64", false);
+ case 0x5b:
+ return this.compare("oeq", "float", true);
+ case 0x5c:
+ return this.compare("une", "float", true);
+ case 0x5d:
+ return this.compare("olt", "float", true);
+ case 0x5e:
+ return this.compare("ogt", "float", true);
+ case 0x5f:
+ return this.compare("ole", "float", true);
+ case 0x60:
+ return this.compare("oge", "float", true);
+ case 0x61:
+ return this.compare("oeq", "double", true);
+ case 0x62:
+ return this.compare("une", "double", true);
+ case 0x63:
+ return this.compare("olt", "double", true);
+ case 0x64:
+ return this.compare("ogt", "double", true);
+ case 0x65:
+ return this.compare("ole", "double", true);
+ case 0x66:
+ return this.compare("oge", "double", true);
+ case 0x67:
+ return this.unaryIntrinsic("ctlz.i32", "i32", I32, ", i1 false");
+ case 0x68:
+ return this.unaryIntrinsic("cttz.i32", "i32", I32, ", i1 false");
+ case 0x69:
+ return this.unaryIntrinsic("ctpop.i32", "i32", I32);
+ case 0x6a:
+ return this.binary("add", "i32", I32);
+ case 0x6b:
+ return this.binary("sub", "i32", I32);
+ case 0x6c:
+ return this.binary("mul", "i32", I32);
+ case 0x6d:
+ case 0x6e:
+ case 0x6f:
+ case 0x70:
+ return this.divRem(op, "i32");
+ case 0x71:
+ return this.binary("and", "i32", I32);
+ case 0x72:
+ return this.binary("or", "i32", I32);
+ case 0x73:
+ return this.binary("xor", "i32", I32);
+ case 0x74:
+ return this.shift("shl", "i32", I32);
+ case 0x75:
+ return this.shift("ashr", "i32", I32);
+ case 0x76:
+ return this.shift("lshr", "i32", I32);
+ case 0x77:
+ return this.rotate("fshl", "i32", I32);
+ case 0x78:
+ return this.rotate("fshr", "i32", I32);
+ case 0x79:
+ return this.unaryIntrinsic("ctlz.i64", "i64", I64, ", i1 false");
+ case 0x7a:
+ return this.unaryIntrinsic("cttz.i64", "i64", I64, ", i1 false");
+ case 0x7b:
+ return this.unaryIntrinsic("ctpop.i64", "i64", I64);
+ case 0x7c:
+ return this.binary("add", "i64", I64);
+ case 0x7d:
+ return this.binary("sub", "i64", I64);
+ case 0x7e:
+ return this.binary("mul", "i64", I64);
+ case 0x7f:
+ case 0x80:
+ case 0x81:
+ case 0x82:
+ return this.divRem(op, "i64");
+ case 0x83:
+ return this.binary("and", "i64", I64);
+ case 0x84:
+ return this.binary("or", "i64", I64);
+ case 0x85:
+ return this.binary("xor", "i64", I64);
+ case 0x86:
+ return this.shift("shl", "i64", I64);
+ case 0x87:
+ return this.shift("ashr", "i64", I64);
+ case 0x88:
+ return this.shift("lshr", "i64", I64);
+ case 0x89:
+ return this.rotate("fshl", "i64", I64);
+ case 0x8a:
+ return this.rotate("fshr", "i64", I64);
+ case 0x8b:
+ return this.unaryIntrinsic("fabs.f32", "float", F32);
+ case 0x8c: {
+ const a = this.pop();
+ this.push(this.emit(`fneg float ${a.v}`), F32);
+ return;
+ }
+ case 0x8d:
+ return this.unaryIntrinsic("ceil.f32", "float", F32);
+ case 0x8e:
+ return this.unaryIntrinsic("floor.f32", "float", F32);
+ case 0x8f:
+ return this.unaryIntrinsic("trunc.f32", "float", F32);
+ case 0x90:
+ return this.unaryIntrinsic("roundeven.f32", "float", F32);
+ case 0x91:
+ return this.unaryIntrinsic("sqrt.f32", "float", F32);
+ case 0x92:
+ return this.binary("fadd", "float", F32);
+ case 0x93:
+ return this.binary("fsub", "float", F32);
+ case 0x94:
+ return this.binary("fmul", "float", F32);
+ case 0x95:
+ return this.binary("fdiv", "float", F32);
+ case 0x96:
+ return this.binaryIntrinsic("minimum.f32", "float", F32);
+ case 0x97:
+ return this.binaryIntrinsic("maximum.f32", "float", F32);
+ case 0x98:
+ return this.binaryIntrinsic("copysign.f32", "float", F32);
+ case 0x99:
+ return this.unaryIntrinsic("fabs.f64", "double", F64);
+ case 0x9a: {
+ const a = this.pop();
+ this.push(this.emit(`fneg double ${a.v}`), F64);
+ return;
+ }
+ case 0x9b:
+ return this.unaryIntrinsic("ceil.f64", "double", F64);
+ case 0x9c:
+ return this.unaryIntrinsic("floor.f64", "double", F64);
+ case 0x9d:
+ return this.unaryIntrinsic("trunc.f64", "double", F64);
+ case 0x9e:
+ return this.unaryIntrinsic("roundeven.f64", "double", F64);
+ case 0x9f:
+ return this.unaryIntrinsic("sqrt.f64", "double", F64);
+ case 0xa0:
+ return this.binary("fadd", "double", F64);
+ case 0xa1:
+ return this.binary("fsub", "double", F64);
+ case 0xa2:
+ return this.binary("fmul", "double", F64);
+ case 0xa3:
+ return this.binary("fdiv", "double", F64);
+ case 0xa4:
+ return this.binaryIntrinsic("minimum.f64", "double", F64);
+ case 0xa5:
+ return this.binaryIntrinsic("maximum.f64", "double", F64);
+ case 0xa6:
+ return this.binaryIntrinsic("copysign.f64", "double", F64);
+ case 0xa7:
+ return this.convert("trunc", "i64", "i32", I32);
+ case 0xa8:
+ case 0xa9:
+ case 0xaa:
+ case 0xab:
+ case 0xae:
+ case 0xaf:
+ case 0xb0:
+ case 0xb1:
+ return this.truncTrap(op);
+ case 0xac:
+ return this.convert("sext", "i32", "i64", I64);
+ case 0xad:
+ return this.convert("zext", "i32", "i64", I64);
+ case 0xb2:
+ return this.convert("sitofp", "i32", "float", F32);
+ case 0xb3:
+ return this.convert("uitofp", "i32", "float", F32);
+ case 0xb4:
+ return this.convert("sitofp", "i64", "float", F32);
+ case 0xb5:
+ return this.convert("uitofp", "i64", "float", F32);
+ case 0xb6:
+ return this.convert("fptrunc", "double", "float", F32);
+ case 0xb7:
+ return this.convert("sitofp", "i32", "double", F64);
+ case 0xb8:
+ return this.convert("uitofp", "i32", "double", F64);
+ case 0xb9:
+ return this.convert("sitofp", "i64", "double", F64);
+ case 0xba:
+ return this.convert("uitofp", "i64", "double", F64);
+ case 0xbb:
+ return this.convert("fpext", "float", "double", F64);
+ case 0xbc:
+ return this.convert("bitcast", "float", "i32", I32);
+ case 0xbd:
+ return this.convert("bitcast", "double", "i64", I64);
+ case 0xbe:
+ return this.convert("bitcast", "i32", "float", F32);
+ case 0xbf:
+ return this.convert("bitcast", "i64", "double", F64);
+ case 0xc0:
+ case 0xc1: {
+ const a = this.pop();
+ const n = op === 0xc0 ? "i8" : "i16";
+ const narrow = this.emit(`trunc i32 ${a.v} to ${n}`);
+ this.push(this.emit(`sext ${n} ${narrow} to i32`), I32);
+ return;
+ }
+ case 0xc2:
+ case 0xc3:
+ case 0xc4: {
+ const a = this.pop();
+ const n = op === 0xc2 ? "i8" : op === 0xc3 ? "i16" : "i32";
+ const narrow = this.emit(`trunc i64 ${a.v} to ${n}`);
+ this.push(this.emit(`sext ${n} ${narrow} to i64`), I64);
+ return;
+ }
+ default:
+ throw new WasmTranslateError(`opcode 0x${op.toString(16)} is not supported`);
+ }
+ }
+
+ prefixed(c: WasmCursor): void {
+ const sub = c.u32();
+ if (sub <= 7) {
+ // Saturating float-to-int conversions.
+ const a = this.pop();
+ const from = a.t === VAL_F32 ? "f32" : "f64";
+ const fromTy = a.t === VAL_F32 ? "float" : "double";
+ const to = sub < 4 ? "i32" : "i64";
+ const signed = (sub & 1) === 0;
+ this.push(
+ this.emit(
+ `call ${to} @llvm.${signed ? "fptosi" : "fptoui"}.sat.${to}.${from}(${fromTy} ${a.v})`,
+ ),
+ to === "i32" ? VAL_I32 : VAL_I64,
+ );
+ return;
+ }
+ switch (sub) {
+ case 8: {
+ // memory.init
+ const seg = c.u32();
+ this.memoryIndex(c);
+ const n = this.pop();
+ const s = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_memory_init(ptr %inst, i32 ${seg}, i32 ${d.v}, i32 ${s.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ case 9:
+ this.add(` call void @scr_wasm_data_drop(ptr %inst, i32 ${c.u32()})`);
+ return;
+ case 10: {
+ this.memoryIndex(c);
+ this.memoryIndex(c);
+ const n = this.pop();
+ const s = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_memory_copy(ptr %inst, i32 ${d.v}, i32 ${s.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ case 11: {
+ this.memoryIndex(c);
+ const n = this.pop();
+ const v = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_memory_fill(ptr %inst, i32 ${d.v}, i32 ${v.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ case 12: {
+ const seg = c.u32();
+ const t = c.u32();
+ const n = this.pop();
+ const s = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_table_init(ptr %inst, i32 ${t}, i32 ${seg}, i32 ${d.v}, i32 ${s.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ case 13:
+ this.add(` call void @scr_wasm_elem_drop(ptr %inst, i32 ${c.u32()})`);
+ return;
+ case 14: {
+ const dt = c.u32();
+ const st = c.u32();
+ const n = this.pop();
+ const s = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_table_copy(ptr %inst, i32 ${dt}, i32 ${st}, i32 ${d.v}, i32 ${s.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ case 15: {
+ const t = c.u32();
+ const n = this.pop();
+ const v = this.pop();
+ this.push(
+ this.emit(`call i32 @scr_wasm_table_grow(ptr %inst, i32 ${t}, ptr ${v.v}, i32 ${n.v})`),
+ VAL_I32,
+ );
+ return;
+ }
+ case 16: {
+ const t = c.u32();
+ const tab = this.loadPtrSlot(this.mt.tableSlot(t));
+ const sizePtr = this.emit(`getelementptr inbounds i8, ptr ${tab}, i64 8`);
+ this.push(this.emit(`load i32, ptr ${sizePtr}, align 8`), VAL_I32);
+ return;
+ }
+ case 17: {
+ const t = c.u32();
+ const n = this.pop();
+ const v = this.pop();
+ const d = this.pop();
+ this.add(
+ ` call void @scr_wasm_table_fill(ptr %inst, i32 ${t}, i32 ${d.v}, ptr ${v.v}, i32 ${n.v})`,
+ );
+ return;
+ }
+ default:
+ throw new WasmTranslateError(`opcode 0xfc ${sub} is not supported`);
+ }
+ }
+}
+
+/** The functions through which the stack can grow without bound, which
+ * check the stack pointer on entry: members of a cycle of direct calls
+ * (including self-recursion), and functions that call through a table or
+ * call an import (an indirect call can reach any function, and an import
+ * can call back into an export). Every other call chain is finite, so its
+ * depth is bounded by the module and the guard's reserve absorbs it. */
+export function stackCheckedFunctions(m: WasmModule): Set {
+ const n = m.funcTypes.length;
+ const callees: number[][] = [];
+ const checked = new Set();
+ for (let f = 0; f < n; f++) callees.push([]);
+ for (let i = 0; i < m.codes.length; i++) {
+ const f = m.importedFuncs + i;
+ const body = m.codes[i]!.body;
+ const c = new WasmCursor(m.bytes, body.start);
+ while (c.pos < body.end) {
+ const op = c.u8();
+ if (op === 0x10) {
+ const callee = c.u32();
+ if (callee < m.importedFuncs) checked.add(f);
+ else callees[f]!.push(callee);
+ } else if (op === 0x11) {
+ c.u32();
+ c.u32();
+ checked.add(f);
+ } else {
+ skipImmediates(c, op);
+ }
+ }
+ }
+ // Tarjan's strongly connected components, iteratively.
+ const index = new Int32Array(n).fill(-1);
+ const low = new Int32Array(n);
+ const onStack = new Uint8Array(n);
+ const stack: number[] = [];
+ let next = 0;
+ for (let root = m.importedFuncs; root < n; root++) {
+ if (index[root] !== -1) continue;
+ const workNode: number[] = [root];
+ const workEdge: number[] = [0];
+ index[root] = low[root] = next++;
+ stack.push(root);
+ onStack[root] = 1;
+ while (workNode.length > 0) {
+ const top = workNode.length - 1;
+ const v = workNode[top]!;
+ const edges = callees[v]!;
+ const e = workEdge[top]!;
+ if (e < edges.length) {
+ workEdge[top] = e + 1;
+ const w = edges[e]!;
+ if (index[w] === -1) {
+ index[w] = low[w] = next++;
+ stack.push(w);
+ onStack[w] = 1;
+ workNode.push(w);
+ workEdge.push(0);
+ } else if (onStack[w]) {
+ low[v] = Math.min(low[v]!, index[w]!);
+ }
+ continue;
+ }
+ workNode.pop();
+ workEdge.pop();
+ if (workNode.length > 0) {
+ const parent = workNode[workNode.length - 1]!;
+ low[parent] = Math.min(low[parent]!, low[v]!);
+ }
+ if (low[v] === index[v]) {
+ const component: number[] = [];
+ for (;;) {
+ const w = stack.pop()!;
+ onStack[w] = 0;
+ component.push(w);
+ if (w === v) break;
+ }
+ if (component.length > 1 || edges.includes(v)) for (const w of component) checked.add(w);
+ }
+ }
+ }
+ return checked;
+}
+
+/** A decimal double literal as LLVM's exact hexadecimal form. */
+function hexDouble(text: string): string {
+ const view = new DataView(new ArrayBuffer(8));
+ view.setFloat64(0, Number(text));
+ return `0x${hex8(view.getUint32(0))}${hex8(view.getUint32(4))}`.toUpperCase().replace("0X", "0x");
+}
+
+function hexOf(bytes: Uint8Array): string {
+ let s = "";
+ for (let i = 0; i < bytes.length; i++) s += bytes[i]!.toString(16).padStart(2, "0");
+ return s;
+}
+
+/** The symbol prefix of a module's translation. */
+export function wasmSymbolPrefix(sha256: Uint8Array): string {
+ return `scr_wasm_${hexOf(sha256).slice(0, 16)}`;
+}
+
+/** Translates a module that binary.ts validated within the translatable
+ * feature level. Throws WasmTranslateError for constructs outside it. */
+export function translateWasmModule(
+ bytes: Uint8Array,
+ sha256: Uint8Array,
+ opts: WasmTranslateOptions,
+): WasmTranslation {
+ const m = parseWasmModule(bytes);
+ for (const t of m.tables) llType(t.elemType);
+ const mt = new ModuleTranslator(m, wasmSymbolPrefix(sha256), opts);
+ mt.translate();
+ const descriptor = mt.descriptor(sha256);
+ const decls = RUNTIME_DECLS.slice();
+ // Initial-exec: executables and their static runtime define it, so the
+ // check is one thread-pointer-relative load, not a TLS lookup call.
+ if (opts.boundsChecks)
+ decls.push("@scr_wasm_stack_limit = external thread_local(initialexec) global i64");
+ return { decls, text: mt.text, descriptor };
+}
diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json
index 1fc595f5f..e84d5b8e2 100644
--- a/packages/compiler/surface-manifest.json
+++ b/packages/compiler/surface-manifest.json
@@ -20,7 +20,7 @@
"name": "package imports",
"status": "unsupported",
"code": "SC1010",
- "note": "relative imports (./file, ../dir/file), package.json-mediated project 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, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
+ "note": "relative imports (./file, ../dir/file), package.json-mediated project 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, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
},
{
"id": "diagnostic.sc1012",
@@ -823,6 +823,13 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
+ {
+ "id": "node-builtin.dns.promises",
+ "kind": "node-builtin",
+ "name": "dns/promises",
+ "status": "static",
+ "note": "recognized module (bare and node:-prefixed specifiers)"
+ },
{
"id": "node-builtin.events",
"kind": "node-builtin",
@@ -2289,6 +2296,13 @@
"status": "static",
"note": "recognized module (bare and node:-prefixed specifiers)"
},
+ {
+ "id": "node-builtin.stream.web",
+ "kind": "node-builtin",
+ "name": "stream/web",
+ "status": "static",
+ "note": "recognized module (bare and node:-prefixed specifiers)"
+ },
{
"id": "node-builtin.string_decoder",
"kind": "node-builtin",
@@ -2819,6 +2833,19 @@
"name": "util/types.isWeakSet",
"status": "static"
},
+ {
+ "id": "node-builtin.v8",
+ "kind": "node-builtin",
+ "name": "v8",
+ "status": "static",
+ "note": "recognized module (bare and node:-prefixed specifiers)"
+ },
+ {
+ "id": "node-builtin.v8.getHeapStatistics",
+ "kind": "node-builtin",
+ "name": "v8.getHeapStatistics",
+ "status": "static"
+ },
{
"id": "node-builtin.worker_threads",
"kind": "node-builtin",
@@ -4111,7 +4138,7 @@
"kind": "stdlib",
"name": "Date constructor",
"status": "static",
- "note": "zero arguments, or one milliseconds/date-string argument; values are the read-only TimeClip scalar slice"
+ "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.getDate",
@@ -4234,7 +4261,7 @@
"kind": "stdlib",
"name": "Date.parse",
"status": "static",
- "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.toDateString",
@@ -5686,9 +5713,8 @@
"id": "stdlib.webassembly",
"kind": "stdlib",
"name": "globalThis.WebAssembly",
- "status": "unsupported",
- "code": "SC2020",
- "note": "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)"
+ "status": "static",
+ "note": "compile, instantiate, validate, Module, Instance, instanceof, the error classes, exports, memories, tables, globals and Module transfer to workers, over the modules translated at build time; the rest refuses with SC2020 (the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations))"
},
{
"id": "stdlib.webcrypto.CryptoKey",
diff --git a/packages/compiler/test/class-method-mutations.test.ts b/packages/compiler/test/class-method-mutations.test.ts
index 2258bd54a..b508a3949 100644
--- a/packages/compiler/test/class-method-mutations.test.ts
+++ b/packages/compiler/test/class-method-mutations.test.ts
@@ -114,3 +114,24 @@ test("a computed key in an Object.assign source keeps checked dispatch", async (
.replace("LITERAL", "Object.assign(target, { [key]: value })");
expect(await callersOf(source, "%dyn.class.properties")).not.toEqual([]);
});
+
+const range = `
+class Range {
+ hi: number;
+ constructor(hi: number) { this.hi = hi; }
+ *[Symbol.iterator](): Generator { for (let i = 0; i < this.hi; i++) yield i; }
+}
+const r = new Range(3);
+console.log(r[Symbol.iterator]().next().value);
+`;
+
+test("a well-known symbol method call lowers to the class's method", async () => {
+ expect(await callersOf(range, "%Range.sym:iterator")).not.toEqual([]);
+});
+
+test("a reflection call with a well-known symbol key keeps checked dispatch", async () => {
+ const write = "Object.defineProperty(r, Symbol.iterator, { value: function* () {} });\n";
+ // The checked path reads the declared method through its value thunk.
+ const callers = await callersOf(`${range}${write}`, "%Range.sym:iterator");
+ expect(callers.filter((name) => !name.startsWith("%method.value:"))).toEqual([]);
+});
diff --git a/packages/compiler/test/native-callable-values.test.ts b/packages/compiler/test/native-callable-values.test.ts
index e64a9319e..6e65b0c99 100644
--- a/packages/compiler/test/native-callable-values.test.ts
+++ b/packages/compiler/test/native-callable-values.test.ts
@@ -102,19 +102,19 @@ test.each(["llvm"] as const)(
writeFileSync(
join(pkg, "index.js"),
`
-const inspect = (value) => eval(value);
+const inspect = (value) => process.on("__proto__", value);
function stopped(value) {
- eval(value);
+ process.on("__proto__", value);
return { done() { return "wrong"; } };
}
-try { console.log(stopped("0").done()); }
-catch (error) { console.log(String(error).includes("SC2009")); }
+try { console.log(stopped(() => {}).done()); }
+catch (error) { console.log(String(error).includes("SC2020")); }
const first = { inspect };
const second = { inspect };
console.log(typeof first.inspect, first.inspect === second.inspect);
for (const callback of [first.inspect, second.inspect]) {
- try { callback({}); }
- catch (error) { console.log(String(error).includes("SC2009")); }
+ try { callback(() => {}); }
+ catch (error) { console.log(String(error).includes("SC2020")); }
}
`,
);
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5779-sha512-lengths.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5779-sha512-lengths.ts.json
new file mode 100644
index 000000000..60582ec60
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5779-sha512-lengths.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5779-sha512-lengths.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json
new file mode 100644
index 000000000..d9ad97beb
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5817-constructor-function-instanceof.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5817-constructor-function-instanceof.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json
new file mode 100644
index 000000000..f22e1aaeb
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5818-constructor-function-emitter-defaults.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/5818-constructor-function-emitter-defaults.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6055-class-generator-iterables.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6055-class-generator-iterables.ts.json
new file mode 100644
index 000000000..661995e3f
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6055-class-generator-iterables.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6055-class-generator-iterables.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6056-class-symbol-method-calls.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6056-class-symbol-method-calls.ts.json
new file mode 100644
index 000000000..445389b5e
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6056-class-symbol-method-calls.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6056-class-symbol-method-calls.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json
index 5de83361a..36f02246c 100644
--- a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6126-punycode-dependency/main.cjs.json
@@ -12,7 +12,7 @@
"end": 376
},
"milestone": "M4",
- "hint": "relative imports (./file, ../dir/file), package.json-mediated project 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, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
+ "hint": "relative imports (./file, ../dir/file), package.json-mediated project 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, dns/promises, util, util/types, string_decoder, querystring, readline, events, stream, stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, cluster, tty, async_hooks, timers, timers/promises, diagnostics_channel, perf_hooks, module, v8, and console modules (bare or node:-prefixed) and node:test (node:-prefixed only, like in Node) are supported"
}
]
}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6230-date-parse-legacy.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6230-date-parse-legacy.ts.json
new file mode 100644
index 000000000..b7cad9619
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6230-date-parse-legacy.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6230-date-parse-legacy.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-date-local-fields.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-date-local-fields.ts.json
new file mode 100644
index 000000000..6f078d295
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6231-date-local-fields.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6231-date-local-fields.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-date-fields-js.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-date-fields-js.cjs.json
new file mode 100644
index 000000000..10f48c83a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6232-date-fields-js.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6232-date-fields-js.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-object-identity-dynamic.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-object-identity-dynamic.cjs.json
new file mode 100644
index 000000000..a5dd87184
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6233-object-identity-dynamic.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6233-object-identity-dynamic.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-object-prototype-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-object-prototype-values.cjs.json
new file mode 100644
index 000000000..bce305944
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6234-object-prototype-values.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6234-object-prototype-values.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-object-identity-typed.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-object-identity-typed.ts.json
new file mode 100644
index 000000000..c48f4164b
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6235-object-identity-typed.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6235-object-identity-typed.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-builtin-function-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-builtin-function-values.cjs.json
new file mode 100644
index 000000000..21b9f85c2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6236-builtin-function-values.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6236-builtin-function-values.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6403-error-own-name-order.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6403-error-own-name-order.ts.json
new file mode 100644
index 000000000..da39be0a0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6403-error-own-name-order.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6403-error-own-name-order.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6404-error-own-name-order-js.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6404-error-own-name-order-js.js.json
new file mode 100644
index 000000000..b19997ac3
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6404-error-own-name-order-js.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6404-error-own-name-order-js.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-json-mixed-scalar-tables/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-json-mixed-scalar-tables/main.ts.json
new file mode 100644
index 000000000..c7ec1ab56
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6410-json-mixed-scalar-tables/main.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6410-json-mixed-scalar-tables/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-union-array-tuple-destructuring.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-union-array-tuple-destructuring.ts.json
new file mode 100644
index 000000000..94d8b78e7
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6411-union-array-tuple-destructuring.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6411-union-array-tuple-destructuring.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6440-http-socket-path.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6440-http-socket-path.ts.json
new file mode 100644
index 000000000..63142a38a
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6440-http-socket-path.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6440-http-socket-path.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6441-http-request-options-record.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6441-http-request-options-record.ts.json
new file mode 100644
index 000000000..d596d3442
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6441-http-request-options-record.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6441-http-request-options-record.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6442-https-request-options-record.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6442-https-request-options-record.ts.json
new file mode 100644
index 000000000..ccc648aee
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6442-https-request-options-record.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6442-https-request-options-record.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6443-net-lookup-http-close-text.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6443-net-lookup-http-close-text.ts.json
new file mode 100644
index 000000000..cad9675b2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6443-net-lookup-http-close-text.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6443-net-lookup-http-close-text.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-builtin-base-class-values.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-builtin-base-class-values.ts.json
new file mode 100644
index 000000000..bdd8fcbc8
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-builtin-base-class-values.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6450-builtin-base-class-values.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-promise-race-never-entry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-promise-race-never-entry.ts.json
new file mode 100644
index 000000000..bb709e7f3
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6450-promise-race-never-entry.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6450-promise-race-never-entry.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-array-length-fill-spread.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-array-length-fill-spread.ts.json
new file mode 100644
index 000000000..0302374fc
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-array-length-fill-spread.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6451-array-length-fill-spread.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-builtin-base-class-expressions.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-builtin-base-class-expressions.cjs.json
new file mode 100644
index 000000000..34cde5811
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6451-builtin-base-class-expressions.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6451-builtin-base-class-expressions.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-constructor-function-emitter.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-constructor-function-emitter.cjs.json
new file mode 100644
index 000000000..ae5c0050e
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6453-constructor-function-emitter.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6453-constructor-function-emitter.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6454-constructor-function-stream-bases.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6454-constructor-function-stream-bases.cjs.json
new file mode 100644
index 000000000..bc48a7f35
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6454-constructor-function-stream-bases.cjs.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6454-constructor-function-stream-bases.cjs"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6463-node-readable-stream-producers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6463-node-readable-stream-producers.ts.json
new file mode 100644
index 000000000..e413951ae
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6463-node-readable-stream-producers.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6463-node-readable-stream-producers.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6464-node-writable-stream-producers.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6464-node-writable-stream-producers.ts.json
new file mode 100644
index 000000000..fd805a2af
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6464-node-writable-stream-producers.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6464-node-writable-stream-producers.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-node-stream-js-boundary/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-node-stream-js-boundary/main.ts.json
new file mode 100644
index 000000000..95278eab8
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-node-stream-js-boundary/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6465-node-stream-js-boundary/consume.js",
+ "/tests/corpus/6465-node-stream-js-boundary/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-optional-members-over-index-signature.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-optional-members-over-index-signature.ts.json
new file mode 100644
index 000000000..91a157207
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6465-optional-members-over-index-signature.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6465-optional-members-over-index-signature.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-never-assertion-argument.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-never-assertion-argument.ts.json
new file mode 100644
index 000000000..17f25aa22
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-never-assertion-argument.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6466-never-assertion-argument.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-node-stdio-stream-surface.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-node-stdio-stream-surface.ts.json
new file mode 100644
index 000000000..8ebcddd22
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6466-node-stdio-stream-surface.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6466-node-stdio-stream-surface.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-fromentries-record-assertion.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-fromentries-record-assertion.ts.json
new file mode 100644
index 000000000..21ca66953
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-fromentries-record-assertion.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6467-fromentries-record-assertion.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-node-stream-untyped-sources/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-node-stream-untyped-sources/main.ts.json
new file mode 100644
index 000000000..dc843c047
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6467-node-stream-untyped-sources/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6467-node-stream-untyped-sources/sources.js",
+ "/tests/corpus/6467-node-stream-untyped-sources/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-node-stdio-writable-slot.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-node-stdio-writable-slot.ts.json
new file mode 100644
index 000000000..baf428dee
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6468-node-stdio-writable-slot.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6468-node-stdio-writable-slot.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts.json
new file mode 100644
index 000000000..60b1360b0
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts.json
@@ -0,0 +1,7 @@
+{
+ "order": [
+ "/tests/corpus/6469-node-stdio-writable-js-pipe/pipe.js",
+ "/tests/corpus/6469-node-stdio-writable-js-pipe/main.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-promise-reject-async-return.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-promise-reject-async-return.ts.json
new file mode 100644
index 000000000..dda9bd1e8
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-promise-reject-async-return.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6470-promise-reject-async-return.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-stream-transform-any-data-callback.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-stream-transform-any-data-callback.ts.json
new file mode 100644
index 000000000..3d4a39486
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6470-stream-transform-any-data-callback.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6470-stream-transform-any-data-callback.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-stream-data-buffer-or-string.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-stream-data-buffer-or-string.ts.json
new file mode 100644
index 000000000..e8ff1e084
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-stream-data-buffer-or-string.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6471-stream-data-buffer-or-string.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-union-literal-results-into-record.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-union-literal-results-into-record.ts.json
new file mode 100644
index 000000000..1a02071a1
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6471-union-literal-results-into-record.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6471-union-literal-results-into-record.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-asserted-member-read.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-asserted-member-read.ts.json
new file mode 100644
index 000000000..dfb88018b
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-asserted-member-read.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6472-asserted-member-read.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-child-data-unknown-chunk.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-child-data-unknown-chunk.ts.json
new file mode 100644
index 000000000..3c7183bb6
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6472-child-data-unknown-chunk.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6472-child-data-unknown-chunk.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-unknown-spread-into-index-record.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-unknown-spread-into-index-record.ts.json
new file mode 100644
index 000000000..abf47b7db
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6473-unknown-spread-into-index-record.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6473-unknown-spread-into-index-record.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-index-record-spread-literals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-index-record-spread-literals.ts.json
new file mode 100644
index 000000000..8991f227d
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6474-index-record-spread-literals.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6474-index-record-spread-literals.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-dynamic-spread-literals.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-dynamic-spread-literals.ts.json
new file mode 100644
index 000000000..29207c929
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6475-dynamic-spread-literals.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6475-dynamic-spread-literals.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-boolean-callbacks.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-boolean-callbacks.ts.json
new file mode 100644
index 000000000..3279178b8
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-boolean-callbacks.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6480-boolean-callbacks.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-generic-callback-extended-results.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-generic-callback-extended-results.ts.json
new file mode 100644
index 000000000..d3b5c41b7
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6480-generic-callback-extended-results.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6480-generic-callback-extended-results.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-enum-runtime-keys.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-enum-runtime-keys.ts.json
new file mode 100644
index 000000000..7fc1885aa
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6481-enum-runtime-keys.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/6481-enum-runtime-keys.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6482-promise-all-tuples.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6482-promise-all-tuples.ts.json
new file mode 100644
index 000000000..5ceea99a5
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6482-promise-all-tuples.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "