Merge main

This commit is contained in:
Chris Tate
2026-10-20 12:55:55 -05:00
781 changed files with 38975 additions and 1652 deletions
+47 -1
View File
@@ -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 }}"
+4 -1
View File
@@ -134,7 +134,7 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
<dd>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 <code>x</code>, <code>Point</code>, or <code>Point.x</code>, show only the matching units. See <a href="/docs/coverage#dynamic-tier">Dynamic tier</a>.</dd>
<dt><code>--wasm &lt;file.wasm&gt;</code></dt>
<dd>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 <code>.wasm</code> files shipped by packages that use WebAssembly; a <code>scriptc.wasm</code> array in <code>package.json</code> 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.</dd>
<dd>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 <code>.wasm</code> files shipped by packages that use WebAssembly; a <code>scriptc.wasm</code> array in <code>package.json</code> 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 <code>scriptc.wasm</code> array but not this flag. Repeatable.</dd>
<dt><code>--sanitize</code></dt>
<dd>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 <code>--emit=asm</code> or <code>--emit=obj</code>.</dd>
@@ -178,6 +178,9 @@ Verifies and loads the installed precompiled runtime objects and vendored librar
<dt><code>SCRIPTC_RUNTIME_PACK</code></dt>
<dd>A runtime pack directory to use instead of an installed <code>@scriptc/runtime-&lt;target&gt;</code> package.</dd>
<dt><code>SCRIPTC_CHECKER</code></dt>
<dd>The TypeScript 7 API server that checks the program: <code>tsc-ts</code> (the default), which scriptc includes, or <code>tsgo</code>, from the <code>typescript</code> package scriptc also includes. Both give the same diagnostics and generated code. On Windows the default is <code>tsgo</code>, because tsc-ts's API server does not yet accept the named pipe scriptc connects over there.</dd>
</dl>
```console
-1
View File
@@ -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
+10 -6
View File
@@ -52,10 +52,14 @@ Type-only imports follow Node.js's type stripping. `import type` and `export typ
scriptc compiles a package automatically when it:
- ships its own type declarations, and
- ships readable, unminified JavaScript.
- ships its own type declarations, or is typed by its DefinitelyTyped `@types/` package, or is a dependency of another package that has no declarations at all, and
- ships readable JavaScript: unminified, or minified by a bundler whose helpers keep their names.
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself. A package whose declarations come from a separate `@types/` package doesn't ship its own; its diagnostic names the package and the `--npm-static` option that compiles it anyway.
The declarations check your program; the package's JavaScript compiles with the types scriptc infers from it. A program that doesn't typecheck against the inferred types, for example because an `@types/` package describes another release, fails the build at that use.
A package that doesn't meet these requirements, or that uses something scriptc can't compile, gets a package-specific diagnostic that explains why. The usual fixes are to switch to a version or alternative package that compiles, or to replace the call with code you write yourself.
`graceful-fs` is the exception: it compiles as `node:fs` instead of from its JavaScript, with one [documented difference](/docs/limitations#graceful-fs).
<span id="value-conversion" />
<span id="static-package-compilation-experimental" />
@@ -63,17 +67,17 @@ A package that doesn't meet these requirements, or that uses something scriptc c
## Explicit package selection
Some packages rely on separately installed declarations, such as an `@types/` package, or on types inferred from their JavaScript. To try compiling one of them, name it with `--npm-static`:
Some packages your program imports directly ship no declarations, so their types can only be inferred from their JavaScript. To try compiling one of them, name it with `--npm-static`:
```console
$ scriptc build app.ts --npm-static=three -o app >/dev/null
```
Naming a package skips the declaration requirement above, but the package still has to compile. The option selects only the packages it names: a dependency of a named package that doesn't ship its own declarations is still refused. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
Naming a package skips the declaration requirement above, but the package still has to compile. Add a trailing `...` to select a package together with every package it depends on, for example `--npm-static=fs-extra...`. `--npm-static=auto` uses the default automatic selection. The option accepts a comma-separated list and can be repeated.
## CommonJS packages
CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes do, and uses the other modules only inside functions that its top-level code can't call. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms.
CommonJS packages compile with Node.js's module semantics. Each `require()` evaluates its module at the call, once, and later calls return the cached exports. A package can assign `module.exports` and run code before it requires its dependencies. If that code reads a `const` or `let` require binding before its `require` runs, the read throws the same `ReferenceError` as Node.js. A `var` binding that earlier code can reach is reported at build time. A `require()` of a JSON file, such as a package's own `package.json`, returns one object parsed at build time, shared by every module that requires it. A require cycle compiles when each of its modules assigns its exports before requiring anything, as semver's classes and esbuild's per-file output do, and uses the other modules only inside functions that its top-level code can't call. Bundles built by esbuild compile with their bundled CommonJS modules and lazily initialized ES modules, each evaluated once, at its first use, as under Node.js. Other require cycles are reported at build time. The [limitations](/docs/limitations) page lists the supported `require` forms.
## Unsupported code inside a package
+10
View File
@@ -102,6 +102,8 @@ title: "Limitations"
<include>../limitations/builtin-apis/process-memory.mdx</include>
<include>../limitations/builtin-apis/v8.mdx</include>
<include>../limitations/builtin-apis/performance-entries.mdx</include>
<include>../limitations/builtin-apis/builtin-module-lookup.mdx</include>
@@ -212,6 +214,10 @@ title: "Limitations"
<include>../limitations/console-and-process-streams.mdx</include>
### graceful-fs
<include>../limitations/graceful-fs.mdx</include>
### DNS lookup errors
<include>../limitations/dns-lookup-errors.mdx</include>
@@ -224,6 +230,10 @@ title: "Limitations"
<include>../limitations/udp-socket-options.mdx</include>
### Unix domain sockets and request options
<include>../limitations/unix-domain-sockets.mdx</include>
### Process arguments and errors
<include>../limitations/process-arguments-and-errors.mdx</include>
@@ -1,5 +1,5 @@
- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported.
Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
- **Date:** Date unions, spread constructor arguments, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale formatters are unsupported.
Zero-argument, single number/string, and local-time year/month field constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, `toUTCString`, `toJSON` (a `string | null`: null for an invalid date, as in Node), `toString`, `toDateString`, `toTimeString`, `String(date)` and template literal conversion, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile.
JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization.
A TypeScript Date converted to `unknown` becomes a Date with the same time value; each conversion creates a new Date, so two `unknown` values converted from one TypeScript Date are not `===`.
See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
See [Dates and timezones](#dates-and-timezones) for timezone data.
@@ -1,2 +1,4 @@
- **Globals:** Stored `globalThis` references support identity and probes for absent capabilities.
Most builtin members require direct global access; access through a stored global object can raise `SC2020`.
`Temporal` is not defined, as in Node.js 24: `typeof Temporal` is `"undefined"`, `globalThis.Temporal` is `undefined`, and any other use throws `ReferenceError: Temporal is not defined`.
Programs that name it type-check against TypeScript's `Temporal` declarations.
@@ -1,2 +1,4 @@
- **Performance entries:** `performance.mark()`, `measure()`, and the `getEntries` methods return entries with Node's fields, `toJSON()`, inspection, and identity.
`Object.keys`, `for...in`, object spread, and `hasOwnProperty` on an entry are unsupported, and an entry stored in an `unknown` slot prints as a plain object.
- **Event-loop metrics:** `performance.eventLoopUtilization()` counts the main thread's CPU time since the event loop started as active time, so blocking system calls count as idle where Node counts them as active.
`monitorEventLoopDelay()` supports `enable()`, `disable()`, `reset()`, `percentile()`, and the `min`, `max`, `mean`, `stddev`, `count`, and `exceeds` readings; the BigInt readings, `percentiles`, and non-literal option objects are unsupported.
@@ -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.
@@ -1,4 +1,10 @@
- **Streams and readline:** Stream chunks are Buffers or strings.
`NodeJS.ReadableStream` and `NodeJS.WritableStream`, and `stream.Readable` and `stream.Writable` under `@types/node`, accept every stream: when a program passes file, zlib, or `node:stream` streams through them, child-process pipes and `process.stdout` and `process.stderr` join those streams as `Readable` and `Writable` instances.
`readable.pipe(process.stdout)` and `readable.pipe(process.stderr)` compile and, as in Node, do not end the process stream.
When an untyped value reaches `NodeJS.ReadableStream` or `NodeJS.WritableStream`, those two interfaces hold any value, so JavaScript stream objects (constructor-function streams, plain objects with `on` and `pipe`) pass through TypeScript functions typed with them and come back to JavaScript as the same objects.
Member calls on such a value use the native stream when it is one and call the object's own members otherwise; `readable.pipe()` into a JavaScript destination follows Node's pipe protocol.
A `pipeline` stage and a `for await` source that arrive as untyped values must be native streams at run time; other values throw a not-supported error.
Native streams expose no `emit` to JavaScript, so a JavaScript stream whose own `pipe()` emits on its destination (the legacy `Stream` pipe) cannot pipe into one.
`Readable.from` accepts arrays, Sets, Map and Set iterators, generators, async generators, and class iterables and async iterables whose values are strings or Buffers, and in JavaScript any iterable or async iterable of them; a stream that mixes strings and Buffers is unsupported.
In JavaScript, a sync iterator value with a `then` method that is not a promise, and an object whose `[Symbol.asyncIterator]` or `[Symbol.iterator]` method is a generator method, are unsupported.
Object-mode streams of other values (including `Readable.from` over a `Map`, whose values are entry arrays) are unsupported.
@@ -11,6 +17,7 @@
`stream.compose` accepts two or more streams, or one `Duplex`; functions, iterables, and web streams as stages are unsupported, and the `ERR_INVALID_ARG_VALUE` message for a stream that is no longer readable or writable names the stream's class instead of printing the stream.
`pipeline` from `node:stream/promises` accepts a trailing `{ signal, end }` object, and `finished` accepts `readable`, `writable`, `error`, `cleanup`, and `signal` options, with boolean literals for the flags.
Stream constructors accept a `signal` option in an options object literal, and `stream.addAbortSignal` accepts any stream.
A `transform` or `flush` option callback whose completion callback types `data` as `any`, as @types/node's `TransformCallback` does, may pass a Buffer, a string, or nothing; any other value throws a TypeError where the callback is called, instead of Node's `ERR_INVALID_ARG_TYPE` stream error.
`node:readline` and `node:readline/promises` (also reached as `readline.promises`) interfaces, from `createInterface(options)` or `createInterface(input, output, completer, terminal)`, read `process.stdin` or a readable stream: `question`, `prompt`, `write` (with a key object literal of `name`, `ctrl`, `meta`, and `shift`), `pause`, `resume`, `'line'`/`'close'`/`'pause'`/`'resume'`/`'error'`/`'history'`/`'SIGINT'`/`'SIGTSTP'`/`'SIGCONT'` listeners, and `for await` compile.
The `history`, `historySize`, `removeHistoryDuplicates`, `completer`, `tabSize`, and `escapeCodeTimeout` options are validated as in Node.
With `terminal: true`, or with no `terminal` option and `process.stdout` as the output when it is a TTY, the interface runs Node's terminal mode: the editing keys, the kill ring, undo and redo, history recording and recall, Tab completion through the completer, Ctrl+C, Ctrl+D, and Ctrl+Z, `TERM=dumb`, and the same escape sequences written to the output.
@@ -26,3 +33,4 @@
The `readline/promises` `Readline` class writes to the same outputs.
`readline.emitKeypressEvents` emits `'keypress'` events on `process.stdin` or a readable stream, shared with terminal interfaces on that stream as in Node; a listener's first parameter must admit `undefined`, which it receives for escape sequences.
Chained listener registration is unsupported.
`node:stream/web` exports the same `ReadableStream` class as the global; its other classes are unsupported.
@@ -0,0 +1,5 @@
- **V8 heap statistics:** `node:v8` can be imported.
`v8.getHeapStatistics()` returns Node's fields in Node's order, computed from the runtime allocator like `process.memoryUsage()`: `total_heap_size` and `total_physical_size` are the bytes the heap holds, `used_heap_size` the bytes allocated, `heap_size_limit` the machine's memory (the heap has no fixed ceiling), and `total_available_size` the limit minus the used bytes.
Fields that describe V8 itself (`total_heap_size_executable`, `malloced_memory`, `peak_malloced_memory`, the global handle sizes, `external_memory`, and `does_zap_garbage`) are `0`, and `number_of_native_contexts` is `1`.
The values are approximate and are not V8 heap figures.
The other `node:v8` members observe the V8 engine, such as heap snapshots, profiles, flags, and serialization, and throw `SC2020` where they are used.
@@ -5,6 +5,10 @@ Some builtins can be stored and passed as functions:
- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`.
- `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`.
In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles.
Calls through these values bypass `node:test` mock timers.
Other builtin globals stored in JavaScript remain opaque identity values.
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere.
`util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
@@ -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.
@@ -12,3 +12,7 @@ Message listeners may return `void` or `Promise<void>`.
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.
@@ -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.
@@ -1,7 +1,7 @@
String constructors and `Date.parse` accept ECMAScript date-only forms, date-times with an explicit `Z` or numeric offset, and GMT certificate-validity strings from the supported X509 APIs.
Other V8-specific forms and offset-less local date-times produce an Invalid Date or `NaN`; `SC6005` reports string literals in those forms.
String constructors and `Date.parse` use V8's date-string grammar: ECMAScript date-time strings and the legacy forms Node.js accepts, such as RFC 1123 HTTP dates (`Mon, 01 Jan 2024 00:00:00 GMT`), `Date#toString` output, and `Jan 2, 2024`.
Strings without a timezone, and `new Date(year, monthIndex, ...)` fields, are local time.
Local calendar getters use the operating system's timezone database.
Local calendar getters and local-time conversions use the operating system's timezone database.
Historical results can differ from Node.js when timezone data differs.
For valid extreme years outside a platform calendar API's range, scriptc uses a calendar-equivalent year in the 400-year Gregorian cycle to obtain the timezone rule.
@@ -1,3 +1,4 @@
After a failed `dns.lookup` or `dns.lookupService`, Node calls the callback with the error alone, so the address and family parameters are `undefined`.
A callback whose address parameter is typed `string` (as Node's own type declarations type it) receives `""` and `NaN` there, or an empty array for `all: true`, because a typed string or number cannot hold `undefined`.
An untyped callback receives the error alone, as in Node.
The promise forms in `node:dns/promises` (also reached as `dns.promises`) reject with the error and are unaffected.
+4
View File
@@ -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`.
@@ -3,3 +3,9 @@
Independent concrete uses have no specialization-count limit; recursively expanding demands and type layouts produce diagnostics.
Other generic calls compile when the target resolves statically.
Generic function values require a concrete signature at use and a binding that is never reassigned.
- **Variadic wrappers:** A generic higher-order wrapper is specialized to each wrapped function's exact signature.
`Parameters<F>`, `ReturnType<F>`, and rest parameters typed as fixed tuples become that signature's parameter list.
A function expression with no parameters that reads `arguments` compiles only when its value is asserted to, or contextually typed as, one fixed-arity signature; `arguments` is then that signature's tuple.
Forwarding it with `fn.apply(this, arguments)` passes `this` through.
Reading `fn.name` on a wrapped function requires its name to be known where the wrapper is called, such as a function declaration or a `const`-bound function.
`Parameters<F>` over a signature with optional or array-typed rest parameters is unsupported, because that tuple has no fixed length.
@@ -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.
@@ -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`.
@@ -1,7 +1,31 @@
Compiled programs can't load WebAssembly modules yet.
Code that only mentions the `WebAssembly` types type-checks, including packages whose declarations name them, and `typeof WebAssembly` answers as it does in Node.js.
Every use of the API itself is rejected: `WebAssembly.compile`, `instantiate` and `validate`, `new WebAssembly.Module` and the other constructors, `instanceof` checks, and reads of `globalThis.WebAssembly`.
TypeScript sources fail to build with SC2020 at each use, and JavaScript sources throw that diagnostic when the statement runs.
scriptc translates WebAssembly modules to native code when it builds the program; there is no interpreter or JIT in the executable.
A build translates every module whose bytes it can see: files at paths the compiler resolves (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`, `new URL("./x.wasm", import.meta.url)`, `path.join(__dirname, ...)`), including `.so` WebAssembly shared libraries such as quickjs-wasi's extensions, modules inlined as base64 or byte-array literals, `.wasm` files shipped by packages whose code uses WebAssembly, and files listed in a `scriptc.wasm` array in `package.json`.
At run time `WebAssembly.compile`, `instantiate`, `validate` and `new WebAssembly.Module` find the translated module by the SHA-256 of the bytes they are given.
Bytes the build did not see, including bytes produced at run time, reject with a `CompileError` that says the module was not translated, and `WebAssembly.validate` answers `false` for them.
`scriptc coverage` lists the modules a build finds, with their size, whether they validate, and the WebAssembly proposals they use.
`scriptc coverage` lists the modules a program would load, with their size, whether they validate, and the WebAssembly proposals they use.
It finds modules at paths the compiler resolves, modules inlined as base64 literals, `.wasm` files shipped by packages that use WebAssembly, and files named with [`--wasm`](/docs/cli) or a `scriptc.wasm` array in `package.json`.
Translated modules can use the WebAssembly 2.0 feature set: the MVP plus sign-extension operators, non-trapping float-to-int conversions, bulk memory, multi-value, mutable globals, reference types, and extended constant expressions.
Modules that use SIMD, threads, exception handling, tail calls, typed function references or GC, memory64, or multiple memories are not translated, so loading them rejects with the `CompileError` above; packages that fall back to a scalar build when that happens, such as undici and es-module-lexer, take their fallback.
Supported: `WebAssembly.compile`, `instantiate` (with bytes or a `Module`), `validate`, `new WebAssembly.Module`, `new WebAssembly.Instance`, `new WebAssembly.Memory`, `new WebAssembly.Table`, `new WebAssembly.Global`, `WebAssembly.Module.imports`, `exports` and `customSections`, and `instanceof` for every class; the error classes `CompileError`, `LinkError` and `RuntimeError`, constructed with or without `new`; `globalThis.WebAssembly` and its `compile`, `instantiate` and `validate` functions as values.
Instances expose exported functions, memories (`buffer`, `grow`), tables (`get`, `set`, `grow`, `length`) and globals (`value`, `valueOf`).
Imports can be functions, numbers or BigInts for immutable globals, and memories, tables and globals that the program constructs or other instances export; their limits, types and mutability are checked with Node.js's `LinkError` messages.
Function references from a table or global reach JavaScript as callable functions, and exported functions can be stored in tables and globals, which is what dynamic linkers such as quickjs-wasi's extension loader do.
Traps throw `WebAssembly.RuntimeError` with Node.js's messages, unbounded recursion throws `RangeError`, and an exception thrown by an imported function propagates through the WebAssembly frames unchanged.
A `WebAssembly.Module` can be sent to a worker through `workerData` or `postMessage`.
Not supported yet: the `Tag` and `Exception` constructors, shared memories (`new WebAssembly.Memory({ shared: true, ... })` throws a `TypeError`), `compileStreaming` and `instantiateStreaming`, and the classes used as values other than with `new`, `instanceof` and `WebAssembly.Module`'s static functions.
TypeScript sources fail to build with SC2020 at each such use, and JavaScript sources throw that diagnostic when the statement runs.
Programs built for `wasm32-wasi` can't use the WebAssembly API.
Differences from Node.js:
- Growing a memory, from JavaScript or inside the module, does not detach the previous `ArrayBuffer`.
The memory never moves, so views created before the growth stay valid but keep their old length; code that reads `memory.buffer` again after growing, as WebAssembly glue code does, behaves as in Node.js.
- `valueOf()` answers a `WebAssembly.Global`'s value only when the receiver's static type is `WebAssembly.Global`; through an untyped value it answers the object, and converting a `Global` to a primitive uses the object rather than its value.
- Instances are never freed, together with their memories, imports and exports.
- A load whose result the module never uses may be removed instead of trapping when its address is out of bounds.
Loads whose result is dropped explicitly still trap.
- NaN payloads may differ, for example when a signaling NaN passes through arithmetic or a JavaScript number.
- `WebAssembly.compile` and `instantiate` with bytes settle after the current microtasks, as in Node.js, but can settle after `setImmediate` callbacks that Node.js would run later.
- The executable reads `.wasm` files at run time from the paths they had when it was built.
+65 -65
View File
@@ -239,9 +239,9 @@
"apiEntries": 51,
"static": {
"supported": 0,
"partial": 0,
"partial": 10,
"refused": 0,
"not-implemented": 51,
"not-implemented": 41,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -329,8 +329,8 @@
"apiEntries": 92,
"static": {
"supported": 1,
"partial": 15,
"refused": 1,
"partial": 16,
"refused": 0,
"not-implemented": 75,
"by-design": 0,
"unreviewed": 0,
@@ -539,9 +539,9 @@
"apiEntries": 131,
"static": {
"supported": 0,
"partial": 19,
"partial": 25,
"refused": 0,
"not-implemented": 112,
"not-implemented": 106,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -809,10 +809,10 @@
"apiEntries": 68,
"static": {
"supported": 0,
"partial": 3,
"partial": 4,
"refused": 0,
"not-implemented": 0,
"by-design": 65,
"by-design": 64,
"unreviewed": 0,
"not-applicable": 0
}
@@ -869,9 +869,9 @@
"apiEntries": 89,
"static": {
"supported": 0,
"partial": 7,
"partial": 8,
"refused": 1,
"not-implemented": 81,
"not-implemented": 80,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -16626,7 +16626,7 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"status": "partial",
"detail": "Derived from the exact API rows in this chapter.",
"verification": "derived"
}
@@ -16718,9 +16718,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -16741,9 +16741,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17132,9 +17132,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17201,9 +17201,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17316,9 +17316,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17339,9 +17339,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17362,9 +17362,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17730,9 +17730,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -17753,9 +17753,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -36590,9 +36590,9 @@
"inherited": true
},
"static": {
"status": "refused",
"detail": "Explicitly rejected by a named compiler diagnostic.",
"verification": "explicit-refusal"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -54688,9 +54688,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -54803,9 +54803,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -55286,9 +55286,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -55309,9 +55309,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -55677,9 +55677,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -55815,9 +55815,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
@@ -75973,9 +75973,9 @@
"anchor": "v8getheapstatistics",
"nodeStability": null,
"static": {
"status": "by-design",
"detail": "Intentionally outside the static native execution model.",
"verification": "architectural-policy"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
@@ -81011,9 +81011,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented static async-iteration subset.",
"verification": "test-backed"
}
},
{
@@ -3,20 +3,20 @@
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"summary": {
"taskCount": 3453,
"tierItemCount": 3453,
"taskCount": 3454,
"tierItemCount": 3454,
"tiers": {
"static": {
"audit-partial": 1179,
"verify-gap": 2146,
"replace-refusal": 41,
"audit-partial": 1197,
"verify-gap": 2130,
"replace-refusal": 40,
"classify": 2,
"implement": 85
}
},
"priorities": {
"high": 2287,
"normal": 694,
"normal": 695,
"low": 472
}
},
@@ -12300,12 +12300,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dnsPromises.lookup",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
}
}
},
@@ -12325,12 +12328,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dnsPromises.lookupService",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
}
}
},
@@ -12750,12 +12756,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dnsPromises.getDefaultResultOrder",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
}
}
},
@@ -12825,12 +12834,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.lookup",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -12950,12 +12963,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.getServers",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -12975,12 +12992,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.lookup",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -13000,12 +13021,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.lookupService",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -13400,12 +13425,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.setDefaultResultOrder",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -13425,12 +13454,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:dns",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:dns.dns.getDefaultResultOrder",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
}
}
},
@@ -34202,12 +34235,25 @@
"priority": "high",
"tiers": {
"static": {
"status": "refused",
"action": "replace-refusal",
"verification": "explicit-refusal",
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:unsupported:stdlib.webassembly",
"tests": []
"source": "surface-manifest:stdlib.webassembly",
"tests": [
"tests/corpus/webassembly-api.mjs",
"tests/corpus/webassembly-typed.ts",
"tests/corpus/webassembly-worker.ts",
"tests/corpus/webassembly-spec-int/main.mjs",
"tests/corpus/webassembly-spec-float/main.mjs",
"tests/corpus/webassembly-spec-control/main.mjs",
"tests/corpus/webassembly-spec-memory/main.mjs",
"tests/corpus/webassembly-spec-table/main.mjs",
"tests/corpus/webassembly-spec-imports/main.mjs",
"tests/corpus/webassembly-jsapi.mjs",
"tests/corpus/webassembly-wpt-jsapi.mjs",
"tests/corpus/webassembly-dylink/main.mjs"
]
}
}
},
@@ -54231,12 +54277,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:perf_hooks.histogram.percentile",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -54356,12 +54405,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:perf_hooks.histogram.reset",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -54881,12 +54933,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:perf_hooks.histogram.disable",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -54906,12 +54961,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:perf_hooks.histogram.enable",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -55306,12 +55364,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.perf_hooks.monitorEventLoopDelay",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -55466,12 +55527,15 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:perf_hooks",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:perf_hooks.performance.eventLoopUtilization",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
}
}
},
@@ -77813,6 +77877,29 @@
}
}
},
{
"id": "v8:cefde4a8297c",
"chapter": "v8",
"label": "v8.getHeapStatistics()",
"signature": "`v8.getHeapStatistics()`",
"apiSymbol": "v8.getHeapStatistics",
"docsUrl": "https://nodejs.org/docs/v24.22.0/api/v8.html#v8getheapstatistics",
"nodeStability": null,
"priority": "normal",
"tiers": {
"static": {
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.v8.getHeapStatistics",
"tests": [
"tests/corpus/v8-heap-statistics.ts",
"tests/corpus/v8-require-optional.cjs"
]
}
}
},
{
"id": "webcrypto:22408f834c48",
"chapter": "webcrypto",
@@ -81965,12 +82052,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:webstreams",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:webstreams.ReadableStream",
"tests": [
"tests/corpus/stream-web-readable.ts",
"tests/corpus/stream-web-require.cjs",
"tests/corpus/2862-web-readable-async-iteration.ts"
]
}
}
},
@@ -251,9 +251,9 @@
"apiEntries": 51,
"static": {
"supported": 0,
"partial": 0,
"partial": 10,
"refused": 0,
"not-implemented": 51,
"not-implemented": 41,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -341,8 +341,8 @@
"apiEntries": 92,
"static": {
"supported": 1,
"partial": 15,
"refused": 1,
"partial": 16,
"refused": 0,
"not-implemented": 75,
"by-design": 0,
"unreviewed": 0,
@@ -566,9 +566,9 @@
"apiEntries": 131,
"static": {
"supported": 0,
"partial": 19,
"partial": 25,
"refused": 0,
"not-implemented": 112,
"not-implemented": 106,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -866,10 +866,10 @@
"apiEntries": 68,
"static": {
"supported": 0,
"partial": 3,
"partial": 4,
"refused": 0,
"not-implemented": 0,
"by-design": 65,
"by-design": 64,
"unreviewed": 0,
"not-applicable": 0
}
@@ -926,9 +926,9 @@
"apiEntries": 89,
"static": {
"supported": 0,
"partial": 7,
"partial": 8,
"refused": 1,
"not-implemented": 81,
"not-implemented": 80,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -30720,8 +30720,15 @@
"inherited": false
},
"static": {
"status": "not-implemented",
"evidence": "derived:descendants"
"status": "partial",
"evidence": "derived:descendants",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs",
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
},
"anchorSource": "chapter"
},
@@ -30835,8 +30842,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dnsPromises.lookup",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -30858,8 +30869,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dnsPromises.lookupService",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31249,8 +31264,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dnsPromises.getDefaultResultOrder",
"tests": [
"tests/corpus/dns-promises-lookup.ts",
"tests/corpus/dns-promises-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31364,8 +31383,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.lookup",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31479,8 +31503,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.getServers",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31502,8 +31531,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.lookup",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31548,8 +31582,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.lookupService",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31916,8 +31955,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.setDefaultResultOrder",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -31939,8 +31983,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:dns"
"status": "partial",
"evidence": "compiler-feature:dns.dns.getDefaultResultOrder",
"tests": [
"tests/corpus/5648-dns-lookup-forms.ts",
"tests/corpus/5649-dns-service-servers.ts",
"tests/corpus/5650-dns-lookup-errors-untyped.cjs"
]
},
"anchorSource": "exact"
},
@@ -52249,6 +52298,18 @@
"tests/corpus/6295-jose-hs256/main.mjs",
"tests/corpus/6331-jose-root-entry/main.ts",
"tests/corpus/text-codec-values/main.ts",
"tests/corpus/webassembly-api.mjs",
"tests/corpus/webassembly-dylink/main.mjs",
"tests/corpus/webassembly-jsapi.mjs",
"tests/corpus/webassembly-spec-control/main.mjs",
"tests/corpus/webassembly-spec-float/main.mjs",
"tests/corpus/webassembly-spec-imports/main.mjs",
"tests/corpus/webassembly-spec-int/main.mjs",
"tests/corpus/webassembly-spec-memory/main.mjs",
"tests/corpus/webassembly-spec-table/main.mjs",
"tests/corpus/webassembly-typed.ts",
"tests/corpus/webassembly-worker.ts",
"tests/corpus/webassembly-wpt-jsapi.mjs",
"tests/fixtures/node-types/navigator.ts",
"tests/harness/fetch-conformance.test.ts",
"tests/harness/project-config.test.ts"
@@ -53600,8 +53661,22 @@
"inherited": true
},
"static": {
"status": "refused",
"evidence": "surface-manifest:unsupported:stdlib.webassembly"
"status": "partial",
"evidence": "surface-manifest:stdlib.webassembly",
"tests": [
"tests/corpus/webassembly-api.mjs",
"tests/corpus/webassembly-typed.ts",
"tests/corpus/webassembly-worker.ts",
"tests/corpus/webassembly-spec-int/main.mjs",
"tests/corpus/webassembly-spec-float/main.mjs",
"tests/corpus/webassembly-spec-control/main.mjs",
"tests/corpus/webassembly-spec-memory/main.mjs",
"tests/corpus/webassembly-spec-table/main.mjs",
"tests/corpus/webassembly-spec-imports/main.mjs",
"tests/corpus/webassembly-jsapi.mjs",
"tests/corpus/webassembly-wpt-jsapi.mjs",
"tests/corpus/webassembly-dylink/main.mjs"
]
},
"anchorSource": "exact"
},
@@ -75571,7 +75646,9 @@
"tests/corpus/5212-perf-marks-measures.ts",
"tests/corpus/5213-perf-timing-errors.ts",
"tests/corpus/5214-perf-entry-inspect.ts",
"tests/corpus/5215-perf-timing-validation.js"
"tests/corpus/5215-perf-timing-validation.js",
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "chapter"
@@ -77612,8 +77689,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "compiler-feature:perf_hooks.histogram.percentile",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -77727,8 +77808,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "compiler-feature:perf_hooks.histogram.reset",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -78210,8 +78295,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "compiler-feature:perf_hooks.histogram.disable",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -78233,8 +78322,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "compiler-feature:perf_hooks.histogram.enable",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -78601,8 +78694,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "surface-manifest:node-builtin.perf_hooks.monitorEventLoopDelay",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -78751,8 +78848,12 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:perf_hooks"
"status": "partial",
"evidence": "compiler-feature:perf_hooks.performance.eventLoopUtilization",
"tests": [
"tests/corpus/6397-event-loop-metrics.ts",
"tests/fixtures/node-types/event-loop-metrics.ts"
]
},
"anchorSource": "exact"
},
@@ -104477,6 +104578,8 @@
"status": "partial",
"evidence": "derived:descendants",
"tests": [
"tests/corpus/v8-heap-statistics.ts",
"tests/corpus/v8-require-optional.cjs",
"tests/harness/node-test.test.ts"
]
},
@@ -105599,8 +105702,12 @@
"anchor": "v8getheapstatistics",
"nodeStability": null,
"static": {
"status": "by-design",
"evidence": "compiler-chapter-policy:v8"
"status": "partial",
"evidence": "surface-manifest:node-builtin.v8.getHeapStatistics",
"tests": [
"tests/corpus/v8-heap-statistics.ts",
"tests/corpus/v8-require-optional.cjs"
]
},
"anchorSource": "exact"
},
@@ -111212,6 +111319,8 @@
"tests/corpus/2630-stream-consumers-errors.ts",
"tests/corpus/2862-web-readable-async-iteration.ts",
"tests/corpus/4875-stream-consumers-array-buffer.ts",
"tests/corpus/stream-web-readable.ts",
"tests/corpus/stream-web-require.cjs",
"tests/harness/fetch-conformance.test.ts"
]
},
@@ -111480,8 +111589,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:webstreams"
"status": "partial",
"evidence": "compiler-feature:webstreams.ReadableStream",
"tests": [
"tests/corpus/stream-web-readable.ts",
"tests/corpus/stream-web-require.cjs",
"tests/corpus/2862-web-readable-async-iteration.ts"
]
},
"anchorSource": "exact"
},
+13 -1
View File
@@ -28,6 +28,7 @@
"webstreams": "unreviewed"
},
"evidenceByManifestPrefix": {
"stdlib.webassembly": ["tests/corpus/webassembly-api.mjs", "tests/corpus/webassembly-typed.ts", "tests/corpus/webassembly-worker.ts", "tests/corpus/webassembly-spec-int/main.mjs", "tests/corpus/webassembly-spec-float/main.mjs", "tests/corpus/webassembly-spec-control/main.mjs", "tests/corpus/webassembly-spec-memory/main.mjs", "tests/corpus/webassembly-spec-table/main.mjs", "tests/corpus/webassembly-spec-imports/main.mjs", "tests/corpus/webassembly-jsapi.mjs", "tests/corpus/webassembly-wpt-jsapi.mjs", "tests/corpus/webassembly-dylink/main.mjs"],
"node-builtin.fs.readFileSync": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts", "tests/corpus/fs-read-encodings.ts"],
"syntax.using-declarations": ["tests/corpus/2841-using-sync.ts"],
"syntax.await-using-declarations": ["tests/corpus/2842-await-using.ts"],
@@ -47,6 +48,10 @@
"node-builtin.dgram": ["tests/corpus/2597-dgram-send-ladders.cjs"],
"node-builtin.diagnostics_channel": ["tests/corpus/2214-dc-bind-store.cjs"],
"node-builtin.dns": ["tests/corpus/2597-dgram-send-ladders.cjs"],
"node-builtin.dns.promises": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"],
"node-builtin.stream.web": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs"],
"node-builtin.v8": ["tests/corpus/v8-require-optional.cjs"],
"node-builtin.v8.getHeapStatistics": ["tests/corpus/v8-heap-statistics.ts", "tests/corpus/v8-require-optional.cjs"],
"node-builtin.events": ["tests/corpus/1677-emitter-listeners.ts", "tests/corpus/2620-ee-override-once-order.ts"],
"node-builtin.fs": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts"],
"node-builtin.fs.realpathSync.native": ["tests/corpus/fs-native-realpath.ts"],
@@ -134,6 +139,8 @@
"node-builtin.perf_hooks.performance.getEntries": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts", "tests/corpus/5215-perf-timing-validation.js"],
"node-builtin.perf_hooks.performance.clearMarks": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5213-perf-timing-errors.ts", "tests/corpus/5215-perf-timing-validation.js"],
"node-builtin.perf_hooks.performance.clearMeasures": ["tests/corpus/5212-perf-marks-measures.ts"],
"node-builtin.perf_hooks.performance.eventLoopUtilization": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"],
"node-builtin.perf_hooks.monitorEventLoopDelay": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"],
"node-builtin.process": ["tests/corpus/990-process-basics.ts", "tests/corpus/2314-process-introspection.ts"],
"node-builtin.process.hrtime": ["tests/corpus/process-hrtime.js"],
"node-builtin.process.memoryUsage": ["tests/corpus/5210-process-memory-usage.ts"],
@@ -242,6 +249,8 @@
{ "chapter": "util", "symbols": ["util.parseEnv"], "status": "partial", "evidence": ["tests/corpus/node24-env-parse.ts", "tests/corpus/node24-env-parse-errors.ts", "tests/corpus/node24-env-imports.cjs", "tests/fixtures/node-types/env-files.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "process", "symbols": ["process.memoryUsage.rss"], "status": "partial", "evidence": ["tests/corpus/5210-process-memory-usage.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.now"], "status": "partial", "evidence": ["tests/corpus/2427-perf-hooks-now.ts", "tests/corpus/2446-performance-global.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.eventLoopUtilization"], "status": "partial", "evidence": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"] },
{ "chapter": "perf_hooks", "symbols": ["histogram.enable", "histogram.disable", "histogram.percentile", "histogram.reset"], "status": "partial", "evidence": ["tests/corpus/6397-event-loop-metrics.ts", "tests/fixtures/node-types/event-loop-metrics.ts"] },
{ "chapter": "perf_hooks", "symbols": ["performance.mark", "performance.measure", "performance.getEntries", "performance.getEntriesByName", "performance.getEntriesByType", "performance.clearMarks", "performance.clearMeasures"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5213-perf-timing-errors.ts", "tests/corpus/5214-perf-entry-inspect.ts", "tests/corpus/5215-perf-timing-validation.js"] },
{ "chapter": "perf_hooks", "symbols": ["PerformanceEntry", "PerformanceMark", "PerformanceMeasure"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts"] },
{ "chapter": "perf_hooks", "parent": "Class: `PerformanceEntry`", "symbols": ["duration", "entryType", "name", "startTime"], "status": "partial", "evidence": ["tests/corpus/5212-perf-marks-measures.ts", "tests/corpus/5214-perf-entry-inspect.ts"] },
@@ -381,7 +390,10 @@
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts", "tests/corpus/5625-promisify-exec.ts", "tests/corpus/5626-promisify-forms.ts", "tests/fixtures/node-types/promisify-fs.ts", "tests/corpus/5628-promisify-timers.ts", "tests/fixtures/node-types/promisify-timers.ts", "tests/corpus/5629-promisify-stream.ts", "tests/corpus/5630-promisify-zlib.ts", "tests/fixtures/node-types/promisify-zlib.ts", "tests/corpus/5631-promisify-crypto.ts", "tests/fixtures/node-types/promisify-crypto.ts", "tests/corpus/5632-promisify-dns.ts", "tests/fixtures/node-types/promisify-dns.ts", "tests/corpus/6428-promisify-callback-functions.ts", "tests/fixtures/node-types/promisify-callback-functions.ts", "tests/harness/project-config.test.ts"] },
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] },
{ "chapter": "zlib", "signatures": ["Class: `zlib.Deflate`", "Class: `zlib.Inflate`", "Class: `zlib.Gzip`", "Class: `zlib.Gunzip`", "Class: `zlib.DeflateRaw`", "Class: `zlib.InflateRaw`", "Class: `zlib.Unzip`", "Class: `zlib.ZlibBase`", "`zlib.close([callback])`", "`zlib.flush([kind, ]callback)`", "`zlib.params(level, strategy, callback)`", "`zlib.reset()`", "`bytesWritten` Type: {number}", "Class: `Options`"], "status": "partial", "evidence": ["tests/corpus/5370-zlib-stream-roundtrip.ts", "tests/corpus/5371-zlib-stream-chunking.cjs", "tests/corpus/5372-zlib-stream-errors.cjs", "tests/corpus/5373-zlib-stream-options.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts", "tests/corpus/5375-zlib-stream-promises.ts", "tests/corpus/5377-zlib-stream-backpressure.cjs"] },
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] }
{ "chapter": "zlib", "signatures": ["`zlib.constants`"], "status": "partial", "evidence": ["tests/corpus/5376-zlib-constants.cjs", "tests/corpus/5374-zlib-stream-flush-params.ts"] },
{ "chapter": "dns", "symbols": ["dns.lookup", "dns.lookupService", "dns.getServers", "dns.getDefaultResultOrder", "dns.setDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/5648-dns-lookup-forms.ts", "tests/corpus/5649-dns-service-servers.ts", "tests/corpus/5650-dns-lookup-errors-untyped.cjs"] },
{ "chapter": "dns", "symbols": ["dnsPromises.lookup", "dnsPromises.lookupService", "dnsPromises.getDefaultResultOrder"], "status": "partial", "evidence": ["tests/corpus/dns-promises-lookup.ts", "tests/corpus/dns-promises-untyped.cjs"] },
{ "chapter": "webstreams", "symbols": ["ReadableStream"], "status": "partial", "evidence": ["tests/corpus/stream-web-readable.ts", "tests/corpus/stream-web-require.cjs", "tests/corpus/2862-web-readable-async-iteration.ts"] }
],
"dedicated": [
{ "symbols": ["assert.doesNotThrow", "assert.doesNotReject"], "status": "partial", "evidence": ["tests/corpus/node24-no-error-assertions.ts", "tests/corpus/node24-no-error-assertions.cjs", "tests/corpus/node24-no-error-assertions-coercion.js", "tests/fixtures/node-types/no-error-assertions.ts", "tests/harness/project-config.test.ts"] },
+1
View File
@@ -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",
+2
View File
@@ -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"),
]) {
+8
View File
@@ -116,6 +116,13 @@ async function tryFastPath(): Promise<number | null> {
// the whole compiler graph to rediscover it.
const helperRuntimePackTarget = !values.sanitize ? startup.precompiledRuntimePackTarget() : null;
const helperObjectRoute = helperRuntimePackTarget !== null;
// The full compiler reports a checker it cannot find.
let checker: string | undefined;
try {
checker = startup.checkerCacheIdentity();
} catch {
return null;
}
const hit = await startup.readRoutedExecutableCache(root, {
entryPath: input,
outDir,
@@ -126,6 +133,7 @@ async function tryFastPath(): Promise<number | null> {
...(optimization === "dev" || optimization === "speed" ? { optimization } : {}),
...(values.strip ? { strip: true as const } : {}),
...(values["detached-exit"] ? { detachedExit: true as const } : {}),
...(checker === undefined ? {} : { checker }),
npmStatic,
ffiProfile: ffiPath === null ? null : { path: ffiPath, bytes: ffiBytes! },
target: `${process.env["SCRIPTC_TARGET"] ?? "native"}:${buildPlatform}:${process.arch}:${
+189 -12
View File
@@ -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<T extends WritableStream>(destination: T, options?: { end?: boolean }): T;
unpipe(destination?: WritableStream): this;
[Symbol.asyncIterator](): AsyncIterableIterator<string | Buffer>;
}
/* The writable-stream interface every Node stream implements:
* process.stdout/process.stderr, fs.WriteStream and the node:stream
* Writable family. Written only to the process streams, it compiles as
* their descriptor; with other streams flowing into it, as the
* node:stream Writable (the process streams convert into one). */
interface WritableStream {
readonly writable: boolean;
write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
write(
data: string | Uint8Array,
encoding: BufferEncoding,
callback?: (error?: Error | null) => void,
): boolean;
end(callback?: () => void): void;
end(data: string | Uint8Array, callback?: () => void): void;
end(data: string | Uint8Array, encoding: BufferEncoding, callback?: () => void): void;
/* Node's EventEmitter signature: listeners take untyped arguments. */
on(event: string, listener: (...args: any[]) => void): void;
once(event: string, listener: (...args: any[]) => void): void;
}
}
@@ -1271,8 +1304,37 @@ declare module "node:perf_hooks" {
getEntriesByType(type: EntryType): PerformanceEntry[];
clearMarks(name?: string): void;
clearMeasures(name?: string): void;
eventLoopUtilization(
utilization1?: EventLoopUtilization,
utilization2?: EventLoopUtilization,
): EventLoopUtilization;
}
export const performance: Performance;
/* Event-loop metrics: utilization (idle/active ms since the loop started)
* and monitorEventLoopDelay's sampling histogram (nanoseconds). */
export interface EventLoopUtilization {
idle: number;
active: number;
utilization: number;
}
export interface EventLoopMonitorOptions {
resolution?: number | undefined;
}
export interface Histogram {
readonly count: number;
readonly exceeds: number;
readonly max: number;
readonly mean: number;
readonly min: number;
readonly stddev: number;
percentile(percentile: number): number;
reset(): void;
}
export interface IntervalHistogram extends Histogram {
enable(): boolean;
disable(): boolean;
}
export function monitorEventLoopDelay(options?: EventLoopMonitorOptions): IntervalHistogram;
}
declare module "perf_hooks" {
export * from "node:perf_hooks";
@@ -1878,6 +1940,13 @@ declare module "node:fs" {
declare module "fs" {
export * from "node:fs";
}
/* graceful-fs — served as node:fs (it monkeypatches fs and retries on
* EMFILE/ENFILE; compiled programs call fs directly, without the retry
* queue). gracefulify is a no-op statement. The @types/graceful-fs shape. */
declare module "graceful-fs" {
export * from "node:fs";
export function gracefulify<T>(fsModule: T): T;
}
/* fs/promises — the SAME synchronous operations behind already-settled
* promises: success fulfills, failure REJECTS with the would-be thrown
@@ -3059,8 +3128,10 @@ declare module "child_process" {
destroy(): void;
on(event: "drain" | "finish", listener: () => void): void;
on(event: "error", listener: (err: Error) => void): void;
on(event: string, listener: (...args: any[]) => void): void;
once(event: "drain" | "finish", listener: () => void): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: string, listener: (...args: any[]) => void): void;
}
export interface ChildProcess extends Disposable {
@@ -4687,6 +4758,11 @@ declare module "net" {
on(event: "close", listener: (hadError: boolean) => void): void;
on(event: "error", listener: (err: Error) => void): void;
on(event: "session", listener: (session: Buffer) => void): void;
/* The host name's resolution, before the connect. */
on(
event: "lookup",
listener: (err: Error | null, address: string, family: number, host: string) => void,
): void;
/* addListener IS on (Node aliases them) — the suite spells both. */
addListener(event: "data", listener: (chunk: Buffer) => void): void;
/* After setEncoding the chunk is a string. */
@@ -4698,6 +4774,11 @@ declare module "net" {
addListener(event: "close", listener: (hadError: boolean) => void): void;
addListener(event: "error", listener: (err: Error) => void): void;
addListener(event: "session", listener: (session: Buffer) => void): void;
/* The host name's resolution, before the connect. */
addListener(
event: "lookup",
listener: (err: Error | null, address: string, family: number, host: string) => void,
): void;
once(event: "data", listener: (chunk: Buffer) => void): void;
/* After setEncoding the chunk is a string. */
once(event: "data", listener: (chunk: string) => void): void;
@@ -4708,6 +4789,11 @@ declare module "net" {
once(event: "close", listener: (hadError: boolean) => void): void;
once(event: "error", listener: (err: Error) => void): void;
once(event: "session", listener: (session: Buffer) => void): void;
/* The host name's resolution, before the connect. */
once(
event: "lookup",
listener: (err: Error | null, address: string, family: number, host: string) => void,
): void;
}
export interface Server {
/* Whether the server keeps the event loop alive (ref'd by default). */
@@ -4726,6 +4812,8 @@ declare module "net" {
/* The positional bind address (the options form's host in argument
* spelling). */
listen(port: number, host: string, callback?: () => void): Server;
/* A Unix-socket server; the socket file is removed when it closes. */
listen(path: string, callback?: () => void): Server;
/* The explicit-interface bind: host is an IP literal (absent = the
* host-less dual-stack any); ipv6Only sets IPV6_V6ONLY and reusePort
* requests kernel connection distribution where Node supports it. */
@@ -5049,10 +5137,11 @@ declare module "http" {
* callback receives the response as an IncomingMessage (statusCode,
* headers, body via on("data"/"end")). */
export interface RequestOptions {
hostname?: string;
host?: string;
port?: number;
path?: string;
hostname?: string | null;
host?: string | null;
/* A string port (a URL's .port) is accepted, as by Node. */
port?: number | string | null;
path?: string | null;
method?: string;
timeout?: number;
headers?: OutgoingHttpHeaders;
@@ -5066,6 +5155,8 @@ declare module "http" {
agent?: Agent | boolean | null;
/* Aborting destroys the request with an AbortError. */
signal?: AbortSignal;
/* HTTP over the Unix socket at this path instead of host:port. */
socketPath?: string;
/* The options-record stance: Node ignores an options record's
* unknown keys, so every key typechecks here — the option WALK in
* the lowering decides per key (lower, fence by name, or drop
@@ -5341,6 +5432,7 @@ declare module "https" {
agent?: Agent | boolean | null;
/* Aborting destroys the request with an AbortError. */
signal?: AbortSignal;
socketPath?: string;
/* The options-record stance (see http.RequestOptions). */
[option: string]: unknown;
}
@@ -6158,10 +6250,79 @@ declare module "dns" {
export function getServers(): string[];
export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim";
export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void;
/* The promise forms (node:dns/promises). */
export const promises: typeof import("dns/promises");
}
declare module "node:dns" {
export * from "dns";
}
/* node:dns/promises — lookup and lookupService as promises over the same
* resolution as the callback forms: argument errors throw synchronously,
* failures reject with the callbacks' errors. */
declare module "dns/promises" {
import type { LookupAddress, LookupAllOptions, LookupOneOptions, LookupOptions } from "dns";
export function lookup(hostname: string, family: number): Promise<LookupAddress>;
export function lookup(hostname: string, options: LookupOneOptions): Promise<LookupAddress>;
export function lookup(hostname: string, options: LookupAllOptions): Promise<LookupAddress[]>;
export function lookup(
hostname: string,
options: LookupOptions,
): Promise<LookupAddress | LookupAddress[]>;
export function lookup(hostname: string): Promise<LookupAddress>;
export function lookupService(
address: string,
port: number,
): Promise<{ hostname: string; service: string }>;
export function getServers(): string[];
export function getDefaultResultOrder(): "ipv4first" | "ipv6first" | "verbatim";
export function setDefaultResultOrder(order: "ipv4first" | "ipv6first" | "verbatim"): void;
}
declare module "node:dns/promises" {
export * from "dns/promises";
}
/* node:v8 — importable for the packages that require it at top level.
* getHeapStatistics answers allocator-derived figures in Node's shape
* (approximate: the runtime heap's total and used bytes, the machine's
* memory as the limit; V8-only figures are 0). The other members observe
* V8 itself and fence at their use with the by-design reason; they are
* declared so programs that type-check under their own tsc reach that
* fence. */
declare module "v8" {
type DoesZapCodeSpaceFlag = 0 | 1;
export interface HeapInfo {
total_heap_size: number;
total_heap_size_executable: number;
total_physical_size: number;
total_available_size: number;
used_heap_size: number;
heap_size_limit: number;
malloced_memory: number;
peak_malloced_memory: number;
does_zap_garbage: DoesZapCodeSpaceFlag;
number_of_native_contexts: number;
number_of_detached_contexts: number;
total_global_handles_size: number;
used_global_handles_size: number;
external_memory: number;
}
export function getHeapStatistics(): HeapInfo;
export function cachedDataVersionTag(): number;
export function getHeapSpaceStatistics(): unknown[];
export function getHeapCodeStatistics(): unknown;
export function getHeapSnapshot(options?: unknown): unknown;
export function writeHeapSnapshot(fileName?: string, options?: unknown): string;
export function setFlagsFromString(flags: string): void;
export function setHeapSnapshotNearHeapLimit(limit: number): void;
export function serialize(value: unknown): Buffer;
export function deserialize(buffer: Uint8Array): unknown;
export function takeCoverage(): void;
export function stopCoverage(): void;
export function isStringOneByteRepresentation(content: string): boolean;
}
declare module "node:v8" {
export * from "v8";
}
/* node:worker_threads — native workers use statically compiled entry points.
* Lowering and runtime diagnostics define the supported options and methods. */
@@ -6317,7 +6478,7 @@ declare module "node:cluster" {
/* node:tty — the fd probe (the same isatty(3) behind process.*.isTTY)
* and the supported structural view of a process output stream. */
declare module "tty" {
export interface WriteStream {
export interface WriteStream extends NodeJS.WritableStream {
write(data: string | Uint8Array, callback?: (error?: Error | null) => void): boolean;
write(
data: string | Uint8Array,
@@ -6567,8 +6728,8 @@ declare module "stream" {
resume(): this;
isPaused(): boolean;
setEncoding(encoding: string): this;
pipe<T extends Writable>(destination: T, options?: { end?: boolean }): T;
unpipe(destination?: Writable): this;
pipe<T extends NodeJS.WritableStream>(destination: T, options?: { end?: boolean }): T;
unpipe(destination?: NodeJS.WritableStream): this;
destroy(error?: Error): this;
readonly readable: boolean;
readonly readableEnded: boolean;
@@ -6951,6 +7112,22 @@ declare module "node:stream/consumers" {
import streamConsumers = require("stream/consumers");
export = streamConsumers;
}
/* node:stream/web — the module exports the web stream globals themselves
* (require("stream/web").ReadableStream === globalThis.ReadableStream).
* ReadableStream is the implemented class; its reader and controller are
* exported as types. */
declare module "stream/web" {
export const ReadableStream: typeof globalThis.ReadableStream;
export type ReadableStream<T = unknown> = globalThis.ReadableStream<T>;
export type ReadableStreamDefaultReader<T = unknown> = globalThis.ReadableStreamDefaultReader<T>;
export type ReadableStreamDefaultController<T = unknown> =
globalThis.ReadableStreamDefaultController<T>;
export type UnderlyingSource<T = unknown> = globalThis.UnderlyingSource<T>;
export type QueuingStrategy<T = unknown> = globalThis.QueuingStrategy<T>;
}
declare module "node:stream/web" {
export * from "stream/web";
}
interface ErrorConstructor {
stackTraceLimit: number;
}
+1
View File
@@ -39,6 +39,7 @@
},
"dependencies": {
"@scriptc/runtime": "workspace:*",
"tsc-ts": "0.0.3",
"typescript": "7.0.2",
"typescript5": "npm:typescript@5.9.3"
},
@@ -16,6 +16,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
fileHandle: features.fileHandle,
fetch: features.fetch,
...(features.eventTarget ? { eventTarget: true } : {}),
...(features.wasm ? { wasm: true } : {}),
...(features.warningReplace ? { warningReplace: true } : {}),
...(features.stdioExtended ? { stdioExtended: true } : {}),
zlib: features.zlib,
@@ -25,18 +26,21 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
dc: features.dc,
dynAsync: features.dynAsync,
...(features.dynTier ? { dynTier: true } : {}),
...(features.streamDyn ? { streamDyn: true } : {}),
events: features.processEvents,
emitter: features.emitter,
symbol: features.symbol,
bigint: features.bigint,
searchParams: features.searchParams,
qs: features.qs,
dynBuiltinClass: features.dynBuiltinClass,
parseArgs: features.parseArgs,
stream: features.stream,
net: features.net,
http: features.http,
http2: features.http2,
dgram: features.dgram,
dnsPromises: features.dnsPromises,
watch: features.fsWatch,
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
nodeTest: features.nodeTest,
@@ -44,5 +48,6 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
tlsCa: features.tlsCa,
cipher: features.cipher,
webcrypto: features.webcrypto,
perfLoop: features.perfLoop,
};
}
@@ -42,18 +42,21 @@ export async function compilePackedLibrary(
bigint: options.bigint ?? false,
searchParams: options.searchParams ?? false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
tls: false,
cipher: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
};
const selection = await loadRuntimePack({
+58 -5
View File
@@ -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.
@@ -1,4 +1,5 @@
/* LlEmitter's module-level output: module parts, helper definitions, library declarations and FFI callback definitions. */
import { emitWasmModules } from "./wasm-modules.js";
import { type LlEmitter, ffiCallbackDummyLl, llStrBytes } from "./emitter.js";
import {
mangleAsyncSpawn,
@@ -177,6 +178,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
// programs additionally stamp the httpReq/httpRes ops — the C main's
// install lines, gated on the same predicates native/executable.ts links by.
const usesNet = runtimeFeatures.net;
const usesNetLookup = runtimeFeatures.netLookup;
const usesDgram = runtimeFeatures.dgram;
const usesHttp2 = runtimeFeatures.http2;
const usesHttp = runtimeFeatures.http;
@@ -226,6 +228,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
if (usesFsWatch) emitter.declare(`declare void @scr_watch_install()`);
if (emitter.ffiHasForeignCallback) emitter.declare(`declare void @scr_ffi_install()`);
if (usesStream) emitter.declare(`declare void @scr_stream_install()`);
if (usesNetLookup) emitter.declare(`declare void @scr_net_lookup_install()`);
if (usesNet) {
emitter.declare(`declare void @scr_net_install()`);
emitter.declare(`declare void @scr_net_dyn_install()`);
@@ -310,6 +313,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
emitter.declare(`declare zeroext i1 @scr_loop_run(ptr)`);
if (emitter.mod.commonJsEntry === true)
emitter.declare(`declare void @scr_loop_commonjs_entry()`);
if (emitter.readsLoopMetrics()) emitter.declare(`declare void @scr_perf_loop_mark()`);
emitter.declare(`declare zeroext i1 @scr_report_unhandled_rejections()`);
emitter.declare(`declare void @scr_discard_unhandled_rejections()`);
}
@@ -728,6 +732,9 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
out.push(fnDefs[0] ?? "");
for (let i = 1; i < fnDefs.length; i++) out.push("", fnDefs[i]!);
out.push("");
// WebAssembly modules translated into this program, and the registry.
for (const line of emitWasmModules(emitter.mod, flushedDecls, emitter.wasmBoundsChecks))
out.push(line);
// main(): scr_init, the program-dependent error-vt interval stamps,
// scr_lib_init(argc, argv), then the entry function. An uncaught
@@ -837,6 +844,7 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
...(snapshotsTlsCa ? [` call void @scr_tls_ca_install()`] : []),
...(usesFetch ? [` call void @scr_fetch_install()`] : []),
...(usesNet ? [` call void @scr_net_install()`, ` call void @scr_net_dyn_install()`] : []),
...(usesNetLookup ? [` call void @scr_net_lookup_install()`] : []),
...(usesHttp ? [` call void @scr_http_dyn_install()`] : []),
...(usesDgram ? [` call void @scr_dgram_install()`] : []),
...(usesHttp2 ? [` call void @scr_http2_dyn_install()`] : []),
@@ -870,6 +878,8 @@ export function emitModuleParts(emitter: LlEmitter): string[] {
...(runsLoop
? [
...(emitter.mod.commonJsEntry === true ? [` call void @scr_loop_commonjs_entry()`] : []),
// performance.eventLoopUtilization measures from here on.
...(emitter.readsLoopMetrics() ? [` call void @scr_perf_loop_mark()`] : []),
` %loop_rejection = call zeroext i1 @scr_loop_run(ptr ${asyncEntry ? "%top" : "null"})`,
` %lexc = call zeroext i1 @scr_exc_pending()`,
` br i1 %lexc, label %luncaught, label %lok`,
@@ -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<string, { sym: string; len: number }>();
@@ -1175,6 +1181,13 @@ export class LlEmitter {
this.emitLibraryIdentity = options.emitLibraryIdentity !== false;
this.runtimeAbiMarker = options.runtimeAbiMarker === true;
this.detachedExit = options.detachedExit === true;
this.wasmBoundsChecks =
options.wasmBoundsChecks === true ||
wasmBoundsChecks({
sanitize: false,
pointerBits: options.pointerBits ?? 64,
targetTriple: options.targetTriple ?? "",
});
this.rcHelpers = options.inlineRc !== undefined ? new Set() : null;
this.rcMode = options.inlineRc;
this.rcHelpersNoinline = options.rcHelpersNoinline === true;
@@ -4201,6 +4214,20 @@ export class LlEmitter {
/** Moves need plain slots that nothing else reads behind the binding's
* back: no debugger view, no coroutine frame, no borrowed (+0) return. */
/** Whether the program reads event-loop utilization (perf.loopStart /
* perf.idleTime): main then marks the loop's start before running it. */
private loopMetricsRead: boolean | undefined;
readsLoopMetrics(): boolean {
if (this.loopMetricsRead !== undefined) return this.loopMetricsRead;
const visitor = {
expr: (e: IrExpr): boolean =>
!(e.kind === "libCall" && (e.fn === "perf.loopStart" || e.fn === "perf.idleTime")),
stmt: (): boolean => true,
};
this.loopMetricsRead = !this.mod.functions.every((fn) => everyStmtList(fn.body, visitor));
return this.loopMetricsRead;
}
private lastUseMovesAllowed(): boolean {
return (
this.debug === null &&
@@ -5564,6 +5591,10 @@ export class LlEmitter {
return childDataThunkFor(this, param);
}
childDataDynThunkFor(chunk: "bytes" | "str"): string {
return childDataDynThunkFor(this, chunk);
}
emitterFixedAdapter(cbT: IrType & { kind: "func" }): { fn: string; shim: string } {
return emitterFixedAdapter(this, cbT);
}
@@ -55,6 +55,23 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
if (sym) return sym;
sym = `sc_race_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : [];
if (from.kind === "void" && !toArms.some((a) => a.kind === "undefinedT")) {
// A Promise<never> entry (the frontend admits void entries without an
// undefined result arm only for those): it can only reject, so its
// fulfillment adapter never runs.
host.declare(`declare void @scr_trap(ptr)`);
const msg = host.cstr("scriptc: a Promise<never> race entry fulfilled\n");
host.resolveThunkDefs.push(
`define internal void @${sym}(ptr %dst, ptr %src) ${FN_ATTRS} { ; race ${key}`,
`entry:`,
` call void @scr_trap(ptr ${msg})`,
` unreachable`,
`}`,
``,
);
return sym;
}
if (to.kind !== "union")
throw new InternalCompilerError("llvm emitter bug: race adapter to a non-union");
const toDef = host.unionsById.get(to.unionId);
@@ -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" },
@@ -535,6 +535,27 @@ export function emitDynamicExpr(
e.key.type.kind === "dyn" ? host.dyn.dynComputedKeyGetHelper() : host.dyn.dynKeyGetHelper();
const t = B.tmp();
const generic = `call ptr @${helper}(ptr ${d.name}, ptr ${k.name}, i1 ${e.optional ? "true" : "false"})`;
const inherited = e.key.kind === "strLit" && INHERITED_OBJECT_MEMBERS.has(e.key.value);
// Object.prototype itself answers its own members before the ordinary
// read (which fences reflection on the prototype).
let prototypeJoin: { member: string; from: string; join: string } | null = null;
if (inherited) {
host.declare(`declare ptr @scr_dyn_object_prototype_member(ptr, ptr)`);
const member = B.tmp();
B.line(
`${member} = call ptr @scr_dyn_object_prototype_member(ptr ${d.name}, ptr ${k.name})`,
);
const isMember = B.tmp();
B.line(`${isMember} = icmp ne ptr ${member}, null`);
const from = B.newLabel("dkg.op");
const read = B.newLabel("dkg.rd");
const join = B.newLabel("dkg.opj");
B.condBr(isMember, from, read);
B.startBlock(from);
B.br(join);
B.startBlock(read);
prototypeJoin = { member, from, join };
}
if (e.key.kind === "strLit") {
// A constant key on an ordinary object reads through the site's
// cache; every other receiver keeps the shared helper.
@@ -560,7 +581,27 @@ export function emitDynamicExpr(
} else {
B.line(`${t} = ${generic}`);
}
const out = host.own({ name: t, type: e.type });
let result = t;
if (prototypeJoin) {
// An undefined read of an Object.prototype member name answers the
// member the receiver inherits (scr_dyn_invoke.c; the frontend
// links that unit for these reads).
host.declare(`declare ptr @scr_dyn_inherited_read(ptr, ptr, ptr)`);
const read = B.tmp();
B.line(
`${read} = call ptr @scr_dyn_inherited_read(ptr ${d.name}, ptr ${k.name}, ptr ${t})`,
);
const readEnd = B.newLabel("dkg.rde");
B.br(readEnd);
B.startBlock(readEnd);
B.br(prototypeJoin.join);
B.startBlock(prototypeJoin.join);
result = B.tmp();
B.line(
`${result} = phi ptr [ ${prototypeJoin.member}, %${prototypeJoin.from} ], [ ${read}, %${readEnd} ]`,
);
}
const out = host.own({ name: result, type: e.type });
host.emitPendingCheck();
return out;
}
@@ -1204,3 +1245,13 @@ function emitUnionEqWrappedScalar(
function wrappedStringLiteral(e: IrExpr): string | null {
return e.kind === "unionWrap" && e.value.kind === "strLit" ? e.value.value : null;
}
/** Object.prototype member names whose constant-key dynamic reads fall back
* to the inherited member (runtime-features.ts links the unit). */
export const INHERITED_OBJECT_MEMBERS: ReadonlySet<string> = new Set([
"constructor",
"valueOf",
"toString",
"hasOwnProperty",
"propertyIsEnumerable",
]);
@@ -15,6 +15,7 @@ import {
isDirentShape,
isDynTypedRefType,
isRefCounted,
shapeHasAccessorSlots,
typeKey,
} from "../../ir/ir.js";
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
@@ -941,6 +942,18 @@ export function streamTypedRefMaterializeAdapter(
B.line(`${copied} = call ptr @scr_dyn_copy_property_descriptors(ptr ${out}, ptr ${bag})`);
B.line(`call void @scr_dyn_release_v(ptr ${copied})`);
}
// An Error subclass instance's own `name` sits where its class's
// construction creates it: after a field, or after every other
// property when no constructor sets it.
const nameAfter = meta.def.errorNameAfter;
if (meta.root.def.name === "%Error" && !meta.def.runtime && nameAfter !== null) {
host.declare(`declare void @scr_error_dyn_place_name(ptr, ptr, ${host.sizeType})`);
B.line(
nameAfter === undefined
? `call void @scr_error_dyn_place_name(ptr ${out}, ptr null, ${host.sizeType} 0)`
: `call void @scr_error_dyn_place_name(ptr ${out}, ptr ${host.cstr(nameAfter)}, ${host.sizeType} ${Buffer.byteLength(nameAfter, "utf8")})`,
);
}
// A frozen, sealed or non-extensible instance's view says so.
if (withAttributes) {
host.declare(`declare void @scr_dyn_class_view_attributes(ptr, double)`);
@@ -965,6 +978,7 @@ export function streamTypedRefMaterializeAdapter(
* Declared-field records and tuples cover the live stream values. */
const ordinary =
shape.indexValue ||
shapeHasAccessorSlots(shape) ||
shape.fields.some((field) => field.name === "handleEvent" && field.type.kind === "func");
if (ordinary) {
const out = B.tmp();
@@ -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)`);
@@ -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":
@@ -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":
@@ -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");
}
@@ -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]!);
@@ -127,6 +127,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
"str.encodeUriComponent": "scr_str_encode_uri_component",
"error.cause": "scr_error_cause",
"error.setCause": "scr_error_set_cause",
"error.setCode": "scr_error_set_code_value",
"error.setCodeDyn": "scr_error_set_code_dyn",
"error.assign": "scr_error_assign_dyn",
"error.hasKey": "scr_error_has_key",
"error.hasCode": "scr_error_has_code",
"error.defineCause": "scr_error_define_cause",
"error.deleteCause": "scr_error_delete_cause",
"error.hasCause": "scr_error_has_cause",
@@ -278,6 +283,14 @@ export const LIB_FN_SYMS: Record<string, string> = {
"perf.entryDetail": "scr_perf_entry_detail",
"perf.entryKeep": "scr_perf_entry_keep",
"perf.entryRef": "scr_perf_entry_ref",
"perf.loopStart": "scr_perf_loop_start",
"perf.idleTime": "scr_perf_idle_time",
"perf.eldNew": "scr_eld_new",
"perf.eldEnable": "scr_eld_enable",
"perf.eldDisable": "scr_eld_disable",
"perf.eldReset": "scr_eld_reset",
"perf.eldPercentile": "scr_eld_percentile",
"perf.eldStat": "scr_eld_stat",
"process.memoryUsageTake": "scr_process_memory_usage_take",
"process.memoryUsageField": "scr_process_memory_usage_field",
"process.rss": "scr_process_rss",
@@ -310,12 +323,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
"date.now": "scr_date_now",
"date.newNow": "scr_date_now",
"date.newMs": "scr_date_new_ms",
"date.newString": "scr_date_parse_get_time",
"date.parse": "scr_date_parse_get_time",
"date.newString": "scr_date_parse",
"date.parse": "scr_date_parse",
"date.getTime": "scr_date_get_time",
"date.valueOf": "scr_date_get_time",
"date.parseGetTime": "scr_date_parse_get_time",
"date.parseGetTime": "scr_date_parse",
"date.utc": "scr_date_utc",
"date.newLocal": "scr_date_new_local",
"date.getFullYear": "scr_date_get_full_year_local",
"date.getUTCFullYear": "scr_date_get_full_year_utc",
"date.getMonth": "scr_date_get_month_local",
@@ -612,6 +626,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"insp.regex": "scr_insp_regex",
"insp.date": "scr_insp_date",
"insp.error": "scr_insp_error",
"insp.errorHasCause": "scr_insp_error_has_cause",
"insp.errorBagHasProperties": "scr_insp_error_bag_has_properties",
"insp.errorCause": "scr_insp_error_cause",
"insp.begin": "scr_insp_begin",
"insp.layout": "scr_insp_layout",
"insp.entry": "scr_insp_entry",
@@ -726,6 +743,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
"child.unref": "scr_child_unref",
"child.ref": "scr_child_ref",
"procStream.write": "scr_proc_stream_write",
"stream.stdioWritable": "scr_stream_stdio_writable",
"stream.unboxDyn": "scr_stream_unbox_dyn",
"stream.pipeDyn": "scr_stream_pipe_dyn",
"stream.unpipeDyn": "scr_stream_unpipe_dyn",
"process.kill": "scr_process_kill_named",
"process.killNum": "scr_process_kill",
// fs.promises (scr_bytes_io.c / scr_lib.c): the promise forms settle
@@ -858,6 +879,24 @@ export const LIB_FN_SYMS: Record<string, string> = {
"event.get": "scr_event_obj_get",
"event.set": "scr_event_obj_set",
"blob.new": "scr_blob_new",
"wasm.compile": "scr_wasm_compile",
"wasm.validate": "scr_wasm_validate",
"wasm.instantiate": "scr_wasm_instantiate",
"wasm.moduleNew": "scr_wasm_module_new",
"wasm.instanceNew": "scr_wasm_instance_new",
"wasm.is": "scr_wasm_is",
"wasm.errorNew": "scr_wasm_error_new",
"wasm.memoryNew": "scr_wasm_memory_construct",
"wasm.tableNew": "scr_wasm_table_construct",
"wasm.globalNew": "scr_wasm_global_construct",
"wasm.withoutNew": "scr_wasm_construct_without_new",
"wasm.moduleImports": "scr_wasm_module_imports",
"wasm.moduleExports": "scr_wasm_module_exports",
"wasm.moduleCustomSections": "scr_wasm_module_custom_sections",
"wasm.valueOf": "scr_wasm_value_of",
"wasm.function": "scr_wasm_function",
"wasm.namespace": "scr_wasm_namespace",
"wasm.globalView": "scr_wasm_global_view",
"blob.fileNew": "scr_file_new",
"blob.slice": "scr_blob_slice",
"blob.text": "scr_blob_text",
@@ -926,8 +965,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.promiseAll": "scr_dyn_promise_all",
"dyn.promiseAllSettled": "scr_dyn_promise_all_settled",
"dyn.promiseAny": "scr_dyn_promise_any",
"dyn.promiseResolve": "scr_dyn_promise_resolve",
"dyn.typeof": "scr_dyn_typeof",
"dyn.objectTag": "scr_dyn_object_tag",
"dyn.objectTag": "scr_dyn_object_tag_of",
"dyn.freeze": "scr_dyn_freeze",
"dyn.isFrozen": "scr_dyn_is_frozen",
"class.defineField": "scr_class_define_field",
@@ -941,7 +981,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.nativeMapIs": "scr_dyn_native_map_is",
"dyn.nativeUrlIs": "scr_dyn_native_url_is",
"dyn.nativeDateIs": "scr_dyn_native_date_is",
"dyn.globalConstructor": "scr_dyn_global_constructor",
"dyn.builtinFunction": "scr_dyn_builtin_function",
"date.nativeNew": "scr_dyn_native_date_new",
"date.nativeNewFields": "scr_dyn_native_date_new_fields",
"date.nativeNewValue": "scr_dyn_native_date_new_value",
"date.checkedValue": "scr_dyn_native_date_value",
"dyn.nativeRegexIs": "scr_dyn_native_regex_is",
"dyn.toString": "scr_dyn_to_string_argument",
@@ -1104,6 +1148,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"readable.opFind": "scr_stream_op_find",
"readable.opReduce": "scr_stream_op_reduce",
"readable.pipe": "scr_stream_pipe",
"readable.pipeGeneral": "scr_stream_pipe_general",
"writable.cork": "scr_stream_cork",
"writable.uncork": "scr_stream_uncork",
"stream.destroyErr": "scr_stream_destroy",
@@ -1151,6 +1196,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
"net.sockReadyState": "scr_net_sock_ready_state",
"net.sockSetKeepAlive": "scr_net_sock_set_keepalive_ret",
"net.sockOnCloseDyn": "scr_net_sock_on_close_dyn",
"net.sockOnLookup": "scr_net_sock_on_lookup",
"net.serverCloseDyn": "scr_net_server_close_dyn",
"net.serverUnixPath": "scr_net_server_unix_path",
"net.serverAddressCheck": "scr_net_server_address_check",
"net.sockOnWriteFlushDyn": "scr_net_sock_on_write_flush_dyn",
"net.sockOnFinishDyn": "scr_net_sock_on_finish_dyn",
"net.isIP": "scr_net_is_ip",
@@ -1414,6 +1463,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.classPrototype": "scr_dyn_class_prototype",
"dyn.classBasePrototype": "scr_dyn_class_base_prototype",
"dyn.classInherit": "scr_dyn_class_inherit",
"dyn.builtinClass": "scr_dyn_builtin_class",
"dyn.builtinInstanceOf": "scr_dyn_builtin_instanceof",
"dyn.functionInstanceOf": "scr_dyn_function_instanceof",
"dyn.classSuper": "scr_dyn_class_super",
"dyn.assignPrototype": "scr_dyn_assign_prototype",
"dyn.definePrototypeProps": "scr_dyn_define_prototype_props",
@@ -1586,6 +1638,7 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"readable.opReduce",
"readable.resume",
"readable.pipe",
"readable.pipeGeneral",
"readable.unpipe",
"writable.write",
"writable.writeStr",
@@ -1626,6 +1679,8 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"sc.arrayBuffer",
"net.listen",
"net.listenCb",
"net.listenPath",
"net.listenPathCb",
"net.listenOpts",
"net.listenOptsCb",
"net.listenOptsReusePort",
@@ -1644,6 +1699,10 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"http.requestCb",
"http.requestUrl",
"http.requestUrlCb",
"http.requestDyn",
"http.requestDynCb",
"https.requestDyn",
"https.requestDynCb",
"https.request",
"https.requestCb",
"https.requestUrl",
@@ -1665,4 +1724,5 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"timers.queueMicrotaskDyn",
"timers.setImmediateFnValue",
"timers.immediatePromise",
"dyn.builtinFunction",
]);
@@ -36,6 +36,24 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
host.emitPendingCheck();
return out;
}
if (e.fn === "stream.dynNative") {
// Checked-dynamic stream slots: the boxed native stream (+1) or NULL.
if (e.type.kind !== "union")
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} result is not a union`);
const def = host.unionsById.get(e.type.unionId);
const streamTag = def ? def.arms.findIndex((a) => a.kind === "object") : -1;
const nullTag = def ? def.arms.findIndex((a) => a.kind === "nullT") : -1;
if (streamTag < 0 || nullTag < 0)
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} union lacks its arms`);
const value = host.emitExpr(e.args[0]!);
const side = host.emitExpr(e.args[1]!);
host.declare(`declare ptr @scr_stream_dyn_native(ptr, double)`);
const raw = B.tmp();
B.line(
`${raw} = call ptr @scr_stream_dyn_native(ptr ${value.name}, double ${side.name}) ; +1 or NULL`,
);
return host.wrapNullable(raw, raw, def!.arms[streamTag]!, streamTag, e.type, nullTag);
}
if (e.fn === "stream.onData") {
// The callback MOVES into the stream's listener registry; the
// adapter is per callback shape — runtime-provided for the
@@ -56,6 +74,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
host.declare(`declare void @scr_child_stream_thunk0(ptr, ptr)`);
} else if (param.kind === "union") {
adapter = host.childDataThunkFor(param);
} else if (param.kind === "dyn") {
adapter = host.childDataDynThunkFor("bytes");
} else {
adapter = "scr_child_stream_thunk_bytes";
host.declare(`declare void @scr_child_stream_thunk_bytes(ptr, ptr)`);
@@ -78,7 +98,8 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
if (e.fn === "stream.onDataStr") {
host.usesTimers = true;
const cbT = e.args[1]!.type;
if (cbT.kind !== "func" || cbT.params[0]?.kind !== "string") {
const strParam = cbT.kind === "func" ? cbT.params[0]?.kind : undefined;
if (strParam !== "string" && strParam !== "dyn") {
throw new InternalCompilerError(
"llvm emitter bug: stream.onDataStr callback not a string func",
);
@@ -87,10 +108,12 @@ export function emitStreamLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlV
const cb = host.emitExpr(e.args[1]!);
const once = host.emitExpr(e.args[2]!);
host.toHeap(cb);
host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
let adapter = "scr_child_stream_thunk_str";
if (strParam === "dyn") adapter = host.childDataDynThunkFor("str");
else host.declare(`declare void @scr_child_stream_thunk_str(ptr, ptr)`);
host.declare(`declare void @scr_child_stream_on_data_str(ptr, ptr, ptr, i1 zeroext)`);
B.line(
`call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @scr_child_stream_thunk_str, i1 ${once.name})`,
`call void @scr_child_stream_on_data_str(ptr ${s0.name}, ptr ${cb.name}, ptr @${adapter}, i1 ${once.name})`,
);
return { name: "", type: e.type };
}
@@ -0,0 +1,77 @@
/* The WebAssembly modules translated into a program (IrModule.wasmModules):
* each module's translation (wasm/translate.ts) and the registry the
* runtime looks modules up in (scr_wasm_registry, scr_wasm_abi.h). The
* translations join the program module, so the native helper optimizes,
* partitions and caches them with it. Programs that do not use the
* WebAssembly API carry no wasmModules and emit nothing here. */
import type { IrModule } from "../../ir/ir.js";
import { decodeBase64 } from "../../wasm/base64.js";
import { translateWasmModule, WasmTranslateError } from "../../wasm/translate.js";
function hexBytes(hex: string): Uint8Array {
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16);
return out;
}
/** Whether translated memories check bounds explicitly instead of relying
* on guard pages and the runtime's fault handler: sanitizer builds (whose
* runtime owns SIGSEGV), 32-bit targets, and Windows, which has no
* fault handler yet. */
export function wasmBoundsChecks(options: {
sanitize: boolean;
pointerBits: 32 | 64;
targetTriple: string;
}): boolean {
if (options.sanitize || options.pointerBits === 32) return true;
const target = options.targetTriple || process.platform;
return /windows|mingw|cygwin|win32|msvc/.test(target);
}
/** The top-level lines of every translated module plus the registry.
* `declared` holds the declarations the module already carries; runtime
* entry points it lacks are declared here. A module the translator
* cannot translate is left out of the registry, so loading it at run
* time rejects with the CompileError for untranslated bytes. */
export function emitWasmModules(
mod: IrModule,
declared: ReadonlySet<string>,
boundsChecks: boolean,
): string[] {
const modules = mod.wasmModules;
if (modules === undefined) return [];
const out: string[] = [];
const decls = new Set(declared);
const descriptors: string[] = [];
for (const module of modules) {
const bytes = decodeBase64(module.bytes);
if (bytes === null) continue;
let translation;
try {
translation = translateWasmModule(bytes, hexBytes(module.sha256), {
boundsChecks,
customSections: mod.wasmCustomSections === true,
});
} catch (error) {
if (error instanceof WasmTranslateError) continue;
throw error;
}
for (const decl of translation.decls) {
if (decls.has(decl)) continue;
decls.add(decl);
out.push(decl);
}
for (const part of translation.text) out.push(part);
descriptors.push(translation.descriptor);
}
if (descriptors.length === 0) {
out.push(`@scr_wasm_registry = constant { i64, ptr } { i64 0, ptr null }`);
} else {
out.push(
`@sc_wasm_registry_modules = private constant [${descriptors.length} x ptr] [${descriptors.map((d) => `ptr ${d}`).join(", ")}]`,
`@scr_wasm_registry = constant { i64, ptr } { i64 ${descriptors.length}, ptr @sc_wasm_registry_modules }`,
);
}
out.push("");
return out;
}
@@ -25,6 +25,9 @@ export interface NativeLinkFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
/** The WebAssembly API (scr_wasm.c). Absent when unused, so programs
* without it keep their link identity. */
wasm?: boolean;
/** The program assigns process.emitWarning: links the
* SCR_WARNING_REPLACE variant of the dyn-async unit. */
warningReplace?: boolean;
@@ -40,18 +43,24 @@ export interface NativeLinkFeatures {
/** Dynamic-tier jsval operations: the scr_js_* units. Absent (not false)
* for programs without them, so their link features stay unchanged. */
dynTier?: boolean;
/** Checked-dynamic stream slots: the scr_stream_dyn.c unit. Absent (not
* false) for programs without them. */
streamDyn?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
bigint: boolean;
searchParams: boolean;
qs: boolean;
dynBuiltinClass: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
http: boolean;
http2: boolean;
dgram: boolean;
/** node:dns/promises: scr_dns_promises.c. */
dnsPromises: boolean;
watch: boolean;
foreignFfi: boolean;
nodeTest: boolean;
@@ -59,6 +68,8 @@ export interface NativeLinkFeatures {
/** crypto.createCipheriv/createDecipheriv: scr_cipher.c plus the mbedTLS
* archive. */
cipher: boolean;
/** perf_hooks event-loop metrics: scr_perf_loop.c. */
perfLoop: boolean;
tlsCa: boolean;
/** crypto.subtle importKey/sign/verify/digest and CryptoKey values:
* scr_webcrypto.c. */
@@ -113,11 +113,16 @@ export interface CcOptions {
* the IR): compiles the scr_js_* units (two-tier design I3). Programs
* without them link no scr_js_ symbol. */
dynTier?: boolean;
/** Checked-dynamic stream slots (stream.dynNative / stream.pipeDyn):
* compiles scr_stream_dyn.c. */
streamDyn?: boolean;
/** The program constructs EventTarget/Event/CustomEvent values or
* calls events.addAbortListener (the event.* libCalls), or uses the
* native fetch AbortSignal built on the same engine: compiles
* scr_event_target.c. */
eventTarget?: boolean;
/** The WebAssembly API: compiles scr_wasm.c. */
wasm?: boolean;
/** SCR_WARNING_REPLACE: process.emitWarning replacement support. */
warningReplace?: boolean;
/** SCR_STDIO_EXTENDED: stdin encoding, string data listeners, resize. */
@@ -150,6 +155,11 @@ export interface CcOptions {
* line (escape-only programs ride the always-linked component encoder
* and never flip this). */
qs?: boolean;
/** The program uses EventEmitter or a stream class as an untyped
* constructor value (a constructor function's base): compiles
* scr_dyn_builtin_class.c, which also references the inspection,
* dyn-invoke and symbol units. */
dynBuiltinClass?: boolean;
/** The program uses native util parsing, comparison, or styling. Optional
* utility units also require their symbol, inspection, and warning features. */
parseArgs?: boolean;
@@ -177,6 +187,11 @@ export interface CcOptions {
* on the IR): compiles scr_dgram.c into the binary — the net gating
* precedent, so dgram-free binaries keep their exact link line. */
dgram?: boolean;
/** The program uses node:dns/promises (the dns.promisesLookup libCalls):
* compiles scr_dns_promises.c over the dgram unit, so callback-only
* binaries keep their exact link line. */
dnsPromises?: boolean;
/** The program uses fs.watch (moduleUsesFsWatch on the IR): compiles
* scr_watch.c into the binary — the net gating precedent, so watch-free
* binaries keep their exact link line. */
@@ -209,6 +224,8 @@ export interface CcOptions {
cipher?: boolean;
/** crypto.subtle importKey/sign/verify/digest: compiles scr_webcrypto.c. */
webcrypto?: boolean;
/** perf_hooks event-loop metrics: compiles scr_perf_loop.c. */
perfLoop?: boolean;
/** Internal compiler hook: called only after a strict native artifact hit
* or a successful stable build has installed `outPath`. The executable
* frontend cache uses it to publish its stamp after native dependencies
@@ -182,6 +182,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.opts.dc, "scr_dc.c"),
unit((u) => u.opts.dynTier, "scr_js_value.c"),
unit((u) => u.opts.eventTarget || u.nativeFetch, "scr_event_target.c"),
unit((u) => u.opts.wasm, "scr_wasm.c"),
unit(
(u) =>
u.opts.dynAsync ||
@@ -195,6 +196,8 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
// The performance timeline (every perf_hooks user-timing call selects
// dynAsync, whose unit carries the timeline's buffer warning).
unit((u) => u.opts.dynAsync, "scr_perf.c"),
// perf_hooks event-loop metrics, only for programs that read them.
unit((u) => u.opts.perfLoop, "scr_perf_loop.c"),
// Blob and File (every blob.* call selects dynAsync, whose unit boxes
// their reads as dyn promises).
unit((u) => u.opts.dynAsync, "scr_blob.c"),
@@ -235,11 +238,23 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
(u) => u.opts.parseArgs && u.opts.inspect && u.opts.dynAsync && u.opts.dynInvoke && u.regex,
"scr_util_callable.c",
),
unit(
(u) =>
u.opts.dynBuiltinClass &&
u.opts.inspect &&
u.opts.dynInvoke &&
u.opts.symbol &&
(u.opts.emitter || u.opts.workers),
"scr_dyn_builtin_class.c",
),
unit((u) => u.opts.stream, "scr_stream.c", "scr_fs_stream.c"),
// The zlib stream classes reference both the zlib engine and the
// stream unit.
unit((u) => u.opts.zlib && u.opts.stream, "scr_zlib_stream.c"),
unit((u) => u.opts.stream && u.opts.dynAsync, "scr_stream_ops.c"),
// Checked-dynamic stream slots test values and call JavaScript stream
// members through the dyn-invoke unit.
unit((u) => u.opts.streamDyn === true, "scr_stream_dyn.c"),
// FileHandle streams reference both the stream unit and FileHandle.
unit((u) => u.opts.stream && u.opts.fileHandle, "scr_fs_stream_fh.c"),
// The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
@@ -252,6 +267,7 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
unit((u) => u.http, "scr_http.c"),
unit((u) => u.opts.http2, "scr_http2.c"),
unit((u) => u.opts.dgram, "scr_dgram.c"),
unit((u) => u.opts.dgram && u.opts.dnsPromises, "scr_dns_promises.c"),
unit((u) => u.opts.watch, "scr_watch.c"),
unit((u) => u.opts.foreignFfi, "scr_ffi_queue.c"),
unit((u) => u.opts.nodeTest, "scr_test.c"),
@@ -82,7 +82,9 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
workers: boolean;
youngNursery: boolean;
eventTarget: boolean;
wasm: boolean;
dynTier: boolean;
streamDyn: boolean;
warningReplace: boolean;
stdioExtended: boolean;
nativeFetch: boolean;
@@ -272,7 +274,9 @@ export function effectiveRuntimeFeatures(
workers: features.workers === true,
youngNursery: features.youngNursery === true,
eventTarget: features.eventTarget === true,
wasm: features.wasm === true,
dynTier: features.dynTier === true,
streamDyn: features.streamDyn === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
nativeFetch,
@@ -107,12 +107,14 @@ const features = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -120,6 +122,7 @@ const features = {
tlsCa: false,
cipher: false,
webcrypto: false,
perfLoop: false,
};
test.each([
@@ -44,12 +44,14 @@ const BASE: NativeLinkFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -57,6 +59,7 @@ const BASE: NativeLinkFeatures = {
tlsCa: false,
cipher: false,
webcrypto: false,
perfLoop: false,
};
async function fixture(target: NativeTargetSpec = MACOS_ARM64_TARGET) {
@@ -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:
@@ -50,18 +50,21 @@ export async function warmNativeCaches(
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
tls: false,
cipher: false,
webcrypto: false,
perfLoop: false,
tlsCa: false,
},
});
+4 -4
View File
@@ -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";
@@ -444,16 +444,17 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
}
}
// ── the WebAssembly global: every reached use refuses (webAssemblyDiag) ─
// ── the WebAssembly global (builtins/webassembly.ts, wasm/translate.ts):
// the core API lowers over modules translated at build time; the rest
// refuses at each use (webAssemblyDiag) ─
{
const refusal = webAssemblyDiag("WebAssembly", { file: "", start: 0, end: 0 });
add({
id: "stdlib.webassembly",
kind: "stdlib",
name: "globalThis.WebAssembly",
status: "unsupported",
code: refusal.code,
...(refusal.hint !== undefined ? { note: refusal.hint } : {}),
status: "static",
note: `compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, instanceof, the error classes, exports, imports and Module transfer to workers, over the modules translated at build time; the rest refuses with ${refusal.code} (${refusal.hint ?? ""})`,
});
}
+15 -23
View File
@@ -59,8 +59,9 @@
* order that differs from insertion order where the program
* observes it (SC6002), localeCompare over text the built-in
* collation does not cover (SC6003), a call feeding an instance
* to a cast to an unrelated class (SC6004), and Date strings the native parser does not
* accept (SC6005)
* to a cast to an unrelated class (SC6004); SC6005 (Date strings
* outside the native parser's grammar) is retired — the runtime
* parses date strings with V8's grammar
* SC9xxx internal compiler errors (still source-anchored)
*/
import type { SrcLoc } from "../ir/ir.js";
@@ -200,18 +201,6 @@ export function unrelatedClassCastDivergenceDiag(
};
}
/** SC6005 — a Date string literal outside the grammar the native parser
* accepts (ISO 8601 with an explicit offset, date-only forms, and the
* certificate-time shape): Node parses many more forms. */
export function dateLiteralDivergenceDiag(text: string, loc: SrcLoc): ScrDiagnostic {
return {
code: "SC6005",
message: `the date string '${text}' parses to an invalid date (NaN) natively, while Node accepts it (the native parser takes ISO 8601 date-times with an explicit offset, such as '2024-01-02T03:04:05Z', and date-only forms)`,
loc,
hint: "write the date in ISO 8601 with an explicit offset ('Z' or '+HH:MM'), or build it from numbers with Date.UTC",
};
}
/* ── SC5xxx: native FFI ───────────────────────────────────────────────── */
/** SC5001 — the outbound native-FFI manifest is malformed or unreadable. */
@@ -282,11 +271,11 @@ export const UNSUPPORTED: Record<string, UnsupportedEntry> = {
"imports (#alias via the imports field, self-name references via " +
"exports), statically compiled npm packages, " +
"and the built-in fs, fs/promises, path, os, url, crypto, zlib, " +
"child_process, net, http, tls, https, http2, dgram, dns, util, " +
"child_process, net, http, tls, https, http2, dgram, dns, dns/promises, util, " +
"util/types, string_decoder, querystring, readline, events, stream, " +
"stream/promises, stream/consumers, buffer, assert, assert/strict, worker_threads, " +
"stream/promises, stream/consumers, stream/web, buffer, assert, assert/strict, worker_threads, " +
"cluster, tty, async_hooks, timers, timers/promises, " +
"diagnostics_channel, perf_hooks, module, and console modules (bare or " +
"diagnostics_channel, perf_hooks, module, v8, and console modules (bare or " +
"node:-prefixed) and node:test (node:-prefixed only, like in Node) " +
"are supported",
},
@@ -751,17 +740,20 @@ export function noLoweringDiag(
};
}
/** A REACHED use of the WebAssembly API (SC2020): scriptc.d.ts declares
* its types so packages that mention them typecheck, but no module
* compiles to native code yet. `surface` names what was used
* ('WebAssembly.compile', 'new WebAssembly.Module', 'globalThis.WebAssembly'). */
/** A REACHED use of a part of the WebAssembly API scriptc does not
* implement yet (SC2020): the streaming functions, Tag and Exception, the
* classes as values. compile, instantiate, validate, the Module, Instance,
* Memory, Table and Global constructors, Module.imports/exports/
* customSections and the error classes lower (builtins/webassembly.ts).
* `surface` names what was used ('WebAssembly.compileStreaming',
* 'WebAssembly.Memory'). */
export function webAssemblyDiag(surface: string, loc: SrcLoc): ScrDiagnostic {
return {
code: "SC2020",
message: `'${surface}' is not supported yet: scriptc does not compile WebAssembly modules to native code yet`,
message: `'${surface}' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, and the error classes over modules translated at build time, but not this part of the API`,
loc,
milestone: "later",
hint: "WebAssembly support is planned as translation of each module whose bytes the build can see (an inline literal, or a file read from a path the compiler resolves) into native code; 'scriptc coverage' lists the modules this program would load (https://scriptc.dev/limitations)",
hint: "the streaming functions, Tag and Exception, and the classes used as values other than with new and instanceof are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)",
};
}
@@ -45,12 +45,14 @@ const native: EarlyExecutableNativeFeatures = {
bigint: false,
searchParams: false,
qs: false,
dynBuiltinClass: false,
parseArgs: false,
stream: false,
net: false,
http: false,
http2: false,
dgram: false,
dnsPromises: false,
watch: false,
foreignFfi: false,
nodeTest: false,
@@ -58,6 +60,7 @@ const native: EarlyExecutableNativeFeatures = {
tlsCa: false,
cipher: false,
webcrypto: false,
perfLoop: false,
};
async function fixture(): Promise<{
@@ -199,7 +202,7 @@ test("early executable cache misses on source and resolution changes", async ()
expect(await readEarlyExecutableCache(f.root, f.options)).toBeNull();
});
test("early executable keys isolate compile modes, paths, implementation, and FFI manifests", async () => {
test("early executable keys isolate compile modes, paths, implementation, checker, and FFI manifests", async () => {
const f = await fixture();
const tracker = new FrontendInputTracker();
tracker.run(() => trackedReadFile(f.source));
@@ -216,6 +219,7 @@ test("early executable keys isolate compile modes, paths, implementation, and FF
{ ...f.options, optimization: "dev" },
{ ...f.options, optimization: "speed" },
{ ...f.options, detachedExit: true },
{ ...f.options, checker: "tsc-ts-executable-identity" },
{ ...f.options, npmStatic: "auto" },
{ ...f.options, npmStatic: ["commander"] },
{ ...f.options, outPath: join(f.options.outDir, "other") },
@@ -56,6 +56,7 @@ export interface EarlyExecutableNativeFeatures {
fileHandle: boolean;
fetch: boolean;
eventTarget?: boolean;
wasm?: boolean;
warningReplace?: boolean;
stdioExtended?: boolean;
zlib: boolean;
@@ -65,23 +66,27 @@ export interface EarlyExecutableNativeFeatures {
dc: boolean;
dynAsync: boolean;
dynTier?: boolean;
streamDyn?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
bigint: boolean;
searchParams: boolean;
qs: boolean;
dynBuiltinClass: boolean;
parseArgs: boolean;
stream: boolean;
net: boolean;
http: boolean;
http2: boolean;
dgram: boolean;
dnsPromises: boolean;
watch: boolean;
foreignFfi: boolean;
nodeTest: boolean;
tls: boolean;
cipher: boolean;
perfLoop: boolean;
tlsCa: boolean;
webcrypto: boolean;
}
@@ -116,6 +121,9 @@ export interface EarlyExecutableCacheOptions {
windowsSubsystem?: "gui";
/** Omitted unless --detached-exit is set. */
detachedExit?: true;
/** Omitted for the default tsgo checker; with SCRIPTC_CHECKER=tsc-ts, the
* tsc-ts executable's file identity. */
checker?: string;
/** Omitted unless SCRIPTC_YOUNG_NURSERY=1 (backend/young-nursery.ts). */
youngNursery?: true;
npmStatic: readonly string[] | "auto" | null;
@@ -185,12 +193,14 @@ const BOOLEAN_NATIVE_KEYS = [
"bigint",
"searchParams",
"qs",
"dynBuiltinClass",
"parseArgs",
"stream",
"net",
"http",
"http2",
"dgram",
"dnsPromises",
"watch",
"foreignFfi",
"nodeTest",
@@ -198,6 +208,7 @@ const BOOLEAN_NATIVE_KEYS = [
"tlsCa",
"cipher",
"webcrypto",
"perfLoop",
] as const satisfies readonly (keyof EarlyExecutableNativeFeatures)[];
function validNativeFeatures(value: unknown): value is EarlyExecutableNativeFeatures {
@@ -230,6 +241,7 @@ function cacheKey(options: EarlyExecutableCacheOptions): string {
...(options.strip ? ["strip"] : []),
...(options.windowsSubsystem === "gui" ? ["windows-subsystem-gui"] : []),
...(options.detachedExit ? ["detached-exit"] : []),
...(options.checker !== undefined ? [`checker:${options.checker}`] : []),
...(options.youngNursery ? ["young-nursery"] : []),
options.npmStatic === null
? "<npm-static-off>"
@@ -270,6 +282,7 @@ function routeKey(options: Omit<EarlyExecutableRouteOptions, "nativeEnvironment"
if (options.strip) hash.update("strip\0");
if (options.windowsSubsystem === "gui") hash.update("windows-subsystem-gui\0");
if (options.detachedExit) hash.update("detached-exit\0");
if (options.checker !== undefined) hash.update(`checker:${options.checker}\0`);
if (options.youngNursery) hash.update("young-nursery\0");
hash
.update(
@@ -42,6 +42,9 @@ export const SUPPORTED_BUILTIN_MODULES = [
"module",
"console",
"process",
"v8",
"stream/web",
"dns/promises",
"punycode",
"constants",
] as const;
@@ -76,14 +79,33 @@ export function builtinDefaultImportModule(spec: string): string | null {
export function canonicalBuiltinModule(spec: string): string | null {
const bare = spec.startsWith("node:") ? spec.slice(5) : spec;
if (bare === spec && PREFIX_ONLY_BUILTIN_MODULES.has(bare)) return null;
return (SUPPORTED_BUILTIN_MODULES as readonly string[]).includes(bare) ? bare : null;
if ((SUPPORTED_BUILTIN_MODULES as readonly string[]).includes(bare)) return bare;
return bare === spec ? npmBuiltinAliasOf(spec) : null;
}
/** npm packages served as a builtin module instead of compiling their
* JavaScript (maintainer decision): graceful-fs monkeypatches the fs
* module object and queues retries on EMFILE/ENFILE, which statically
* lowered fs calls cannot honor, and it behaves as fs everywhere else. Its
* import and require resolve to node:fs; gracefulify is a no-op statement.
* The one documented difference: no retry queueing under file-descriptor
* exhaustion. */
const NPM_BUILTIN_ALIASES: Record<string, string | undefined> = { "graceful-fs": "fs" };
/** The builtin module an npm package specifier is served as, or null. */
export function npmBuiltinAliasOf(spec: string): string | null {
return Object.hasOwn(NPM_BUILTIN_ALIASES, spec) ? (NPM_BUILTIN_ALIASES[spec] ?? null) : null;
}
/** Both specifier spellings of every supported builtin (prefix-only ones
* keep just the node: form) — the ambient-module allowlist preflight uses. */
export const SUPPORTED_NODE_MODULES: readonly string[] = SUPPORTED_BUILTIN_MODULES.flatMap((m) =>
PREFIX_ONLY_BUILTIN_MODULES.has(m) ? [`node:${m}`] : [m, `node:${m}`],
);
* keep just the node: form) — the ambient-module allowlist preflight uses
* — plus the npm packages served as builtins. */
export const SUPPORTED_NODE_MODULES: readonly string[] = [
...SUPPORTED_BUILTIN_MODULES.flatMap((m) =>
PREFIX_ONLY_BUILTIN_MODULES.has(m) ? [`node:${m}`] : [m, `node:${m}`],
),
...Object.keys(NPM_BUILTIN_ALIASES),
];
/* ── out-of-scope builtins ───────────────────────────────────────────────
* Builtin modules a compiled binary is not going to serve — not "yet",
@@ -92,8 +114,8 @@ export const SUPPORTED_NODE_MODULES: readonly string[] = SUPPORTED_BUILTIN_MODUL
* so 'node:sqlite' keys as 'sqlite'. Exact-match only: an npm package
* whose name collides with none of these keys keeps the generic wording.
* Everything absent from this table stays the plain "not supported yet"
* story — genuinely pending surface (console's Console class, stream/web)
* must not read as permanently refused. */
* story — genuinely pending surface (console's Console class) must not
* read as permanently refused. */
/** Node's own HTTP/TLS/stream implementation internals, requireable by
* legacy convention (underscore-prefixed lib/ modules). They expose
@@ -102,11 +124,15 @@ export const SUPPORTED_NODE_MODULES: readonly string[] = SUPPORTED_BUILTIN_MODUL
const NODE_INTERNAL_REASON =
"a Node-internal module: it exposes Node's own implementation objects, which the scriptc runtime does not replicate";
const V8_REASON =
"it observes V8 engine internals — heap statistics and snapshots, GC and CPU profiles, serialize/deserialize — and a compiled binary embeds no V8 engine to observe";
/** node:v8 is importable (packages require it at top level and touch it
* only on optional paths), and getHeapStatistics answers allocator-derived
* figures; every other member observes the engine itself. Member fences
* carry this reason instead of a module-level refusal. */
export const V8_MEMBER_REASON =
"by design: this member observes V8 engine internals — heap snapshots and spaces, GC and CPU profiles, flags, serialize/deserialize — and a compiled binary embeds no V8 engine to observe";
const OUT_OF_SCOPE_BUILTIN_REASONS: Record<string, string | undefined> = {
v8: V8_REASON,
vm: "it compiles and runs JavaScript source at runtime, and a compiled binary embeds no JavaScript engine to run it",
inspector:
"it drives the V8 inspector protocol — debugger, profiler, heap access — and a compiled binary embeds no V8 engine to inspect",
"inspector/promises":
@@ -142,3 +168,10 @@ export function unsupportedModuleFeatureOf(spec: string): string {
: undefined;
return reason === undefined ? `the '${spec}' module` : `the '${spec}' module (${reason})`;
}
/** The ESNext temporal global's name. Node 24 does not define it: programs
* that name it type-check against TS 7's lib slice (program.ts adds it only
* for them) and lower to Node's answers. Spelled in two pieces so the
* compiler's own sources never trip program.ts's scan when scriptc
* compiles itself. */
export const TEMPORAL_GLOBAL = "Tempo" + "ral";
@@ -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<string, IrType>();
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
@@ -1,4 +1,5 @@
import { lowerReadlineCursorCall } from "./builtins/readline.js";
import { lowerMonitorEventLoopDelayCall } from "./builtins/performance.js";
import {
lowerEnvironmentData,
lowerReceiveMessageOnPort,
@@ -15,6 +16,8 @@ import { lowerStreamModuleCall } from "./lower-stream.js";
import { lowerCallbackPromisify } from "./builtins/promisify-callback.js";
import { lowerNodeModuleCall } from "./builtins/module-resolution.js";
import { builtinModuleFnOf, builtinFenceHintOf } from "./surfaces.js";
import { resultIsDiscarded } from "./call-position.js";
import { locOf, requireSpecOf } from "../program.js";
/** `events.addAbortListener(signal, listener)`: the runtime registers the
* once/resist-stop-propagation abort listener (or queues the call for an
@@ -54,6 +57,38 @@ export function lowerAddAbortListener(
return { kind: "dynObjLit", fields: [{ key, value: dispose }], type: DYN, loc };
}
/** graceful-fs's `gracefulify(fsModule)` (graceful-fs is served as node:fs —
* builtin-modules.ts): it patches a module object with the retry queue,
* which statically lowered fs calls never consult, so the call statement
* is a no-op. The argument still evaluates unless it is a plain reference
* (the patched module, typically `require("fs")` or a namespace binding).
* The returned module object has no representation; other positions
* fence. */
function lowerGracefulify(lowerer: Lowerer, expr: ts.CallExpression, loc: SrcLoc): IrExpr {
const args = expr.arguments.nodes;
if (!resultIsDiscarded(expr) || args.length !== 1 || ts.isSpreadElement(args[0]!)) {
lowerer.noLowering(
"gracefulify",
expr,
"graceful-fs compiles as node:fs; gracefulify(fsModule) lowers as a statement whose result is unused",
);
}
let arg: ts.Expression = args[0]!;
while (ts.isParenthesizedExpression(arg)) arg = arg.expression;
const plain =
ts.isIdentifier(arg) ||
(ts.isCallExpression(arg) && requireSpecOf(arg) !== null) ||
(ts.isPropertyAccessExpression(arg) && ts.isIdentifier(arg.expression));
if (plain) return { kind: "seqExpr", stmts: [], result: dynUndefinedExpr(loc), type: DYN, loc };
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc: locOf(arg) }],
result: dynUndefinedExpr(loc),
type: DYN,
loc,
};
}
/** Named imports, namespace members and callable aliases share validation
* and dispatch. Resolve diagnostic provenance only if no handler accepts. */
export function lowerBuiltinCall(
@@ -70,6 +105,9 @@ export function lowerBuiltinCall(
return lowerAddAbortListener(lowerer, expr, loc);
}
if (bi.module === "events" && bi.member === "once") return lowerEventsOnce(lowerer, expr, loc);
if (bi.module === "perf_hooks" && bi.member === "monitorEventLoopDelay") {
return lowerMonitorEventLoopDelayCall(lowerer, expr);
}
if (bi.module === "events") {
const helper = lowerEventsEmitterHelper(lowerer, expr, bi.member, loc);
if (helper) return helper;
@@ -126,6 +164,8 @@ export function lowerBuiltinCall(
if (bi.module === "worker_threads" && bi.member === "markAsUncloneable") {
return lowerUncloneable(lowerer, expr);
}
if (bi.module === "fs" && bi.member === "gracefulify")
return lowerGracefulify(lowerer, expr, loc);
const builtinFn = builtinModuleFnOf(lowerer, bi.module, bi.member);
if (!builtinFn) {
lowerer.noLowering(
@@ -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<Record<string, Readonly<Record<string, number>>>> = {
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,
};
}
@@ -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<ts.Node>();
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;
}
}
@@ -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);
@@ -52,6 +52,20 @@ export function refuseExecExceptionCode(lowerer: Lowerer, node: ts.Node): never
* declaration is not stdlib). Reads only: writes keep their fence (no
* compiled program constructs an errno error). Null for non-error
* receivers and non-stdlib members, so the chain keeps trying. */
/** Error members with slots of their own: never read through the dynamic
* node, even when an intersection restates them. */
const ERROR_SLOT_MEMBERS = new Set([
"code",
"cause",
"stack",
"message",
"name",
"errno",
"syscall",
"path",
"dest",
]);
export function lowerErrorCodeProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
@@ -135,6 +149,33 @@ export function lowerErrorCodeProperty(
declared,
);
}
// A member an `Error & { ... }` intersection adds beyond Error's own and
// the system-error metadata (`Object.assign(new Error(m), { status })`):
// an own property of the error's checked-dynamic node, read at the
// declared type (the intersection mapping admits only checkable types).
if (
recvT.className === "%Error" &&
!ERROR_SLOT_MEMBERS.has(expr.name.text) &&
!expr.questionDotToken &&
!lowerer.isStdlibMember(expr) &&
lowerer.typeOf(expr.expression).isIntersectionType()
) {
const declared = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (declared && declared.kind !== "void") {
const receiver = lowerer.lowerExpr(expr.expression);
const loc = locOf(expr);
return lowerer.coerceToExpected(
{
kind: "dynKeyGet",
value: lowerer.coerceToExpected(receiver, DYN),
key: { kind: "strLit", value: expr.name.text, type: STRING, loc },
type: DYN,
loc,
},
declared,
);
}
}
// A program class under the receiver's class can carry its own `code`
// (a declared field or a property added through an untyped reference),
// which Node reads instead of the errno code: read through the error's
@@ -23,6 +23,17 @@ import { stripTypeCasts } from "./arguments.js";
export function builtinImportOf(
lowerer: Lowerer,
ident: ts.Identifier,
): { module: string; member: string } | null {
const binding = builtinModuleBindingOf(lowerer, ident);
// node:stream/web's exports ARE the web stream globals: its bindings
// lower through the globals' own paths (stdlibGlobalNameOf resolves the
// alias to the same name), not through module-member tables.
return binding?.module === "stream/web" ? null : binding;
}
function builtinModuleBindingOf(
lowerer: Lowerer,
ident: ts.Identifier,
): { module: string; member: string } | null {
const symbol =
ts.isShorthandPropertyAssignment(ident.parent) && ident.parent.name === ident
@@ -48,6 +48,7 @@ import {
} from "./operating-system.js";
import { lowerQuerystringParseCall, lowerQuerystringStringifyCall } from "./querystring.js";
import { lowerFsGlobCallbackCall, lowerFsGlobSyncCall, lowerFspGlobCall } from "./fs-glob.js";
import { lowerV8HeapStatisticsCall } from "./v8.js";
export function lowerBuiltinModuleCall(
lowerer: Lowerer,
@@ -134,6 +135,9 @@ export function lowerBuiltinModuleCall(
if (bi.module === "child_process" && (bi.member === "execFileSync" || bi.member === "execSync")) {
return lowerer.lowerExecSyncCall(expr, bi.member === "execSync", loc);
}
if (bi.module === "v8" && bi.member === "getHeapStatistics") {
return lowerV8HeapStatisticsCall(lowerer, expr, loc);
}
if (bi.module === "os" && bi.member === "networkInterfaces") {
return lowerOsNetworkInterfacesCall(lowerer, expr, loc);
}
@@ -18,6 +18,7 @@ import {
} from "../../../ir/ir.js";
import { PERF_ENTRY_FIELDS, PERF_ENTRY_MARKER, perfEntryJsonShape } from "../../type-mapper.js";
import { builtinImportOf } from "./module-bindings.js";
import { lowerOptionalArgument } from "../optional-arguments.js";
import { checkedIterableSpread } from "../checked-iterable-spread.js";
/** True for any spelling of node:perf_hooks' `performance` object — the
@@ -134,6 +135,12 @@ export function lowerPerfHooksCall(
loc,
};
}
if (
access.name.text === "eventLoopUtilization" &&
isPerfHooksPerformanceExpr(lowerer, access.expression)
) {
return lowerEventLoopUtilization(lowerer, expr);
}
const timeline = PERF_TIMELINE_CALLS.get(access.name.text);
if (timeline && isPerfHooksPerformanceExpr(lowerer, access.expression)) {
return lowerPerfTimelineCall(lowerer, expr, access.name.text, timeline, loc);
@@ -142,7 +149,7 @@ export function lowerPerfHooksCall(
lowerer.noLowering(
`perf_hooks performance.${access.name.text}`,
expr,
"performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks and clearMeasures are the lowered surface",
"performance.now() (and its .bind(performance) function value), mark, measure, the getEntries family, clearMarks, clearMeasures and eventLoopUtilization are the lowered surface",
);
}
return null;
@@ -513,3 +520,305 @@ export function fencePerfEntryKeyWalk(
`${what} over performance entries (Node keeps their fields as prototype getters, so own-key walks see none; read the fields or call toJSON())`,
);
}
/* ── event-loop metrics: eventLoopUtilization and monitorEventLoopDelay ── */
/** The stdlib IntervalHistogram (monitorEventLoopDelay's result), by the
* receiver's checker type. Its value is the runtime histogram handle. */
function isIntervalHistogram(lowerer: Lowerer, node: ts.Expression): boolean {
const symbol = lowerer.typeOf(node).getSymbol();
return symbol?.name === "IntervalHistogram" && lowerer.isStdlibSymbol(symbol);
}
/** IntervalHistogram's numeric members, as perf.eldStat selectors. */
const HISTOGRAM_STATS: ReadonlyMap<string, number> = new Map([
["min", 0],
["max", 1],
["mean", 2],
["stddev", 3],
["count", 4],
["exceeds", 5],
]);
/** `h.min`, `h.max`, … on an IntervalHistogram: the runtime's current
* statistic (Node's hdr_histogram values; nanoseconds). Null for every
* other read. */
export function lowerHistogramProperty(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
): IrExpr | null {
const which = HISTOGRAM_STATS.get(expr.name.text);
if (
which === undefined ||
expr.questionDotToken ||
!isIntervalHistogram(lowerer, expr.expression)
)
return null;
const loc = locOf(expr);
return {
kind: "libCall",
fn: "perf.eldStat",
args: [lowerer.lowerExprExpecting(expr.expression, F64), numLit(which, loc)],
type: F64,
loc,
};
}
/** enable(), disable(), reset() and percentile(p) on an IntervalHistogram.
* Other members (the BigInt twins, the percentiles Map) fence by name. */
export function lowerHistogramMethodCall(
lowerer: Lowerer,
expr: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (access.questionDotToken || !isIntervalHistogram(lowerer, access.expression)) return null;
const loc = locOf(expr);
const name = access.name.text;
const handle = (): IrExpr => lowerer.lowerExprExpecting(access.expression, F64);
const args = expr.arguments.nodes;
if ((name === "enable" || name === "disable" || name === "reset") && args.length === 0) {
const fn =
name === "enable"
? "perf.eldEnable"
: name === "disable"
? "perf.eldDisable"
: "perf.eldReset";
return { kind: "libCall", fn, args: [handle()], type: name === "reset" ? VOID : BOOL, loc };
}
if (name === "percentile" && args.length === 1 && !ts.isSpreadElement(args[0]!)) {
// Node validates first: NaN, p <= 0 and p > 100 throw ERR_OUT_OF_RANGE.
const h = lowerer.declareHiddenLocal("%histogram", F64);
const p = lowerer.declareHiddenLocal("%percentile", F64);
const pRef = (): IrExpr => varRef(p.id, F64, loc);
const cmp = (op: "!==" | "<=" | ">", left: IrExpr, right: IrExpr): IrExpr => ({
kind: "bin",
op,
left,
right,
type: BOOL,
loc,
});
const or = (left: IrExpr, right: IrExpr): IrExpr => ({
kind: "logical",
op: "||",
left,
right,
type: BOOL,
loc,
});
const outOfRange = or(
or(cmp("!==", pRef(), pRef()), cmp("<=", pRef(), numLit(0, loc))),
cmp(">", pRef(), numLit(100, loc)),
);
const message: IrExpr = {
kind: "strConcat",
left: {
kind: "strLit",
value: 'The value of "percentile" is out of range. It must be > 0 && <= 100. Received ',
type: STRING,
loc,
},
right: { kind: "toString", operand: pRef(), type: STRING, loc },
type: STRING,
loc,
};
const thrown: IrExpr = {
kind: "libCall",
fn: "error.nodeThrow",
args: [
numLit(2, loc),
{ kind: "strLit", value: "ERR_OUT_OF_RANGE", type: STRING, loc },
message,
],
type: F64,
loc,
};
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: h.id, init: handle(), loc },
{ kind: "varDecl", localId: p.id, init: lowerer.lowerExprExpecting(args[0]!, F64), loc },
],
result: {
kind: "ternary",
cond: outOfRange,
then: thrown,
else_: {
kind: "libCall",
fn: "perf.eldPercentile",
args: [varRef(h.id, F64, loc), pRef()],
type: F64,
loc,
},
type: F64,
loc,
},
type: F64,
loc,
};
}
lowerer.noLowering(
`IntervalHistogram.${name}`,
expr,
"enable(), disable(), reset(), percentile(p) and the min, max, mean, stddev, count and exceeds readings are the lowered surface",
);
}
/** `monitorEventLoopDelay(options?)` from node:perf_hooks, in any import
* spelling (`lowerBuiltinCall`): a new sampling histogram handle. The
* options are an inline object literal; the runtime validates the
* resolution as Node does (an integer of at least 1). */
export function lowerMonitorEventLoopDelayCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
const args = expr.arguments.nodes;
let resolution: IrExpr = numLit(10, loc);
if (args.length > 1 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(
"this monitorEventLoopDelay call form",
expr,
"pass at most one options object",
);
}
if (args.length === 1) {
const options = args[0]!;
if (!ts.isObjectLiteralExpression(options)) {
lowerer.noLowering(
"monitorEventLoopDelay with a non-literal options object",
options,
"spell the options inline: monitorEventLoopDelay({ resolution: 20 })",
);
}
for (const property of options.properties.nodes) {
const shorthand = ts.isShorthandPropertyAssignment(property);
if (
(!shorthand && !ts.isPropertyAssignment(property)) ||
!ts.isIdentifier(property.name) ||
property.name.text !== "resolution"
) {
lowerer.noLowering(
"monitorEventLoopDelay options other than resolution",
property,
"resolution is the one option",
);
}
// An undefined resolution takes the default, as Node's destructuring.
resolution = lowerOptionalArgument(
lowerer,
shorthand ? property.name : property.initializer,
F64,
numLit(10, loc),
);
}
}
return { kind: "libCall", fn: "perf.eldNew", args: [resolution], type: F64, loc };
}
/** performance.eventLoopUtilization(u1?, u2?) — Node's
* lib/internal/perf/event_loop_utilization.js over the runtime's loop
* start and idle time, one lifted helper per arity. */
function lowerEventLoopUtilization(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
const loc = locOf(expr);
const args = expr.arguments.nodes;
const resultT = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (resultT?.kind !== "record" || args.length > 2 || args.some(ts.isSpreadElement)) {
lowerer.noLowering("this performance.eventLoopUtilization call form", expr);
}
const helper = eluHelperOf(lowerer, args.length, resultT);
return {
kind: "call",
callee: helper,
args: args.map((arg) => lowerer.lowerExprExpecting(arg, resultT)),
type: resultT,
loc,
};
}
function eluHelperOf(lowerer: Lowerer, arity: number, recT: IrType & { kind: "record" }): string {
const name = `%perf.elu.${arity}`;
if (lowerer.liftedFns.some((f) => f.name === name)) return name;
const loc: SrcLoc = { file: "<builtin>", start: 0, end: 0 };
const f64Local = (id: string) => ({ id, name: id, type: F64, mutable: false });
const params = Array.from({ length: arity }, (_, i) => ({
localId: `u${i + 1}.0`,
name: `u${i + 1}`,
type: recT as IrType,
}));
const locals = [
...params.map((p) => ({ id: p.localId, name: p.name, type: recT as IrType, mutable: false })),
f64Local("ls.0"),
f64Local("idle.0"),
f64Local("active.0"),
];
const ref = (id: string): IrExpr => varRef(id, F64, loc);
const field = (param: string, f: string): IrExpr => ({
kind: "recordGet",
obj: varRef(param, recT, loc),
shapeId: recT.shapeId,
field: f,
type: F64,
loc,
});
const bin = (op: "+" | "-" | "/" | "<=", left: IrExpr, right: IrExpr): IrExpr => ({
kind: "bin",
op,
left,
right,
type: op === "<=" ? BOOL : F64,
loc,
});
const record = (idle: IrExpr, active: IrExpr, utilization: IrExpr): IrExpr => ({
kind: "recordLit",
fields: [
{ name: "idle", value: idle },
{ name: "active", value: active },
{ name: "utilization", value: utilization },
],
type: recT,
loc,
});
const result = (idle: IrExpr, active: IrExpr): IrStmt => ({
kind: "return",
value: record(idle, active, bin("/", active, bin("+", idle, active))),
loc,
});
const decl = (id: string, init: IrExpr): IrStmt => ({ kind: "varDecl", localId: id, init, loc });
const body: IrStmt[] = [
decl("ls.0", { kind: "libCall", fn: "perf.loopStart", args: [], type: F64, loc }),
{
kind: "if",
cond: bin("<=", ref("ls.0"), numLit(0, loc)),
then: [
{ kind: "return", value: record(numLit(0, loc), numLit(0, loc), numLit(0, loc)), loc },
],
else_: null,
loc,
},
];
if (arity === 2) {
body.push(
decl("idle.0", bin("-", field("u1.0", "idle"), field("u2.0", "idle"))),
decl("active.0", bin("-", field("u1.0", "active"), field("u2.0", "active"))),
result(ref("idle.0"), ref("active.0")),
);
} else {
body.push(
decl("idle.0", { kind: "libCall", fn: "perf.idleTime", args: [], type: F64, loc }),
decl(
"active.0",
bin(
"-",
bin("-", { kind: "libCall", fn: "perf.now", args: [], type: F64, loc }, ref("ls.0")),
ref("idle.0"),
),
),
arity === 0
? result(ref("idle.0"), ref("active.0"))
: result(
bin("-", ref("idle.0"), field("u1.0", "idle")),
bin("-", ref("active.0"), field("u1.0", "active")),
),
);
}
lowerer.liftedFns.push({ name, params, returnType: recT, locals, body, loc });
return name;
}
@@ -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
@@ -1,7 +1,8 @@
import { checkedPromiseAll } from "../checked-promise-all.js";
import { checkedPromiseAll, thenReader } from "../checked-promise-all.js";
import { InternalCompilerError } from "../../../errors.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { newFnCtx, type Lowerer } from "../lowerer.js";
import { varRef } from "../../../ir/build.js";
import { maybePromiseResolve, maybePromiseUnionOf } from "../maybe-promise.js";
import { isJsSourceFile, locOf } from "../../program.js";
import {
@@ -10,6 +11,7 @@ import {
type IrFunction,
type IrStmt,
type IrType,
UNDEFINED_T,
VOID,
arrayOf,
isUnitType,
@@ -17,6 +19,168 @@ import {
typeKey,
} from "../../../ir/ir.js";
/** `Promise.all([p, q, r])` over DIFFERENT promise types — the checker's
* tuple overload, `Promise<[A, B, C]>`. One interned async helper per
* entry/result signature takes the entries as arguments, awaits the
* runtime combinator over the same promises (each widened to
* Promise<void>, a type-only view), so settlement is Node's exactly: the
* first rejection in settlement order wins and every later one counts as
* handled; then it reads each (now fulfilled) entry and returns the
* tuple. Entries keep their identity — no value converts through the
* checked-dynamic tree. Non-promise entries (numbers, strings, booleans,
* unit values — never thenable) pass straight into their slot; a
* checked-dynamic entry (a JavaScript function's result) joins through
* PromiseResolve: the promise it holds, a promise that adopts a thenable,
* or a promise fulfilled with its plain value. The tuple
* fulfills one microtask turn after the combinator does. Null when the
* result type or an entry has no typed home here. */
function lowerTuplePromiseAll(
lowerer: Lowerer,
call: ts.CallExpression,
elems: IrExpr[],
): IrExpr | null {
const resultT = lowerer.mapTypeOf(lowerer.typeOf(call));
if (resultT?.kind !== "promise" || resultT.inner.kind !== "record") return null;
const shape = lowerer.shapes.get(resultT.inner.shapeId);
if (!shape?.tuple || shape.fields.length !== elems.length) return null;
const plainValue = (t: IrType): boolean =>
t.kind === "f64" || t.kind === "string" || t.kind === "bool" || isUnitType(t);
// A checked-dynamic entry (a JavaScript function's result) may hold a
// promise or a plain value at run time; it joins through its promise view.
const settles = (t: IrType): boolean => t.kind === "promise" || t.kind === "dyn";
if (!elems.some((e) => settles(e.type))) return null;
if (!elems.every((e) => settles(e.type) || plainValue(e.type))) return null;
const loc = locOf(call);
const key = `${elems.map((e) => typeKey(e.type)).join(",")}->${typeKey(resultT.inner)}`;
let name = lowerer.arrHofHelpers.get(`promise.allTuple:${key}`);
if (name === undefined) {
name = `%promise.allTuple.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(`promise.allTuple:${key}`, name);
const ctx = newFnCtx(false, null, null, resultT.inner);
ctx.isAsync = true;
lowerer.fnStack.push(ctx);
try {
const params = elems.map((e, i) => ({
localId: `entry.${i}`,
name: `entry${i}`,
type: e.type,
}));
const ref = (i: number): IrExpr => varRef(`entry.${i}`, elems[i]!.type, loc);
const dynPromise: IrType = { kind: "promise", inner: DYN };
const body: IrStmt[] = [];
// Each dyn entry's promise view, in entry order: the promise it
// holds, a promise that adopts a thenable's settlement, or a promise
// fulfilled with the plain value (PromiseResolve's rule).
const views = new Map<number, IrExpr>();
elems.forEach((e, i) => {
if (e.type.kind !== "dyn") return;
const view = lowerer.declareHiddenLocal("%allEntry", dynPromise);
body.push({
kind: "varDecl",
localId: view.id,
init: {
kind: "libCall",
fn: "dyn.promiseResolve",
args: [ref(i), thenReader(lowerer, loc)],
type: dynPromise,
loc,
},
loc,
});
views.set(i, varRef(view.id, dynPromise, loc));
});
const widened: IrExpr[] = [];
elems.forEach((e, i) => {
const view = views.get(i);
if (view !== undefined) {
widened.push({
kind: "promiseVoidWiden",
value: view,
type: { kind: "promise", inner: VOID },
loc,
});
return;
}
if (e.type.kind !== "promise") return;
widened.push(
e.type.inner.kind === "void"
? ref(i)
: {
kind: "promiseVoidWiden",
value: ref(i),
type: { kind: "promise", inner: VOID },
loc,
},
);
});
const settled: IrExpr = {
kind: "awaitExpr",
value: {
kind: "intrinsic",
name: "promise.all",
args: [
{
kind: "arrayLit",
elems: widened,
type: arrayOf({ kind: "promise", inner: VOID }),
loc,
},
],
type: { kind: "promise", inner: VOID },
loc,
},
type: VOID,
loc,
};
// Shape fields are sorted by name ("10" before "2"); a tuple field's
// name is its position.
const fields = shape.fields.map((field) => {
const i = Number(field.name);
const entry = elems[i]!;
let value: IrExpr;
const view = views.get(i);
if (view !== undefined) {
value = { kind: "awaitExpr", value: view, type: DYN, loc };
} else if (entry.type.kind === "promise") {
value =
entry.type.inner.kind === "void"
? { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc }
: { kind: "awaitExpr", value: ref(i), type: entry.type.inner, loc };
} else {
value = ref(i);
}
return { name: field.name, value: lowerer.coerceToExpected(value, field.type) };
});
body.push(
{ kind: "exprStmt", expr: settled, loc },
{
kind: "return",
value: { kind: "recordLit", fields, type: resultT.inner, loc },
loc,
},
);
lowerer.liftedFns.push({
name,
params,
returnType: resultT.inner,
locals: [
...params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
...lowerer.ctx.locals,
],
body,
async: true,
loc,
});
} catch (e) {
lowerer.arrHofHelpers.delete(`promise.allTuple:${key}`);
throw e;
} finally {
lowerer.fnStack.pop();
}
}
return { kind: "call", callee: name, args: elems, type: resultT, loc };
}
/** `Promise.race([...])` on THE Promise global: the entries lower
* individually (the array never materializes — promise-element arrays
* have no representation) into a promise.race intrinsic; the result
@@ -74,6 +238,17 @@ export function lowerPromiseStaticCall(
}
return resultArms.some((a) => typeEquals(a, inner));
};
// A Promise<never> entry (the deadline idiom: `new Promise<never>((_,
// reject) => setTimeout(reject, ms))`) can only reject, so it races
// under any result type; its inner type maps to void.
const neverEntry = (el: ts.Expression): boolean => {
const t = lowerer.typeOf(el);
const arg =
t.getSymbol()?.name === "Promise"
? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
: undefined;
return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
};
const entries = argNode.elements.nodes.map((el) => {
const entry = lowerer.lowerExpr(el);
if (entry.type.kind !== "promise") {
@@ -83,7 +258,7 @@ export function lowerPromiseStaticCall(
"JS would resolve plain values — wrap them: new Promise((r) => r(v))",
);
}
if (!compatible(entry.type.inner)) {
if (!compatible(entry.type.inner) && !(entry.type.inner.kind === "void" && neverEntry(el))) {
lowerer.unsupported(
"SC1090",
el,
@@ -143,6 +318,10 @@ export function lowerPromiseStaticCall(
return { kind: "intrinsic", name: "promise.all", args: [entriesArr], type: resultT, loc };
}
if (!isJsSourceFile(call.getSourceFile())) {
const tuple = lowerTuplePromiseAll(lowerer, call, elems);
if (tuple) return tuple;
}
if (
isJsSourceFile(call.getSourceFile()) &&
elems.every((entry) => lowerer.dynConvertible(entry.type))
@@ -1,5 +1,5 @@
import { isBuiltinClassInstance } from "./receiver-types.js";
import { strLit } from "../../../ir/build.js";
import { numLit, strLit } from "../../../ir/build.js";
import * as ts from "../../ts7/adapter.js";
import { type Lowerer } from "../lowerer.js";
import { locOf } from "../../program.js";
@@ -192,6 +192,25 @@ export function lowerBuiltinExtraProperty(
// +1-or-NULL runtime pair.
if (kind === "child" && (name === "stdin" || name === "stdout" || name === "stderr")) {
const receiver = lowerer.lowerExpr(expr.expression);
// Under the general stream representation (general-streams.ts) the
// declared stream type maps to a runtime stream class: the read
// answers the child's bridged stream (one per stdio slot, so repeated
// reads keep identity), fed by the same pipe.
const general = lowerer.generalNodeStreams ? generalChildStdioType(lowerer, expr, name) : null;
if (general) {
const type: IrType = {
kind: "union",
unionId: lowerer.unions.intern([general, { kind: "nullT" }]),
};
const read: IrExpr = {
kind: "libCall",
fn: "child.stdioStream",
args: [receiver, numLit(name === "stdin" ? 0 : name === "stdout" ? 1 : 2, loc)],
type,
loc,
};
return lowerer.maybeNarrow(read, expr);
}
const streamType = name === "stdin" ? CHILDWRITER_T : CHILDSTREAM_T;
const type: IrType = {
kind: "union",
@@ -259,3 +278,24 @@ export function lowerBuiltinExtraProperty(
}
return null;
}
/** The runtime stream class a child stdio read produces under the general
* stream representation, or null when the declared type keeps the stdio
* handle (the fallback declarations' ChildStdin). */
function generalChildStdioType(
lowerer: Lowerer,
expr: ts.PropertyAccessExpression,
name: "stdin" | "stdout" | "stderr",
): IrType | null {
const className = name === "stdin" ? "%Writable" : "%Readable";
const mapped = lowerer.mapTypeOf(lowerer.typeOf(expr));
if (!mapped) return null;
// Checked-dynamic stream slots: the declared stream type maps to dyn
// (dyn-streams.ts); the read still produces the bridged stream.
if (lowerer.dynNodeStreams && mapped.kind === "dyn") return { kind: "object", className };
const arms =
mapped.kind === "union" ? (lowerer.unions.get(mapped.unionId)?.arms ?? []) : [mapped];
return arms.some((arm) => arm.kind === "object" && arm.className === className)
? { kind: "object", className }
: null;
}
@@ -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 };
}
@@ -2,17 +2,44 @@ import * as ts from "../../ts7/adapter.js";
import { type Lowerer, PoisonError } from "../lowerer.js";
import { locOf } from "../../program.js";
import { webAssemblyDiag } from "../../../diagnostics/diagnostic.js";
import { STRING, type IrExpr } from "../../../ir/ir.js";
import { BOOL, DYN, F64, STRING, type IrExpr } from "../../../ir/ir.js";
import { dynUndefinedExpr } from "../../../ir/build.js";
import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
import { isWebAssemblyType } from "../../type-mapper.js";
/* The WebAssembly API. scriptc.d.ts declares its types (lib.dom's
* namespace) so that packages whose declarations mention them typecheck,
* and the type mapper gives those types the checked-dynamic representation.
* No module compiles to native code yet, so every reached use of the API
* value fences at its expression with webAssemblyDiag: a build error in
* TypeScript sources, a located runtime fence in JavaScript ones. typeof
* needs no value and folds to Node's answer. */
* namespace) and the type mapper gives them the checked-dynamic
* representation. Modules whose bytes the build sees are translated to
* native code (wasm/translate.ts) and found at run time by the SHA-256 of
* their bytes (scr_wasm.c):
* - WebAssembly.compile, instantiate and validate calls, new
* WebAssembly.Module, Instance, Memory, Table and Global, and
* WebAssembly.Module.imports, exports and customSections lower to
* wasm.* libCalls; members of the values they produce (exports, memory
* buffers, globals, tables) dispatch through the native handles;
* - the classes called without `new` throw Node's TypeError
* (wasm.withoutNew);
* - `v instanceof WebAssembly.<Class>` lowers to wasm.is;
* - the error classes construct with or without `new` (wasm.errorNew).
* Every other reached use of the API value (the classes as values, the
* streaming functions, Tag and Exception) still fences at its expression
* with webAssemblyDiag: a build error in TypeScript sources, a located
* runtime fence in JavaScript ones. typeof needs no value and folds to Node's
* answer. A lowering marks the lowerer, so the build translates the
* modules it discovers (lowerToIr). */
/** The classes `wasm.is` answers, by kind. */
const WASM_CLASSES = [
"Module",
"Instance",
"Memory",
"Table",
"Global",
"CompileError",
"LinkError",
"RuntimeError",
] as const;
/** Strips the spellings that leave a value unchanged: parentheses, type
* assertions, `satisfies` and non-null assertions. */
function bare(expr: ts.Expression): ts.Expression {
@@ -130,3 +157,248 @@ export function lowerWebAssemblyTypeof(lowerer: Lowerer, operand: ts.Expression)
}
return null;
}
/** The member `WebAssembly.<member>` names (any spelling of the namespace),
* or null. */
function webAssemblyMember(lowerer: Lowerer, expr: ts.Expression): string | null {
const surface = webAssemblySurface(lowerer, expr);
if (surface === null || !surface.startsWith("WebAssembly.")) return null;
const member = surface.slice("WebAssembly.".length);
return member.includes(".") ? null : member;
}
function lowerArg(lowerer: Lowerer, arg: ts.Expression): IrExpr {
return ts.isObjectLiteralExpression(arg)
? lowerDynObjectLiteral(lowerer, arg)
: lowerer.lowerExprExpecting(arg, DYN);
}
/** An argument whose evaluation has no effect: a literal, a plain
* identifier, or an empty object or array literal. */
function isInert(arg: ts.Expression): boolean {
const node = bare(arg);
if (ts.isObjectLiteralExpression(node)) return node.properties.nodes.length === 0;
if (ts.isArrayLiteralExpression(node)) return node.elements.nodes.length === 0;
return (
ts.isIdentifier(node) ||
ts.isNumericLiteral(node) ||
node.kind === ts.SyntaxKind.BigIntLiteral ||
ts.isStringLiteral(node) ||
ts.isNoSubstitutionTemplateLiteral(node) ||
node.kind === ts.SyntaxKind.NullKeyword ||
node.kind === ts.SyntaxKind.TrueKeyword ||
node.kind === ts.SyntaxKind.FalseKeyword
);
}
/** The arguments, padded with undefined to `count`. Extra arguments are
* ignored, as the API ignores them, when evaluating them has no effect;
* null for spreads or other extra arguments (they stay refused). */
function lowerArgs(
lowerer: Lowerer,
args: readonly ts.Expression[],
count: number,
loc: IrExpr["loc"],
): IrExpr[] | null {
if (args.some(ts.isSpreadElement)) return null;
if (args.length > count) {
if (!args.slice(count).every(isInert)) return null;
args = args.slice(0, count);
}
const out = args.map((arg) => lowerArg(lowerer, arg));
while (out.length < count) out.push(dynUndefinedExpr(loc));
return out;
}
function errorNew(member: string, args: IrExpr[], loc: IrExpr["loc"]): IrExpr {
const kind = (WASM_CLASSES as readonly string[]).indexOf(member);
return {
kind: "libCall",
fn: "wasm.errorNew",
args: [{ kind: "numLit", value: kind, type: F64, loc }, args[0]!, args[1]!],
type: DYN,
loc,
};
}
/** The classes that require `new`, in wasm.withoutNew's numbering. */
const NEW_ONLY = ["Module", "Instance", "Memory", "Table", "Global"];
/** `WebAssembly.Module.imports(module)`, `exports(module)` and
* `customSections(module, name)`. Their results are arrays of descriptors
* or ArrayBuffers built as checked-dynamic values, then taken into the
* call's own type. Null for anything else. */
function lowerModuleStatic(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
const surface = webAssemblySurface(lowerer, call.expression);
const name = surface?.startsWith("WebAssembly.Module.")
? surface.slice("WebAssembly.Module.".length)
: null;
if (name !== "imports" && name !== "exports" && name !== "customSections") return null;
const loc = locOf(call);
const args = lowerArgs(lowerer, call.arguments.nodes, name === "customSections" ? 2 : 1, loc);
if (args === null) return null;
lowerer.usesWebAssembly = true;
if (name === "customSections") lowerer.usesWasmCustomSections = true;
const fn =
name === "imports"
? "wasm.moduleImports"
: name === "exports"
? "wasm.moduleExports"
: "wasm.moduleCustomSections";
const result: IrExpr = { kind: "libCall", fn, args, type: DYN, loc };
return lowerer.coerceInto(call, result, lowerer.irTypeOf(call));
}
/** `WebAssembly.compile(bytes)`, `WebAssembly.instantiate(source,
* imports?)`, `WebAssembly.validate(bytes)`, the error classes called
* without `new`, the other classes called without `new` (a TypeError), and
* Module's static functions. Null for anything else. */
export function lowerWebAssemblyCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
if (call.questionDotToken) return null;
const member = webAssemblyMember(lowerer, call.expression);
if (member === null) return lowerModuleStatic(lowerer, call);
const loc = locOf(call);
const newOnly = NEW_ONLY.indexOf(member);
if (newOnly >= 0) {
// The arguments evaluate, then the call throws.
const args = lowerArgs(lowerer, call.arguments.nodes, 2, loc);
if (args === null) return null;
lowerer.usesWebAssembly = true;
return {
kind: "libCall",
fn: "wasm.withoutNew",
args: [{ kind: "numLit", value: newOnly, type: F64, loc }, args[0]!, args[1]!],
type: DYN,
loc,
};
}
const isError = member === "CompileError" || member === "LinkError" || member === "RuntimeError";
if (member !== "compile" && member !== "instantiate" && member !== "validate" && !isError)
return null;
const used = member === "instantiate" || isError ? 2 : 1;
const args = lowerArgs(lowerer, call.arguments.nodes, used, loc);
if (args === null) return null;
lowerer.usesWebAssembly = true;
let result: IrExpr;
if (isError) result = errorNew(member, args, loc);
else if (member === "validate")
result = { kind: "libCall", fn: "wasm.validate", args: [args[0]!], type: BOOL, loc };
else if (member === "compile")
result = {
kind: "libCall",
fn: "wasm.compile",
args: [args[0]!],
type: { kind: "promise", inner: DYN },
loc,
};
else
result = {
kind: "libCall",
fn: "wasm.instantiate",
args: [args[0]!, args[1]!],
type: { kind: "promise", inner: DYN },
loc,
};
return result;
}
/** `new WebAssembly.Module(bytes)`, `new WebAssembly.Instance(module,
* imports?)`, `new WebAssembly.Memory(descriptor)`, `new
* WebAssembly.Table(descriptor, value?)`, `new
* WebAssembly.Global(descriptor, value?)` and the error classes. Null for
* anything else. */
export function lowerWebAssemblyNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
const member = webAssemblyMember(lowerer, expr.expression);
if (member === null) return null;
const isError = member === "CompileError" || member === "LinkError" || member === "RuntimeError";
if (!NEW_ONLY.includes(member) && !isError) return null;
const loc = locOf(expr);
const used = member === "Module" || member === "Memory" ? 1 : 2;
const args = lowerArgs(lowerer, expr.arguments?.nodes ?? [], used, loc);
if (args === null) return null;
lowerer.usesWebAssembly = true;
let result: IrExpr;
if (isError) result = errorNew(member, args, loc);
else if (member === "Module")
result = { kind: "libCall", fn: "wasm.moduleNew", args: [args[0]!], type: DYN, loc };
else if (member === "Memory")
result = { kind: "libCall", fn: "wasm.memoryNew", args: [args[0]!], type: DYN, loc };
else if (member === "Table")
result = { kind: "libCall", fn: "wasm.tableNew", args: [args[0]!, args[1]!], type: DYN, loc };
else if (member === "Global")
result = { kind: "libCall", fn: "wasm.globalNew", args: [args[0]!, args[1]!], type: DYN, loc };
else
result = {
kind: "libCall",
fn: "wasm.instanceNew",
args: [args[0]!, args[1]!],
type: DYN,
loc,
};
return result;
}
/** `v instanceof WebAssembly.<Class>`. Null for anything else. */
export function lowerWebAssemblyInstanceof(
lowerer: Lowerer,
expr: ts.BinaryExpression,
): IrExpr | null {
const member = webAssemblyMember(lowerer, expr.right);
if (member === null) return null;
const kind = (WASM_CLASSES as readonly string[]).indexOf(member);
if (kind < 0) return null;
const loc = locOf(expr);
lowerer.usesWebAssembly = true;
const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN);
return {
kind: "libCall",
fn: "wasm.is",
args: [value, { kind: "numLit", value: kind, type: F64, loc }],
type: BOOL,
loc,
};
}
/** WebAssembly API values: `WebAssembly.compile`, `instantiate` and
* `validate` read as functions, the namespace itself (`WebAssembly`,
* `globalThis.WebAssembly`), and the global object cast to a type with a
* `WebAssembly` member (pi codemode's spelling). Null for anything else,
* which refuseWebAssemblyUse then fences. */
export function lowerWebAssemblyValue(lowerer: Lowerer, expr: ts.Expression): IrExpr | null {
const surface = webAssemblySurface(lowerer, expr);
if (surface === null) return null;
const loc = locOf(expr);
const node = bare(expr);
const fn = (name: string, args: IrExpr[]): IrExpr => {
lowerer.usesWebAssembly = true;
return { kind: "libCall", fn: name as "wasm.namespace", args, type: DYN, loc };
};
if (surface === "globalThis.WebAssembly") {
// The cast spelling names the global object, not its member.
const cast =
(ts.isAsExpression(expr) || ts.isTypeAssertion(expr)) &&
isGlobalThis(lowerer, expr.expression);
return cast ? fn("wasm.globalView", []) : fn("wasm.namespace", []);
}
if (surface === "WebAssembly") return fn("wasm.namespace", []);
const index = ["WebAssembly.compile", "WebAssembly.instantiate", "WebAssembly.validate"].indexOf(
surface,
);
if (index < 0 || !ts.isPropertyAccessExpression(node)) return null;
return fn("wasm.function", [{ kind: "numLit", value: index, type: F64, loc }]);
}
/** `global.valueOf()` on a receiver typed as a WebAssembly value: a
* Global answers its value (the generic dyn.valueOf answers the object
* itself for native handles other than Date). Null for other receivers. */
export function lowerWebAssemblyValueOf(
lowerer: Lowerer,
receiver: ts.Expression,
recv: IrExpr,
loc: IrExpr["loc"],
): IrExpr | null {
const ctx = { checker: lowerer.checker, isStdlibFile: lowerer.isStdlibFile };
if (!isWebAssemblyType(lowerer.typeOf(receiver), ctx)) return null;
lowerer.usesWebAssembly = true;
return { kind: "libCall", fn: "wasm.valueOf", args: [recv], type: DYN, loc };
}
@@ -1,3 +1,4 @@
import { forwardedTupleType, pureForwardedValues, stripTypeWrappers } from "./typed-arguments.js";
import { dynUndefinedExpr } from "../../ir/build.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
@@ -26,13 +27,13 @@ import { checkedIterableSpread } from "./checked-iterable-spread.js";
import { type ParamShape } from "./call-signatures.js";
import { arrayValueRead, arrayValueType } from "./array-values.js";
import { buildArrayConversion } from "./coercions/builders.js";
import { typeKey } from "../type-mapper.js";
import { restTupleElementTypes, typeKey } from "../type-mapper.js";
function fixedTupleSpreadInfo(
lowerer: Lowerer,
node: ts.Expression,
): { type: IrType & { kind: "record" }; fields: { name: string; type: IrType }[] } | null {
const type = lowerer.mapTypeOf(lowerer.typeOf(node));
const type = forwardedTupleType(lowerer, node) ?? lowerer.mapTypeOf(lowerer.typeOf(node));
if (type?.kind !== "record") return null;
const shape = lowerer.shapes.get(type.shapeId);
if (!shape?.tuple || shape.fields.length === 0) return null;
@@ -42,6 +43,18 @@ function fixedTupleSpreadInfo(
};
}
/** `...t` over a provably EMPTY tuple bound to a plain name (`[]`, a rest
* tuple whose type parameter or `Parameters<F>` resolved to no positions):
* it contributes no arguments, and reading the name has no effect. */
function emptyTupleSpread(lowerer: Lowerer, node: ts.Expression): boolean {
let operand = node;
while (ts.isParenthesizedExpression(operand)) operand = operand.expression;
if (!ts.isIdentifier(operand)) return false;
const forwarded = forwardedTupleType(lowerer, operand);
if (forwarded) return forwarded.kind === "array";
return restTupleElementTypes(lowerer.typeOf(operand), lowerer.typeCtx)?.length === 0;
}
export function completeArgs(
lowerer: Lowerer,
argNodes: readonly ts.Expression[],
@@ -59,7 +72,9 @@ export function completeArgs(
// graph. Ordinary calls and fixed tuples never need that analysis.
const runtimeSpreads = argNodes.filter(
(arg): arg is ts.SpreadElement =>
ts.isSpreadElement(arg) && !fixedTupleSpreadInfo(lowerer, arg.expression),
ts.isSpreadElement(arg) &&
!fixedTupleSpreadInfo(lowerer, arg.expression) &&
!emptyTupleSpread(lowerer, arg.expression),
);
// Spread operands lowered while choosing the completion, each exactly once.
const loweredSpreads = new Map<ts.Expression, IrExpr>();
@@ -190,6 +205,12 @@ export function completeArgs(
sources.push(arg);
continue;
}
if (emptyTupleSpread(lowerer, arg.expression)) continue;
const pure = pureForwardedValues(lowerer, arg.expression);
if (pure) {
for (const value of pure) sources.push({ ir: value });
continue;
}
const tuple = fixedTupleSpreadInfo(lowerer, arg.expression);
if (!tuple) {
sources.push(arg);
@@ -204,7 +225,13 @@ export function completeArgs(
// values as separately written arguments while the saved tuple stays
// alive through the call.
const spreadLoc = locOf(arg);
const value = lowerer.coerceInto(arg.expression, lowerer.lowerExpr(arg.expression), tuple.type);
// A forwarded tuple seen through type assertions (`...(arguments as
// unknown as Parameters<F>)`) spreads the tuple itself.
const operand =
forwardedTupleType(lowerer, arg.expression) !== null
? stripTypeWrappers(arg.expression)
: arg.expression;
const value = lowerer.coerceInto(arg.expression, lowerer.lowerExpr(operand), tuple.type);
const saved = lowerer.declareHiddenLocal("%callSpread", tuple.type);
const savedRef = (): IrExpr => ({
kind: "varRef",
@@ -506,9 +533,38 @@ export function completeFuncValueArgs(
shapes.push({ type: rest, mode: "rest" });
return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call);
}
// A fixed-tuple spread (`fn(...args)` over a tuple-typed rest binding)
// has a compile-time arity: completion flattens it into the positional
// slots exactly like a direct call's.
if (
funcType.rest !== true &&
call.arguments.nodes.some(ts.isSpreadElement) &&
call.arguments.nodes.every(
(arg) =>
!ts.isSpreadElement(arg) ||
fixedTupleSpreadInfo(lowerer, arg.expression) !== null ||
emptyTupleSpread(lowerer, arg.expression),
)
) {
const shapes: ParamShape[] = funcType.params.map((type) => ({
type,
mode: omittedArgFor(lowerer, type, loc) ? "omittable" : "required",
}));
return completeArgs(lowerer, call.arguments.nodes, shapes, loc, call);
}
const args = call.arguments.nodes.map((arg, index) =>
lowerer.lowerExprExpecting(arg, funcType.params[index]),
);
// Effect-free surplus arguments of a fixed-arity value (`(r: void) =>
// void` drops its void slot) evaluate to nothing observable: drop them,
// as the callee ignores them.
while (
funcType.rest !== true &&
args.length > funcType.params.length &&
isSafeToDiscard(args[args.length - 1]!)
) {
args.pop();
}
for (let i = args.length; i < funcType.params.length; i++) {
const absent = omittedArgFor(lowerer, funcType.params[i]!, loc);
if (!absent) {
@@ -1,3 +1,8 @@
import {
typedArgumentsCandidate,
typedArgumentsShapes,
typedArgumentsTarget,
} from "./typed-arguments.js";
import { InternalCompilerError } from "../../errors.js";
import * as ts from "../ts7/adapter.js";
import { bodyReadsArguments } from "../arguments-usage.js";
@@ -15,6 +20,7 @@ import {
import { isJsSourceFile, isNodeEsmFile } from "../program.js";
import {
genResultRecord,
restTupleParamTypes,
isAnyArrayType,
isUnitOnlyTsType,
jsOpenObjectType,
@@ -56,6 +62,10 @@ export interface ParamShape {
bodyType?: IrType;
callDefault?: IrExpr;
entryNarrow?: IrType;
/** One flattened position of a fixed-tuple rest parameter
* (restTupleShapes): the declaration's last parameter owns every shape
* from its index on, and the body binds the tuple (declareParams). */
restTuple?: true;
}
export interface FnSig {
@@ -373,6 +383,28 @@ export function isThisParameter(param: ts.ParameterDeclaration): boolean {
return ts.isIdentifier(param.name) && param.name.text === "this";
}
/** A TypeScript rest parameter bound to a name whose type is a fixed tuple
* (`...args: [A, B]`, `...args: A` with A bound to a tuple, `...args:
* Parameters<F>` with F bound): one required shape per position. Call
* sites and function values see the equivalent fixed signature; the body
* rebuilds the tuple (declareParams) unless it only forwards it. Null for
* every other parameter. */
export function restTupleShapes(
lowerer: Lowerer,
param: ts.ParameterDeclaration,
tsType?: ts.Type,
): ParamShape[] | null {
if (
param.dotDotDotToken === undefined ||
!ts.isIdentifier(param.name) ||
isJsSourceFile(param.getSourceFile())
) {
return null;
}
const elems = restTupleParamTypes(tsType ?? lowerer.typeOf(param.name), lowerer.typeCtx);
return elems ? elems.map((type) => ({ type, mode: "required", restTuple: true })) : null;
}
/** ParamShapes for a whole parameter list. */
export function paramShapes(
lowerer: Lowerer,
@@ -380,21 +412,47 @@ export function paramShapes(
signature?: ts.Signature,
blameOf?: (param: ts.ParameterDeclaration, index: number) => ts.Node,
): ParamShape[] {
if (!signature)
return params.filter((p) => !isThisParameter(p)).map((param) => lowerer.paramShape(param));
const out: ParamShape[] = [];
if (!signature) {
for (const param of params) {
if (isThisParameter(param)) continue;
const flattened = restTupleShapes(lowerer, param);
if (flattened) out.push(...flattened);
else out.push(lowerer.paramShape(param));
}
return out;
}
const symbols = signature.getParameters();
return params.map((declParam, i) => {
const shapeAt = (declParam: ts.ParameterDeclaration, i: number): ParamShape => {
const stage = lowerer.stageSourceType(declParam.name);
if (stage) return { type: stage, mode: "required" as const };
const symbol = symbols[i];
const tsType = symbol
? lowerer.checker.getTypeOfSymbol(symbol)
: lowerer.typeOf(declParam.name);
// A call-site instantiation whose concrete argument type does not map
// is that argument's blocker: name it there, with its concrete type,
// rather than as the declaration's unbound type parameter.
if (
blameOf !== undefined &&
!isJsSourceFile(declParam.getSourceFile()) &&
(tsType.flags & ts.TypeFlags.Any) === 0 &&
declParam.type !== undefined &&
mentionsTypeParameter(lowerer, lowerer.checker.getTypeFromTypeNode(declParam.type)) &&
lowerer.mapTypeOf(tsType) === null
) {
lowerer.badType(blameOf(declParam, i), tsType);
}
let mapped = jsOpenObjectType(
declParam,
lowerer.runtimeOptionalBindingType(
declParam.name,
lowerer.mapTypeOf(tsType) ?? lowerer.irTypeOf(declParam.name),
lowerer.mapTypeOf(tsType) ??
// A TypeScript instantiation at `any` (a type argument inferred
// from an untyped value) takes the checked-dynamic slot.
((tsType.flags & ts.TypeFlags.Any) !== 0 && !isJsSourceFile(declParam.getSourceFile())
? DYN
: lowerer.irTypeOf(declParam.name)),
),
lowerer.shapes,
lowerer.unions,
@@ -428,7 +486,32 @@ export function paramShapes(
);
}
return { type: mapped, mode: declParam.questionToken ? "omittable" : "required" };
};
params.forEach((declParam, i) => {
const symbol = symbols[i];
const flattened = restTupleShapes(
lowerer,
declParam,
symbol ? lowerer.checker.getTypeOfSymbol(symbol) : undefined,
);
if (flattened) out.push(...flattened);
else out.push(shapeAt(declParam, i));
});
return out;
}
/** A declared type that is (or is a union over) a bare type parameter
* or an instantiation of a generic alias/interface over type arguments. */
function mentionsTypeParameter(lowerer: Lowerer, t: ts.Type): boolean {
if (t.flags & ts.TypeFlags.TypeParameter) return true;
const mentions = (part: ts.Type): boolean => mentionsTypeParameter(lowerer, part);
if (t.isUnionType()) return ts.constituentTypes(t).some(mentions);
if (t.getAliasTypeArguments().some(mentions)) return true;
return (
(t.flags & ts.TypeFlags.Object) !== 0 &&
((t as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) !== 0 &&
lowerer.checker.getTypeArguments(t as ts.TypeReference).some(mentions)
);
}
/** A default expression may itself yield undefined. Keep that arm in the
@@ -828,6 +911,16 @@ export function lambdaSignature(
| ts.SetAccessorDeclaration,
): { shapes: ParamShape[]; funcType: IrType & { kind: "func" } } {
if (!node.body) lowerer.unsupported("SC1090", node, "function overload signatures");
// A parameterless function expression forwarding its `arguments`,
// asserted to a fixed signature: that signature is its ABI.
if (typedArgumentsCandidate(node) && !node.typeParameters?.nodes.length) {
const target = typedArgumentsTarget(lowerer, node);
if (target) {
const shapes = typedArgumentsShapes(target);
lowerer.typedArgumentsLambdas.add(node);
return { shapes, funcType: funcTypeFromParamShapes(shapes, target.ret) };
}
}
// A JS replacement of a native stream's write method receives the
// actual call tuple, including overloads and omitted arguments. Node's
// contextual overload signature cannot define this closure's ABI.
@@ -11,7 +11,7 @@ const COMBINATOR_LIB_CALLS = {
/** `value.then` through the program's own property dispatch, so a class
* instance's `then` method is visible to the runtime's thenable check. */
function thenReader(lowerer: Lowerer, loc: SrcLoc): IrExpr {
export function thenReader(lowerer: Lowerer, loc: SrcLoc): IrExpr {
const name = "%promise.thenOf";
const type = funcOf([DYN], DYN);
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
@@ -18,6 +18,7 @@ import type { Lowerer } from "./lowerer.js";
import type { ParamShape } from "./call-signatures.js";
import { classPrototypeData } from "./class-prototypes.js";
import { classInstanceOf } from "./class-dynamic-dispatch.js";
import { inheritedBuiltinCtorFence } from "./lower-classes.js";
const PREFIX = "%class.construct:";
const PACKED = `${PREFIX}packed`;
@@ -380,6 +381,29 @@ export class ClassConstructionDispatch {
changed = true;
continue;
}
const type: IrType = { kind: "classval", className: name };
const callee = varRef("p.0", DYN, loc);
// A constructor inherited from a runtime stream or EventEmitter
// takes no arguments here: arguments report at run time, as they do
// at compile time for typed construction.
const fence = inheritedBuiltinCtorFence(lowerer, info, loc);
if (fence !== null && fn.name !== PACKED && fn.params.length > 1) {
fn.body.unshift({
kind: "if",
cond: {
kind: "libCall",
fn: "dyn.classIs",
args: [callee, { kind: "strLit", value: typeKey(type), type: STRING, loc }],
type: BOOL,
loc,
},
then: [fence],
else_: null,
loc,
});
changed = true;
continue;
}
const args = constructorArguments(
lowerer,
info.ctorParams,
@@ -388,8 +412,6 @@ export class ClassConstructionDispatch {
fn.name === PACKED ? varRef("p.1", DYN, loc) : undefined,
);
if (!args) continue;
const type: IrType = { kind: "classval", className: name };
const callee = varRef("p.0", DYN, loc);
const value = constructedValue(lowerer, info, {
kind: "newValue",
callee: { kind: "dynCheck", value: callee, type, loc },
@@ -400,6 +422,28 @@ export class ClassConstructionDispatch {
const body: IrStmt[] = [
{ kind: "return", value: lowerer.coerceToExpected(value, DYN), loc },
];
// A spread call's argument count is known only at run time.
if (fence !== null && fn.name === PACKED)
body.unshift({
kind: "if",
cond: {
kind: "bin",
op: ">",
left: {
kind: "libCall",
fn: "dyn.arrLen",
args: [varRef("p.1", DYN, loc)],
type: F64,
loc,
},
right: { kind: "numLit", value: 0, type: F64, loc },
type: BOOL,
loc,
},
then: [fence],
else_: null,
loc,
});
fn.body.unshift({
kind: "if",
cond: {
@@ -1,3 +1,4 @@
import { primitiveConstructorOr } from "./lower-exprs.js";
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
import { constructedValue } from "./constructor-returns.js";
import { eventTargetApiMembers, eventTargetInheritedMember } from "./lower-event-target.js";
@@ -2070,6 +2071,49 @@ export class ClassDynamicDispatch {
// A read compares the bag with the receiver and then reads it:
// evaluate it once, so each read retains the receiver once.
const bagRead = varRef("bag.0", DYN, loc);
const bagGet: IrExpr | null = read
? {
kind: "libCall",
fn: "dyn.bagGet",
args: [bagRead, read.key, varRef("p.0", DYN, loc)],
type: DYN,
loc,
}
: null;
// A class instance whose class chain declares no member of an
// Object.prototype method's name (classes that do answer in
// their own branches) inherits it unless an own property
// shadows it; a class constructor's static bag reads as before.
const instanceRead: IrExpr | null =
read && INHERITED_OBJECT_METHODS.has(name)
? {
kind: "ternary",
cond: {
kind: "dynTest",
test: "function",
value: varRef("p.0", DYN, loc),
type: BOOL,
loc,
},
then: bagGet!,
else_: {
kind: "ternary",
cond: this.readBagHas(lowerer, "dyn.hasKey", bagRead, read.key, loc),
then: bagGet!,
else_: {
kind: "libCall",
fn: "dyn.builtinMethod",
args: [{ kind: "strLit", value: "Object", type: STRING, loc }, read.key],
type: DYN,
loc,
},
type: DYN,
loc,
},
type: DYN,
loc,
}
: bagGet;
const fallback: IrExpr = read
? {
kind: "ternary",
@@ -2081,13 +2125,7 @@ export class ClassDynamicDispatch {
loc,
},
then: { ...read, value: bagRead },
else_: {
kind: "libCall",
fn: "dyn.bagGet",
args: [bagRead, read.key, varRef("p.0", DYN, loc)],
type: DYN,
loc,
},
else_: instanceRead!,
type: DYN,
loc,
}
@@ -2112,7 +2150,20 @@ export class ClassDynamicDispatch {
]
: [
{ kind: "varDecl", localId: "bag.0", init: bag, loc },
{ kind: "return", value: fallback, loc },
{
kind: "return",
// Primitives answer their wrapper constructors.
value:
name === "constructor"
? primitiveConstructorOr(
lowerer,
varRef("p.0", DYN, loc),
fallback,
loc,
)
: fallback,
loc,
},
],
loc,
};
@@ -4820,19 +4871,40 @@ export class ClassDynamicDispatch {
if (!field && !accessor) {
const instance = lowerer.coerceToExpected(receiver, DYN);
const bag = this.readPropertyBagOrEmpty(lowerer, instance, loc);
return [
{
kind: "return",
value: {
kind: "libCall",
fn: "dyn.bagGet",
args: [bag, { kind: "strLit", value: name, type: STRING, loc }, instance],
type: DYN,
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
const own: IrExpr = {
kind: "libCall",
fn: "dyn.bagGet",
args: [bag, key, instance],
type: DYN,
loc,
};
// An Object.prototype method no class in the chain declares: an
// own property shadows it, otherwise the read answers the same
// interned value `Object.prototype.<name>` reads.
if (INHERITED_OBJECT_METHODS.has(name) && !findMethodOn(lowerer, info, name)) {
return [
{
kind: "return",
value: {
kind: "ternary",
cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc),
then: own,
else_: {
kind: "libCall",
fn: "dyn.builtinMethod",
args: [{ kind: "strLit", value: "Object", type: STRING, loc }, key],
type: DYN,
loc,
},
type: DYN,
loc,
},
loc,
},
loc,
},
];
];
}
return [{ kind: "return", value: own, loc }];
}
const type = field ?? accessor!.sig.ret;
if (!canConvertToDyn(type, getRecord, getUnion)) return fence();
@@ -5355,3 +5427,12 @@ export class ClassDynamicDispatch {
return body;
}
}
/** Object.prototype methods every class instance inherits unless its class
* chain declares them. */
const INHERITED_OBJECT_METHODS: ReadonlySet<string> = new Set([
"valueOf",
"toString",
"hasOwnProperty",
"propertyIsEnumerable",
]);
@@ -0,0 +1,35 @@
import type { IrExpr, IrType } from "../../ir/ir.js";
import type { Lowerer } from "./lowerer.js";
import { accessorCall } from "./lower-classes.js";
/** The generator a class instance's `[Symbol.iterator]` returns, when that
* method is a generator method taking no arguments: `obj[Symbol.iterator]()`,
* called once, as for-of, spread and Array.from call it. Null for other
* values, and for classes whose iterator the program can replace at run
* time (an instance property, a prototype write). */
export function classGeneratorIterable(lowerer: Lowerer, value: IrExpr): IrExpr | null {
if (value.type.kind !== "object" || !iteratesThroughGenerator(lowerer, value.type)) return null;
const className = value.type.className;
const ret = lowerer.findMethodOn(lowerer.classes.get(className)!, "sym:iterator")!.sig.ret;
return accessorCall(lowerer, className, "sym:iterator", value, [], ret, value.loc);
}
/** Whether values of `type` iterate through a generator method (the check
* classGeneratorIterable makes, from the type alone). */
export function iteratesThroughGenerator(lowerer: Lowerer, type: IrType): boolean {
if (type.kind !== "object") return false;
const info = lowerer.classes.get(type.className);
if (!info || info.def.runtime || info.fields.has("sym:iterator")) return false;
if (
lowerer.prototypeMethodAccesses.has("sym:iterator") ||
lowerer.replacedSymbolMethods.has("sym:iterator")
)
return false;
const found = lowerer.findMethodOn(info, "sym:iterator");
return (
!!found &&
found.sig.params.length === 0 &&
found.sig.ret.kind === "generator" &&
!found.sig.ret.async
);
}
@@ -43,9 +43,19 @@ export function collectClassMethodMutations(
const jsAssignments: (ts.PropertyAccessExpression | ts.ElementAccessExpression)[] = [];
const computedMethods: ts.Expression[] = [];
const typedClasses: (ts.ClassDeclaration | ts.ClassExpression)[] = [];
const symbolMethods = new Set<string>();
let unknownKey: ts.Node | undefined;
let numericKey: ts.Node | undefined;
let unknownOwnField = false;
// `sym:iterator` for `Symbol.iterator`: the name a class method declared
// with a well-known symbol key takes.
const wellKnownMethodName = (key: ts.Expression): string | null => {
while (ts.isParenthesizedExpression(key)) key = key.expression;
if (!ts.isPropertyAccessExpression(key)) return null;
const member = lowerer.stdlibGlobalMember(key, "Symbol");
if (member === null || !(lowerer.typeOf(key).flags & ts.TypeFlags.ESSymbolLike)) return null;
return `sym:${member}`;
};
// JavaScript method inference and dynamic class factories keep their
// existing dispatch contract. The native-call proof applies to typed
// declarations (including JavaScript files TypeScript checks), while
@@ -555,6 +565,19 @@ export function collectClassMethodMutations(
lowerer.prototypeMethodAccesses.set(name, site);
}
for (const { key, receiver, site } of computedWrites) {
// A class method declared with a well-known symbol key is not a write;
// any other write with one can replace that method.
const symbolName = key ? wellKnownMethodName(unwrap(key)) : null;
if (symbolName) {
const declaration =
(ts.isMethodDeclaration(site) ||
ts.isGetAccessorDeclaration(site) ||
ts.isSetAccessorDeclaration(site)) &&
(ts.isClassDeclaration(site.parent) || ts.isClassExpression(site.parent));
if (declaration) symbolMethods.add(symbolName);
else lowerer.replacedSymbolMethods.add(symbolName);
continue;
}
const type = key ? lowerer.typeOf(key) : undefined;
const names = type && (literalValues(type) ?? (key ? literalLoopKeys(unwrap(key)) : null));
if (names) {
@@ -585,8 +608,10 @@ export function collectClassMethodMutations(
if (String(Number(name)) === name) markName(name, numericKey);
}
}
if (unknownKey)
if (unknownKey) {
for (const method of methods) lowerer.prototypeMethodAccesses.set(method, unknownKey);
for (const method of symbolMethods) lowerer.replacedSymbolMethods.add(method);
}
if (!unknownKey && !unknownOwnField)
for (const field of arrowFields) {
if (!receiverFields.has(field)) lowerer.receiverFreeCallbackFields.add(field);
@@ -188,7 +188,8 @@ function protocolSignature(
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): ProtocolSignature | null {
const recvTs = lowerer.typeOf(access.expression);
// A guarded optional step's receiver still carries its nullish arms.
const recvTs = lowerer.checker.getNonNullableType(lowerer.typeOf(access.expression));
const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text);
if (!property) return null;
const declarations = property.declarations;
@@ -240,10 +241,11 @@ export function lowerProtocolMethodCall(
access: ts.PropertyAccessExpression,
recv: IrExpr,
): IrExpr | null {
// An optional chain that already guarded this step (`s?.m(x)`, the chain
// re-dispatching with the narrowed receiver) calls like a plain one.
if (
recv.type.kind !== "dyn" ||
call.questionDotToken ||
access.questionDotToken ||
lowerer.chainBlocked(call, access) ||
call.arguments.nodes.some(ts.isSpreadElement)
)
return null;
@@ -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) &&
@@ -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;
}
@@ -4,6 +4,7 @@ import { InternalCompilerError } from "../../../errors.js";
import type { IrExpr, IrParam, IrStmt, IrType, SrcLoc } from "../../../ir/ir.js";
import {
canAdaptDynFuncTo,
canDynCheckTo,
DYN,
F64,
isUnitType,
@@ -341,6 +342,19 @@ export function funcCoerceAdapter(
voidRet = toT.ret.kind === "dyn" ? "dyn" : "strand";
}
}
// An untyped result (a generic wrapper's `any` handed back `as F`) takes
// the checked-dynamic extraction to the slot's result: a record checks
// its shape, a Promise keeps its identity and checks its payload when it
// settles.
const checkedRet =
strandRet &&
fromT.ret.kind === "dyn" &&
canDynCheckTo(
toT.ret,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
);
if (checkedRet) strandRet = false;
const key = `fnadapt:${typeKey(fromT)}:${typeKey(toT)}`;
const existing = lowerer.coercions.retags.get(key);
if (existing) return existing;
@@ -440,6 +454,10 @@ export function funcCoerceAdapter(
`a '${lowerer.fmt(fromT)}' function invoked through a '${lowerer.fmt(toT)}' slot (the result cannot convert to '${lowerer.fmt(toT.ret)}' — the checker's loose function compatibility admitted the assignment, but the call has no exact lowering)`,
),
];
} else if (checkedRet) {
body = [
{ kind: "return", value: { kind: "dynCheck", value: call, type: toT.ret, loc }, loc },
];
} else {
const result = lowerer.coerceToExpected(call, toT.ret);
if (!typeEquals(result.type, toT.ret))
@@ -23,7 +23,19 @@ export function widthLiftPlan(lowerer: Lowerer, src: IrType, dst: IrType): Width
}
if (dst.kind === "union") {
if (src.kind === "union") {
return lowerer.unionRetagMappable(src.unionId, dst.unionId) ? { how: "retag" } : null;
if (lowerer.unionRetagMappable(src.unionId, dst.unionId)) return { how: "retag" };
// A field the checker types `undefined` alone (an object literal's
// omitted member, normalized to `?: undefined` across a union of
// literal types) carries the unit-only union. Into a slot with an
// undefined arm it lifts as that arm; its null arm cannot occur.
const srcArms = lowerer.unions.get(src.unionId)?.arms ?? [];
if (
srcArms.length > 0 &&
srcArms.every(isUnitType) &&
lowerer.armTag(dst.unionId, UNDEFINED_T) >= 0
)
return { how: "unitRetag" };
return null;
}
// A unit-typed source can't wrap here (unionWrap requires the
// LITERAL unit — and no lowered shape carries a bare unit field).
@@ -125,6 +125,19 @@ export function applyWidthLift(
throw new InternalCompilerError("lowerer bug: planned retag lift failed to intern");
return { kind: "call", callee: retag, args: [value], type: dst, loc };
}
case "unitRetag": {
if (dst.kind !== "union" || value.type.kind !== "union")
throw new InternalCompilerError("lowerer bug: unit retag lift shape");
const fromId = value.type.unionId;
const trappable = new Set<number>();
(lowerer.unions.get(fromId)?.arms ?? []).forEach((arm, tag) => {
if (lowerer.armTag(dst.unionId, arm) < 0) trappable.add(tag);
});
const retag = lowerer.unionRetagHelper(fromId, dst.unionId, loc, trappable);
if (!retag)
throw new InternalCompilerError("lowerer bug: planned unit retag lift failed to intern");
return { kind: "call", callee: retag, args: [value], type: dst, loc };
}
case "liftWrap": {
if (dst.kind !== "union")
throw new InternalCompilerError("lowerer bug: liftWrap lift against a non-union");
@@ -44,6 +44,7 @@ import {
lowerArrayCallback,
requireProducedArrayElement,
callbackArrayElement,
truthyFilterCallback,
} from "./callback-arguments.js";
import {
reverseArrayLoop,
@@ -85,7 +86,9 @@ export function lowerArrayHofCall(
const valueT = callbackValueType(lowerer, elem, present);
if (booleanFilter) {
if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode);
if (valueT.kind === "dyn" || valueT.kind === "void" || isUnitType(valueT)) {
// Checked-dynamic elements (`anyText.split("\n").map(...).filter(Boolean)`)
// keep their runtime kind, so their truthiness is the runtime's.
if (valueT.kind === "void" || isUnitType(valueT)) {
lowerer.badType(argNode, lowerer.typeOf(argNode));
}
}
@@ -231,33 +234,6 @@ function filterResultElem(
return outElem;
}
/** A native closure for the ambient Boolean callback. Reifying the
* JavaScript constructor itself is unnecessary; Array#filter only observes
* its one-argument ToBoolean result. */
function truthyFilterCallback(
lowerer: Lowerer,
valueT: IrType,
loc: SrcLoc,
): IrExpr & { type: IrType & { kind: "func" } } {
const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: BOOL };
const key = `filterBoolean:${typeKey(valueT)}`;
let name = lowerer.arrHofHelpers.get(key);
if (!name) {
name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
const value = varRef("v.0", valueT, loc);
lowerer.liftedFns.push({
name,
params: [{ localId: "v.0", name: "v", type: valueT }],
returnType: BOOL,
locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }],
body: [{ kind: "return", value: { kind: "toBool", operand: value, type: BOOL, loc }, loc }],
loc,
});
}
return { kind: "closure", fnName: name, captures: [], type: fnT, loc };
}
function filterPredicateOk(lowerer: Lowerer, ret: IrType): boolean {
if (ret.kind === "bool") return true;
// VOID is a TYPE erasure, not a runtime value: TS lets a value-returning
@@ -44,6 +44,7 @@ import {
lowerArrayCallback,
callbackArrayElement,
} from "./callback-arguments.js";
import { classGeneratorIterable } from "../class-iterables.js";
/** Build the argument array for a mutating/copying operation while keeping
* the receiver's scalar payload ABI. A missing read is represented as the
@@ -265,6 +266,22 @@ function appendWidenedArrayValues(
}
/** The Array constructor's element and count forms, shared by calls and new. */
/** The value of `.fill(v)` when `expr` is that call's receiver. */
function fillArgumentOfReceiver(expr: ts.Expression): ts.Expression | null {
let receiver: ts.Node = expr;
let parent = expr.parent;
while (ts.isParenthesizedExpression(parent)) {
receiver = parent;
parent = parent.parent;
}
if (!ts.isPropertyAccessExpression(parent) || parent.expression !== receiver) return null;
if (parent.name.text !== "fill" || parent.questionDotToken) return null;
const call = parent.parent;
if (!ts.isCallExpression(call) || call.expression !== parent) return null;
const value = call.arguments.nodes[0];
return value !== undefined && !ts.isSpreadElement(value) ? value : null;
}
export function lowerArrayConstructor(
lowerer: Lowerer,
expr: ts.CallExpression | ts.NewExpression,
@@ -295,6 +312,15 @@ export function lowerArrayConstructor(
const contextual = lowerer.checker.getContextualType(expr);
if (contextual) result = lowerer.mapTypeOf(contextual);
}
// `Array(n).fill(v)`: the checker types the length-constructed array as
// any[], but fill gives every element v's type.
if (result?.kind !== "array") {
const fillValue = fillArgumentOfReceiver(expr);
const elem = fillValue
? lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(fillValue)))
: null;
if (elem !== null && isSupportedArrayElem(elem)) result = arrayOf(elem);
}
if (isJsSourceFile(expr.getSourceFile()) && (result === null || result.kind === "dyn"))
result = arrayOf(DYN);
if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) {
@@ -913,7 +939,7 @@ export function lowerArrayFromCall(
// and materializes holes while keeping element reference identity.
if ((args.length === 1 || args.length === 2) && !ts.isObjectLiteralExpression(args[0]!)) {
const source = lowerer.lowerExpr(args[0]!);
const src =
let src =
source.type.kind === "string" && args.length === 2
? strCharsCall(lowerer, source, loc)
: source;
@@ -1008,6 +1034,9 @@ export function lowerArrayFromCall(
if (src.type.kind === "searchParams" && args.length === 1) {
return lowerCheckedArrayFrom(lowerer, lowerer.coerceInto(args[0]!, src, DYN), loc);
}
// A class whose [Symbol.iterator] is a generator method iterates the
// generator that method returns.
src = classGeneratorIterable(lowerer, src) ?? src;
// A generator (or an iterator adapted into one) drains in order.
if (src.type.kind === "generator" && args.length === 1) {
const arrT = lowerer.mapTypeOf(lowerer.typeOf(call));
@@ -1,12 +1,18 @@
import * as ts from "../../ts7/adapter.js";
import type { Lowerer } from "../lowerer.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { varRef } from "../../../ir/build.js";
import { typeKey } from "../../type-mapper.js";
import {
BOOL,
F64,
type IrExpr,
type IrType,
type SrcLoc,
UNDEFINED_T,
funcOf,
isSupportedArrayElem,
isUnitType,
typeEquals,
} from "../../../ir/ir.js";
@@ -44,6 +50,24 @@ export function lowerArrayCallback(
bindUntyped = false,
expectedReturn?: IrType,
): { fnArg: IrExpr & { type: IrType & { kind: "func" } }; arity: number } {
// The ambient `Boolean` as the callback (`flags.every(Boolean)`,
// `xs.some(Boolean)`, `xs.map(Boolean)`): Boolean(value) answers the
// ToBoolean of its FIRST argument and ignores the rest, so the callback
// is the interned one-parameter truthiness closure over the lead slot.
// The constructor's own generic signature has no compiled value form.
// JavaScript files keep their primitive-constructor closure.
if (
ts.isIdentifier(argNode) &&
argNode.text === "Boolean" &&
!isJsSourceFile(argNode.getSourceFile()) &&
lowerer.isStdlibSymbol(lowerer.resolveValueSymbol(argNode) ?? undefined)
) {
const valueT = lead[0];
if (valueT === undefined || valueT.kind === "void" || isUnitType(valueT))
lowerer.badType(argNode, lowerer.typeOf(argNode));
if (valueT.kind === "union") lowerer.requireTruthyUnion(valueT.unionId, argNode);
return { fnArg: truthyFilterCallback(lowerer, valueT, locOf(argNode)), arity: 1 };
}
const full = [...lead, F64, arrT];
// A DYN-receiver HOF's callback (`parsed.flatMap((value) => ...)`):
// the contextual signature types the unannotated param `any` (the
@@ -248,3 +272,39 @@ function runtimeOptionalPairVariant(lowerer: Lowerer, expected: IrType, mapped:
: t;
return lowerer.isRuntimeOptionalRecordVariant(strip(expected), strip(mapped));
}
/** A native closure for the ambient Boolean callback. Reifying the
* JavaScript constructor itself is unnecessary; Array#filter only observes
* its one-argument ToBoolean result. */
export function truthyFilterCallback(
lowerer: Lowerer,
valueT: IrType,
loc: SrcLoc,
): IrExpr & { type: IrType & { kind: "func" } } {
const fnT: IrType & { kind: "func" } = { kind: "func", params: [valueT], ret: BOOL };
const key = `filterBoolean:${typeKey(valueT)}`;
let name = lowerer.arrHofHelpers.get(key);
if (!name) {
name = `%arr.filterBoolean.${lowerer.arrHofHelpers.size}`;
lowerer.arrHofHelpers.set(key, name);
const value = varRef("v.0", valueT, loc);
lowerer.liftedFns.push({
name,
params: [{ localId: "v.0", name: "v", type: valueT }],
returnType: BOOL,
locals: [{ id: "v.0", name: "v", type: valueT, mutable: false }],
body: [
{
kind: "return",
value:
valueT.kind === "dyn"
? { kind: "dynTest", test: "truthy", value, type: BOOL, loc }
: { kind: "toBool", operand: value, type: BOOL, loc },
loc,
},
],
loc,
});
}
return { kind: "closure", fnName: name, captures: [], type: fnT, loc };
}
@@ -42,6 +42,8 @@ import {
import { strCharsCall } from "./array-construction.js";
import { arrayElementRead } from "./array-iteration.js";
import { isSearchParamsIterable } from "../builtins/url.js";
import { classGeneratorIterable } from "../class-iterables.js";
import { generatorDrain } from "../iterator-adapters.js";
/** Ambient Map method calls. `get`/`set`/`has`/`delete`/`clear` lower to
* mapIntrinsic; `forEach` desugars to a direct call of a synthetic loop
@@ -1048,6 +1050,14 @@ function setFromSeedValue(
}
: null;
}
// A generator, or a class whose [Symbol.iterator] is a generator method:
// the Set takes every value it yields, in order.
const generator =
classGeneratorIterable(lowerer, source) ?? (source.type.kind === "generator" ? source : null);
if (generator) {
const drained = generatorDrain(lowerer, generator, setT.elem, loc);
return drained ? { kind: "setNew", seed: drained, type: setT, loc } : null;
}
let seed: IrExpr;
if (source.type.kind === "set" && typeEquals(source.type, setT)) {
return { kind: "setIntrinsic", method: "clone", receiver: source, args: [], type: setT, loc };
@@ -1246,6 +1256,24 @@ export function mapFromSeedValue(
}
: null;
}
// A generator of entries, or a class whose [Symbol.iterator] is one: the
// Map takes every [key, value] pair it yields, in order.
const generator =
classGeneratorIterable(lowerer, seed) ?? (seed.type.kind === "generator" ? seed : null);
if (generator) {
const entry: IrType = {
kind: "record",
shapeId: lowerer.shapes.intern(
[
{ name: "0", type: mapT.key },
{ name: "1", type: mapT.value },
],
true,
),
};
const drained = generatorDrain(lowerer, generator, entry, loc);
return drained ? mapFromSeedValue(lowerer, drained, mapT) : null;
}
if (seed.type.kind === "map") {
if (!typeEquals(seed.type, mapT)) return null;
return { kind: "mapIntrinsic", method: "clone", receiver: seed, args: [], type: mapT, loc };
@@ -28,6 +28,8 @@ import { tryLowerExpression } from "../expressions/try-lower-expression.js";
import { locOf } from "../../program.js";
import { typeKey } from "../../type-mapper.js";
import { type WidthLift } from "../lowerer.js";
import { classGeneratorIterable, iteratesThroughGenerator } from "../class-iterables.js";
import { generatorDrain } from "../iterator-adapters.js";
/** Object() and new Object() preserve known object inputs. Primitive
* boxing and unproven checked-dynamic values retain their refusal. */
@@ -148,12 +150,29 @@ export function objectIterOverIndexShape(
}
// The overflow value must surface as the element type (identity, an
// arm of a union element, or a dyn element over a dyn signature).
// A cast that narrows the signature's union (`Object.entries(headers as
// Record<string, string | string[] | undefined>)` over a signature that
// also holds numbers): each overflow value checks into the element type,
// so a value of a dropped arm throws instead of mistyping.
let ivNarrows = false;
if (valueT) {
const ivSurfaces =
typeEquals(iv, valueT) ||
(valueT.kind === "union" && lowerer.armTag(valueT.unionId, iv) >= 0) ||
(valueT.kind === "dyn" && iv.kind === "dyn");
if (!ivSurfaces) {
if (
!ivSurfaces &&
iv.kind === "union" &&
lowerer.dynConvertible(iv) &&
lowerer.dynConvertible(valueT)
) {
const ivUnion = iv;
const narrowArms =
valueT.kind === "union" ? (lowerer.unions.get(valueT.unionId)?.arms ?? []) : [valueT];
ivNarrows =
narrowArms.length > 0 && narrowArms.every((a) => lowerer.armTag(ivUnion.unionId, a) >= 0);
}
if (!ivSurfaces && !ivNarrows) {
lowerer.unsupported(
"SC1090",
node,
@@ -267,6 +286,21 @@ export function objectIterOverIndexShape(
}
}
}
// A cast that narrows the field's type (the element type keeps
// some of its arms): the value checks into the element type, so a
// dropped arm throws instead of mistyping.
if (lowerer.dynConvertible(f.type) && lowerer.dynConvertible(valueT)) {
const armsOf = (t: IrType): IrType[] =>
t.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? []) : [t];
const target = armsOf(valueT);
if (armsOf(f.type).some((a) => target.some((b) => typeEquals(a, b))))
return {
kind: "dynCheck",
value: { kind: "dynFrom", value: raw, type: DYN, loc },
type: valueT,
loc,
};
}
return null;
};
const pushedOf = (element: IrExpr): IrExpr =>
@@ -337,17 +371,37 @@ export function objectIterOverIndexShape(
const ksT = arrayOf(STRING);
const ksRef = varRef("ks.0", ksT, loc);
const kRef = varRef("k.0", STRING, loc);
const readValue: IrExpr | null = valueT
? {
kind: "recordKeyGet",
obj: rRef,
shapeId: argIr.shapeId,
key: kRef,
overflowOnly: true,
type: valueT,
loc,
}
: null;
const readValue: IrExpr | null = !valueT
? null
: ivNarrows
? {
kind: "dynCheck",
value: {
kind: "dynFrom",
value: {
kind: "recordKeyGet",
obj: rRef,
shapeId: argIr.shapeId,
key: kRef,
overflowOnly: true,
type: iv,
loc,
},
type: DYN,
loc,
},
type: valueT,
loc,
}
: {
kind: "recordKeyGet",
obj: rRef,
shapeId: argIr.shapeId,
key: kRef,
overflowOnly: true,
type: valueT,
loc,
};
const loopPushed: IrExpr =
member === "keys"
? kRef
@@ -472,9 +526,37 @@ function lowerTypedObjectFromEntriesCall(
if (call.arguments.nodes.length !== 1 || ts.isSpreadElement(call.arguments.nodes[0]!))
return null;
const argNode = call.arguments.nodes[0]!;
const argIr = lowerer.mapTypeOf(lowerer.typeOf(argNode));
let argIr = lowerer.mapTypeOf(lowerer.typeOf(argNode));
// A generator of entries, or a class whose [Symbol.iterator] is one,
// drains into the entry array the object is built from.
let drained: IrExpr | null = null;
if (argIr?.kind === "generator" || (argIr && iteratesThroughGenerator(lowerer, argIr))) {
const value = lowerer.lowerExpr(argNode);
const generator =
classGeneratorIterable(lowerer, value) ?? (value.type.kind === "generator" ? value : null);
if (generator?.type.kind !== "generator") return null;
// The entries take the result's value type: `[string, V]` for an
// object `{ [k: string]: V }`.
const result = lowerer.mapTypeOf(lowerer.typeOf(call));
const valueT =
result?.kind === "record" ? lowerer.shapes.get(result.shapeId)?.indexValue : undefined;
if (!valueT) return null;
const entry: IrType = {
kind: "record",
shapeId: lowerer.shapes.intern(
[
{ name: "0", type: STRING },
{ name: "1", type: valueT },
],
true,
),
};
drained = generatorDrain(lowerer, generator, entry, locOf(call));
if (!drained) return null;
argIr = drained.type;
}
if (argIr?.kind !== "array") return null;
const represented = tryLowerExpression(lowerer, argNode);
const represented = drained ?? tryLowerExpression(lowerer, argNode);
if (represented && !typeEquals(represented.type, argIr)) return null;
// `Object.fromEntries(rows)` over a `string[][]` VALUE — the env-line
// idiom (`envArray.map((env) => env.split('='))`). The checker has no
@@ -522,7 +604,7 @@ function lowerTypedObjectFromEntriesCall(
// Enumeration/filtering may keep the array in checked-dynamic storage.
// Validate its tuple layout before calling a helper with a native-array
// ABI; opaque tuple values themselves retain their original references.
const receiver = lowerer.lowerExprExpecting(argNode, argIr);
const receiver = drained ?? lowerer.lowerExprExpecting(argNode, argIr);
const key = `obj.fromEntries:${argIr.elem.shapeId}:${resultT.shapeId}`;
let helper = lowerer.arrHofHelpers.get(key);
if (!helper) {
@@ -34,6 +34,7 @@ import {
genValue,
stepDrivable,
} from "../lower-generators.js";
import { classGeneratorIterable, iteratesThroughGenerator } from "../class-iterables.js";
import { exitBeforeReturns } from "./for-of.js";
import {
iteratorSlotAdapter,
@@ -317,9 +318,11 @@ function isHelperObject(lowerer: Lowerer, expr: ts.Expression): boolean {
* it rather than a wrapper. A generator (including a helper object or an
* array iterator) is itself; an array, string or typed array gives its
* values iterator; a Set its values and a Map its entries, as a generator
* over the live collection. The result converts into the call's
* IteratorObject type. Null for other arguments (objects with their own
* next method, untyped values), which keep the Iterator fence. */
* over the live collection; a class instance whose `[Symbol.iterator]` is
* a generator method gives the generator it returns. The result converts
* into the call's IteratorObject type. Null for other arguments (objects
* with their own next method, untyped values), which keep the Iterator
* fence. */
export function lowerIteratorFromCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -332,7 +335,9 @@ export function lowerIteratorFromCall(
const argT = lowerer.mapTypeOf(lowerer.typeOf(argNode));
if (resultT?.kind !== "generator" || resultT.async || argT === null) return null;
const iterableKinds = ["generator", "array", "string", "bytes", "set", "map"];
if (!iterableKinds.includes(argT.kind) || (argT.kind === "generator" && argT.async)) return null;
const classIterable = iteratesThroughGenerator(lowerer, argT);
if (!classIterable && !iterableKinds.includes(argT.kind)) return null;
if (argT.kind === "generator" && argT.async) return null;
const loc = locOf(call);
const value = lowerer.lowerExpr(argNode);
const own = (yieldT: IrType): GenType => ({
@@ -344,6 +349,7 @@ export function lowerIteratorFromCall(
let iterator: IrExpr | null = null;
const t = value.type;
if (t.kind === "generator") iterator = value;
else if (classIterable) iterator = classGeneratorIterable(lowerer, value);
else if (t.kind === "array")
iterator = lowerArrayIteratorValue(lowerer, value, "values", own(t.elem), loc);
else if (t.kind === "string")
@@ -302,6 +302,53 @@ function lowerMethodReceiver(
return lowerer.coerceInto(node, lowered, expected);
}
/** A `string | undefined | null` receiver of a string method: evaluated
* once, each unit arm throws the TypeError Node's member read throws
* ("Cannot read properties of undefined (reading 'replace')") before the
* arguments evaluate, and the string arm is the receiver. */
function nullableStringReceiver(lowerer: Lowerer, node: ts.Expression, member: string): IrExpr {
const lowered = lowerer.lowerExpr(node);
if (lowered.type.kind !== "union") return lowerer.coerceInto(node, lowered, STRING);
const loc = locOf(node);
const unionId = lowered.type.unionId;
const local = lowerer.declareHiddenLocal("%nullableReceiver", lowered.type);
const ref: IrExpr = { kind: "varRef", localId: local.id, type: lowered.type, loc };
const arms = lowerer.unions.get(unionId)?.arms ?? [];
let result: IrExpr = {
kind: "unionNarrow",
unionId,
tag: arms.findIndex((arm) => arm.kind === "string"),
value: ref,
type: STRING,
loc,
};
for (const [tag, arm] of arms.entries()) {
if (!isUnitType(arm)) continue;
const unit = arm.kind === "nullT" ? "null" : "undefined";
result = {
kind: "ternary",
cond: { kind: "unionIsTag", unionId, tag, value: ref, negated: false, type: BOOL, loc },
then: nodeThrowExpr(
1,
"",
`Cannot read properties of ${unit} (reading '${member}')`,
STRING,
loc,
),
else_: result,
type: STRING,
loc,
};
}
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: lowered, loc }],
result,
type: STRING,
loc,
};
}
/** Relative string indexing and code-point decoding over the UTF-16 intrinsics. */
export function lowerStringIndexCall(
lowerer: Lowerer,
@@ -733,6 +780,15 @@ export function lowerStringPaddingCall(
return { kind: "call", callee: helper, args: values, type: STRING, loc };
}
/** String methods whose regex-argument forms lower in lowerRegexMethodCall. */
const REGEX_ARGUMENT_STRING_METHODS = new Set([
"replace",
"replaceAll",
"split",
"search",
"matchAll",
]);
export function lowerRegexMethodCall(
lowerer: Lowerer,
call: ts.CallExpression,
@@ -747,11 +803,30 @@ export function lowerRegexMethodCall(
// by construction — the symbol gate doesn't apply to `any` receivers.
if (dynReceiver === undefined && !lowerer.isStdlibMember(access)) return null;
const name = access.name.text;
const receiverKind = dynReceiver
? "string"
: lowerer.mapTypeOf(lowerer.typeOf(access.expression))?.kind;
const mappedReceiver = dynReceiver ? null : lowerer.mapTypeOf(lowerer.typeOf(access.expression));
// A nullable string receiver of a REGEX-argument call
// (`part.replace(re, "")` where an unchecked index read typed `part`
// `string | undefined`) takes the string path, as
// lowerStringMethodCall's gate does for the string-argument forms:
// lowerMethodReceiver throws Node's "Cannot read properties of
// undefined" on the unit arms. (`match` keeps its own gate below.)
const regexArgument = call.arguments.nodes[0];
const nullableString =
REGEX_ARGUMENT_STRING_METHODS.has(name) &&
regexArgument !== undefined &&
!ts.isSpreadElement(regexArgument) &&
lowerer.mapTypeOf(lowerer.typeOf(regexArgument))?.kind === "regex" &&
mappedReceiver?.kind === "union" &&
(lowerer.unions.get(mappedReceiver.unionId)?.arms.some((arm) => arm.kind === "string") ??
false) &&
(lowerer.unions
.get(mappedReceiver.unionId)
?.arms.every((arm) => arm.kind === "string" || isUnitType(arm)) ??
false);
const receiverKind = dynReceiver || nullableString ? "string" : mappedReceiver?.kind;
const lowerReceiver = (): IrExpr => {
if (dynReceiver) return dynReceiver();
if (nullableString) return nullableStringReceiver(lowerer, access.expression, name);
if (receiverKind === "string")
return lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text);
if (receiverKind === "regex")
@@ -14,7 +14,6 @@ import * as ts from "../ts7/adapter.js";
import { locOf } from "../program.js";
import { isNodeModulesPath } from "../resolve.js";
import {
dateLiteralDivergenceDiag,
keyOrderDivergenceDiag,
localeCompareDivergenceDiag,
unrelatedClassCastDivergenceDiag,
@@ -57,7 +56,6 @@ export function scanDivergences(
const literals: ts.ObjectLiteralExpression[] = [];
const observers: ts.Expression[] = [];
const casts: (ts.AsExpression | ts.TypeAssertion)[] = [];
const dates: { call: ts.CallExpression | ts.NewExpression; arg: ts.Expression }[] = [];
const localeCompares: ts.CallExpression[] = [];
/** binding name → member names assigned through it (`q.a = 2`). */
const assignedMembers = new Map<string, Set<string>>();
@@ -99,20 +97,9 @@ export function scanDivergences(
member === "dir")
) {
for (const a of node.arguments.nodes) if (!isLiteralLike(a)) observers.push(a);
} else if (recv.text === "Date" && member === "parse") {
if (node.arguments.nodes[0] !== undefined)
dates.push({ call: node, arg: node.arguments.nodes[0] });
}
}
}
} else if (ts.isNewExpression(node)) {
if (
ts.isIdentifier(node.expression) &&
node.expression.text === "Date" &&
node.arguments?.nodes.length === 1
) {
dates.push({ call: node, arg: node.arguments.nodes[0]! });
}
} else if (ts.isForInStatement(node)) {
observers.push(node.expression);
} else if (ts.isVariableDeclaration(node)) {
@@ -399,55 +386,9 @@ export function scanDivergences(
}
}
// ── Date strings the native parser does not accept ────────────────
for (const { call, arg } of dates) {
const callee = ts.isNewExpression(call)
? call.expression
: ts.isPropertyAccessExpression(call.expression)
? call.expression.expression
: undefined;
if (callee === undefined || !lowerer.isStdlibGlobal(callee, "Date")) continue;
for (const literal of dateStringLiterals(checker, arg)) {
const text = stringLiteralText(literal)!;
if (nativeDateParses(text) || !/\d/.test(text)) continue;
out.push(dateLiteralDivergenceDiag(text, locOf(literal)));
}
}
return out;
}
/** The string literals a Date argument can be: the literal itself, or
* each element of a const array of literals a for-of binding iterates. */
function dateStringLiterals(checker: ts.TypeChecker, arg: ts.Expression): ts.Expression[] {
if (stringLiteralText(arg) !== null) return [arg];
if (!ts.isIdentifier(arg)) return [];
const symbol = safely(() => checker.getSymbolAtLocation(arg));
const decl = symbol === undefined ? undefined : symbol.valueDeclaration;
const list = decl?.parent;
const loop = list?.parent;
if (
decl === undefined ||
!ts.isVariableDeclaration(decl) ||
list === undefined ||
loop === undefined ||
!ts.isForOfStatement(loop) ||
loop.initializer !== list
)
return [];
let iterated: ts.Expression = loop.expression;
if (ts.isIdentifier(iterated)) {
const source = safely(() => checker.getSymbolAtLocation(iterated));
const init = source === undefined ? undefined : source.valueDeclaration;
if (init === undefined || !ts.isVariableDeclaration(init) || init.initializer === undefined)
return [];
if (init.parent === undefined || (init.parent.flags & ts.NodeFlags.Const) === 0) return [];
iterated = init.initializer;
}
if (!ts.isArrayLiteralExpression(iterated)) return [];
return iterated.elements.nodes.filter((e) => stringLiteralText(e) !== null);
}
/** A checker query that fails here only loses a warning. */
function safely<T>(f: () => T): T | undefined {
try {
@@ -870,20 +811,3 @@ function classExtends(
function className(decl: ts.ClassLikeDeclaration): string {
return decl.name?.text ?? "(anonymous class)";
}
const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
/** The date-string grammar the native runtime accepts (scr_lib.c
* scr_date_parse_get_time): ECMAScript's date-time format with an explicit
* offset (date-only forms are UTC) and the certificate-time shape
* "Mon DD HH:MM:SS YYYY GMT". Everything else parses to NaN natively. */
export function nativeDateParses(text: string): boolean {
const iso =
/^(?:[+-]\d{6}|\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/.test(text) ||
/^(?:[+-]\d{6}|\d{4})-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(
text,
);
if (iso) return true;
const cert = /^([A-Za-z]{3}) {1,2}\d{1,2} \d{2}:\d{2}:\d{2} \d{4} GMT$/.exec(text);
return cert !== null && MONTHS.includes(cert[1]!.toLowerCase());
}
@@ -0,0 +1,314 @@
/* Checked-dynamic stream slots (TypeMapperCtx.dynNodeStreams).
*
* Under the general stream representation (general-streams.ts),
* NodeJS.ReadableStream and NodeJS.WritableStream map to the runtime stream
* classes. JavaScript code also hands such slots its own stream objects:
* constructor-function streams over EventEmitter or the legacy Stream, plain
* objects implementing on/pipe/write/end, streams of bundled npm code. A
* program that moves an untyped value into a stream slot therefore lowers
* again with checked-dynamic slots: those two interfaces map to dyn, so a
* slot holds either a boxed native stream (its typed capsule, which unboxes
* to the same stream) or the JavaScript object itself, and a value returned
* to JavaScript is the object that came in.
*
* A member call on such a slot dispatches per value at run time. A native
* stream unboxes and takes the ordinary class-method lowering (the same
* stream surface a typed stream has); any other value calls its own member
* with JavaScript's dynamic semantics (dynInvoke). Programs without such a
* flow never take this pass, so their IR is unchanged. */
import type { IrExpr, IrStmt, IrType } from "../../ir/ir.js";
import { dynUndefinedExpr, numLit, strLit, varRef } from "../../ir/build.js";
import { DYN } from "../../ir/ir.js";
import * as ts from "../ts7/adapter.js";
import { locOf } from "../program.js";
import { isDeclaredInAmbientNamespace } from "../type-mapper.js";
import { resultIsDiscarded } from "./call-position.js";
import { lowerObjectMethodCall } from "./lower-calls.js";
import type { Lowerer } from "./lowerer.js";
/** The side a NodeJS stream interface names: "r" for NodeJS.ReadableStream,
* "w" for NodeJS.WritableStream (through null/undefined and intersections),
* or null for any other type. */
export function nodeStreamInterfaceSide(lowerer: Lowerer, type: ts.Type): "r" | "w" | null {
const parts = (t: ts.Type): ts.Type[] =>
(t.flags & ts.TypeFlags.Intersection) !== 0 ? ts.constituentTypes(t).flatMap(parts) : [t];
for (const part of parts(lowerer.checker.getNonNullableType(type))) {
const psym = part.getSymbol() ?? part.getAliasSymbol();
if (psym?.name !== "ReadableStream" && psym?.name !== "WritableStream") continue;
const declared = psym.declarations.some(
(d) =>
ts.isInterfaceDeclaration(d) &&
lowerer.isStdlibFile(d.getSourceFile()) &&
isDeclaredInAmbientNamespace(d, "NodeJS"),
);
if (declared) return psym.name === "ReadableStream" ? "r" : "w";
}
return null;
}
/** The runtime class a native value of the side unboxes to. */
export function nativeStreamClass(side: "r" | "w"): IrType {
return { kind: "object", className: side === "r" ? "%Readable" : "%Writable" };
}
/** The native stream of a checked-dynamic value (+1, typed as the side's
* class) or null: the stream.dynNative libCall, which tests the value's
* typed capsule without throwing. */
function dynNativeStream(lowerer: Lowerer, value: IrExpr, side: "r" | "w"): IrExpr {
const native = nativeStreamClass(side);
return {
kind: "libCall",
fn: "stream.dynNative",
args: [value, numLit(side === "r" ? 1 : 2, value.loc)],
type: { kind: "union", unionId: lowerer.unions.intern([native, { kind: "nullT" }]) },
loc: value.loc,
};
}
/** Runs `lower` with `node` lowering to `value` and its member calls taking
* `type` as the receiver representation. */
function withStreamReceiver<T>(
lowerer: Lowerer,
node: ts.Expression,
value: IrExpr,
type: IrType,
lower: () => T,
): T {
const previous = lowerer.receiverTypeOverrides.get(node);
lowerer.receiverTypeOverrides.set(node, type);
try {
return lowerer.withExpressionOverride(node, value, lower);
} finally {
if (previous) lowerer.receiverTypeOverrides.set(node, previous);
else lowerer.receiverTypeOverrides.delete(node);
}
}
/** `stream.member(args)` on a NodeJS.ReadableStream / NodeJS.WritableStream
* receiver under checked-dynamic stream slots, or null when the call is not
* one. */
export function lowerDynStreamMemberCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
if (!lowerer.dynNodeStreams) return null;
// The native arm re-enters the call lowering with the receiver bound.
if (lowerer.chainRecvByNode.has(access.expression)) return null;
if (access.questionDotToken || call.questionDotToken || !ts.isIdentifier(access.name))
return null;
if (call.arguments.nodes.some(ts.isSpreadElement)) return null;
const side = nodeStreamInterfaceSide(lowerer, lowerer.typeOf(access.expression));
if (side === null) return null;
const loc = locOf(call);
const receiverValue = lowerer.lowerExpr(access.expression);
const receiver = lowerer.declareHiddenLocal("%streamReceiver", receiverValue.type);
const receiverRef = varRef(receiver.id, receiver.type, locOf(access.expression));
const stmts: IrStmt[] = [{ kind: "varDecl", localId: receiver.id, init: receiverValue, loc }];
const seq = (result: IrExpr): IrExpr => ({
kind: "seqExpr",
stmts,
result,
type: result.type,
loc,
});
// A typed stream whose declared type is one of the interfaces (a child
// stdio read): the class-method path on its own representation.
if (receiverValue.type.kind !== "dyn") {
const typed =
receiverValue.type.kind === "object"
? withStreamReceiver(lowerer, access.expression, receiverRef, receiverValue.type, () =>
lowerObjectMethodCall(lowerer, call, access),
)
: lowerer.withExpressionOverride(access.expression, receiverRef, () =>
lowerer.lowerCall(call),
);
if (typed === null) lowerer.noLowering(`${access.name.text} on this stream`, call);
return seq(typed);
}
const nativeType = nativeStreamClass(side);
const nullable = dynNativeStream(lowerer, receiverRef, side);
const nativeOrNull = lowerer.declareHiddenLocal("%streamNative", nullable.type);
stmts.push({ kind: "varDecl", localId: nativeOrNull.id, init: nullable, loc });
const unionId = nullable.type.kind === "union" ? nullable.type.unionId : "";
const tag = lowerer.armTag(unionId, nativeType);
const native = lowerer.declareHiddenLocal("%streamNativeValue", nativeType);
const nativeRef = varRef(native.id, nativeType, locOf(access.expression));
const nativeDecl: IrStmt = {
kind: "varDecl",
localId: native.id,
init: {
kind: "unionNarrow",
unionId,
tag,
value: varRef(nativeOrNull.id, nullable.type, loc),
type: nativeType,
loc,
},
loc,
};
const typed = withStreamReceiver(lowerer, access.expression, nativeRef, nativeType, () =>
lowerObjectMethodCall(lowerer, call, access),
);
if (typed === null)
lowerer.noLowering(
`${side === "r" ? "NodeJS.ReadableStream" : "NodeJS.WritableStream"}.${access.name.text}`,
call,
);
// Any other value calls its own member, with the arguments as untyped
// values (typed callbacks box behind checked thunks).
const invoked: IrExpr = {
kind: "dynInvoke",
recv: receiverRef,
method: access.name.text,
calleeName: access.getText(),
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
type: DYN,
loc,
};
const cond: IrExpr = {
kind: "unionIsTag",
unionId,
tag,
negated: false,
value: varRef(nativeOrNull.id, nullable.type, loc),
type: { kind: "bool" },
loc,
};
const resultType = resultIsDiscarded(call) ? null : lowerer.irTypeOf(call);
if (resultType === null || resultType.kind === "void") {
stmts.push({
kind: "if",
cond,
then: [nativeDecl, { kind: "exprStmt", expr: typed, loc }],
else_: [{ kind: "exprStmt", expr: invoked, loc }],
loc,
});
return seq(dynUndefinedExpr(loc));
}
const result = lowerer.declareHiddenLocal("%streamResult", resultType);
result.mutable = true;
stmts.push(
{ kind: "varDecl", localId: result.id, init: null, loc },
{
kind: "if",
cond,
then: [
nativeDecl,
{
kind: "assign",
localId: result.id,
value: lowerer.coerceInto(call, typed, resultType),
loc,
},
],
else_: [
{
kind: "assign",
localId: result.id,
value: lowerer.coerceInto(call, invoked, resultType),
loc,
},
],
loc,
},
);
return seq(varRef(result.id, resultType, loc));
}
/** `for await (const chunk of stream)` over a checked-dynamic
* NodeJS.ReadableStream: a native stream iterates through the Readable
* lowering; any other value throws the not-supported Error (async
* iteration of JavaScript stream objects has no lowering). */
export function lowerForAwaitDynStream(
lowerer: Lowerer,
stmt: ts.ForOfStatement,
lowerLoop: () => IrStmt | null,
): IrStmt | null {
if (!lowerer.dynNodeStreams || lowerer.chainRecvByNode.has(stmt.expression)) return null;
if (nodeStreamInterfaceSide(lowerer, lowerer.typeOf(stmt.expression)) !== "r") return null;
const loc = locOf(stmt.expression);
const value = lowerer.lowerExpr(stmt.expression);
const native: IrType =
value.type.kind === "dyn" ? { kind: "object", className: "%Duplex" } : value.type;
const local = lowerer.declareHiddenLocal("%streamIterated", native);
const init: IrExpr =
value.type.kind === "dyn"
? {
kind: "libCall",
fn: "stream.unboxDyn",
args: [
value,
strLit(
"for await over stream objects other than native streams is not supported yet",
loc,
),
],
type: native,
loc,
}
: value;
const loop = withStreamReceiver(
lowerer,
stmt.expression,
varRef(local.id, native, loc),
native,
lowerLoop,
);
if (loop === null) return null;
return {
kind: "block",
body: [{ kind: "varDecl", localId: local.id, init, loc }, loop],
loc: locOf(stmt),
};
}
/** A statement that runs `onNative` with the native stream a
* checked-dynamic value boxes (typed as the side's class), or `otherwise`
* for any other value. */
export function dynNativeBranch(
lowerer: Lowerer,
value: IrExpr,
side: "r" | "w",
onNative: (native: IrExpr) => IrStmt,
otherwise: IrStmt,
): IrStmt {
const loc = value.loc;
const nativeType = nativeStreamClass(side);
const nullable = dynNativeStream(lowerer, value, side);
const nativeOrNull = lowerer.declareHiddenLocal("%streamNative", nullable.type);
const unionId = nullable.type.kind === "union" ? nullable.type.unionId : "";
const tag = lowerer.armTag(unionId, nativeType);
const native = lowerer.declareHiddenLocal("%streamNativeValue", nativeType);
const nullableRef = varRef(nativeOrNull.id, nullable.type, loc);
return {
kind: "block",
body: [
{ kind: "varDecl", localId: nativeOrNull.id, init: nullable, loc },
{
kind: "if",
cond: {
kind: "unionIsTag",
unionId,
tag,
negated: false,
value: nullableRef,
type: { kind: "bool" },
loc,
},
then: [
{
kind: "varDecl",
localId: native.id,
init: { kind: "unionNarrow", unionId, tag, value: nullableRef, type: nativeType, loc },
loc,
},
onNative(varRef(native.id, nativeType, loc)),
],
else_: [otherwise],
loc,
},
],
loc,
};
}
@@ -0,0 +1,98 @@
import * as ts from "../ts7/adapter.js";
import { isClassOwnEnumerableFieldName } from "../../ir/ir.js";
import type { ClassInfo } from "./lower-classes.js";
type ErrorClassLink = Pick<ClassInfo, "decl" | "ctor" | "paramProps"> & {
base: ErrorClassLink | null;
};
/** The property key a member or element-access name spells statically:
* an identifier, a quoted string, a number in its canonical spelling, or a
* computed string literal. Null for other keys. */
function staticKeyText(name: ts.Node): string | null {
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
if (ts.isNumericLiteral(name)) return String(Number(name.text));
if (ts.isComputedPropertyName(name)) return staticKeyText(name.expression);
return null;
}
/** Whether `node` is an assignment to `this.<name>` or `this["<name>"]`,
* and the name. */
function thisAssignmentName(node: ts.Node): string | null {
if (!ts.isBinaryExpression(node) || node.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
return null;
const target = node.left;
if (ts.isPropertyAccessExpression(target) && target.expression.kind === ts.SyntaxKind.ThisKeyword)
return ts.isIdentifier(target.name) ? target.name.text : null;
if (ts.isElementAccessExpression(target) && target.expression.kind === ts.SyntaxKind.ThisKeyword)
return staticKeyText(target.argumentExpression);
return null;
}
/** Where an Error subclass instance's own `name` sits among its fields when
* a class of its chain gives it one during construction. A property's
* position is fixed when it is created: a `name` field takes its place
* among the fields (parameter properties first, then the declared ones),
* and `this.name = ...` in a constructor follows the fields created so far,
* including the ones that constructor created before it. The answer is the
* field `name` follows, null when it precedes every field, and undefined
* when no class of the chain sets it during construction. `fields` are the
* instance's own enumerable fields. */
export function errorNameAfterField(
info: ErrorClassLink,
fields: ReadonlySet<string>,
): string | null | undefined {
const chain: ErrorClassLink[] = [];
for (let current: ErrorClassLink | null | undefined = info; current; current = current.base)
if (current.decl) chain.unshift(current);
const created = new Set<string>();
let last: string | null = null;
const create = (name: string): void => {
if (!fields.has(name) || created.has(name)) return;
created.add(name);
last = name;
};
for (const link of chain) {
for (const property of link.paramProps ?? []) create(property.name);
for (const member of link.decl!.members.nodes) {
if (!ts.isPropertyDeclaration(member)) continue;
const key = staticKeyText(member.name);
if (key === null) continue;
if (
member.modifiers?.nodes.some(
(m) => m.kind === ts.SyntaxKind.StaticKeyword || m.kind === ts.SyntaxKind.DeclareKeyword,
)
)
continue;
if (key === "name") return last;
create(key);
}
const body = link.ctor?.body;
if (!body) continue;
let found = false;
ts.walkPreorder(body, (node) => {
if (ts.isTypeNode(node)) return "skip";
if (ts.isFunctionLike(node) && !ts.isArrowFunction(node)) return "skip";
const name = thisAssignmentName(node);
if (name === "name") {
found = true;
return "stop";
}
if (name !== null) create(name);
return undefined;
});
if (found) return last;
}
return undefined;
}
/** The fields an Error subclass's own-property view lists. */
export function errorViewFields(fields: readonly { name: string }[]): Set<string> {
return new Set(
fields
.map((field) => field.name)
.filter(
(name) => isClassOwnEnumerableFieldName(name) && name !== "name" && name !== "message",
),
);
}
@@ -30,6 +30,7 @@ import { generatorDrain } from "../iterator-adapters.js";
import { isSearchParamsIterable } from "../builtins/url.js";
import { varRef } from "../../../ir/build.js";
import { arrayValueStore } from "../array-values.js";
import { classGeneratorIterable } from "../class-iterables.js";
/** JavaScript: whether `expr` is or holds an existing checked-dynamic
* object — a binding stored as a dyn value whose checker type is an
@@ -431,6 +432,9 @@ export function lowerArrayLiteral(
loc: locOf(el),
};
}
// `[...obj]` over a class whose [Symbol.iterator] is a generator
// method spreads the generator that method returns.
src = classGeneratorIterable(lowerer, src) ?? src;
// `[...it]` over a generator (or an iterator adapted into one):
// drain it once, in order, into a fresh element array.
if (src.type.kind === "generator") {
@@ -1,5 +1,6 @@
/* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */
import * as ts from "../../ts7/adapter.js";
import { cleanFuncAdaptable } from "../coercions/function-adapters.js";
import { type Lowerer, jsFuncNameOf } from "../lowerer.js";
import {
BIGINT_T,
@@ -481,6 +482,22 @@ export function lowerNullishPair(
);
}
}
// An optional callback defaulted to a function of another signature
// (`o.tags ?? (() => ({}))`, fewer parameters or a narrower result): the
// result is the callback's own function type, and the default adapts into
// it (JavaScript ignores surplus arguments). The checker's merged union
// signature would lose the callback's result shape.
if (present?.kind === "func" && present.rest !== true) {
const raw = lowerer.lowerExpr(expr.right);
if (
raw.type.kind === "func" &&
(typeEquals(raw.type, present) || cleanFuncAdaptable(lowerer, raw.type, present))
) {
const right = lowerer.coerceInto(expr.right, raw, present);
lowerer.nullishFuncResults.add(expr);
return { kind: "nullish", left, right, type: present, loc };
}
}
lowerer.unsupported(
"SC1090",
expr,
@@ -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[] = [];

Some files were not shown because too many files have changed in this diff Show More